line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Business::Stripe::Subscription;
|
2
|
|
|
|
|
|
|
|
3
|
2
|
|
|
2
|
|
176326
|
use HTTP::Tiny;
|
|
2
|
|
|
|
|
105787
|
|
|
2
|
|
|
|
|
82
|
|
4
|
2
|
|
|
2
|
|
1533
|
use JSON::PP;
|
|
2
|
|
|
|
|
33482
|
|
|
2
|
|
|
|
|
181
|
|
5
|
2
|
|
|
2
|
|
1492
|
use Data::Dumper;
|
|
2
|
|
|
|
|
12477
|
|
|
2
|
|
|
|
|
141
|
|
6
|
|
|
|
|
|
|
|
7
|
2
|
|
|
2
|
|
17
|
use strict;
|
|
2
|
|
|
|
|
7
|
|
|
2
|
|
|
|
|
49
|
|
8
|
2
|
|
|
2
|
|
10
|
use warnings;
|
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
3456
|
|
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
our $VERSION = '1.0';
|
11
|
|
|
|
|
|
|
$VERSION = eval $VERSION;
|
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
my $http = HTTP::Tiny->new;
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
# Create Subscription class object
|
16
|
|
|
|
|
|
|
sub new {
|
17
|
2
|
|
|
2
|
1
|
973
|
my $class = shift;
|
18
|
2
|
|
|
|
|
13
|
my %attrs = @_;
|
19
|
|
|
|
|
|
|
|
20
|
2
|
|
50
|
|
|
15
|
$attrs{'currency'} //= 'GBP';
|
21
|
|
|
|
|
|
|
|
22
|
2
|
|
|
|
|
6
|
$attrs{'error'} = '';
|
23
|
|
|
|
|
|
|
|
24
|
2
|
|
33
|
|
|
7
|
$attrs{'cancel_url'} //= "$ENV{'REQUEST_SCHEME'}://$ENV{'HTTP_HOST'}$ENV{'SCRIPT_NAME'}";
|
25
|
2
|
|
33
|
|
|
6
|
$attrs{'success_url'} //= "$ENV{'REQUEST_SCHEME'}://$ENV{'HTTP_HOST'}$ENV{'SCRIPT_NAME'}";
|
26
|
2
|
50
|
33
|
|
|
10
|
$attrs{'error'} = 'cancel_url and success_url cannot be derived from the environment and need to be provided' unless ($attrs{'cancel_url'} and $attrs{'success_url'});
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
# This is changed during testing only
|
29
|
2
|
|
50
|
|
|
11
|
$attrs{'url'} //= 'https://api.stripe.com/v1/';
|
30
|
|
|
|
|
|
|
|
31
|
2
|
100
|
|
|
|
13
|
$attrs{'error'} = 'Secret API key provided as Public key' if $attrs{'api_public'} =~ /^sk_/;
|
32
|
2
|
100
|
|
|
|
8
|
$attrs{'error'} = 'Public API key provided as Secret key' if $attrs{'api_secret'} =~ /^pk_/;
|
33
|
2
|
100
|
|
|
|
11
|
$attrs{'error'} = 'Public API key provided is not a valid key' unless $attrs{'api_public'} =~ /^pk_/;
|
34
|
2
|
100
|
|
|
|
7
|
$attrs{'error'} = 'Secret API key provided is not a valid key' unless $attrs{'api_secret'} =~ /^sk_/;
|
35
|
2
|
50
|
|
|
|
7
|
$attrs{'error'} = 'Secret API key is missing' unless $attrs{'api_secret'};
|
36
|
2
|
50
|
|
|
|
6
|
$attrs{'error'} = 'Public API key is missing' unless $attrs{'api_public'};
|
37
|
|
|
|
|
|
|
|
38
|
2
|
|
|
|
|
8
|
return bless \%attrs, $class;
|
39
|
|
|
|
|
|
|
}
|
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# Returns true if last operation was success
|
42
|
|
|
|
|
|
|
sub success {
|
43
|
2
|
|
|
2
|
1
|
11
|
my $self = shift;
|
44
|
2
|
|
|
|
|
14
|
return !$self->{'error'};
|
45
|
|
|
|
|
|
|
}
|
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# Returns error if last operation failed
|
48
|
|
|
|
|
|
|
sub error {
|
49
|
1
|
|
|
1
|
1
|
2
|
my $self = shift;
|
50
|
1
|
|
|
|
|
6
|
return $self->{'error'};
|
51
|
|
|
|
|
|
|
}
|
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
# Create headers for calling Stripe API
|
54
|
|
|
|
|
|
|
sub _get_header {
|
55
|
0
|
|
|
0
|
|
|
my $self = shift;
|
56
|
|
|
|
|
|
|
return {
|
57
|
|
|
|
|
|
|
'headers' => {
|
58
|
0
|
|
|
|
|
|
'Authorization' => 'Bearer ' . $self->{'api_secret'},
|
59
|
|
|
|
|
|
|
'Stripe-Version' => '2022-11-15',
|
60
|
|
|
|
|
|
|
},
|
61
|
|
|
|
|
|
|
'agent' => "Perl-Business::Stripe::Subscription-v$VERSION",
|
62
|
|
|
|
|
|
|
};
|
63
|
|
|
|
|
|
|
}
|
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
# Create Stripe customer object
|
66
|
|
|
|
|
|
|
sub customer {
|
67
|
0
|
|
|
0
|
1
|
|
my ($self, $customer) = @_;
|
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
70
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
71
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Name missing from Customer object' unless $customer->{'name'};
|
72
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
73
|
|
|
|
|
|
|
|
74
|
0
|
|
|
|
|
|
my $response = $http->post_form($self->{'url'} . 'customers', $customer, $self->_get_header);
|
75
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
76
|
0
|
|
|
|
|
|
my $payload = decode_json($response->{'content'});
|
77
|
0
|
0
|
|
|
|
|
if ($payload->{'object'} eq 'customer') {
|
78
|
0
|
|
|
|
|
|
return $payload->{'id'};
|
79
|
|
|
|
|
|
|
}
|
80
|
|
|
|
|
|
|
}
|
81
|
0
|
|
|
|
|
|
return undef;
|
82
|
|
|
|
|
|
|
}
|
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# Create Stripe subsciption object
|
85
|
|
|
|
|
|
|
sub subscription {
|
86
|
0
|
|
|
0
|
1
|
|
my ($self, $customer, $plan) = @_;
|
87
|
|
|
|
|
|
|
|
88
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
89
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Customer missing' unless $customer;
|
90
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription plan missing' unless $plan;
|
91
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
92
|
|
|
|
|
|
|
|
93
|
0
|
|
|
|
|
|
my $success_url = $self->{'success_url'};
|
94
|
0
|
0
|
|
|
|
|
if ($self->{'append_customer'}) {
|
95
|
0
|
0
|
|
|
|
|
if ($success_url =~ /\?/) {
|
96
|
0
|
|
|
|
|
|
$success_url .= "&customer=$customer";
|
97
|
|
|
|
|
|
|
} else {
|
98
|
0
|
|
|
|
|
|
$success_url .= "?customer=$customer";
|
99
|
|
|
|
|
|
|
}
|
100
|
|
|
|
|
|
|
}
|
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
my $session = {
|
103
|
|
|
|
|
|
|
'success_url' => $success_url,
|
104
|
0
|
|
|
|
|
|
'cancel_url' => $self->{'cancel_url'},
|
105
|
|
|
|
|
|
|
'payment_method_types[0]' => 'card',
|
106
|
|
|
|
|
|
|
'mode' => 'subscription',
|
107
|
|
|
|
|
|
|
'customer' => $customer,
|
108
|
|
|
|
|
|
|
'line_items[0][price]' => $plan,
|
109
|
|
|
|
|
|
|
'line_items[0][quantity]' => 1,
|
110
|
|
|
|
|
|
|
};
|
111
|
0
|
0
|
|
|
|
|
$session->{'subscription_data[trial_period_days]'} = $self->{'trial_days'} if $self->{'trial_days'};
|
112
|
|
|
|
|
|
|
|
113
|
0
|
|
|
|
|
|
my $response = $http->post_form($self->{'url'} . 'checkout/sessions', $session, $self->_get_header);
|
114
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
115
|
0
|
|
|
|
|
|
my $payload = decode_json($response->{'content'});
|
116
|
0
|
0
|
|
|
|
|
if ($payload->{'object'} eq 'checkout.session') {
|
117
|
0
|
|
|
|
|
|
return $payload->{'url'};
|
118
|
|
|
|
|
|
|
}
|
119
|
|
|
|
|
|
|
}
|
120
|
0
|
|
|
|
|
|
$self->{'error'} = 'Failed to update checkout session';
|
121
|
0
|
|
|
|
|
|
return undef;
|
122
|
|
|
|
|
|
|
}
|
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
# Retrieve subscription object from Stripe
|
125
|
|
|
|
|
|
|
sub get_subscription {
|
126
|
0
|
|
|
0
|
1
|
|
my ($self, $subscription) = @_;
|
127
|
|
|
|
|
|
|
|
128
|
0
|
0
|
|
|
|
|
if (!$subscription) {
|
129
|
0
|
|
|
|
|
|
$self->{'error'} = 'Subscription missing';
|
130
|
0
|
|
|
|
|
|
$self->_error('Subscription missing');
|
131
|
0
|
|
|
|
|
|
return undef;
|
132
|
|
|
|
|
|
|
}
|
133
|
|
|
|
|
|
|
|
134
|
0
|
|
|
|
|
|
return $http->get("https://api.stripe.com/v1/subscriptions/$subscription", $self->_get_header);
|
135
|
|
|
|
|
|
|
}
|
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
# Cancel subscription at end of current period
|
138
|
|
|
|
|
|
|
sub cancel {
|
139
|
0
|
|
|
0
|
1
|
|
my ($self, $subscription, $cancel) = @_;
|
140
|
|
|
|
|
|
|
|
141
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
142
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription missing' unless $subscription;
|
143
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
144
|
|
|
|
|
|
|
|
145
|
0
|
0
|
|
|
|
|
$cancel = 1 unless defined $cancel;
|
146
|
0
|
0
|
|
|
|
|
my $state = $cancel ? 'true' : 'false';
|
147
|
|
|
|
|
|
|
|
148
|
0
|
|
|
|
|
|
my $vars = {
|
149
|
|
|
|
|
|
|
'cancel_at_period_end' => $state,
|
150
|
|
|
|
|
|
|
};
|
151
|
|
|
|
|
|
|
|
152
|
0
|
|
|
|
|
|
my $response = $http->post_form("https://api.stripe.com/v1/subscriptions/$subscription", $vars, $self->_get_header);
|
153
|
|
|
|
|
|
|
|
154
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
155
|
0
|
|
|
|
|
|
return $cancel;
|
156
|
|
|
|
|
|
|
}
|
157
|
0
|
|
|
|
|
|
$self->{'error'} = 'Failed to set cancellation';
|
158
|
0
|
|
|
|
|
|
return undef;
|
159
|
|
|
|
|
|
|
}
|
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
# Cancel subscription immediately
|
162
|
|
|
|
|
|
|
sub cancel_now {
|
163
|
0
|
|
|
0
|
1
|
|
my ($self, $subscription) = @_;
|
164
|
|
|
|
|
|
|
|
165
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
166
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription missing' unless $subscription;
|
167
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
168
|
|
|
|
|
|
|
|
169
|
0
|
|
|
|
|
|
my $response = $http->delete("https://api.stripe.com/v1/subscriptions/$subscription", $self->_get_header);
|
170
|
|
|
|
|
|
|
|
171
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
172
|
0
|
|
|
|
|
|
return $response->{'content'}->{'id'} eq $subscription;
|
173
|
|
|
|
|
|
|
}
|
174
|
0
|
|
|
|
|
|
$self->{'error'} = 'Cancellation failed';
|
175
|
0
|
|
|
|
|
|
return undef;
|
176
|
|
|
|
|
|
|
}
|
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
# Change subscripotion to a different price plan
|
179
|
|
|
|
|
|
|
sub update {
|
180
|
0
|
|
|
0
|
1
|
|
my ($self, $subscription, $plan) = @_;
|
181
|
|
|
|
|
|
|
|
182
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
183
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription missing' unless $subscription;
|
184
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription plan missing' unless $plan;
|
185
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
186
|
|
|
|
|
|
|
|
187
|
0
|
|
|
|
|
|
my $res = $http->post_form("https://api.stripe.com/v1/subscriptions/$subscription", {}, $self->_get_header);
|
188
|
0
|
|
|
|
|
|
my $payload = decode_json($res->{'content'});
|
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
# Don't change to the same plan
|
191
|
0
|
0
|
|
|
|
|
if ($payload->{'items'}->{'data'}[0]->{'price'}->{'id'} eq $plan) {
|
192
|
0
|
|
|
|
|
|
$self->{'error'} = 'Cannot change to the same price plan';
|
193
|
0
|
|
|
|
|
|
return 0;
|
194
|
|
|
|
|
|
|
}
|
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
my $vars = {
|
197
|
0
|
|
|
|
|
|
'items[0][id]' => $payload->{'items'}->{'data'}[0]->{'id'},
|
198
|
|
|
|
|
|
|
'items[0][price]' => $plan,
|
199
|
|
|
|
|
|
|
'proration_behavior' => 'create_prorations',
|
200
|
|
|
|
|
|
|
'cancel_at_period_end' => 'false',
|
201
|
|
|
|
|
|
|
};
|
202
|
|
|
|
|
|
|
|
203
|
0
|
|
|
|
|
|
my $response = $http->post_form("https://api.stripe.com/v1/subscriptions/$subscription", $vars, $self->_get_header);
|
204
|
|
|
|
|
|
|
|
205
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
206
|
0
|
|
|
|
|
|
return $response->{'content'}->{'id'} eq $subscription;
|
207
|
|
|
|
|
|
|
}
|
208
|
0
|
|
|
|
|
|
$self->{'error'} = 'Update failed';
|
209
|
0
|
|
|
|
|
|
return undef;
|
210
|
|
|
|
|
|
|
}
|
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# Update card details
|
213
|
|
|
|
|
|
|
sub new_card {
|
214
|
0
|
|
|
0
|
1
|
|
my ($self, $customer, $subscription) = @_;
|
215
|
|
|
|
|
|
|
|
216
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
217
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Customer missing' unless $customer;
|
218
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription missing' unless $subscription;
|
219
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
my $session = {
|
222
|
|
|
|
|
|
|
'success_url' => $self->{'success_url'},
|
223
|
0
|
|
|
|
|
|
'cancel_url' => $self->{'cancel_url'},
|
224
|
|
|
|
|
|
|
'payment_method_types[0]' => 'card',
|
225
|
|
|
|
|
|
|
'mode' => 'setup',
|
226
|
|
|
|
|
|
|
'customer' => $customer,
|
227
|
|
|
|
|
|
|
'setup_intent_data[metadata][subscription_id]' => $subscription,
|
228
|
|
|
|
|
|
|
};
|
229
|
|
|
|
|
|
|
|
230
|
0
|
|
|
|
|
|
my $response = $http->post_form($self->{'url'} . 'checkout/sessions', $session, $self->_get_header);
|
231
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
232
|
0
|
|
|
|
|
|
return decode_json $response->{'content'};
|
233
|
|
|
|
|
|
|
}
|
234
|
|
|
|
|
|
|
|
235
|
0
|
|
|
|
|
|
$self->{'error'} = 'Failed to obtain card update URL';
|
236
|
0
|
|
|
|
|
|
return undef;
|
237
|
|
|
|
|
|
|
}
|
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
# Set default card
|
240
|
|
|
|
|
|
|
sub set_card {
|
241
|
0
|
|
|
0
|
1
|
|
my ($self, $customer, $subscription, $session) = @_;
|
242
|
|
|
|
|
|
|
|
243
|
0
|
|
|
|
|
|
$self->{'error'} = '';
|
244
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Customer missing' unless $customer;
|
245
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Subscription missing' unless $subscription;
|
246
|
0
|
0
|
|
|
|
|
$self->{'error'} = 'Checkout session missing' unless $session;
|
247
|
0
|
0
|
|
|
|
|
return undef if $self->{'error'};
|
248
|
|
|
|
|
|
|
|
249
|
0
|
|
|
|
|
|
my $response = $http->get($self->{'url'} . "checkout/sessions/$session", $self->_get_header);
|
250
|
0
|
|
|
|
|
|
my $json = decode_json $response->{'content'};
|
251
|
|
|
|
|
|
|
|
252
|
0
|
0
|
|
|
|
|
if (!$json->{'setup_intent'}) {
|
253
|
0
|
|
|
|
|
|
$self->{'error'} = 'Failed to get setup intent card';
|
254
|
0
|
|
|
|
|
|
return undef;
|
255
|
|
|
|
|
|
|
}
|
256
|
|
|
|
|
|
|
|
257
|
0
|
|
|
|
|
|
$response = $http->get($self->{'url'} . "setup_intents/" . $json->{'setup_intent'}, $self->_get_header);
|
258
|
|
|
|
|
|
|
|
259
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
260
|
0
|
|
|
|
|
|
$json = decode_json $response->{'content'};
|
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
my $payload = {
|
263
|
0
|
|
|
|
|
|
'default_payment_method' => $json->{'payment_method'},
|
264
|
|
|
|
|
|
|
};
|
265
|
|
|
|
|
|
|
|
266
|
0
|
|
|
|
|
|
$response = $http->post_form($self->{'url'} . "subscriptions/$subscription", $payload, $self->_get_header);
|
267
|
|
|
|
|
|
|
|
268
|
0
|
0
|
|
|
|
|
if ($response->{'success'}) {
|
269
|
0
|
|
|
|
|
|
return 1;
|
270
|
|
|
|
|
|
|
}
|
271
|
|
|
|
|
|
|
}
|
272
|
|
|
|
|
|
|
|
273
|
0
|
|
|
|
|
|
$self->{'error'} = 'Failed to set default card';
|
274
|
0
|
|
|
|
|
|
return undef;
|
275
|
|
|
|
|
|
|
}
|
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
1;
|
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
__END__
|