File Coverage

blib/lib/Mojolicious/Plugin/StripePayment.pm
Criterion Covered Total %
statement 102 126 80.9
branch 27 48 56.2
condition 36 81 44.4
subroutine 18 23 78.2
pod 1 1 100.0
total 184 279 65.9


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