add recurring_billing handling
[Business-OnlinePayment-PlugnPay.git] / PlugnPay.pm
1 package Business::OnlinePayment::PlugnPay;
2
3 use strict;
4 use vars qw($VERSION $DEBUG);
5 use Carp qw(carp croak);
6
7 use base qw(Business::OnlinePayment::HTTPS);
8
9 $VERSION = '0.03';
10 $VERSION = eval $VERSION;
11 $DEBUG   = 0;
12
13 sub debug {
14     my $self = shift;
15
16     if (@_) {
17         my $level = shift || 0;
18         if ( ref($self) ) {
19             $self->{"__DEBUG"} = $level;
20         }
21         else {
22             $DEBUG = $level;
23         }
24         $Business::OnlinePayment::HTTPS::DEBUG = $level;
25     }
26     return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG;
27 }
28
29 sub set_defaults {
30     my $self = shift;
31     my %opts = @_;
32
33     # standard B::OP methods/data
34     $self->server("pay1.plugnpay.com");
35     $self->port("443");
36     $self->path("/payment/pnpremote.cgi");
37
38     $self->build_subs(qw( 
39                           order_number avs_code cvv2_response
40                           response_page response_code response_headers
41                      ));
42
43     # module specific data
44     if ( $opts{debug} ) {
45         $self->debug( $opts{debug} );
46         delete $opts{debug};
47     }
48
49     my %_defaults = ();
50     foreach my $key (keys %opts) {
51       $key =~ /^default_(\w*)$/ or next;
52       $_defaults{$1} = $opts{$key};
53       delete $opts{$key};
54     }
55     $self->{_defaults} = \%_defaults;
56
57 }
58
59 sub _map_fields {
60     my ($self) = @_;
61
62     my %content = $self->content();
63
64     #ACTION MAP
65     my %actions = (
66         'normal authorization' => 'auth',     # Authorization/Settle transaction
67         'credit'               => 'newreturn',# Credit (refund)
68         'void'                 => 'void',     # Void
69     );
70
71     $content{'mode'} = $actions{ lc( $content{'action'} ) }
72       || $content{'action'};
73
74     # TYPE MAP
75     my %types = (
76         'visa'             => 'CC',
77         'mastercard'       => 'CC',
78         'american express' => 'CC',
79         'discover'         => 'CC',
80         'cc'               => 'CC',
81         'check'            => 'ECHECK',
82     );
83
84     $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
85
86     # PAYMETHOD MAP
87     my %paymethods = (
88         'CC'           => 'credit',
89         'ECHECK'       => 'onlinecheck',
90     );
91
92     $content{'paymethod'} = $paymethods{ $content{'type'} };
93
94     $self->transaction_type( $content{'type'} );
95
96     $content{'transflags'} = 'recurring'
97       if lc( $content{'recurring_billing'} ) eq 'yes';
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 required_fields {
127     my($self,@fields) = @_;
128
129     my @missing;
130     my %content = $self->content();
131     foreach(@fields) {
132       next
133         if (exists $content{$_} && defined $content{$_} && $content{$_}=~/\S+/);
134       push(@missing, $_);
135     }
136
137     Carp::croak("missing required field(s): " . join(", ", @missing) . "\n")
138       if(@missing);
139
140 }
141
142 sub submit {
143     my ($self) = @_;
144
145     die "Processor does not support a test mode"
146       if $self->test_transaction;
147
148     $self->_map_fields();
149
150     my %content = $self->content;
151
152     my %required;
153     $required{CC_auth} =  [ qw( mode publisher-name card-amount card-name
154                                 card-number card-exp paymethod ) ];
155     $required{CC_newreturn} = [ @{$required{CC_auth}}, qw( publisher-password ) ];
156     $required{CC_void} =  [ qw( mode publisher-name publisher-password orderID
157                                 card-amount ) ];
158     #$required{ECHECK_auth} =  [ qw( mode publisher-name accttype routingnum
159     #                                accountnum checknum paymethod ) ];
160     my %optional;
161     $optional{CC_auth} =  [ qw( publisher-email authtype required dontsndmail
162                                 easycard client convert cc-mail transflags
163                                 card-address1 card-address2 card-city card-state
164                                 card-prov card-zip card-country card-cvv
165                                 currency phone fax email shipinfo shipname
166                                 address1 address2 city state province zip
167                                 country ipaddress accttype orderID tax
168                                 shipping app-level order-id acct_code magstripe
169                                 marketdata carissuenum cardstartdate descrcodes
170                                 retailterms transflags ) ];
171     $optional{CC_newreturn} = [ qw( orderID card-address1 card-address2
172                                     card-city card-state card-zip card-country
173                                     notify-email
174                                   ) ];
175     $optional{CC_void}      = [ qw( notify-email ) ];
176
177     #$optional{ECHECK_auth}      = $optional{CC_auth};      # ?
178     #$optional{ECHECK_newreturn} = $optional{CC_newreturn}; # ?  legal combo?
179     #$optional{ECHECK_void}      = $optional{CC_void};      # ?  legal combo?
180
181     my $type_action = $self->transaction_type(). '_'. $content{mode};
182     unless ( exists($required{$type_action}) ) {
183       $self->error_message("plugnpay can't handle transaction type: ".
184         "$content{action} on " . $self->transaction_type() );
185       $self->is_success(0);
186       return;
187     }
188
189     my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
190
191     $self->_revmap_fields(
192
193         'publisher-name'     => 'login',
194         'publisher-password' => 'password',
195
196         'card-amount'        => 'amount',
197         'card-name'          => 'name',
198         'card-address1'      => 'address',
199         'card-city'          => 'city',
200         'card-state'         => 'state',
201         'card-zip'           => 'zip',
202         'card-country'       => 'country',
203         'card-number'        => 'card_number',
204         'card-exp'           => \$expdate_mmyy,    # MMYY from 'expiration'
205         'card-cvv'           => 'cvv2',
206         'order-id'           => 'invoice_number',
207         'orderID'            => 'order_number',
208
209
210     );
211
212     my %shipping_params = ( shipname => (($content{ship_first_name} || '') .
213                                         ' '. ($content{ship_last_name} || '')),
214                             address1 => $content{ship_address},
215                             map { $_ => $content{ "ship_$_" } } 
216                               qw ( city state zip country )
217                           );
218
219
220     foreach ( keys ( %shipping_params ) ) {
221       if ($shipping_params{$_} && $shipping_params{$_} =~ /^\s*$/) {
222         delete $shipping_params{$_};
223       }
224     }
225     $shipping_params{shipinfo} = 1 if scalar(keys(%shipping_params));
226
227     my %params = ( $self->get_fields( @{$required{$type_action}},
228                                       @{$optional{$type_action}},
229                                     ),
230                    (%shipping_params)
231                  );
232
233     $params{'txn-type'} = 'auth' if $params{mode} eq 'void';
234
235     foreach ( keys ( %{($self->{_defaults})} ) ) {
236       $params{$_} = $self->{_defaults}->{$_} unless exists($params{$_});
237     }
238
239     
240     $self->required_fields(@{$required{$type_action}});
241     
242     warn join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $DEBUG > 1;
243     my ( $page, $resp, %resp_headers ) = 
244       $self->https_post( %params );
245
246     $self->response_code( $resp );
247     $self->response_page( $page );
248     $self->response_headers( \%resp_headers );
249
250     warn "$page\n" if $DEBUG > 1;
251     # $page should contain key/value pairs
252
253     my $status ='';
254     my %results = map { s/\s*$//;
255                         my ($name, $value) = split '=', $_, 2;
256                         $name  =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
257                         $value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
258                         $name, $value;
259                       } split '&', $page;
260
261     # AVS and CVS values may be set on success or failure
262     $self->avs_code( $results{ 'avs-code' } );
263     $self->cvv2_response( $results{ cvvresp } );
264     $self->result_code( $results{ 'resp-code' } );
265     $self->order_number( $results{ orderID } );
266     $self->authorization( $results{ 'auth-code' } );
267     $self->error_message( $results{ MErrMsg } );
268
269
270     if ( $resp =~ /^(HTTP\S+ )?200/
271       &&($results{ FinalStatus } eq "success" ||
272          $results{ FinalStatus } eq "pending" && $results{ mode } eq 'newreturn'
273         )
274        ) {
275         $self->is_success(1);
276     } else {
277         $self->is_success(0);
278     }
279 }
280
281 1;
282
283 __END__
284
285 =head1 NAME
286
287 Business::OnlinePayment::PlugnPay - plugnpay backend for Business::OnlinePayment
288
289 =head1 SYNOPSIS
290
291   use Business::OnlinePayment;
292   
293   my $tx = new Business::OnlinePayment( 'PlugnPay' );
294   
295   # See the module documentation for details of content()
296   $tx->content(
297       type           => 'CC',
298       action         => 'Normal Authorization',
299       description    => 'Business::OnlinePayment::plugnpay test',
300       amount         => '49.95',
301       invoice_number => '100100',
302       customer_id    => 'jef',
303       name           => 'Jeff Finucane',
304       address        => '123 Anystreet',
305       city           => 'Anywhere',
306       state          => 'GA',
307       zip            => '30004',
308       email          => 'plugnpay@weasellips.com',
309       card_number    => '4111111111111111',
310       expiration     => '12/09',
311       cvv2           => '123',
312       order_number   => 'string',
313   );
314   
315   $tx->submit();
316   
317   if ( $tx->is_success() ) {
318       print(
319           "Card processed successfully: ", $tx->authorization, "\n",
320           "order number: ",                $tx->order_number,  "\n",
321           "CVV2 response: ",               $tx->cvv2_response, "\n",
322           "AVS code: ",                    $tx->avs_code,      "\n",
323       );
324   }
325   else {
326       print(
327           "Card was rejected: ", $tx->error_message, "\n",
328           "order number: ",      $tx->order_number,  "\n",
329       );
330   }
331
332 =head1 DESCRIPTION
333
334 This module is a back end driver that implements the interface
335 specified by L<Business::OnlinePayment> to support payment handling
336 via plugnpay's payment solution.
337
338 See L<Business::OnlinePayment> for details on the interface this
339 modules supports.
340
341 =head1 Standard methods
342
343 =over 4
344
345 =item set_defaults()
346
347 This method sets the 'server' attribute to 'pay1.plugnpay.com' and
348 the port attribute to '443'.  This method also sets up the
349 L</Module specific methods> described below.
350
351 =item submit()
352
353 =back
354
355 =head1 Unofficial methods
356
357 This module provides the following methods which are not officially part of the
358 standard Business::OnlinePayment interface (as of 3.00_06) but are nevertheless
359 supported by multiple gateways modules and expected to be standardized soon:
360
361 =over 4
362
363 =item L<order_number()|/order_number()>
364
365 =item L<avs_code()|/avs_code()>
366
367 =item L<cvv2_response()|/cvv2_response()>
368
369 =back
370
371 =head1 Module specific methods
372
373 This module provides the following methods which are not currently
374 part of the standard Business::OnlinePayment interface:
375
376 =over 4
377
378 =item L<expdate_mmyy()|/expdate_mmyy()>
379
380 =item L<debug()|/debug()>
381
382 =back
383
384 =head1 Settings
385
386 The following default settings exist:
387
388 =over 4
389
390 =item server
391
392 pay1.plugnpay.com
393
394 =item port
395
396 443
397
398 =item path
399
400 /payment/pnpremote.cgi
401
402 =back
403
404 =head1 Parameters passed to constructor
405
406 If any of the key/value pairs passed to the constructor have a key
407 beginning with "default_" then those values are passed to plugnpay as
408 a the corresponding form field (without the "default_") whenever
409 content(%content) lacks that key.
410
411 =head1 Handling of content(%content)
412
413 The following rules apply to content(%content) data:
414
415 =head2 type
416
417 If 'type' matches one of the following keys it is replaced by the
418 right hand side value:
419
420   'visa'               => 'CC',
421   'mastercard'         => 'CC',
422   'american express'   => 'CC',
423   'discover'           => 'CC',
424
425 The value of 'type' is used to set transaction_type().  Currently this
426 module only supports the above values.
427
428 =head1 Setting plugnpay parameters from content(%content)
429
430 The following rules are applied to map data to plugnpay parameters
431 from content(%content):
432
433     # plugnpay param     => $content{<key>}
434       publisher-name     => 'login',
435       publisher-password => 'password',
436
437       card-amount        => 'amount',
438       card-number        => 'card_number',
439       card-exp           => \( $month.$year ), # MM/YY from 'expiration'
440       ssl_cvv            => 'cvv2',
441       order-id           => 'invoice_number',
442
443       card-name          => 'name',
444       card-address1      => 'address',
445       card-city          => 'city',
446       card-state         => 'state',
447       card-zip           => 'zip'
448       card-country       => 'country',
449       orderID            => 'order_number'     # can be set via order_number()
450
451       shipname           => 'ship_first_name' . ' ' . 'ship_last_name',
452       address1           => 'ship_address',
453       city               => 'ship_city',
454       state              => 'ship_state',
455       zip                => 'ship_zip',
456       country            => 'ship_country',
457
458       transflags         => 'recurring' if ($content{recurring_billing}) eq 'yes',
459
460 =head1 Mapping plugnpay transaction responses to object methods
461
462 The following methods provides access to the transaction response data
463 resulting from a plugnpay request (after submit()) is called:
464
465 =head2 order_number()
466
467 This order_number() method returns the orderID field for transactions
468 to uniquely identify the transaction.
469
470 =head2 result_code()
471
472 The result_code() method returns the resp-code field for transactions.
473 It is the alphanumeric return code indicating the outcome of the attempted
474 transaction.
475
476 =head2 error_message()
477
478 The error_message() method returns the MErrMsg field for transactions.
479 This provides more details about the transaction result.
480
481 =head2 authorization()
482
483 The authorization() method returns the auth-code field,
484 which is the approval code obtained from the card processing network.
485
486 =head2 avs_code()
487
488 The avs_code() method returns the avs-code field from the transaction result.
489
490 =head2 cvv2_response()
491
492 The cvv2_response() method returns the cvvresp field, which is a
493 response message returned with the transaction result.
494
495 =head2 expdate_mmyy()
496
497 The expdate_mmyy() method takes a single scalar argument (typically
498 the value in $content{expiration}) and attempts to parse and format
499 and put the date in MM/YY format as required by the plugnpay
500 specification.  If unable to parse the expiration date simply leave it
501 as is and let the plugnpay system attempt to handle it as-is.
502
503 =head2 debug()
504
505 Enable or disble debugging.  The value specified here will also set
506 $Business::OnlinePayment::HTTPS::DEBUG in submit() to aid in
507 troubleshooting problems.
508
509 =head1 COMPATIBILITY
510
511 This module implements an interface to the plugnpay Remote Client Integration
512 Specification Rev. 10.03.2007
513
514 =head1 AUTHORS
515
516 Jeff Finucane <plugnpay@weasellips.com>
517
518 Based on Business::OnlinePayment::PayflowPro written by Ivan Kohler
519 and Phil Lobbes.
520
521 =head1 SEE ALSO
522
523 perl(1), L<Business::OnlinePayment>, L<Carp>, and the Remote Client Integration
524 Specification from plugnpay.
525
526 =cut