depend on B:OP:HTTPS 0.06 for Crypt::SSLeay fixes
[Business-OnlinePayment-PayflowPro.git] / PayflowPro.pm
1 package Business::OnlinePayment::PayflowPro;
2
3 use strict;
4 use vars qw($VERSION $DEBUG);
5 use Carp qw(carp croak);
6 use CGI;
7 use Digest::MD5;
8 use Business::OnlinePayment::HTTPS 0.06;
9
10 use base qw(Business::OnlinePayment::HTTPS);
11
12 $VERSION = '0.07_04';
13 $VERSION = eval $VERSION;
14 $DEBUG   = 0;
15
16 sub request_id {
17     my $self = shift;
18     my $md5  = Digest::MD5->new();
19     $md5->add( $$, time(), rand(time) );
20     return $md5->hexdigest();
21 }
22
23 sub debug {
24     my $self = shift;
25
26     if (@_) {
27         my $level = shift || 0;
28         if ( ref($self) ) {
29             $self->{"__DEBUG"} = $level;
30         }
31         else {
32             $DEBUG = $level;
33         }
34         $Business::OnlinePayment::HTTPS::DEBUG = $level;
35     }
36     return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG;
37 }
38
39 sub set_defaults {
40     my $self = shift;
41     my %opts = @_;
42
43     # standard B::OP methods/data
44     #$self->server("payflow.verisign.com");
45     $self->server("payflowpro.verisign.com");
46     $self->port("443");
47     $self->path("/transaction");
48
49     $self->build_subs(qw( 
50                           partner vendor client_certification_id
51                           headers test_server
52                           cert_path
53                           order_number avs_code cvv2_response
54                           response_page response_code response_headers
55                      ));
56
57     # module specific data
58     if ( $opts{debug} ) {
59         $self->debug( $opts{debug} );
60         delete $opts{debug};
61     }
62
63     $self->test_server( "pilot-payflowpro.verisign.com" );
64
65 }
66
67 sub _map_fields {
68     my ($self) = @_;
69
70     my %content = $self->content();
71
72     #ACTION MAP
73     my %actions = (
74         'normal authorization' => 'S',    # Sale transaction
75         'credit'               => 'C',    # Credit (refund)
76         'authorization only'   => 'A',    # Authorization
77         'post authorization'   => 'D',    # Delayed Capture
78         'void'                 => 'V',    # Void
79     );
80
81     $content{'action'} = $actions{ lc( $content{'action'} ) }
82       || $content{'action'};
83
84     # TYPE MAP
85     my %types = (
86         'visa'             => 'C',
87         'mastercard'       => 'C',
88         'american express' => 'C',
89         'discover'         => 'C',
90         'cc'               => 'C',
91
92         #'check'            => 'ECHECK',
93     );
94
95     $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
96
97     $self->transaction_type( $content{'type'} );
98
99     # stuff it back into %content
100     $self->content(%content);
101 }
102
103 sub _revmap_fields {
104     my ( $self, %map ) = @_;
105     my %content = $self->content();
106     foreach ( keys %map ) {
107         $content{$_} =
108           ref( $map{$_} )
109           ? ${ $map{$_} }
110           : $content{ $map{$_} };
111     }
112     $self->content(%content);
113 }
114
115 sub expdate_mmyy {
116     my $self       = shift;
117     my $expiration = shift;
118     my $expdate_mmyy;
119     if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) {
120         my ( $month, $year ) = ( $1, $2 );
121         $expdate_mmyy = sprintf( "%02d", $month ) . $year;
122     }
123     return defined($expdate_mmyy) ? $expdate_mmyy : $expiration;
124 }
125
126 sub submit {
127     my ($self) = @_;
128
129     $self->_map_fields();
130
131     my %content = $self->content;
132
133     if ( $self->transaction_type() ne 'C' ) {
134         croak( "PayflowPro can't (yet?) handle transaction type: "
135               . $self->transaction_type() );
136     }
137
138     my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
139     my $zip          = $content{'zip'};
140     $zip =~ s/[^[:alnum:]]//g;
141
142     $self->server( $self->test_server ) if $self->test_transaction;
143
144     my $vendor  = $self->vendor;
145     my $partner = $self->partner;
146
147     $self->_revmap_fields(
148
149         # BUG?: VENDOR B::OP:PayflowPro < 0.05 backward compatibility.  If
150         # vendor not set use login although test indicate undef vendor is ok
151         VENDOR => $vendor ? \$vendor : 'login',
152         PARTNER  => \$partner,
153         USER     => 'login',
154         PWD      => 'password',
155         TRXTYPE  => 'action',
156         TENDER   => 'type',
157         ORIGID   => 'order_number',
158         COMMENT1 => 'description',
159         COMMENT2 => 'invoice_number',
160
161         ACCT    => 'card_number',
162         CVV2    => 'cvv2',
163         EXPDATE => \$expdate_mmyy,    # MM/YY from 'expiration'
164         AMT     => 'amount',
165
166         FIRSTNAME   => 'first_name',
167         LASTNAME    => 'last_name',
168         NAME        => 'name',
169         EMAIL       => 'email',
170         COMPANYNAME => 'company',
171         STREET      => 'address',
172         CITY        => 'city',
173         STATE       => 'state',
174         ZIP         => \$zip,          # 'zip' with non-alnums removed
175         COUNTRY     => 'country',
176     );
177
178     my @required = qw( TRXTYPE TENDER PARTNER VENDOR USER PWD );
179     if ( $self->transaction_type() eq 'C' ) {    # credit card
180         if (   $content{'action'} =~ /^[CDV]$/
181             && defined( $content{'ORIGID'} )
182             && length( $content{'ORIGID'} ) )
183         {
184             push @required, qw(ORIGID);
185         }
186         else {
187
188             # never get here, we croak above if transaction_type ne 'C'
189             push @required, qw(AMT ACCT EXPDATE);
190         }
191     }
192     $self->required_fields(@required);
193
194     my %params = $self->get_fields(
195         qw(
196           VENDOR PARTNER USER PWD TRXTYPE TENDER ORIGID COMMENT1 COMMENT2
197           ACCT CVV2 EXPDATE AMT
198           FIRSTNAME LASTNAME NAME EMAIL COMPANYNAME
199           STREET CITY STATE ZIP COUNTRY
200           )
201     );
202
203     # get header data, get request_id from %content if defined for ease of use
204     my %req_headers = %{ $self->headers || {} };
205     if ( defined $content{"request_id"} ) {
206         $req_headers{"X-VPS-Request-ID"} = $content{"request_id"};
207     }
208     unless ( defined( $req_headers{"X-VPS-Request-ID"} ) ) {
209         $req_headers{"X-VPS-Request-ID"} = $self->request_id();
210     }
211
212     unless ( defined( $req_headers{"X-VPS-VIT-Client-Certification-Id"} ) ) {
213         $req_headers{"X-VPS-VIT-Client-Certification-Id"} =
214           $self->client_certification_id;
215     }
216
217     my %options = (
218         "Content-Type" => "text/namevalue",
219         "headers"      => \%req_headers,
220     );
221
222     my ( $page, $resp, %resp_headers ) =
223       $self->https_post( \%options, \%params );
224
225     $self->response_code( $resp );
226     $self->response_page( $page );
227     $self->response_headers( \%resp_headers );
228
229     # $page should contain name=value[[&name=value]...] pairs
230     my $cgi = CGI->new("$page");
231
232     # AVS and CVS values may be set on success or failure
233     my $avs_code;
234     if ( defined $cgi->param("AVSADDR") or defined $cgi->param("AVSZIP") ) {
235         if ( $cgi->param("AVSADDR") eq "Y" && $cgi->param("AVSZIP") eq "Y" ) {
236             $avs_code = "Y";
237         }
238         elsif ( $cgi->param("AVSADDR") eq "Y" ) {
239             $avs_code = "A";
240         }
241         elsif ( $cgi->param("AVSZIP") eq "Y" ) {
242             $avs_code = "Z";
243         }
244         elsif ( $cgi->param("AVSADDR") eq "N" or $cgi->param("AVSZIP") eq "N" )
245         {
246             $avs_code = "N";
247         }
248         else {
249             $avs_code = "";
250         }
251     }
252
253     $self->avs_code($avs_code);
254     $self->cvv2_response( $cgi->param("CVV2MATCH") );
255     $self->result_code( $cgi->param("RESULT") );
256     $self->order_number( $cgi->param("PNREF") );
257     $self->error_message( $cgi->param("RESPMSG") );
258     $self->authorization( $cgi->param("AUTHCODE") );
259
260     # RESULT must be an explicit zero, not just numerically equal
261     if ( $cgi->param("RESULT") eq "0" ) {
262         $self->is_success(1);
263     }
264     else {
265         $self->is_success(0);
266     }
267 }
268
269 1;
270
271 __END__
272
273 =head1 NAME
274
275 Business::OnlinePayment::PayflowPro - Payflow Pro backend for Business::OnlinePayment
276
277 =head1 SYNOPSIS
278
279   use Business::OnlinePayment;
280   
281   my $tx = new Business::OnlinePayment(
282       'PayflowPro',
283       'vendor'    => 'your_vendor',
284       'partner'   => 'your_partner',
285       'client_certification_id' => 'assigned_certification_id',
286   );
287   
288   # See the module documentation for details of content()
289   $tx->content(
290       type           => 'VISA',
291       action         => 'Normal Authorization',
292       description    => 'Business::OnlinePayment::PayflowPro test',
293       amount         => '49.95',
294       invoice_number => '100100',
295       customer_id    => 'jsk',
296       name           => 'Jason Kohles',
297       address        => '123 Anystreet',
298       city           => 'Anywhere',
299       state          => 'GA',
300       zip            => '30004',
301       email          => 'ivan-payflowpro@420.am',
302       card_number    => '4111111111111111',
303       expiration     => '12/09',
304       cvv2           => '123',
305       order_number   => 'string',
306       request_id     => 'unique_identifier_for_transaction',
307   );
308   
309   $tx->submit();
310   
311   if ( $tx->is_success() ) {
312       print(
313           "Card processed successfully: ", $tx->authorization, "\n",
314           "order number: ",                $tx->order_number,  "\n",
315           "CVV2 response: ",               $tx->cvv2_response, "\n",
316           "AVS code: ",                    $tx->avs_code,      "\n",
317       );
318   }
319   else {
320       my $info = "";
321       $info = " (CVV2 mismatch)" if ( $tx->result_code == 114 );
322       
323       print(
324           "Card was rejected: ", $tx->error_message, $info, "\n",
325           "order number: ",      $tx->order_number,         "\n",
326       );
327   }
328
329 =head1 DESCRIPTION
330
331 This module is a back end driver that implements the interface
332 specified by L<Business::OnlinePayment> to support payment handling
333 via the PayPal's Payflow Pro Internet payment solution.
334
335 See L<Business::OnlinePayment> for details on the interface this
336 modules supports.
337
338 =head1 Standard methods
339
340 =over 4
341
342 =item set_defaults()
343
344 This method sets the 'server' attribute to 'payflowpro.verisign.com' and
345 the port attribute to '443'.  This method also sets up the
346 L</Module specific methods> described below.
347
348 =item submit()
349
350 =back
351
352 =head1 Unofficial methods
353
354 This module provides the following methods which are not officially part of the
355 standard Business::OnlinePayment interface (as of 3.00_06) but are nevertheless
356 supported by multiple gateways modules and expected to be standardized soon:
357
358 =over 4
359
360 =item L<order_number()|/order_number()>
361
362 =item L<avs_code()|/avs_code()>
363
364 =item L<cvv2_response()|/cvv2_response()>
365
366 =back
367
368 =head1 Module specific methods
369
370 This module provides the following methods which are not currently
371 part of the standard Business::OnlinePayment interface:
372
373 =over 4
374
375 =item L<expdate_mmyy()|/expdate_mmyy()>
376
377 =item L<requeset_id()/request_id()>
378
379 =item L<debug()|/debug()>
380
381 =back
382
383 =head2 Deprecated methods
384
385 The following methods are deprecated and may be removed in a future
386 release.  Values for vendor and partner should now be set as arguments to
387 Business::OnlinePayment->new().  The value for cert_path was used to support
388 passing a path to PFProAPI.pm (a Perl module/SDK from Verisign/Paypal) which is
389 no longer used.
390
391 =over 4
392
393 =item vendor()
394
395 =item partner()
396
397 =item cert_path()
398
399 =back
400
401 =head1 Settings
402
403 The following default settings exist:
404
405 =over 4
406
407 =item server
408
409 payflowpro.verisign.com or pilot-payflowpro.verisign.com if
410 test_transaction() is TRUE
411
412 =item port
413
414 443
415
416 =back
417
418 =head1 Handling of content(%content)
419
420 The following rules apply to content(%content) data:
421
422 =head2 action
423
424 If 'action' matches one of the following keys it is replaced by the
425 right hand side value:
426
427   'normal authorization' => 'S', # Sale transaction
428   'credit'               => 'C', # Credit (refund)
429   'authorization only'   => 'A', # Authorization
430   'post authorization'   => 'D', # Delayed Capture
431   'void'                 => 'V',
432
433 If 'action' is 'C', 'D' or 'V' and 'order_number' is not set then
434 'amount', 'card_number' and 'expiration' must be set.
435
436 =head2 type
437
438 If 'type' matches one of the following keys it is replaced by the
439 right hand side value:
440
441   'visa'               => 'C',
442   'mastercard'         => 'C',
443   'american express'   => 'C',
444   'discover'           => 'C',
445   'cc'                 => 'C',
446
447 The value of 'type' is used to set transaction_type().  Currently this
448 module only supports a transaction_type() of 'C' any other values will
449 cause Carp::croak() to be called in submit().
450
451 Note: Payflow Pro supports multiple credit card types, including:
452 American Express/Optima, Diners Club, Discover/Novus, Enroute, JCB,
453 MasterCard and Visa.
454
455 =head1 Setting Payflow Pro parameters from content(%content)
456
457 The following rules are applied to map data to Payflow Pro parameters
458 from content(%content):
459
460       # PFP param => $content{<key>}
461       VENDOR      => $self->vendor ? \( $self->vendor ) : 'login',
462       PARTNER     => \( $self->partner ),
463       USER        => 'login',
464       PWD         => 'password',
465       TRXTYPE     => 'action',
466       TENDER      => 'type',
467       ORIGID      => 'order_number',
468       COMMENT1    => 'description',
469       COMMENT2    => 'invoice_number',
470
471       ACCT        => 'card_number',
472       CVV2        => 'cvv2',
473       EXPDATE     => \( $month.$year ), # MM/YY from 'expiration'
474       AMT         => 'amount',
475
476       FIRSTNAME   => 'first_name',
477       LASTNAME    => 'last_name',
478       NAME        => 'name',
479       EMAIL       => 'email',
480       COMPANYNAME => 'company',
481       STREET      => 'address',
482       CITY        => 'city',
483       STATE       => 'state',
484       ZIP         => \$zip, # 'zip' with non-alphanumerics removed
485       COUNTRY     => 'country',
486
487 The required Payflow Pro parameters for credit card transactions are:
488
489   TRXTYPE TENDER PARTNER VENDOR USER PWD ORIGID
490
491 =head1 Mapping Payflow Pro transaction responses to object methods
492
493 The following methods provides access to the transaction response data
494 resulting from a Payflow Pro request (after submit()) is called:
495
496 =head2 order_number()
497
498 This order_number() method returns the PNREF field, also known as the
499 PayPal Reference ID, which is a unique number that identifies the
500 transaction.
501
502 =head2 result_code()
503
504 The result_code() method returns the RESULT field, which is the
505 numeric return code indicating the outcome of the attempted
506 transaction.
507
508 A RESULT of 0 (zero) indicates the transaction was approved and
509 is_success() will return '1' (one/TRUE).  Any other RESULT value
510 indicates a decline or error and is_success() will return '0'
511 (zero/FALSE).
512
513 =head2 error_message()
514
515 The error_message() method returns the RESPMSG field, which is a
516 response message returned with the transaction result.
517
518 =head2 authorization()
519
520 The authorization() method returns the AUTHCODE field, which is the
521 approval code obtained from the processing network.
522
523 =head2 avs_code()
524
525 The avs_code() method returns a combination of the AVSADDR and AVSZIP
526 fields from the transaction result.  The value in avs_code is as
527 follows:
528
529   Y     - Address and ZIP match
530   A     - Address matches but not ZIP
531   Z     - ZIP matches but not address
532   N     - no match
533   undef - AVS values not available
534
535 =head2 cvv2_response()
536
537 The cvv2_response() method returns the CVV2MATCH field, which is a
538 response message returned with the transaction result.
539
540 =head2 expdate_mmyy()
541
542 The expdate_mmyy() method takes a single scalar argument (typically
543 the value in $content{expiration}) and attempts to parse and format
544 and put the date in MMYY format as required by PayflowPro
545 specification.  If unable to parse the expiration date simply leave it
546 as is and let the PayflowPro system attempt to handle it as-is.
547
548 =head2 request_id()
549
550 The request_id() method uses Digest::MD5 to attempt to generate a
551 request_id for a transaction.  It is recommended that you specify your
552 own unique request_id for each transaction in %content.  A request_id
553 is REQUIRED by the PayflowPro processor.
554
555 =head2 debug()
556
557 Enable or disble debugging.  The value specified here will also set
558 $Business::OnlinePayment::HTTPS::DEBUG in submit() to aid in
559 troubleshooting problems.
560
561 =head1 COMPATIBILITY
562
563 This module implements an interface to the Payflow Pro Perl API, which
564 can be downloaded at https://manager.paypal.com/ with a valid login.
565
566 =head1 AUTHORS
567
568 Ivan Kohler <ivan-payflowpro@420.am>
569
570 Phil Lobbes E<lt>phil at perkpartners.comE<gt>
571
572 Based on Business::OnlinePayment::AuthorizeNet written by Jason Kohles.
573
574 =head1 SEE ALSO
575
576 perl(1), L<Business::OnlinePayment>, L<Carp>, and the PayPal
577 Integration Center Payflow Pro resources at
578 L<https://www.paypal.com/IntegrationCenter/ic_payflowpro.html>
579
580 =cut