File Coverage

blib/lib/Mojolicious/Plugin/NetsPayment.pm
Criterion Covered Total %
statement 162 180 90.0
branch 22 42 52.3
condition 9 23 39.1
subroutine 22 23 95.6
pod 4 4 100.0
total 219 272 80.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::NetsPayment;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::NetsPayment - Make payments using Nets
6              
7             =head1 VERSION
8              
9             0.04
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             use Mojolicious::Lite;
22              
23             plugin NetsPayment => {
24             merchant_id => '...',
25             token => '...',
26             };
27              
28             # register a payment and send the visitor to Nets payment terminal
29             post '/checkout' => sub {
30             my $self = shift->render_later;
31             my %payment = (
32             amount => $self->param('amount'),
33             order_number => scalar $self->param('order_number'),
34             );
35              
36             Mojo::IOLoop->delay(
37             sub {
38             my ($delay) = @_;
39             $self->nets(register => \%payment, $delay->begin);
40             },
41             sub {
42             my ($delay, $res) = @_;
43             return $self->render(text => "Ooops!", status => $res->code) unless $res->code == 302;
44             # store $res->param('transaction_id');
45             $self->redirect_to($res->headers->location);
46             },
47             );
48             };
49              
50             # after redirected back from Nets payment terminal
51             get '/checkout' => sub {
52             my $self = shift->render_later;
53              
54             Mojo::IOLoop->delay(
55             sub {
56             my ($delay) = @_;
57             $self->nets(process => {}, $delay->begin);
58             },
59             sub {
60             my ($delay, $res) = @_;
61             return $self->render(text => $res->param("message"), status => $res->code) unless $res->code == 200;
62             # store $res->param('transaction_id') and $res->param('authorization_id');
63             $self->render(text => "yay!");
64             },
65             );
66             };
67              
68             =head2 Self contained
69              
70             use Mojolicious::Lite;
71              
72             plugin NetsPayment => {
73             merchant_id => '...',
74             token => \ "dummy",
75             };
76              
77             Setting token to a reference will enable this plugin to work without a working
78             nets backend. This is done by replicating the behavior of Nets. This is
79             especially useful when writing unit tests.
80              
81             The following routes will be added to your application to mimic nets:
82              
83             =over 4
84              
85             =item * /nets/Netaxept/Process.aspx
86              
87             L.
88              
89             =item * /nets/Netaxept/Query.aspx
90              
91             L.
92              
93             =item * /nets/Netaxept/Register.aspx
94              
95             L.
96              
97             =item * /nets/Terminal/default.aspx
98              
99             L.
100              
101             =back
102              
103             =cut
104              
105 3     3   2670 use Mojo::Base 'Mojolicious::Plugin';
  3         6  
  3         19  
106 3     3   577 use Mojo::UserAgent;
  3         5  
  3         28  
107 3   50 3   100 use constant DEBUG => $ENV{MOJO_NETS_DEBUG} || 0;
  3         13  
  3         6752  
108              
109             our $VERSION = '0.04';
110              
111             =head1 ATTRIBUTES
112              
113             =head2 base_url
114              
115             $str = $self->base_url;
116              
117             This is the location to Nets payment solution. Will be set to
118             L if the mojolicious application mode is
119             "production" or L if not.
120              
121             =head2 currency_code
122              
123             $str = $self->currency_code;
124              
125             The currency code, following ISO 4217. Default is "NOK".
126              
127             =head2 merchant_id
128              
129             $str = $self->merchant_id;
130              
131             The value for the merchant ID, can be found in the Nets admin gui.
132              
133             =head2 token
134              
135             $str = $self->token;
136              
137             The value for the merchant ID, can be found in the Nets admin gui.
138              
139             =cut
140              
141             has currency_code => 'NOK';
142             has merchant_id => 'dummy_merchant';
143             has token => 'dummy_token';
144             has base_url => 'https://test.epayment.nets.eu';
145             has _ua => sub { Mojo::UserAgent->new; };
146              
147             =head1 HELPERS
148              
149             =head2 nets
150              
151             $self = $c->nets;
152             $c = $c->nets($method => @args);
153              
154             Returns this instance unless any args have been given or calls one of the
155             avaiable L instead. C<$method> need to be without "_payment" at
156             the end. Example:
157              
158             $c->nets(register => { ... }, sub {
159             my ($c, $res) = @_;
160             # ...
161             });
162              
163             =head1 METHODS
164              
165             =head2 process_payment
166              
167             $self = $self->process_payment(
168             $c,
169             {
170             transaction_id => $str, # default to $c->param("transactionId")
171             operation => $str, # default to AUTH
172             # ...
173             },
174             sub {
175             my ($self, $res) = @_;
176             },
177             );
178              
179             From L:
180              
181             All financial transactions are encapsulated by the "Process"-call.
182             Available financial transactions are AUTH, SALE, CAPTURE, CREDIT
183             and ANNUL.
184              
185             Useful C<$res> values:
186              
187             =over 4
188              
189             =item * $res->code
190              
191             Holds the response code from Nets. Will be set to 500 by this module, if the
192             message could not be parsed.
193              
194             =item * $res->param("code")
195              
196             "OK" on success, something else on failure.
197              
198             =item * $res->param("authorization_id")
199              
200             Only set on success. An ID identifying this authorization.
201              
202             =item * $res->param("operation")
203              
204             Only set on success. This is the same value as given to this method.
205              
206             =item * $res->param("transaction_id")
207              
208             Only set on success. This is the same value as given to this method.
209              
210             =item * $res->param("message")
211              
212             Only set if "code" is not "OK". Holds a description of the error.
213             See also L.
214              
215             =item * $res->param("source")
216              
217             Only set if "code" is not "OK". See also L.
218              
219             =back
220              
221             =cut
222              
223             sub process_payment {
224 1     1 1 3 my ($self, $c, $args, $cb) = @_;
225 1         4 my $process_url = $self->_url('/Netaxept/Process.aspx');
226              
227 1 50       6 $args = { transaction_id => $args } unless ref $args;
228 1   50     19 $args->{operation} ||= 'AUTH';
229 1 50 33     11 $args->{transaction_id} ||= $c->param('transactionId') or return $self->$cb($self->_error('transaction_id missing in input'));
230              
231 1   50     804 $process_url->query({
232             merchantId => $self->merchant_id,
233             token => $self->token,
234             operation => $args->{operation} || 'AUTH',
235             transactionId => $args->{transaction_id},
236             $self->_camelize($args),
237             });
238              
239             Mojo::IOLoop->delay(
240             sub {
241 1     1   192 my ($delay) = @_;
242 1         39 $self->_ua->get($process_url, $delay->begin);
243             },
244             sub {
245 1     1   18562 my ($delay, $tx) = @_;
246 1         20 my $res = Mojolicious::Plugin::NetsPayment::Res->new($tx->res);
247              
248 1 50       47 $res->code(0) unless $res->code;
249              
250 1         8 local $@;
251             eval {
252 1         8 my $body = $res->dom->at('ProcessResponse');
253 1         1275 my $code = $body->at('ResponseCode')->text;
254              
255 1 50       392 if($code eq 'OK') {
256 1         27 $res->code(200);
257 1         8 $res->param(authorization_id => $body->at('AuthorizationId')->text);
258 1         248 $res->param(operation => $body->at('Operation')->text);
259 1         43 $res->param(transaction_id => $body->at('TransactionId')->text);
260             }
261             else {
262 0 0       0 $res->code(500) if $res->code == 200;
263 0         0 $res->param(message => $body->at('ResponseText')->text);
264 0         0 $res->param(source => $body->at('ResponseSource')->text);
265             }
266              
267 1         39 $res->param(code => $code);
268 1 50       3 } or do {
269 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
270 0         0 $self->_extract_error($res, $@);
271             };
272              
273 1         39 $self->$cb($res);
274             },
275 1         54 );
276              
277 1         139 $self;
278             }
279              
280             =head2 query_payment
281              
282             $self = $self->query_payment(
283             $c,
284             {
285             transaction_id => $str,
286             },
287             sub {
288             my ($self, $res) = @_;
289             },
290             );
291              
292             From L:
293              
294             To check the status of a transaction at any time, you can use the Query-call.
295              
296             Useful C<$res> values:
297              
298             =over 4
299              
300             =item * $res->param("amount")
301              
302             Holds the "amount" given to L.
303              
304             =item * $res->param("amount_captured")
305              
306             The amount which has been captured on this transaction.
307             This value is the "AmountCaptured" value devided by 100.
308              
309             =item * $res->param("amount_credited")
310              
311             The amount which has been credited on this transaction.
312             This value is the "AmountCredited" value devided by 100.
313              
314             =item * $res->param("annulled")
315              
316             Whether or not this transaction has been annulled.
317             Boolean true or false.
318              
319             =item * $res->param("authorized")
320              
321             Whether or not this transaction has been authorized.
322             Boolean true or false.
323              
324             =item * $res->param("currency_code")
325              
326             The currency code, following ISO 4217. Typical examples include "NOK" and
327             "USD". Often the same as L.
328              
329             =item * $res->param("order_description")
330              
331             Holds the "order_description" given to L.
332              
333             =item * $res->param("order_number")
334              
335             Holds the "order_number" given to L.
336              
337             =item * $res->param("authorization_id")
338              
339             Same as "authorization_id" from L.
340              
341             =item * $res->param("customer_address1")
342              
343             =item * $res->param("customer_address2")
344              
345             =item * $res->param("customer_country")
346              
347             =item * $res->param("customer_email")
348              
349             =item * $res->param("customer_first_name")
350              
351             =item * $res->param("customer_ip")
352              
353             =item * $res->param("customer_last_name")
354              
355             =item * $res->param("customer_number")
356              
357             =item * $res->param("customer_phone_number")
358              
359             =item * $res->param("customer_postcode")
360              
361             =item * $res->param("expiry_date")
362              
363             Which date the card expires on the format YYMM.
364              
365             =item * $res->param("issuer_country")
366              
367             Which country the card was issued, following ISO 3166.
368              
369             =item * $res->param("masked_pan")
370              
371             The personal account number used for this transaction, masked with asterisks.
372              
373             =item * $res->param("payment_method")
374              
375             Which payment method was used for this transaction. Examples: "Visa",
376             "MasterCard", "AmericanExpress", ...
377              
378             =back
379              
380             See also L.
381              
382             =cut
383              
384             sub query_payment {
385 1     1 1 2 my ($self, $c, $args, $cb) = @_;
386 1         4 my $query_url = $self->_url('/Netaxept/Query.aspx');
387              
388 1 50       4 $args = { transaction_id => $args } unless ref $args;
389 1 50       3 $args->{transaction_id} or return $self->$cb($self->_error('transaction_id missing in input'));
390              
391 1         20 $query_url->query({
392             merchantId => $self->merchant_id,
393             token => $self->token,
394             transactionId => $args->{transaction_id},
395             });
396              
397             Mojo::IOLoop->delay(
398             sub {
399 1     1   128 my ($delay) = @_;
400 1         27 $self->_ua->get($query_url, $delay->begin);
401             },
402             sub {
403 1     1   34047 my ($delay, $tx) = @_;
404 1         46 my $res = Mojolicious::Plugin::NetsPayment::Res->new($tx->res);
405              
406 1 50       43 $res->code(0) unless $res->code;
407              
408 1         12 local $@;
409             eval {
410 1         7 my $body = $res->dom->at('PaymentInfo');
411              
412 1         3800 $res->param(amount => $body->at('OrderInformation > Amount')->text / 100);
413 1         285 $res->param(amount_captured => $body->at('Summary > AmountCaptured')->text / 100);
414 1         39 $res->param(amount_credited => $body->at('Summary > AmountCredited')->text / 100);
415 1 50       42 $res->param(annuled => $body->at('Summary > Annuled')->text eq 'true' ? 1 : 0);
416 1 50       37 $res->param(authorized => $body->at('Summary > Authorized')->text eq 'true' ? 1 : 0);
417 1         52 $res->param(currency_code => $body->at('OrderInformation > Currency')->text);
418 1         38 $res->param(order_description => $body->at('OrderInformation > OrderDescription')->text);
419 1         40 $res->param(order_number => $body->at('OrderInformation > OrderNumber')->text);
420              
421 1         43 $res->param(authorization_id => eval { $body->at('Summary > AuthorizationId')->text });
  1         4  
422 1         36 $res->param(customer_address1 => eval { $body->at('CustomerInformation > Address1')->text });
  1         4  
423 1         39 $res->param(customer_address2 => eval { $body->at('CustomerInformation > Address2')->text });
  1         3  
424 1         33 $res->param(customer_country => eval { $body->at('iCustomerInformation > Country')->text });
  1         4  
425 1         33 $res->param(customer_email => eval { $body->at('CustomerInformation > Email')->text });
  1         5  
426 1         40 $res->param(customer_first_name => eval { $body->at('CustomerInformation > FirstName')->text });
  1         2  
427 1         40 $res->param(customer_ip => eval { $body->at('CustomerInformation > IP')->text });
  1         3  
428 1         38 $res->param(customer_last_name => eval { $body->at('CustomerInformation > LastName')->text });
  1         4  
429 1         40 $res->param(customer_number => eval { $body->at('CustomerInformation > CustomerNumber')->text });
  1         4  
430 1         42 $res->param(customer_phone_number => eval { $body->at('CustomerInformation > PhoneNumber')->text });
  1         14  
431 1         42 $res->param(customer_postcode => eval { $body->at('CustomerInformation > Postcode')->text });
  1         3  
432 1         35 $res->param(expiry_date => eval { $body->at('CardInformation > ExpiryDate')->text });
  1         3  
433 1         45 $res->param(issuer_country => eval { $body->at('CardInformation > IssuerCountry')->text });
  1         3  
434 1         46 $res->param(masked_pan => eval { $body->at('CardInformation > MaskedPAN')->text });
  1         4  
435 1         43 $res->param(payment_method => eval { $body->at('CardInformation > PaymentMethod')->text });
  1         4  
436 1         50 1;
437 1 50       1 } or do {
438 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
439 0         0 $self->_extract_error($res, $@);
440             };
441              
442 1         5 $self->$cb($res);
443             },
444 1         97 );
445              
446 1         114 $self;
447             }
448              
449             =head2 register_payment
450              
451             $self = $self->register_payment(
452             $c,
453             {
454             amount => $num, # 99.90, not 9990
455             order_number => $str,
456             redirect_url => $str, # default to current request URL
457             # ...
458             },
459             sub {
460             my ($self, $res) = @_;
461             },
462             );
463              
464             From L:
465              
466             The purpose of the register call is to send all the data needed to
467             complete a transaction to Netaxept servers. The input data is
468             organized into a RegisterRequest, and the output data is formatted
469             as a RegisterResponse.
470              
471             NOTE: "amount" in this API need to be a decimal number, which will be duplicated with 100 to match
472             the Nets documentation.
473              
474             There are many more options that can be passed on to L.
475             Look at L
476             for a complete list. CamelCase arguments can be given in normal form. Examples:
477              
478             # NetsDocumentation | perl_argument_name
479             # --------------------|----------------------
480             # currencyCode | currency_code
481             # customerPhoneNumber | customer_phone_number
482              
483             Useful C<$res> values:
484              
485             =over 4
486              
487             =item * $res->code
488              
489             Set to 302 on success.
490              
491             =item * $res->param("transaction_id")
492              
493             Only set on success. An ID identifying this transaction. Generated by Nets.
494              
495             =item * $res->headers->location
496              
497             Only set on success. This holds a URL to the Nets terminal page, which
498             you will redirect the user to after storing the transaction ID and other
499             customer related details.
500              
501             =back
502              
503             =cut
504              
505             sub register_payment {
506 3     3 1 6 my ($self, $c, $args, $cb) = @_;
507 3         11 my $register_url = $self->_url('/Netaxept/Register.aspx');
508              
509 3 100       18 $args->{amount} or return $self->$cb($self->_error('amount missing in input'));
510 2 100       46 $args->{order_number} or return $self->$cb($self->_error('order_number missing in input'));
511 1         6 local $args->{amount} = $args->{amount} * 100;
512 1   33     13 local $args->{redirect_url} ||= $c->req->url->to_abs;
513              
514 1   50     436 $register_url->query({
515             currencyCode => $self->currency_code,
516             merchantId => $self->merchant_id,
517             token => $self->token,
518             environmentLanguage => 'perl',
519             OS => $^O || 'Mojolicious',
520             $self->_camelize($args),
521             });
522              
523             Mojo::IOLoop->delay(
524             sub {
525 1     1   156 my ($delay) = @_;
526 1         25 $self->_ua->get($register_url, $delay->begin);
527             },
528             sub {
529 1     1   10228 my ($delay, $tx) = @_;
530 1         29 my $res = Mojolicious::Plugin::NetsPayment::Res->new($tx->res);
531              
532 1 50       51 $res->code(0) unless $res->code;
533              
534 1         8 local $@;
535             eval {
536 1         9 my $id = $res->dom->at('RegisterResponse > TransactionId')->text;
537 1         1236 my $terminal_url = $self->_url('/Terminal/default.aspx')->query({merchantId => $self->merchant_id, transactionId => $id});
538              
539 1         108 $res->headers->location($terminal_url);
540 1         59 $res->param(transaction_id => $id);
541 1         189 $res->code(302);
542 1         8 1;
543 1 50       1 } or do {
544 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
545 0         0 $self->_extract_error($res, $@);
546             };
547              
548 1         9 $self->$cb($res);
549             },
550 1         90 );
551              
552 1         158 $self;
553             }
554              
555             =head2 register
556              
557             $app->plugin(NetsPayment => \%config);
558              
559             Called when registering this plugin in the main L application.
560              
561             =cut
562              
563             sub register {
564 3     3 1 121 my ($self, $app, $config) = @_;
565              
566             # self contained
567 3 100       34 if (ref $config->{token}) {
    50          
568 2         5 $self->_add_routes($app);
569 2         102 $self->_ua->server->app($app);
570 2         254 $config->{token} = ${ $config->{token} };
  2         8  
571             }
572             elsif ($app->mode eq 'production') {
573 1   50     17 $config->{base_url} ||= 'https://epayment.nets.eu';
574             }
575              
576             # copy config to this object
577 3         11 for (grep { $self->$_ } keys %$config) {
  3         74  
578 3         33 $self->{$_} = $config->{$_};
579             }
580              
581             $app->helper(
582             nets => sub {
583 7     7   165574 my $c = shift;
584 7 100       70 return $self unless @_;
585 5         15 my $method = shift .'_payment';
586 5         36 $self->$method($c, @_);
587 5         45 return $c;
588             }
589 3         34 );
590             }
591              
592             sub _add_routes {
593 2     2   3 my ($self, $app) = @_;
594 2         45 my $r = $app->routes;
595 2   50     40 my $payments = $self->{payments} ||= {}; # just here for debug purposes, may change without warning
596              
597 2         45 $self->base_url('/nets');
598              
599 2         38 $r->get('/nets/Netaxept/Process.aspx', { template => 'nets/Netaxept/Process', format => 'xml' });
600 2         1308 $r->get('/nets/Netaxept/Query.aspx', { template => 'nets/Netaxept/Query', format => 'xml' });
601             $r->get('/nets/Netaxept/Register.aspx')->to(cb => sub {
602 1     1   18214 my $self = shift;
603 1         3 my $txn_id = 'b127f98b77f741fca6bb49981ee6e846';
604 1         5 $payments->{$txn_id} = $self->req->query_params->to_hash;
605 1         649 $self->render('nets/Netaxept/Register', txn_id => $txn_id, format => 'xml');
606 2         879 });
607             $r->get('/nets/Terminal/default.aspx')->to(cb => sub {
608 1     1   26358 my $self = shift;
609 1   50     6 my $txn_id = $self->param('transactionId') || 'missing';
610 1         539 $self->render('nets/Terminal/default', format => 'html', payment => $payments->{$txn_id});
611 2         1021 });
612              
613 2         988 push @{ $app->renderer->classes }, __PACKAGE__;
  2         40  
614             }
615              
616             sub _camelize {
617 2     2   114 my ($self, $args) = @_;
618 2         7 map { my $k = $_; s/_([a-z])/\U$1/g; ($_ => $args->{$k}); } keys %$args;
  5         7  
  5         23  
  5         26  
619             }
620              
621             sub _error {
622 2     2   7 my ($self, $err) = @_;
623 2         75 my $res = Mojolicious::Plugin::NetsPayment::Res->new;
624 2         79 $res->code(400);
625 2         27 $res->param(message => $err);
626 2         572 $res->param(source => __PACKAGE__);
627 2         102 $res;
628             }
629              
630             sub _extract_error {
631 0     0   0 my ($self, $res, $e) = @_;
632 0         0 my $err;
633              
634 0         0 local $@;
635 0         0 $err = eval { $_[0]->res->dom->Exception->Error->Message->text };
  0         0  
636              
637 0         0 $res->code(500);
638 0 0       0 $res->param(code => '') unless $res->param('code');
639 0   0     0 $res->param(message => $err // $e);
640 0 0       0 $res->param(source => $err ? $self->base_url : __PACKAGE__);
641             }
642              
643             sub _url {
644 6     6   176 my $url = Mojo::URL->new($_[0]->base_url .$_[1]);
645 6         566 warn "[MOJO_NETS] URL $url\n" if DEBUG;
646 6         38 $url;
647             }
648              
649             package
650             Mojolicious::Plugin::NetsPayment::Res;
651 3     3   22 use Mojo::Base 'Mojo::Message::Response';
  3         5  
  3         15  
652 44     44   17401 sub param { shift->body_params->param(@_) }
653              
654             =head1 ERROR HANDLING
655              
656             There are some generic error handling in this module: The C<$res> object
657             passed on to the callbacks will have "source" and "message" set. These can
658             be retrived using the code below:
659              
660             $int = $res->code; # will be 500 on exception
661             $str = $res->param("source");
662             $str = $res->param("message");
663              
664             The "source" might have to special values:
665              
666             =over 4
667              
668             =item * Same as L.
669              
670             If the "source" is set to the value of L then the "message"
671             will contain an exception from Nets.
672              
673             =item * "Mojolicious::Plugin::NetsPayment"
674              
675             If the "source" is set to this package name, then the "message" will be an
676             exception from parse error.
677              
678             =back
679              
680             =head1 SEE ALSO
681              
682             =over 4
683              
684             =item * Overview
685              
686             L
687              
688             =item * API
689              
690             L
691              
692             =back
693              
694             =head1 COPYRIGHT AND LICENSE
695              
696             Copyright (C) 2014, Jan Henning Thorsen
697              
698             This program is free software, you can redistribute it and/or modify it under
699             the terms of the Artistic License version 2.0.
700              
701             =head1 AUTHOR
702              
703             Jan Henning Thorsen - C
704              
705             =cut
706              
707             1;
708              
709             package Mojolicious::Plugin::NetsPayment;
710             __DATA__