File Coverage

blib/lib/Mojolicious/Plugin/StripePayment.pm
Criterion Covered Total %
statement 85 111 76.5
branch 23 44 52.2
condition 30 74 40.5
subroutine 16 22 72.7
pod 1 1 100.0
total 155 252 61.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::StripePayment;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::StripePayment - Make payments using stripe.com
6              
7             =head1 VERSION
8              
9             0.01
10              
11             =head1 DESCRIPTION
12              
13             L is a plugin for the L web
14             framework which allow you to do payments using L.
15              
16             This module is EXPERIMENTAL. The API can change at any time. Let me know
17             if you are using it.
18              
19             =head1 SYNOPSIS
20              
21             =head2 Simple API
22              
23             use Mojolicious::Lite;
24             plugin StripePayment => { secret => $ENV{SUPER_SECRET_STRIPE_KEY} };
25              
26             # Need this form data:
27             # amount=100&stripeToken=tok_123
28             post '/charge' => sub {
29             my $c = shift;
30             $c->delay(
31             sub { $c->stripe->create_charge({}, shift->begin) },
32             sub {
33             my ($delay, $err, $res) = @_;
34             return $c->reply->exception($err) if $err;
35             return $c->render(text => 'Charge created!');
36             },
37             );
38             };
39              
40             =head2 With local database
41              
42             use Mojolicious::Lite;
43              
44             plugin StripePayment => {
45             secret => $ENV{SUPER_SECRET_STRIPE_KEY},
46             auto_capture => 0, # need to disable auto capture of payments
47             };
48              
49             my $pg = Mojo::Pg->new;
50              
51             # Need this form data:
52             # amount=100&stripeToken=tok_123
53             post '/charge' => sub {
54             my $c = shift;
55             $c->delay(
56             sub { $c->stripe->create_charge({}, shift->begin) },
57             sub {
58             my ($delay, $err, $charge) = @_;
59             die $err if $err;
60             $delay->pass($charge);
61             $pg->db->query(
62             "INSERT INTO payments (id, uid, amount, status) (?, ?, ?)",
63             $charge->{id}, $c->session("uid"), $c->param("amount"), "created"
64             $delay->begin
65             );
66             },
67             sub {
68             my ($delay, $charge, $err, $res) = @_;
69             die $err if $err;
70             $c->stripe->capture_charge($charge, $delay->begin);
71             },
72             sub {
73             my ($delay, $charge) = @_;
74             die $err if $err;
75             $pg->query(
76             "UPDATE payments SET status=? WHERE id=?",
77             "captured", $charge->{id},
78             $delay->begin
79             );
80             },
81             sub {
82             my ($delay, $err, $res) = @_;
83             $c->app->log->error($err) if $err;
84             $c->render(text => "Payment captured.");
85             },
86             );
87             };
88              
89             =head2 Testing mode
90              
91             use Mojolicious::Lite;
92              
93             plugin StripePayment => { mocked => 1 };
94              
95             Setting C will enable this plugin to work without an actual connection
96             to stripe.com. This is done by replicating the behavior of Stripe. This is
97             especially useful when writing unit tests.
98              
99             The following routes will be added to your application to mimic Stripe:
100              
101             =over 4
102              
103             =item * POST /mocked/stripe-payment/charges
104              
105             =item * POST /mocked/stripe-payment/charges/:id/capture
106              
107             =item * GET /mocked/stripe-payment/charges/:id
108              
109             =back
110              
111             =cut
112              
113 1     1   697 use Mojo::Base 'Mojolicious::Plugin';
  1         1  
  1         6  
114 1     1   195 use Mojo::UserAgent;
  1         1  
  1         9  
115 1   50 1   29 use constant DEBUG => $ENV{MOJO_STRIPE_DEBUG} || 0;
  1         2  
  1         1566  
116              
117             our $VERSION = '0.01';
118              
119             my @CAPTURE_KEYS = qw( amount application_fee receipt_email statement_descriptor );
120             my @CHARGE_KEYS
121             = qw( amount application_fee receipt_email statement_descriptor currency customer source description capture );
122              
123             # Subject for change
124             our $MOCKED_FAIL_RESPONSE = {};
125             our $MOCKED_SUCCESSFUL_RESPONSE = {
126             id => 'ch_15ceESLV2Qt9u2twk0Arv0Z8',
127             object => 'charge',
128             created => time,
129             paid => \1,
130             status => 'succeeded',
131             refunded => \0,
132             source => {},
133             balance_transaction => 'txn_14sJxWLV2Qt9u2tw35SuFG9X',
134             failure_message => undef,
135             failure_code => undef,
136             amount_refunded => 0,
137             customer => undef,
138             invoice => undef,
139             dispute => 0,
140             metadata => {},
141             statement_descriptor => undef,
142             fraud_details => {},
143             receipt_number => undef,
144             shipping => undef,
145             refunds => {},
146             };
147              
148             =head1 ATTRIBUTES
149              
150             =head2 auto_capture
151              
152             $bool = $self->auto_capture; # default true
153              
154             Whether or not to immediately capture the charge. When false, the charge
155             issues an authorization (or pre-authorization), and will need to be
156             captured later.
157              
158             This is useful if you want to update your local database with information
159             regarding the charge.
160              
161             =head2 base_url
162              
163             $str = $self->base_url;
164              
165             This is the location to Stripe payment solution. Will be set to
166             L.
167              
168             =head2 pub_key
169              
170             $str = $self->pub_key;
171              
172             The value for public API key. Available in the Stripe admin gui.
173              
174             =head2 currency_code
175              
176             $str = $self->currency_code;
177              
178             The currency code, following ISO 4217. Default is "USD".
179              
180             =head2 secret
181              
182             $str = $self->secret;
183              
184             The value for the private API key. Available in the Stripe admin gui.
185              
186             =cut
187              
188             has base_url => 'https://api.stripe.com/v1';
189             has auto_capture => 1;
190             has currency_code => 'USD';
191             has pub_key => 'pk_test_not_secret_at_all';
192             has secret => 'sk_test_super_secret_key';
193             has _ua => sub { Mojo::UserAgent->new; };
194              
195             =head1 HELPERS
196              
197             =head2 stripe.capture_charge
198              
199             $c->stripe->capture_charge(\%args, sub { my ($c, $err, $json) = @_; });
200              
201             Used to capture a payment from a previously created charge object.
202              
203             C<$err> is a string describing the error. Will be empty string on success.
204             C<$json> is a charge object. See L
205             for more details.
206              
207             C<%args> need to contain "id", but can also contain any of amount,
208             application_fee, receipt_email and/or statement_descriptor.
209              
210             =head2 stripe.create_charge
211              
212             $c->stripe->create_charge(\%args, sub { my ($c, $err, $json) = @_; });
213              
214             Used to create a charge object.
215              
216             C<$err> is a string describing the error. Will be empty string on success.
217             C<$json> is a charge object. See L
218             for more details.
219              
220             C<%args> can have any of...
221              
222             =over 4
223              
224             =item * amount
225              
226             This value is required. Default to "amount" from L.
227              
228             =item * application_fee
229              
230             See L.
231              
232             =item * capture
233              
234             Defaults to L.
235              
236             =item * description
237              
238             Defaults to "description" from L.
239              
240             =item * currency
241              
242             Defaults to L.
243              
244             =item * customer
245              
246             See L.
247              
248             =item * receipt_email
249              
250             Default to "stripeEmail" from L.
251              
252             =item * statement_descriptor
253              
254             See L.
255              
256             =item * source
257              
258             This value is required. Alias: "token".
259              
260             Defaults to "stripeToken" from L.
261              
262             =back
263              
264             =head2 stripe.pub_key
265              
266             $str = $c->stripe->pub_key;
267              
268             Useful for client side JavaScript. See als L.
269              
270             =head2 stripe.retrieve_charge
271              
272             $c->stripe->retrieve_charge({id => $str}, sub { my ($c, $err, $json) = @_; });
273              
274             Used to retrieve a charge object.
275              
276             C<$err> is a string describing the error. Will be empty string on success.
277             C<$json> is a charge object. See L
278             for more details.
279              
280             =head1 METHODS
281              
282             =head2 register
283              
284             $app->plugin(StripePayment => \%config);
285              
286             Called when registering this plugin in the main L application.
287              
288             =cut
289              
290             sub register {
291 1     1 1 37 my ($self, $app, $config) = @_;
292              
293             # copy config to this object
294 1         4 for (grep { $self->can($_) } keys %$config) {
  1         8  
295 0         0 $self->{$_} = $config->{$_};
296             }
297              
298             # self contained
299 1 50       5 $self->_mock_interface($app, $config) if $config->{mocked};
300              
301 1     1   500 $app->helper('stripe.capture_charge' => sub { $self->_capture_charge(@_); });
  1         723  
302 1     5   61 $app->helper('stripe.create_charge' => sub { $self->_create_charge(@_); });
  5         71312  
303 1     0   50 $app->helper('stripe.pub_key' => sub { $self->pub_key; });
  0         0  
304 1     0   44 $app->helper('stripe.retrieve_charge' => sub { $self->_retrieve_charge(@_); });
  0         0  
305             }
306              
307             sub _capture_charge {
308 1     1   2 my ($self, $c, $args, $cb) = @_;
309 1         15 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
310 1         81 my %form;
311              
312 1 50       4 $args->{id} or return $c->$cb('id is required', {});
313              
314 1         3 for my $k (@CAPTURE_KEYS) {
315 4 50       8 $form{$k} = $args->{$k} if defined $args->{$k};
316             }
317              
318 1 50 33     4 if (defined $form{statement_descriptor} and 22 < length $form{statement_descriptor}) {
319 0         0 return $c->$cb('statement_descriptor is too long', {});
320             }
321              
322 1         1 push @{$url->path->parts}, 'charges', $args->{id}, 'capture';
  1         4  
323 1         58 warn "[StripePayment] Capture $url $args->{id}\n" if DEBUG;
324              
325             Mojo::IOLoop->delay(
326 1     1   161 sub { $self->_ua->post($url, form => \%form, shift->begin); },
327 1     1   3904 sub { $c->$cb($self->_tx_to_res($_[1])); },
328 1         12 );
329              
330 1         142 return $c;
331             }
332              
333             sub _create_charge {
334 5     5   13 my ($self, $c, $args, $cb) = @_;
335 5         87 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
336 5         384 my %form;
337              
338 5         13 for my $k (@CHARGE_KEYS) {
339 45 100       79 $form{$k} = $args->{$k} if defined $args->{$k};
340             }
341              
342 5   66     35 $form{amount} ||= $c->param('amount');
343 5   33     417 $form{currency} ||= $self->currency_code;
344 5 50 50     53 $form{description} = $c->param('description') || '' unless defined $form{description};
345 5 50       289 $form{metadata} = $self->_expand(metadata => $args) if ref $form{metadata};
346 5 100 33     12 $form{receipt_email} ||= $c->param('stripeEmail') if $c->param('stripeEmail');
347 5 50       383 $form{shipping} = $self->_expand(shipping => $args) if ref $form{shipping};
348 5   66     32 $form{source} ||= $args->{token} || $c->param('stripeToken');
      66        
349 5   66     322 $form{capture} //= $self->auto_capture;
350              
351 5 50 33     40 if (defined $form{statement_descriptor} and 22 < length $form{statement_descriptor}) {
352 0         0 return $c->$cb('statement_descriptor is too long', {});
353             }
354              
355 5 100       15 $form{amount} or return $c->$cb('amount is required', {});
356 4 50       10 $form{currency} or return $c->$cb('currency is required', {});
357 4 100       16 $form{source} or return $c->$cb('source/token is required', {});
358              
359 3         5 push @{$url->path->parts}, 'charges';
  3         26  
360 3         149 warn "[StripePayment] Charge $url $args->{amount} $args->{currency}\n" if DEBUG;
361              
362             Mojo::IOLoop->delay(
363 3     3   367 sub { $self->_ua->post($url, form => \%form, shift->begin); },
364 3     3   12607 sub { $c->$cb($self->_tx_to_res($_[1])); },
365 3         27 );
366              
367 3         347 return $c;
368             }
369              
370             sub _expand {
371 0     0   0 my ($self, $ns, $args) = @_;
372 0 0       0 my $data = delete $args->{$ns} or return;
373              
374 0         0 while (my ($k, $v) = each %$data) {
375 0         0 $args->{"$ns\[$k\]"} = $v;
376             }
377             }
378              
379             sub _mock_interface {
380 1     1   1 my ($self, $app) = @_;
381 1         14 my $secret = $self->secret;
382              
383 1         20 $self->_ua->server->app($app);
384 1         92 $self->base_url('/mocked/stripe-payment');
385 1         3 push @{$app->renderer->classes}, __PACKAGE__;
  1         16  
386              
387             $app->routes->post(
388             '/mocked/stripe-payment/charges' => sub {
389 3     3   21855 my $c = shift;
390 3 50 33     13 if ($c->param('token') and $c->req->url->userinfo eq "$secret:") {
391 0         0 $c->render(json => $MOCKED_FAIL_RESPONSE, code => 400);
392             }
393             else {
394 3   33     1469 local $MOCKED_SUCCESSFUL_RESPONSE->{amount} //= $c->param('amount');
395 3 100 50     209 local $MOCKED_SUCCESSFUL_RESPONSE->{captured} //= $c->param('capture') // 1 ? \1 : \0;
      33        
396 3   33     202 local $MOCKED_SUCCESSFUL_RESPONSE->{currency} //= lc $c->param('currency');
397 3   50     192 local $MOCKED_SUCCESSFUL_RESPONSE->{description} //= $c->param('description') || '';
      33        
398 3 50 33     202 local $MOCKED_SUCCESSFUL_RESPONSE->{livemode} //= $secret =~ /test/ ? \0 : \1;
399 3   66     15 local $MOCKED_SUCCESSFUL_RESPONSE->{receipt_email} //= $c->param('receipt_email');
400 3         177 $c->render(json => $MOCKED_SUCCESSFUL_RESPONSE);
401             }
402             }
403 1         42 );
404             $app->routes->post(
405             '/mocked/stripe-payment/charges/:id/capture' => sub {
406 1     1   5512 my $c = shift;
407 1 50 33     6 if ($c->param('token') and $c->req->url->userinfo eq "$secret:") {
408 0         0 $c->render(json => $MOCKED_FAIL_RESPONSE, code => 400);
409             }
410             else {
411 1   33     297 local $MOCKED_SUCCESSFUL_RESPONSE->{amount} //= $c->param('amount');
412 1         58 local $MOCKED_SUCCESSFUL_RESPONSE->{captured} = \1;
413 1 50 33     9 local $MOCKED_SUCCESSFUL_RESPONSE->{livemode} //= $secret =~ /test/ ? \0 : \1;
414 1   33     6 local $MOCKED_SUCCESSFUL_RESPONSE->{receipt_email} //= $c->param('receipt_email');
415 1         53 $c->render(json => $MOCKED_SUCCESSFUL_RESPONSE);
416             }
417             }
418 1         591 );
419             $app->routes->get(
420             '/mocked/stripe-payment/charges/:id' => sub {
421 0     0   0 my $c = shift;
422 0 0 0     0 if ($c->param('token') and $c->req->url->userinfo eq "$secret:") {
    0          
423 0         0 $c->render(json => $MOCKED_FAIL_RESPONSE, code => 400);
424             }
425             elsif (my $id = $c->param('id')) {
426 0         0 local $MOCKED_SUCCESSFUL_RESPONSE->{id} = $id;
427 0         0 $c->render(json => $MOCKED_SUCCESSFUL_RESPONSE);
428             }
429             else {
430 0 0       0 $c->render(json => $MOCKED_FAIL_RESPONSE, code => 400) unless $c->param('id');
431             }
432             }
433 1         446 );
434             }
435              
436             sub _retrieve_charge {
437 0     0   0 my ($self, $c, $args, $cb) = @_;
438 0         0 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
439              
440 0   0     0 push @{$url->path->parts}, 'charges', $args->{id} || 'invalid';
  0         0  
441 0         0 warn "[StripePayment] Retrieve charge $url\n" if DEBUG;
442              
443 0     0   0 Mojo::IOLoop->delay(sub { $self->_ua->get($url, shift->begin); }, sub { $c->$cb($self->_tx_to_res($_[1])); },);
  0         0  
  0         0  
444              
445 0         0 return $c;
446             }
447              
448             sub _tx_to_res {
449 4     4   8 my ($self, $tx) = @_;
450 4   50     13 my $error = $tx->error || {};
451              
452 4   50     201 return $error->{message} || $error->{code} || '', $tx->res->json;
453             }
454              
455             =head1 SEE ALSO
456              
457             =over 4
458              
459             =item * Overview
460              
461             L
462              
463             =item * API
464              
465             L
466              
467             =back
468              
469             =head1 COPYRIGHT AND LICENSE
470              
471             Copyright (C) 2014, Jan Henning Thorsen
472              
473             This program is free software, you can redistribute it and/or modify it under
474             the terms of the Artistic License version 2.0.
475              
476             =head1 AUTHOR
477              
478             Jan Henning Thorsen - C
479              
480             =cut
481              
482             1;