File Coverage

blib/lib/Business/Stripe/Subscription.pm
Criterion Covered Total %
statement 34 139 24.4
branch 11 88 12.5
condition 5 13 38.4
subroutine 8 17 47.0
pod 11 11 100.0
total 69 268 25.7


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__