File Coverage

blib/lib/Mojolicious/Plugin/NetsPayment.pm
Criterion Covered Total %
statement 157 176 89.2
branch 20 40 50.0
condition 8 21 38.1
subroutine 20 21 95.2
pod 4 4 100.0
total 209 262 79.7


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.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             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 => scalar $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 2     2   2648 use Mojo::Base 'Mojolicious::Plugin';
  2         4  
  2         15  
106 2     2   404 use Mojo::UserAgent;
  2         5  
  2         23  
107 2   50 2   103 use constant DEBUG => $ENV{MOJO_NETS_DEBUG} || 0;
  2         4  
  2         6577  
108              
109             our $VERSION = '0.03';
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 2 my ($self, $c, $args, $cb) = @_;
225 1         6 my $process_url = $self->_url('/Netaxept/Process.aspx');
226              
227 1 50       5 $args = { transaction_id => $args } unless ref $args;
228 1   50     10 $args->{operation} ||= 'AUTH';
229 1 50 33     10 $args->{transaction_id} ||= $c->param('transactionId') or return $self->$cb($self->_error('transaction_id missing in input'));
230              
231 1   50     770 $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   228 my ($delay) = @_;
242 1         29 $self->_ua->get($process_url, $delay->begin);
243             },
244             sub {
245 1     1   21281 my ($delay, $tx) = @_;
246 1         24 my $res = $tx->res;
247              
248 1 50       27 $res->code(0) unless $res->code;
249              
250 1         11 local $@;
251             eval {
252 1         8 my $body = $res->dom->ProcessResponse;
253 1         1610 my $code = $body->ResponseCode->text;
254              
255 1 50       1426 if($code eq 'OK') {
256 1         25 $res->code(200);
257 1         14 $res->param(authorization_id => $body->AuthorizationId->text);
258 1         1744 $res->param(operation => $body->Operation->text);
259 1         1865 $res->param(transaction_id => $body->TransactionId->text);
260             }
261             else {
262 0 0       0 $res->code(500) if $res->code == 200;
263 0         0 $res->param(message => $body->ResponseText->text);
264 0         0 $res->param(source => $body->ResponseSource->text);
265             }
266              
267 1         1919 $res->param(code => $code);
268 1 50       3 } or do {
269 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
270 0         0 $self->_extract_error($tx, $@);
271             };
272              
273 1         147 $self->$cb($res);
274             },
275 1         66 );
276              
277 1         194 $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 3 my ($self, $c, $args, $cb) = @_;
386 1         13 my $query_url = $self->_url('/Netaxept/Query.aspx');
387              
388 1 50       4 $args = { transaction_id => $args } unless ref $args;
389 1 50       5 $args->{transaction_id} or return $self->$cb($self->_error('transaction_id missing in input'));
390              
391 1         25 $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   213 my ($delay) = @_;
400 1         29 $self->_ua->get($query_url, $delay->begin);
401             },
402             sub {
403 1     1   55524 my ($delay, $tx) = @_;
404 1         29 my $res = $tx->res;
405              
406 1 50       33 $res->code(0) unless $res->code;
407              
408 1         11 local $@;
409             eval {
410 1         12 my $body = $res->dom->PaymentInfo;
411              
412 1         7212 $res->param(amount => $body->OrderInformation->Amount->text / 100);
413 1         4127 $res->param(amount_captured => $body->Summary->AmountCaptured->text / 100);
414 1         4688 $res->param(amount_credited => $body->Summary->AmountCredited->text / 100);
415 1 50       4926 $res->param(annuled => $body->Summary->Annuled->text eq 'true' ? 1 : 0);
416 1 50       6696 $res->param(authorized => $body->Summary->Authorized->text eq 'true' ? 1 : 0);
417 1         4316 $res->param(currency_code => $body->OrderInformation->Currency->text);
418 1         4042 $res->param(order_description => $body->OrderInformation->OrderDescription->text);
419 1         5365 $res->param(order_number => $body->OrderInformation->OrderNumber->text);
420              
421 1         7501 $res->param(authorization_id => eval { $body->Summary->AuthorizationId->text });
  1         7  
422 1         8859 $res->param(customer_address1 => eval { $body->CustomerInformation->Address1->text });
  1         11  
423 1         7424 $res->param(customer_address2 => eval { $body->CustomerInformation->Address2->text });
  1         10  
424 1         4699 $res->param(customer_country => eval { $body->iCustomerInformation->Country->text });
  1         11  
425 1         3875 $res->param(customer_email => eval { $body->CustomerInformation->Email->text });
  1         8  
426 1         8513 $res->param(customer_first_name => eval { $body->CustomerInformation->FirstName->text });
  1         9  
427 1         4886 $res->param(customer_ip => eval { $body->CustomerInformation->IP->text });
  1         8  
428 1         4768 $res->param(customer_last_name => eval { $body->CustomerInformation->LastName->text });
  1         9  
429 1         5142 $res->param(customer_number => eval { $body->CustomerInformation->CustomerNumber->text });
  1         8  
430 1         4802 $res->param(customer_phone_number => eval { $body->CustomerInformation->PhoneNumber->text });
  1         9  
431 1         4663 $res->param(customer_postcode => eval { $body->CustomerInformation->Postcode->text });
  1         9  
432 1         4087 $res->param(expiry_date => eval { $body->CardInformation->ExpiryDate->text });
  1         19  
433 1         3359 $res->param(issuer_country => eval { $body->CardInformation->IssuerCountry->text });
  1         6  
434 1         12444 $res->param(masked_pan => eval { $body->CardInformation->MaskedPAN->text });
  1         7  
435 1         3385 $res->param(payment_method => eval { $body->CardInformation->PaymentMethod->text });
  1         8  
436 1         3529 1;
437 1 50       38 } or do {
438 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
439 0         0 $self->_extract_error($tx, $@);
440             };
441              
442 1         7 $self->$cb($res);
443             },
444 1         115 );
445              
446 1         174 $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 15 my ($self, $c, $args, $cb) = @_;
507 3         16 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       16 $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     11 local $args->{redirect_url} ||= $c->req->url->to_abs;
513              
514 1   50     593 $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   176 my ($delay) = @_;
526 1         28 $self->_ua->get($register_url, $delay->begin);
527             },
528             sub {
529 1     1   19266 my ($delay, $tx) = @_;
530 1         26 my $res = $tx->res;
531              
532 1 50       31 $res->code(0) unless $res->code;
533              
534 1         12 local $@;
535             eval {
536 1         11 my $id = $res->dom->RegisterResponse->TransactionId->text;
537 1         2074 my $terminal_url = $self->_url('/Terminal/default.aspx')->query({merchantId => $self->merchant_id, transactionId => $id});
538              
539 1         73 $res->headers->location($terminal_url);
540 1         81 $res->param(transaction_id => $id);
541 1         245 $res->code(302);
542 1         11 1;
543 1 50       3 } or do {
544 0         0 warn "[MOJO_NETS] ! $@" if DEBUG;
545 0         0 $self->_extract_error($tx, $@);
546             };
547              
548 1         7 $self->$cb($res);
549             },
550 1         84 );
551              
552 1         206 $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 2     2 1 100 my ($self, $app, $config) = @_;
565              
566             # self contained
567 2 50       12 if (ref $config->{token}) {
568 2         16 $self->_add_routes($app);
569 2         128 $self->_ua->server->app($app);
570 2         264 $config->{token} = ${ $config->{token} };
  2         10  
571             }
572              
573             # copy config to this object
574 2         7 for (grep { $self->$_ } keys %$config) {
  2         76  
575 2         24 $self->{$_} = $config->{$_};
576             }
577              
578             $app->helper(
579             nets => sub {
580 6     6   275307 my $c = shift;
581 6 100       64 return $self unless @_;
582 5         33 my $method = shift .'_payment';
583 5         39 $self->$method($c, @_);
584 5         59 return $c;
585             }
586 2         28 );
587             }
588              
589             sub _add_routes {
590 2     2   6 my ($self, $app) = @_;
591 2         59 my $r = $app->routes;
592 2   50     51 my $payments = $self->{payments} ||= {}; # just here for debug purposes, may change without warning
593              
594 2         48 $self->base_url('/nets');
595              
596 2         45 $r->get('/nets/Netaxept/Process.aspx', { template => 'nets/Netaxept/Process', format => 'xml' });
597 2         1746 $r->get('/nets/Netaxept/Query.aspx', { template => 'nets/Netaxept/Query', format => 'xml' });
598             $r->get('/nets/Netaxept/Register.aspx')->to(cb => sub {
599 1     1   21552 my $self = shift;
600 1         3 my $txn_id = 'b127f98b77f741fca6bb49981ee6e846';
601 1         6 $payments->{$txn_id} = $self->req->query_params->to_hash;
602 1         684 $self->render('nets/Netaxept/Register', txn_id => $txn_id, format => 'xml');
603 2         1282 });
604             $r->get('/nets/Terminal/default.aspx')->to(cb => sub {
605 1     1   40985 my $self = shift;
606 1   50     9 my $txn_id = $self->param('transactionId') || 'missing';
607 1         612 $self->render('nets/Terminal/default', format => 'html', payment => $payments->{$txn_id});
608 2         2850 });
609              
610 2         1394 push @{ $app->renderer->classes }, __PACKAGE__;
  2         61  
611             }
612              
613             sub _camelize {
614 2     2   154 my ($self, $args) = @_;
615 2         9 map { my $k = $_; s/_([a-z])/\U$1/g; ($_ => $args->{$k}); } keys %$args;
  5         6  
  5         67  
  5         44  
616             }
617              
618             sub _error {
619 2     2   7 my ($self, $err) = @_;
620 2         12 my $res = Mojo::Message::Response->new;
621 2         68 $res->code(400);
622 2         29 $res->param(message => $err);
623 2         820 $res->param(source => __PACKAGE__);
624 2         126 $res;
625             }
626              
627             sub _extract_error {
628 0     0   0 my ($self, $tx, $e) = @_;
629 0         0 my $res = $tx->res;
630 0         0 my $err;
631              
632 0         0 local $@;
633 0         0 $err = eval { $_[0]->res->dom->Exception->Error->Message->text };
  0         0  
634              
635 0         0 $res->code(500);
636 0 0       0 $res->param(code => '') unless $res->param('code');
637 0   0     0 $res->param(message => $err // $e);
638 0 0       0 $res->param(source => $err ? $self->base_url : __PACKAGE__);
639             }
640              
641             sub _url {
642 6     6   176 my $url = Mojo::URL->new($_[0]->base_url .$_[1]);
643 6         1483 warn "[MOJO_NETS] URL $url\n" if DEBUG;
644 6         43 $url;
645             }
646              
647             =head1 ERROR HANDLING
648              
649             There are some generic error handling in this module: The C<$res> object
650             passed on to the callbacks will have "source" and "message" set. These can
651             be retrived using the code below:
652              
653             $int = $res->code; # will be 500 on exception
654             $str = $res->param("source");
655             $str = $res->param("message");
656              
657             The "source" might have to special values:
658              
659             =over 4
660              
661             =item * Same as L.
662              
663             If the "source" is set to the value of L then the "message"
664             will contain an exception from Nets.
665              
666             =item * "Mojolicious::Plugin::NetsPayment"
667              
668             If the "source" is set to this package name, then the "message" will be an
669             exception from parse error.
670              
671             =back
672              
673             =head1 SEE ALSO
674              
675             =over 4
676              
677             =item * Overview
678              
679             L
680              
681             =item * API
682              
683             L
684              
685             =back
686              
687             =head1 COPYRIGHT AND LICENSE
688              
689             Copyright (C) 2014, Jan Henning Thorsen
690              
691             This program is free software, you can redistribute it and/or modify it under
692             the terms of the Artistic License version 2.0.
693              
694             =head1 AUTHOR
695              
696             Jan Henning Thorsen - C
697              
698             =cut
699              
700             1;
701              
702             __DATA__