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