File Coverage

blib/lib/Business/OnlinePayment/PayPal.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             #
2             # $Id: PayPal.pm,v 1.5 2007/02/16 04:48:34 plobbes Exp $
3              
4             package Business::OnlinePayment::PayPal;
5              
6 1     1   1962 use 5.006;
  1         3  
  1         46  
7 1     1   8 use strict;
  1         2  
  1         30  
8 1     1   20 use warnings;
  1         2  
  1         33  
9 1     1   4 use base qw(Business::OnlinePayment);
  1         2  
  1         109  
10 1     1   440 use Business::PayPal::API qw(DirectPayments);
  0            
  0            
11              
12             our $VERSION = '0.11';
13             $VERSION = eval $VERSION;
14              
15             =head1 NAME
16              
17             Business::OnlinePayment::PayPal - PayPal backend for Business::OnlinePayment
18              
19             =head1 SYNOPSIS
20              
21             use Business::OnlinePayment;
22            
23             my $tx = Business::OnlinePayment->new(
24             "PayPal",
25             "Username" => "my_api1.domain.tld",
26             "Password" => "Xdkis9k3jDFk39fj29sD9", ## supplied by PayPal
27             "Signature" => "f7d03YCpEjIF3s9Dk23F2...", ## supplied by PayPal
28             );
29            
30             $tx->content(
31             action => "Normal Authorization",
32             amount => "19.95",
33             type => "Visa",
34             card_number => "4111111111111111",
35             expiration => "01/10",
36             cvv2 => "123",
37             name => "John Doe",
38             address => "123 My Street",
39             city => "Chicago",
40             state => "IL",
41             zip => "61443",
42             IPAddress => "10.0.0.1",
43             );
44            
45             $tx->test_transaction(1);
46             $tx->submit;
47            
48             if ( $tx->is_success ) {
49             print(
50             "SUCCESS:\n",
51             " CorrelationID: ", $tx->correlationid, "\n",
52             " auth: ", $tx->authorization, "\n",
53             " AVS code: ", $tx->avs_code, "\n",
54             " CVV2 code: ", $tx->cvv2_code, "\n",
55             );
56             }
57             else {
58             print(
59             "ERROR: ", $tx->error_message, "\n"
60             );
61             }
62              
63             =head1 DESCRIPTION
64              
65             Business::OnlinePayment::PayPal is a plugin for using PayPal as a
66             payment processor backend with the Business::OnlinePayment API.
67             Specifically, this module uses PayPal's 'DoDirectPayment' operation
68             which utilizes the 'DoDirectPaymentRequest' message type.
69              
70             This module does not do any checks to be sure that all the required
71             fields/arguments/attributes/values, per PayPal's WSDL/XSD, have been
72             provided. In general, PayPal's service will catch errors and return
73             relevant information. However when requests do not meet the minimum
74             message format/structure requirements or if the request contains
75             information not supported by the 'DoDirectPaymentRequest' very generic
76             errors (i.e. PPBaseException) may be sent to STDERR by underlying
77             modules and our response data structure may be completely empty.
78              
79             Anyone using this module or any modules that talk to PayPal should
80             familiarize themselves with the information available at PayPal's
81             integration center. See the L section for links to useful
82             reference material.
83              
84             =head1 METHODS
85              
86             The following methods exist for use with this module.
87              
88             =head2 Convenience methods
89              
90             =over 4
91              
92             =item authorization()
93              
94             Provides access to the TransactionID returned in the PayPal results.
95             This method is part of the Business::OnlinePayment "standard" API.
96              
97             =item transactionid()
98              
99             This method is an alias for the L method.
100              
101             =item correlationid()
102              
103             Provides access to the CorrelationID returned in the PayPal results.
104              
105             =item order_number()
106              
107             This method is an alias for the L method. It is
108             provided for compatibility with the PayflowPro backend.
109              
110             =item server_response()
111              
112             Provides access, via a hashref, to the results hash returned in the
113             Business::PayPal::API results object returned by
114             DoDirectPaymentRequest. This method is part of the
115             Business::OnlinePayment "standard" API.
116              
117             =item result_code()
118              
119             Returns "" or the first ErrorCode returned from
120             DoDirectPaymentRequest. This method is part of the
121             Business::OnlinePayment "standard" API.
122              
123             =item avs_code()
124              
125             Returns the AVSCode returned from DoDirectPaymentRequest.
126              
127             =item cvv2_code()
128              
129             Returns the CVV2Code returned from DoDirectPaymentRequest.
130              
131             =item is_success()
132              
133             Returns 1 or 0 on success or failure of DoDirectPaymentRequest. This
134             method is part of the Business::OnlinePayment "standard" API.
135              
136             =item error_message()
137              
138             Returns a string containing an error message, if any. This method is
139             part of the Business::OnlinePayment "standard" API.
140              
141             =back
142              
143             =head2 set_defaults()
144              
145             Creates accessor methods L, L,
146             L and __map_fields_data (see L).
147              
148             =cut
149              
150             sub set_defaults {
151             my $self = shift;
152              
153             $self->build_subs(qw(avs_code correlationid cvv2_code __map_fields_data));
154              
155             $self->__map_fields_data(
156             {
157             PaymentAction => "action",
158             OrderTotal => "amount", # Payment Detail
159              
160             # Credit Card
161             CreditCardType => "type",
162             CreditCardNumber => "card_number",
163             CVV2 => undef,
164              
165             # Card Owner / Payer Name
166             Payer => "email",
167             FirstName => "name",
168             LastName => undef,
169              
170             # Payer Address
171             Street1 => "address",
172             Street2 => undef,
173             CityName => "city",
174             StateOrProvince => "state",
175             Country => "country",
176             PostalCode => "zip",
177             }
178             );
179             }
180              
181             sub transactionid { shift()->authorization(@_); }
182              
183             sub order_number { shift()->correlationid(@_); }
184              
185             =head2 get_credentials()
186              
187             Get the credential information for Business::PayPal::API that was
188             provided to Business::OnlinePayment::new(). The supported arguments
189             are:
190              
191             =over 4
192              
193             =item * Username Password PKCS12File PKCS12Password
194              
195             =item * Username Password CertFile KeyFile
196              
197             =item * Username Password Signature
198              
199             =back
200              
201             Business::OnlinePayment::PayPal does not currently map arguments to
202             new() from (standard?) names to the PayPal backend specific name. For
203             example, if the argument "login" were passed to new() the module could
204             potentially try to identify that and map that to "Username".
205              
206             NOTE: This requirement/capability seems to be more of a
207             Business::OnlinePayment issue than a backend issue and it isn't clear
208             if behavior like this is needed in this module so I will wait for user
209             feedback to determine if we need/want to implement this.
210              
211             =cut
212              
213             sub get_credentials {
214             my $self = shift;
215              
216             my %credentials;
217             my @cred_vars = (
218             [qw(PKCS12File PKCS12Password)],
219             [qw(CertFile KeyFile)], [qw(Signature)],
220             );
221              
222             foreach my $aref (@cred_vars) {
223             my $need = 0;
224             my @vars = ( qw(Username Password), @$aref );
225              
226             foreach my $var (@vars) {
227              
228             # HACK: Business::OnlinePayment makes method lower case
229             my $method = lc($var);
230             if ( $self->can($method) ) {
231             $credentials{$var} = $self->$method;
232             }
233             else {
234             $need++;
235             }
236             }
237              
238             if ($need) {
239             undef %credentials;
240             }
241             else {
242             last;
243             }
244             }
245             return %credentials;
246             }
247              
248             =head2 get_request_data()
249              
250             Return a hash %data with all the data from content() that we will try
251             to use in our request to PayPal. Tasks performed:
252              
253             =over 4
254              
255             =item *
256              
257             Remove unsupported values from our hash (i.e. description fax login
258             password phone).
259              
260             =item *
261              
262             Translate the value in "action" if necessary, from
263             Business::OnlinePayment names to names used by PayPal. Translations
264             used are:
265              
266             "normal authorization" => "Sale"
267             "authorization only" => "Authorization"
268             "void" => "None"
269              
270             =item *
271              
272             Translate the value in "type" if necessary, from
273             Business::OnlinePayment names to names used by PayPal. See
274             L for details.
275              
276             =item *
277              
278             If necessary, separate ExpMonth and ExpYear values from the single
279             "standard" Business::OnlinePayment "expiration" field. See
280             L for details.
281              
282             =item *
283              
284             Call get_remap_fields to map content() into the %data that we will
285             pass to PayPal. All fields not "mapped" will be passed AS-IS. The
286             mapping used is (map hashref stored in __map_fields_data()):
287              
288             PaymentAction => "action"
289             # Payment Detail
290             OrderTotal => "amount"
291             # Credit Card
292             CreditCardType => "type"
293             CreditCardNumber => "card_number"
294             CVV2 => undef
295             # Card Owner / Payer Name
296             Payer => "email"
297             FirstName => "name"
298             LastName => undef
299             # Payer Address
300             Street1 => "address"
301             Street2 => undef
302             CityName => "city"
303             StateOrProvince => "state"
304             Country => "country"
305             PostalCode => "zip"
306              
307             NOTE: an 'undef' on the right hand side means that field will be
308             looked for as the mixed-case name specified on the left and also as an
309             all lower-case name).
310              
311             =back
312              
313             =cut
314              
315             sub get_request_data {
316             my $self = shift;
317             my %content = $self->content;
318              
319             return () unless (%content);
320              
321             # remove some unsupported content
322             # others? description, invoice_number, customer_id
323             delete @content{qw(description fax login password phone)};
324              
325             # action: map "standard" names to supported as needed
326             if ( $content{action} ) {
327             my $act = lc( $content{action} );
328             my %actions = (
329             "normal authorization" => "Sale",
330             "authorization only" => "Authorization",
331             "void" => "None",
332             );
333             $content{action} = $actions{$act} || $content{action};
334             }
335              
336             # type: translate to supported CreditCardType values
337             if ( $content{type} ) {
338             my $type = $content{type};
339             $content{type} = $self->normalize_creditcardtype($type) || $type;
340             }
341              
342             # expiration: need separate month and year values
343             if ( $content{expiration}
344             and ( !$content{ExpMonth} or !$content{ExpYear} ) )
345             {
346             my $exp = $content{expiration};
347             delete $content{expiration};
348              
349             # we only set ExpMonth/ExpYear if they aren't already set
350             my ( $y, $m ) = $self->parse_expiration($exp);
351             if ( $m and !$content{ExpMonth} ) {
352             $content{ExpMonth} = $m;
353             }
354             if ( $y and !$content{ExpYear} ) {
355             $content{ExpYear} = $y;
356             }
357             }
358              
359             my %data = $self->get_remap_fields(
360             content => \%content,
361             map => $self->__map_fields_data,
362             );
363             return %data;
364             }
365              
366             =head2 submit()
367              
368             Method that overrides the superclass stub. This method performs the
369             following tasks:
370              
371             =over 4
372              
373             =item *
374              
375             Get credentials to be used for authentication with PayPal by calling
376             L.
377              
378             =item *
379              
380             Get request data to be passed to PayPal by calling
381             L.
382              
383             =item *
384              
385             Connect to PayPal and perform a DirectPaymentRequest. The request
386             will be run in test mode (i.e. go to PayPal's "sandbox") if
387             test_transaction() returns true. NOTE: I believe PayPal automatically
388             does AVS checking if possible.
389              
390             =item *
391              
392             Store the entire response in server_response().
393              
394             =item *
395              
396             Set result_code() to "" or the first ErrorCode in Errors (if present).
397              
398             =item *
399              
400             Set avs_code() to the response AVSCode.
401              
402             =item *
403              
404             Set cvv2_code() to the response CVV2Code.
405              
406             =item *
407              
408             Set is_success() to 1 or 0, indicating if the transaction was
409             successful or not.
410              
411             =item *
412              
413             On success, set authorization() with the value of TransactionID. On
414             failure, set error_message() with a string containing all ErrorCode
415             and LongMessage values joined together.
416              
417             =back
418              
419             =cut
420              
421             sub submit {
422             my $self = shift;
423              
424             my %credentials = $self->get_credentials;
425             my %request = $self->get_request_data;
426              
427             my $pp =
428             Business::PayPal::API->new( %credentials,
429             sandbox => $self->test_transaction, );
430              
431             my %resp = $pp->DoDirectPaymentRequest(%request);
432              
433             $self->server_response( \%resp );
434             $self->result_code( $resp{Errors} ? $resp{Errors}->[0]->{ErrorCode} : "" );
435             $self->avs_code( $resp{AVSCode} );
436             $self->cvv2_code( $resp{CVV2Code} );
437              
438             if ( $resp{Ack} and $resp{Ack} eq "Success" ) {
439             $self->is_success(1);
440             $self->authorization( $resp{TransactionID} );
441             $self->correlationid( $resp{CorrelationID} );
442             }
443             else {
444             $self->is_success(0);
445             }
446              
447             if ( $resp{Errors} and @{ $resp{Errors} } ) {
448             my $error = join( "; ",
449             map { $_->{ErrorCode} . ": " . $_->{LongMessage} }
450             @{ $resp{Errors} } );
451             $self->error_message($error);
452             }
453              
454             return $self->is_success;
455             }
456              
457             =head2 get_remap_fields()
458              
459             Options:
460             content => $href (default: { $self->content } )
461             map => $href (default: { } )
462              
463             Combines some of the functionality of get_fields and remap_fields for
464             convenience and also extends/alters their behavior. Unlike
465             Business::OnlinePayment::remap_fields, this doesn't modify content(),
466             and can therefore be called more than once. Also, unlike
467             Business::OnlinePayment::get_fields in 3.x, this doesn't exclude
468             fields content with a value of undef.
469              
470             =cut
471              
472             sub get_remap_fields {
473             my ( $self, %opt ) = @_;
474              
475             my $content = $opt{content} || { $self->content };
476             my $map = $opt{map} || {};
477             my %data;
478              
479             while ( my ( $to, $from ) = each %$map ) {
480             my $tolc = lc($to);
481             my $v;
482             if ( defined $from ) {
483             $v = $content->{$from};
484             delete $content->{$from};
485             }
486             $v ||= $content->{$to} || $content->{$tolc};
487             delete @$content{ $to, $tolc };
488              
489             if ( defined $v ) {
490             $data{$to} = $v;
491             }
492             }
493              
494             %data = ( %$content, %data );
495              
496             return %data;
497             }
498              
499             =head2 normalize_creditcardtype()
500              
501             Attempt to normalize the credit card type to names supported by
502             PayPal. If the module is unable to identify the given type it leaves
503             the value AS-IS and leaves it to PayPal to do what it can with the
504             data given. Supported card types are:
505              
506             Visa | MasterCard | Discover | Amex
507              
508             Translations used are:
509              
510             /^vis/i => "Visa"
511             /^mas/i => "MasterCard"
512             /^ame/i => "Amex"
513             /^dis/i => "Discover"
514              
515             =cut
516              
517             sub normalize_creditcardtype {
518             my ( $self, $cctype ) = @_;
519              
520             if ( $cctype =~ /^vis/i ) { $cctype = "Visa"; }
521             elsif ( $cctype =~ /^mas/i ) { $cctype = "MasterCard"; }
522             elsif ( $cctype =~ /^ame/i ) { $cctype = "Amex"; }
523             elsif ( $cctype =~ /^dis/i ) { $cctype = "Discover"; }
524             else {
525              
526             # Credit Card type '$cctype' not known
527             }
528             return ($cctype);
529             }
530              
531             =head2 parse_expiration()
532              
533             Business::OnlinePayment documents the use of a single expiration or
534             exp_date value. However PayPal requires separate values for both the
535             month and year. There are multiple formates that expiration dates are
536             often specified in so, we try to our best to handle them all.
537              
538             The following formats are supported:
539              
540             YYYY[.-]MM, YYYY[.-]M, YY[-/]M, YY[.-]MM
541             MM[-/]YYYY, M[-/]YYYY, M[-/]YY, MM/YY, MMYY
542              
543             NOTE: this method is based on the parse_exp method found in
544             L.
545              
546             If an unrecognized format is encountered this method it will return an
547             empty list and leave it to PayPal to do what it can with the data
548             given. To avoid having this module attempt to parse 'expiration'
549             explicitly set ExpMonth and ExpYear in content().
550              
551             =cut
552              
553             sub parse_expiration {
554             my ( $self, $exp ) = @_;
555             my ( $y, $m );
556              
557             return () unless ($exp);
558              
559             if (
560             $exp =~ /^(\d{4})[.-](\d{1,2})$/ || # YYYY[.-]MM or YYYY[.-]M
561             $exp =~ /^(\d\d)[-\/](\d)$/ || # YY[-/]M
562             $exp =~ /^(\d\d)[.-](\d\d)$/
563             ) # YY[.-]MM
564             {
565             ( $y, $m ) = ( $1, $2 );
566             }
567             elsif (
568             $exp =~ /^(\d{1,2})[-\/](\d{4})$/ || # MM[-/]YYYY or M[-/]YYYY
569             $exp =~ /^(\d)[-\/](\d\d)$/ || # M[-/]YY
570             $exp =~ /^(\d\d)\/?(\d\d)$/
571             ) # MM/YY or MMYY
572             {
573             ( $y, $m ) = ( $2, $1 );
574             }
575             else {
576             return (); # unable to parse expiration date '$exp'
577             }
578              
579             # HACK: add the current century - 1
580             if ( $y < 100 ) {
581             $y += int( ( ( localtime(time) )[5] + 1900 ) / 100 ) * 100;
582             }
583              
584             return ( $y, sprintf( "%02.0f", $m ) );
585             }
586              
587             1;
588              
589             __END__