File Coverage

blib/lib/Business/iDEAL/Adyen.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Business::iDEAL::Adyen;
2              
3 1     1   106406 use strict;
  1         3  
  1         45  
4 1     1   7 use Carp;
  1         2  
  1         110  
5              
6 1     1   516 use DateTime;
  0            
  0            
7             use Digest::HMAC_SHA1;
8             use LWP::UserAgent;
9             use URI;
10             use XML::Simple;
11              
12             our $VERSION = '0.02';
13              
14             =pod
15              
16             =head1 NAME
17              
18             Business::iDEAL::Adyen - Backend for iDEAL payments through adyen.com
19              
20             =head1 SYNOPSIS
21              
22             use Business::iDEAL::Adyen;
23            
24             # first setup the object
25             my $ideal = Business::iDEAL::Adyen->new({
26             shared_secret => 'your very secure secret',
27             skinCode => 's0m3C0d3',
28             merchantAccount => 'your merchant account',
29             test => 1,
30             });
31            
32             # then fetch a list of bank ids and bank codes
33             my $banks = $ideal->banklist();
34            
35             # after the user has chosen the bank he/she wants to use
36             # it's time to fetch the redirect URL
37             my $redir = $ideal->fetch({
38             # mandatory fields
39             bank_id => 1000,
40             paymentAmount => 1250,
41            
42             # optional fields
43             merchantReference => 'your order ID',
44             currencyCode => 'EUR',
45             shopperLocale => 'nl',
46             shipBeforeDate => '2010-01-01',
47             sessionValidity => '2009-01-01T01:01:01Z',
48             });
49            
50             # redirect your user to his/her bank, like
51             print redirect( $redir );
52              
53             After the user has finalized the payment, he/she'll be returned to
54             your website (as defined in the Adyen skin)
55              
56             use Business::iDEAL::Adyen;
57             use CGI qw/:standard/;
58            
59             # first setup the objects
60             my $cgi = new CGI;
61             my $ideal = Business::iDEAL::Adyen->new({
62             shared_secret => 'your very secure secret',
63             skinCode =>
64             test => 1,
65             });
66            
67             # check user input
68             if( $ideal->check( \%{$cgi->Vars} ) ) {
69            
70             # payment succeeded, so you probably want to update your
71             # database with $cgi->param('merchantReference')
72            
73             } else {
74              
75             # payment was not successful
76             # $ideal->error() contains what went wrong (most likely the
77             # request has been tampered with and the signature is incorrect)
78            
79             }
80              
81              
82             =head1 DESCRIPTION
83              
84             Business::iDEAL::Adyen provides a backend to process iDEAL payments
85             through adyen.com (the non-HPP (Hosted Payment Pages) way).
86              
87             A word of warning to start with (copied verbatim out of Adyen's iDEAL PDF):
88              
89             iDeal API Payments are not enabled by default. If you would like to
90             process iDeal using this method, you can request this through the support
91             channel at L.
92              
93             =head2 METHODS
94              
95             =head3 new
96              
97             C creates a new C object.
98              
99             =head4 options
100              
101             =over 5
102              
103             =item B I<[mandatory]>
104              
105             This option should be the same as the secret entered
106             in the Adyen skin.
107              
108             =item B I<[mandatory]>
109              
110             The code of the skin we're using.
111              
112             =item B I<[mandatory]>
113              
114             The merchant account name
115              
116             =item B
117              
118             A boolean value that switches on the use of the test environment.
119              
120             =back
121              
122             =cut
123              
124             sub new {
125             my ($class, $args) = @_;
126             $args ||= {};
127             $class->_croak("Options must be a hash reference")
128             if ref($args) ne 'HASH';
129              
130             my $self = {};
131             bless $self, $class;
132              
133             # initialize the object
134             $self->_init($args);
135              
136             return $self;
137             }
138              
139             sub _croak {
140             my $self = shift;
141             Carp::croak(@_);
142             }
143              
144             sub _carp {
145             my $self = shift;
146             Carp::carp(@_);
147             }
148              
149             sub _init {
150             my ($self, $args) = @_;
151              
152             # test for mandatory fields
153             for(qw/shared_secret skinCode merchantAccount/) {
154             $self->_croak("$_ not set") unless $args->{$_};
155             }
156              
157             # set some defaults and let user override if needed
158             my %options = (
159             ua => LWP::UserAgent->new(
160             agent => __PACKAGE__." v. ".$VERSION,
161             requests_redirectable => '',
162             ),
163             xso => XML::Simple->new(),
164             hmac => Digest::HMAC_SHA1->new(delete $args->{shared_secret}),
165             prod_base_url => 'https://live.adyen.com',
166             test_base_url => 'https://test.adyen.com',
167             banklist_path => '/hpp/idealbanklist.shtml',
168             redirect_path => '/hpp/redirectIdeal.shtml',
169             %{ $args }
170             );
171              
172             # map all keys to $self->{_$key} for easy access
173             $self->{"_$_"} = $options{$_} for (keys %options);
174             }
175              
176             sub _url {
177             my ($self, $type) = @_;
178              
179             # determine what path we need and whether that exists or not
180             my $path = $self->{"_".$type."_path"} or
181             $self->_croak("Unknown type '$type'");
182              
183             # return the test or production url based on $type input
184             return ($self->{_test} ? $self->{_test_base_url}
185             : $self->{_prod_base_url}).
186             $path;
187             }
188              
189             sub _parse_xml {
190             my ($self, $input) = @_;
191             return unless($input);
192              
193             return $self->{_xso}->XMLin($input);
194             }
195              
196             sub _sign_req {
197             my ($self, $args) = @_;
198              
199             my $plaintext = '';
200             if($args->{paymentAmount}) {
201             # Initial signature (the one we _send_)
202             for(qw/paymentAmount currencyCode shipBeforeDate merchantReference
203             skinCode merchantAccount sessionValidity shopperEmail
204             shopperReference allowedMethods blockedMethods/) {
205             $plaintext .= ( defined $self->{"_$_"} )
206             ? $self->{"_$_"} : ( $args->{$_} || "" );
207             }
208             } else {
209             # Second signature (the one we _receive_)
210             for(qw/authResult pspReference merchantReference skinCode/) {
211             $plaintext .= ( defined $self->{"_$_"} )
212             ? $self->{"_$_"} : ( $args->{$_} || "" );
213             }
214             }
215              
216             $self->{_hmac}->add($plaintext);
217             my $b64_digest = $self->{_hmac}->b64digest;
218             $b64_digest .= '=' while (length($b64_digest) % 4);
219              
220             return $b64_digest;
221             }
222              
223             =pod
224              
225             =head3 banklist
226              
227             In order to offer all iDEAL banks, you will have to fetch a list
228             with their names and codes. This list is subject to change, so check
229             this often (Adyen recommends "regularly (e.g. once a day)").
230             I'd suggest to always check this before a payment.
231              
232             This method will return an arrayref with the bank_ids and bank_names,
233             or undef in case an error occured (see L<"error">)
234              
235             =cut
236              
237             sub banklist {
238             my $self = shift;
239              
240             my $res = $self->{_ua}->get($self->_url('banklist'));
241             if ($res->is_success) {
242             return $self->_parse_xml($res->decoded_content)->{'bank'};
243             } else {
244             $self->{_error} = $res->status_line;
245             return undef;
246             }
247             }
248              
249             =pod
250              
251             =head3 fetch
252              
253             After you've retrieved the L<"banklist">, your users may choose the preferred
254             bank. Now you can feed that 'bank_id', together with the other mandatory
255             options to this method.
256              
257             C will return an URL to the bank's iDEAL page that the user should
258             be directed to.
259              
260             Some fields are mandatory, while others have somewhat sane defaults and
261             may be skipped.
262              
263             =head4 options
264              
265             =over 5
266              
267             =item B I<[mandatory]>
268              
269             This is the bank ID chose by the user and provided by the L<"banklist">
270             method.
271              
272             =item B I<[mandatory]>
273              
274             How much would you like to charge your user? Note that this is in
275             cents, so 12,50 EUR should be inserted as 1250. If a dot or comma
276             is found in this value, it will be stripped. Don't count on this being
277             perfect, so sanitize your own input.
278              
279             =item B
280              
281             This will normally be set to your order number, or anything that's useful
282             to identify the order with. If not set, a semi-random number is generated.
283              
284             =item B
285              
286             Defaults to 'EUR', since iDEAL is a Dutch system and we Dutchmen "embraced"
287             the euro.
288              
289             =item B
290              
291             Defaults to 'nl'. Again, iDEAL is a Dutch system.
292              
293             =item B
294              
295             To make matters easy, we set a I of today + 1 month.
296              
297             =item B
298              
299             By default, we set this value to now + 1 hour, UTC. A user should
300             be able to finish his/her transaction within an hour.
301              
302             =back
303              
304             Other options, as described in the Adyen integration manual, like
305             I, could be passed in as well, but are completely
306             optional.
307              
308             =cut
309              
310             sub fetch {
311             my ($self, $parms) = @_;
312             $parms ||= {};
313             $self->_croak("Parameters must be a hash reference")
314             unless ref($parms) eq 'HASH';
315              
316             # check for mandatory fields that we can't generate
317             for(qw/bank_id paymentAmount/) {
318             $self->_croak("$_ not set") unless($parms->{$_});
319             }
320              
321             # make sure we have the paymentAmount in cents
322             $parms->{paymentAmount} =~ s![,\.]!!;
323              
324             # check for mandatory fields that we could generate where needed
325             $parms->{merchantReference} ||= int(rand(1000000)).time();
326             $parms->{currencyCode} ||= 'EUR';
327             $parms->{shopperLocale} ||= 'nl';
328             $parms->{shipBeforeDate} ||= DateTime->now()
329             ->add(months => 1)
330             ->strftime("%F");
331             $parms->{sessionValidity} ||= DateTime->now(time_zone => 'UTC')
332             ->add(hours => 1)
333             ->strftime("%FT%TZ");
334              
335             # set iDEAL settings
336             $parms->{skipSelection} = 'true';
337             $parms->{brandCode} = 'ideal';
338             $parms->{idealIssuerId} = delete $parms->{bank_id};
339              
340             # set globals
341             $parms->{skinCode} = $self->{_skinCode};
342             $parms->{merchantAccount} = $self->{_merchantAccount};
343              
344             # calculate and set the signature
345             $parms->{merchantSig} = $self->_sign_req($parms);
346              
347             # create URL
348             my $uri = URI->new( $self->_url('redirect') );
349             $uri->query_form( $parms );
350              
351             return $uri->as_string;
352             }
353              
354             =pod
355              
356             =head3 check
357              
358             When a user has proceeded the payment throught the bank's website, he/she'll
359             be returned to your website (as specified on the Adyen's skin pages).
360              
361             This method can be called to check whether the payment succeeded or not.
362             I returns true when the payment was authorized and undef in
363             all other cases (see L<"error"> in that case).
364              
365             =cut
366              
367             sub check {
368             my ($self, $args) = @_;
369              
370             if($args->{merchantSig} && $args->{merchantSig} eq $self->_sign_req($args)){
371             # Signature is ok
372             if($args->{authResult} eq 'AUTHORISED') {
373             # Payment is OK
374             return 1;
375             } else {
376             # Payment failed
377             $self->{_error} = "Payment status: ".$args->{authResult};
378             }
379             } else {
380             # Signature failed
381             $self->{_error} = "Merchant Signature was incorrect";
382             }
383             return undef;
384             }
385              
386             =pod
387              
388             =head3 error
389              
390             If errors occur (most likely signature related), this method will return
391             the latest error that occured.
392              
393             =cut
394              
395             sub error {
396             my $self = shift;
397             return $self->{_error};
398             }
399              
400              
401             =pod
402              
403             =head1 SEE ALSO
404              
405             =over 5
406              
407             =item * L
408              
409             =item * L
410              
411             =item * L
412              
413             =back
414              
415             =head1 BUGS
416              
417             Please report any bugs to L.
418              
419             =head1 AUTHOR
420              
421             Menno Blom,
422             Eblom@cpan.orgE,
423             L
424              
425             =head1 COPYRIGHT AND LICENSE
426              
427             Copyright (C) 2009 by Menno Blom
428              
429             This program is free software; you can redistribute
430             it and/or modify it under the same terms as Perl itself.
431              
432             The full text of the license can be found in the
433             LICENSE file included with this module.
434              
435             =cut
436              
437             1;