Initial import
[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.02';
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     # stuff it back into %content
97     $self->content(%content);
98 }
99
100 sub _revmap_fields {
101     my ( $self, %map ) = @_;
102     my %content = $self->content();
103     foreach ( keys %map ) {
104         $content{$_} =
105           ref( $map{$_} )
106           ? ${ $map{$_} }
107           : $content{ $map{$_} };
108     }
109     $self->content(%content);
110 }
111
112 sub expdate_mmyy {
113     my $self       = shift;
114     my $expiration = shift;
115     my $expdate_mmyy;
116     if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) {
117         my ( $month, $year ) = ( $1, $2 );
118         $expdate_mmyy = sprintf( "%02d/", $month ) . $year;
119     }
120     return defined($expdate_mmyy) ? $expdate_mmyy : $expiration;
121 }
122
123 sub required_fields {
124     my($self,@fields) = @_;
125
126     my @missing;
127     my %content = $self->content();
128     foreach(@fields) {
129       next
130         if (exists $content{$_} && defined $content{$_} && $content{$_}=~/\S+/);
131       push(@missing, $_);
132     }
133
134     Carp::croak("missing required field(s): " . join(", ", @missing) . "\n")
135       if(@missing);
136
137 }
138
139 sub submit {
140     my ($self) = @_;
141
142     die "Processor does not support a test mode"
143       if $self->test_transaction;
144
145     $self->_map_fields();
146
147     my %content = $self->content;
148
149     my %required;
150     $required{CC_auth} =  [ qw( mode publisher-name card-amount card-name
151                                 card-number card-exp paymethod ) ];
152     $required{CC_newreturn} = [ @{$required{CC_auth}}, qw( publisher-password ) ];
153     $required{CC_void} =  [ qw( mode publisher-name publisher-password orderID
154                                 card-amount ) ];
155     #$required{ECHECK_auth} =  [ qw( mode publisher-name accttype routingnum
156     #                                accountnum checknum paymethod ) ];
157     my %optional;
158     $optional{CC_auth} =  [ qw( publisher-email authtype required dontsndmail
159                                 easycard client convert cc-mail transflags
160                                 card-address1 card-address2 card-city card-state
161                                 card-prov card-zip card-country card-cvv
162                                 currency phone fax email shipinfo shipname
163                                 address1 address2 city state province zip
164                                 country ipaddress accttype orderID tax
165                                 shipping app-level order-id acct_code magstripe
166                                 marketdata carissuenum cardstartdate descrcodes
167                                 retailterms) ];
168     $optional{CC_newreturn} = [ qw( orderID card-address1 card-address2
169                                     card-city card-state card-zip card-country
170                                     notify-email
171                                   ) ];
172     $optional{CC_void}      = [ qw( notify-email ) ];
173
174     #$optional{ECHECK_auth}      = $optional{CC_auth};      # ?
175     #$optional{ECHECK_newreturn} = $optional{CC_newreturn}; # ?  legal combo?
176     #$optional{ECHECK_void}      = $optional{CC_void};      # ?  legal combo?
177
178     my $type_action = $self->transaction_type(). '_'. $content{mode};
179     unless ( exists($required{$type_action}) ) {
180       $self->error_message("plugnpay can't handle transaction type: ".
181         "$content{action} on " . $self->transaction_type() );
182       $self->is_success(0);
183       return;
184     }
185
186     my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
187
188     $self->_revmap_fields(
189
190         'publisher-name'     => 'login',
191         'publisher-password' => 'password',
192
193         'card-amount'        => 'amount',
194         'card-name'          => 'name',
195         'card-address1'      => 'address',
196         'card-city'          => 'city',
197         'card-state'         => 'state',
198         'card-zip'           => 'zip',
199         'card-country'       => 'country',
200         'card-number'        => 'card_number',
201         'card-exp'           => \$expdate_mmyy,    # MMYY from 'expiration'
202         'card-cvv'           => 'cvv2',
203         'order-id'           => 'invoice_number',
204         'orderID'            => 'order_number',
205
206
207     );
208
209     my %shipping_params = ( shipname => (($content{ship_first_name} || '') .
210                                         ' '. ($content{ship_last_name} || '')),
211                             address1 => $content{ship_address},
212                             map { $_ => $content{ "ship_$_" } } 
213                               qw ( city state zip country )
214                           );
215
216
217     foreach ( keys ( %shipping_params ) ) {
218       if ($shipping_params{$_} && $shipping_params{$_} =~ /^\s*$/) {
219         delete $shipping_params{$_};
220       }
221     }
222     $shipping_params{shipinfo} = 1 if scalar(keys(%shipping_params));
223
224     my %params = ( $self->get_fields( @{$required{$type_action}},
225                                       @{$optional{$type_action}},
226                                     ),
227                    (%shipping_params)
228                  );
229
230     $params{'txn-type'} = 'auth' if $params{mode} eq 'void';
231
232     foreach ( keys ( %{($self->{_defaults})} ) ) {
233       $params{$_} = $self->{_defaults}->{$_} unless exists($params{$_});
234     }
235
236     
237     $self->required_fields(@{$required{$type_action}});
238     
239     warn join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $DEBUG > 1;
240     my ( $page, $resp, %resp_headers ) = 
241       $self->https_post( %params );
242
243     $self->response_code( $resp );
244     $self->response_page( $page );
245     $self->response_headers( \%resp_headers );
246
247     warn "$page\n" if $DEBUG > 1;
248     # $page should contain key/value pairs
249
250     my $status ='';
251     my %results = map { s/\s*$//;
252                         my ($name, $value) = split '=', $_, 2;
253                         $name  =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
254                         $value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
255                         $name, $value;
256                       } split '&', $page;
257
258     # AVS and CVS values may be set on success or failure
259     $self->avs_code( $results{ 'avs-code' } );
260     $self->cvv2_response( $results{ cvvresp } );
261     $self->result_code( $results{ 'resp-code' } );
262     $self->order_number( $results{ orderID } );
263     $self->authorization( $results{ 'auth-code' } );
264     $self->error_message( $results{ MErrMsg } );
265
266
267     if ( $resp =~ /^(HTTP\S+ )?200/
268       &&($results{ FinalStatus } eq "success" ||
269          $results{ FinalStatus } eq "pending" && $results{ mode } eq 'newreturn'
270         )
271        ) {
272         $self->is_success(1);
273     } else {
274         $self->is_success(0);
275     }
276 }
277
278 1;
279
280 __END__
281
282 =head1 NAME
283
284 Business::OnlinePayment::PlugnPay - plugnpay backend for Business::OnlinePayment
285
286 =head1 SYNOPSIS
287
288   use Business::OnlinePayment;
289   
290   my $tx = new Business::OnlinePayment( 'PlugnPay' );
291   
292   # See the module documentation for details of content()
293   $tx->content(
294       type           => 'CC',
295       action         => 'Normal Authorization',
296       description    => 'Business::OnlinePayment::plugnpay test',
297       amount         => '49.95',
298       invoice_number => '100100',
299       customer_id    => 'jef',
300       name           => 'Jeff Finucane',
301       address        => '123 Anystreet',
302       city           => 'Anywhere',
303       state          => 'GA',
304       zip            => '30004',
305       email          => 'plugnpay@weasellips.com',
306       card_number    => '4111111111111111',
307       expiration     => '12/09',
308       cvv2           => '123',
309       order_number   => 'string',
310   );
311   
312   $tx->submit();
313   
314   if ( $tx->is_success() ) {
315       print(
316           "Card processed successfully: ", $tx->authorization, "\n",
317           "order number: ",                $tx->order_number,  "\n",
318           "CVV2 response: ",               $tx->cvv2_response, "\n",
319           "AVS code: ",                    $tx->avs_code,      "\n",
320       );
321   }
322   else {
323       print(
324           "Card was rejected: ", $tx->error_message, "\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 plugnpay's 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 'pay1.plugnpay.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<debug()|/debug()>
378
379 =back
380
381 =head1 Settings
382
383 The following default settings exist:
384
385 =over 4
386
387 =item server
388
389 pay1.plugnpay.com
390
391 =item port
392
393 443
394
395 =item path
396
397 /payment/pnpremote.cgi
398
399 =back
400
401 =head1 Parameters passed to constructor
402
403 If any of the key/value pairs passed to the constructor have a key
404 beginning with "default_" then those values are passed to plugnpay as
405 a the corresponding form field (without the "default_") whenever
406 content(%content) lacks that key.
407
408 =head1 Handling of content(%content)
409
410 The following rules apply to content(%content) data:
411
412 =head2 type
413
414 If 'type' matches one of the following keys it is replaced by the
415 right hand side value:
416
417   'visa'               => 'CC',
418   'mastercard'         => 'CC',
419   'american express'   => 'CC',
420   'discover'           => 'CC',
421
422 The value of 'type' is used to set transaction_type().  Currently this
423 module only supports the above values.
424
425 =head1 Setting plugnpay parameters from content(%content)
426
427 The following rules are applied to map data to plugnpay parameters
428 from content(%content):
429
430     # plugnpay param     => $content{<key>}
431       publisher-name     => 'login',
432       publisher-password => 'password',
433
434       card-amount        => 'amount',
435       card-number        => 'card_number',
436       card-exp           => \( $month.$year ), # MM/YY from 'expiration'
437       ssl_cvv            => 'cvv2',
438       order-id           => 'invoice_number',
439
440       card-name          => 'name',
441       card-address1      => 'address',
442       card-city          => 'city',
443       card-state         => 'state',
444       card-zip           => 'zip'
445       card-country       => 'country',
446       orderID            => 'order_number'     # can be set via order_number()
447
448       shipname           => 'ship_first_name' . ' ' . 'ship_last_name',
449       address1           => 'ship_address',
450       city               => 'ship_city',
451       state              => 'ship_state',
452       zip                => 'ship_zip',
453       country            => 'ship_country',
454
455
456 =head1 Mapping plugnpay transaction responses to object methods
457
458 The following methods provides access to the transaction response data
459 resulting from a plugnpay request (after submit()) is called:
460
461 =head2 order_number()
462
463 This order_number() method returns the orderID field for transactions
464 to uniquely identify the transaction.
465
466 =head2 result_code()
467
468 The result_code() method returns the resp-code field for transactions.
469 It is the alphanumeric return code indicating the outcome of the attempted
470 transaction.
471
472 =head2 error_message()
473
474 The error_message() method returns the MErrMsg field for transactions.
475 This provides more details about the transaction result.
476
477 =head2 authorization()
478
479 The authorization() method returns the auth-code field,
480 which is the approval code obtained from the card processing network.
481
482 =head2 avs_code()
483
484 The avs_code() method returns the avs-code field from the transaction result.
485
486 =head2 cvv2_response()
487
488 The cvv2_response() method returns the cvvresp field, which is a
489 response message returned with the transaction result.
490
491 =head2 expdate_mmyy()
492
493 The expdate_mmyy() method takes a single scalar argument (typically
494 the value in $content{expiration}) and attempts to parse and format
495 and put the date in MM/YY format as required by the plugnpay
496 specification.  If unable to parse the expiration date simply leave it
497 as is and let the plugnpay system attempt to handle it as-is.
498
499 =head2 debug()
500
501 Enable or disble debugging.  The value specified here will also set
502 $Business::OnlinePayment::HTTPS::DEBUG in submit() to aid in
503 troubleshooting problems.
504
505 =head1 COMPATIBILITY
506
507 This module implements an interface to the plugnpay Remote Client Integration
508 Specification Rev. 10.03.2007
509
510 =head1 AUTHORS
511
512 Jeff Finucane <plugnpay@weasellips.com>
513
514 Based on Business::OnlinePayment::PayflowPro written by Ivan Kohler
515 and Phil Lobbes.
516
517 =head1 SEE ALSO
518
519 perl(1), L<Business::OnlinePayment>, L<Carp>, and the Remote Client Integration
520 Specification from plugnpay.
521
522 =cut