File Coverage

blib/lib/Mojolicious/Plugin/StripePayment.pm
Criterion Covered Total %
statement 91 117 77.7
branch 26 48 54.1
condition 35 78 44.8
subroutine 16 22 72.7
pod 1 1 100.0
total 169 266 63.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.02
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 3     3   2487 use Mojo::Base 'Mojolicious::Plugin';
  3         4  
  3         15  
114 3     3   399 use Mojo::UserAgent;
  3         3  
  3         21  
115 3   50 3   77 use constant DEBUG => $ENV{MOJO_STRIPE_DEBUG} || 0;
  3         3  
  3         4772  
116              
117             our $VERSION = '0.02';
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_RESPONSE = {
125             status => 200,
126             json => {
127             id => 'ch_15ceESLV2Qt9u2twk0Arv0Z8',
128             object => 'charge',
129             created => time,
130             paid => \1,
131             status => 'succeeded',
132             refunded => \0,
133             source => {},
134             balance_transaction => 'txn_14sJxWLV2Qt9u2tw35SuFG9X',
135             failure_message => undef,
136             failure_code => undef,
137             amount_refunded => 0,
138             customer => undef,
139             invoice => undef,
140             dispute => 0,
141             metadata => {},
142             statement_descriptor => undef,
143             fraud_details => {},
144             receipt_number => undef,
145             shipping => undef,
146             refunds => {},
147             }
148             };
149              
150             =head1 ATTRIBUTES
151              
152             =head2 auto_capture
153              
154             $bool = $self->auto_capture; # default true
155              
156             Whether or not to immediately capture the charge. When false, the charge
157             issues an authorization (or pre-authorization), and will need to be
158             captured later.
159              
160             This is useful if you want to update your local database with information
161             regarding the charge.
162              
163             =head2 base_url
164              
165             $str = $self->base_url;
166              
167             This is the location to Stripe payment solution. Will be set to
168             L.
169              
170             =head2 pub_key
171              
172             $str = $self->pub_key;
173              
174             The value for public API key. Available in the Stripe admin gui.
175              
176             =head2 currency_code
177              
178             $str = $self->currency_code;
179              
180             The currency code, following ISO 4217. Default is "USD".
181              
182             =head2 secret
183              
184             $str = $self->secret;
185              
186             The value for the private API key. Available in the Stripe admin gui.
187              
188             =cut
189              
190             has base_url => 'https://api.stripe.com/v1';
191             has auto_capture => 1;
192             has currency_code => 'USD';
193             has pub_key => 'pk_test_not_secret_at_all';
194             has secret => 'sk_test_super_secret_key';
195             has _ua => sub { Mojo::UserAgent->new; };
196              
197             =head1 HELPERS
198              
199             =head2 stripe.capture_charge
200              
201             $c->stripe->capture_charge(\%args, sub { my ($c, $err, $json) = @_; });
202              
203             Used to capture a payment from a previously created charge object.
204              
205             C<$err> is a string describing the error. Will be empty string on success.
206             C<$json> is a charge object. See L
207             for more details.
208              
209             C<%args> need to contain "id", but can also contain any of amount,
210             application_fee, receipt_email and/or statement_descriptor.
211              
212             =head2 stripe.create_charge
213              
214             $c->stripe->create_charge(\%args, sub { my ($c, $err, $json) = @_; });
215              
216             Used to create a charge object.
217              
218             C<$err> is a string describing the error. Will be empty string on success.
219             C<$json> is a charge object. See L
220             for more details.
221              
222             C<%args> can have any of...
223              
224             =over 4
225              
226             =item * amount
227              
228             This value is required. Default to "amount" from L.
229              
230             =item * application_fee
231              
232             See L.
233              
234             =item * capture
235              
236             Defaults to L.
237              
238             =item * description
239              
240             Defaults to "description" from L.
241              
242             =item * currency
243              
244             Defaults to L.
245              
246             =item * customer
247              
248             See L.
249              
250             =item * receipt_email
251              
252             Default to "stripeEmail" from L.
253              
254             =item * statement_descriptor
255              
256             See L.
257              
258             =item * source
259              
260             This value is required. Alias: "token".
261              
262             Defaults to "stripeToken" from L.
263              
264             =back
265              
266             =head2 stripe.pub_key
267              
268             $str = $c->stripe->pub_key;
269              
270             Useful for client side JavaScript. See als L.
271              
272             =head2 stripe.retrieve_charge
273              
274             $c->stripe->retrieve_charge({id => $str}, sub { my ($c, $err, $json) = @_; });
275              
276             Used to retrieve a charge object.
277              
278             C<$err> is a string describing the error. Will be empty string on success.
279             C<$json> is a charge object. See L
280             for more details.
281              
282             =head1 METHODS
283              
284             =head2 register
285              
286             $app->plugin(StripePayment => \%config);
287              
288             Called when registering this plugin in the main L application.
289              
290             =cut
291              
292             sub register {
293 3     3 1 80 my ($self, $app, $config) = @_;
294              
295             # copy config to this object
296 3         8 for (grep { $self->can($_) } keys %$config) {
  3         23  
297 0         0 $self->{$_} = $config->{$_};
298             }
299              
300             # self contained
301 3 50       14 $self->_mock_interface($app, $config) if $config->{mocked};
302              
303 3     1   1384 $app->helper('stripe.capture_charge' => sub { $self->_capture_charge(@_); });
  1         180  
304 3     8   175 $app->helper('stripe.create_charge' => sub { $self->_create_charge(@_); });
  8         101990  
305 3     0   155 $app->helper('stripe.pub_key' => sub { $self->pub_key; });
  0         0  
306 3     0   134 $app->helper('stripe.retrieve_charge' => sub { $self->_retrieve_charge(@_); });
  0         0  
307             }
308              
309             sub _capture_charge {
310 1     1   2 my ($self, $c, $args, $cb) = @_;
311 1         14 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
312 1         73 my %form;
313              
314 1 50       3 $args->{id} or return $c->$cb('id is required', {});
315              
316 1         3 for my $k (@CAPTURE_KEYS) {
317 4 50       7 $form{$k} = $args->{$k} if defined $args->{$k};
318             }
319              
320 1 50 33     4 if (defined $form{statement_descriptor} and 22 < length $form{statement_descriptor}) {
321 0         0 return $c->$cb('statement_descriptor is too long', {});
322             }
323              
324 1         2 push @{$url->path->parts}, 'charges', $args->{id}, 'capture';
  1         5  
325 1         56 warn "[StripePayment] Capture $url\n" if DEBUG;
326              
327             Mojo::IOLoop->delay(
328 1     1   163 sub { $self->_ua->post($url, form => \%form, shift->begin); },
329 1     1   3728 sub { $c->$cb($self->_tx_to_res($_[1])); },
330 1         8 );
331              
332 1         127 return $c;
333             }
334              
335             sub _create_charge {
336 8     8   15 my ($self, $c, $args, $cb) = @_;
337 8         139 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
338 8         599 my %form;
339              
340 8         17 for my $k (@CHARGE_KEYS) {
341 72 100       125 $form{$k} = $args->{$k} if defined $args->{$k};
342             }
343              
344 8   66     50 $form{amount} ||= $c->param('amount');
345 8   33     1805 $form{currency} ||= $self->currency_code;
346 8 50 50     79 $form{description} = $c->param('description') || '' unless defined $form{description};
347 8 50       485 $form{metadata} = $self->_expand(metadata => $args) if ref $form{metadata};
348 8 100 33     17 $form{receipt_email} ||= $c->param('stripeEmail') if $c->param('stripeEmail');
349 8 50       558 $form{shipping} = $self->_expand(shipping => $args) if ref $form{shipping};
350 8   66     50 $form{source} ||= $args->{token} || $c->param('stripeToken');
      66        
351 8 100 66     555 $form{capture} = ($form{capture} // $self->auto_capture) ? 'true' : 'false';
352              
353 8 50 33     69 if (defined $form{statement_descriptor} and 22 < length $form{statement_descriptor}) {
354 0         0 return $c->$cb('statement_descriptor is too long', {});
355             }
356              
357 8 100       25 $form{amount} or return $c->$cb('amount is required', {});
358 7 50       15 $form{currency} or return $c->$cb('currency is required', {});
359 7 100       21 $form{source} or return $c->$cb('source/token is required', {});
360              
361 6         8 push @{$url->path->parts}, 'charges';
  6         15  
362 6         375 warn "[StripePayment] Charge $url $form{amount} $form{currency}\n" if DEBUG;
363              
364             Mojo::IOLoop->delay(
365 6     6   663 sub { $self->_ua->post($url, form => \%form, shift->begin); },
366 6     6   33270 sub { $c->$cb($self->_tx_to_res($_[1])); },
367 6         47 );
368              
369 6         666 return $c;
370             }
371              
372             sub _expand {
373 0     0   0 my ($self, $ns, $args) = @_;
374 0 0       0 my $data = delete $args->{$ns} or return;
375              
376 0         0 while (my ($k, $v) = each %$data) {
377 0         0 $args->{"$ns\[$k\]"} = $v;
378             }
379             }
380              
381             sub _mock_interface {
382 3     3   4 my ($self, $app) = @_;
383 3         51 my $secret = $self->secret;
384              
385 3         68 $self->_ua->server->app($app);
386 3         252 $self->base_url('/mocked/stripe-payment');
387 3         9 push @{$app->renderer->classes}, __PACKAGE__;
  3         44  
388              
389             $app->routes->post(
390             '/mocked/stripe-payment/charges' => sub {
391 5     5   34312 my $c = shift;
392 5 50       16 if ($c->req->url->to_abs->userinfo eq "$secret:") {
393 5   33     1974 local $MOCKED_RESPONSE->{json}{amount} //= $c->param('amount');
394 5 50 50     2269 local $MOCKED_RESPONSE->{json}{captured} //= $c->param('capture') // 1 ? \1 : \0;
      33        
395 5   33     356 local $MOCKED_RESPONSE->{json}{currency} //= lc $c->param('currency');
396 5   50     384 local $MOCKED_RESPONSE->{json}{description} //= $c->param('description') || '';
      33        
397 5 50 33     351 local $MOCKED_RESPONSE->{json}{livemode} //= $secret =~ /test/ ? \0 : \1;
398 5   66     24 local $MOCKED_RESPONSE->{json}{receipt_email} //= $c->param('receipt_email');
399 5         312 $c->render(%$MOCKED_RESPONSE);
400             }
401             else {
402 0         0 $c->render(json => {error => {message => 'Bad secret!', type => 'invalid_request_error'}}, status => 400);
403             }
404             }
405 3         118 );
406             $app->routes->post(
407             '/mocked/stripe-payment/charges/:id/capture' => sub {
408 1     1   5401 my $c = shift;
409 1 50       3 if ($c->req->url->to_abs->userinfo eq "$secret:") {
410 1   33     382 local $MOCKED_RESPONSE->{json}{amount} //= $c->param('amount');
411 1         277 local $MOCKED_RESPONSE->{json}{captured} = \1;
412 1 50 33     9 local $MOCKED_RESPONSE->{json}{livemode} //= $secret =~ /test/ ? \0 : \1;
413 1   33     5 local $MOCKED_RESPONSE->{json}{receipt_email} //= $c->param('receipt_email');
414 1         58 $c->render(%$MOCKED_RESPONSE);
415             }
416             else {
417 0         0 $c->render(json => {error => {message => 'Bad secret!', type => 'invalid_request_error'}}, status => 400);
418             }
419             }
420 3         1612 );
421             $app->routes->get(
422             '/mocked/stripe-payment/charges/:id' => sub {
423 0     0   0 my $c = shift;
424 0 0       0 if ($c->req->url->to_abs->userinfo ne "$secret:") {
    0          
425 0         0 $c->render(json => {error => {message => 'Bad secret!', type => 'invalid_request_error'}}, status => 400);
426             }
427             elsif (my $id = $c->param('id')) {
428 0         0 local $MOCKED_RESPONSE->{id} = $id;
429 0         0 $c->render(%$MOCKED_RESPONSE);
430             }
431             else {
432 0 0       0 $c->render(json => {error => {message => 'Bad secret!', type => 'invalid_request_error'}}, status => 400)
433             unless $c->param('id');
434             }
435             }
436 3         1382 );
437             }
438              
439             sub _retrieve_charge {
440 0     0   0 my ($self, $c, $args, $cb) = @_;
441 0         0 my $url = Mojo::URL->new($self->base_url)->userinfo($self->secret . ':');
442              
443 0   0     0 push @{$url->path->parts}, 'charges', $args->{id} || 'invalid';
  0         0  
444 0         0 warn "[StripePayment] Retrieve charge $url\n" if DEBUG;
445              
446 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  
447              
448 0         0 return $c;
449             }
450              
451             sub _tx_to_res {
452 7     7   12 my ($self, $tx) = @_;
453 7   100     19 my $error = $tx->error || {};
454 7   50     351 my $json = $tx->res->json || {};
455 7         3116 my $err = '';
456              
457 7 100 66     37 if ($error->{code} or $json->{error}) {
458 1   33     6 my $message = $json->{error}{message} || $json->{error}{type} || $error->{message};
459 1   33     5 my $type = $json->{error}{param} || $json->{error}{code} || $error->{code};
460              
461 1   50     6 $err = sprintf '%s: %s', $type || 'Unknown', $message || 'Could not find any error message.';
      50        
462             }
463              
464 7         32 return $err, $json;
465             }
466              
467             =head1 SEE ALSO
468              
469             =over 4
470              
471             =item * Overview
472              
473             L
474              
475             =item * API
476              
477             L
478              
479             =back
480              
481             =head1 COPYRIGHT AND LICENSE
482              
483             Copyright (C) 2014, Jan Henning Thorsen
484              
485             This program is free software, you can redistribute it and/or modify it under
486             the terms of the Artistic License version 2.0.
487              
488             =head1 AUTHOR
489              
490             Jan Henning Thorsen - C
491              
492             =cut
493              
494             1;