add Reverse Authorization support, patch from dougforpres
[Business-OnlinePayment-IPPay.git] / IPPay.pm
1 package Business::OnlinePayment::IPPay;
2
3 use strict;
4 use Carp;
5 use Tie::IxHash;
6 use XML::Simple;
7 use XML::Writer;
8 use Locale::Country;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 use vars qw($VERSION $DEBUG @ISA $me);
12
13 @ISA = qw(Business::OnlinePayment::HTTPS);
14 $VERSION = '0.07_01';
15 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
16
17 $DEBUG = 0;
18 $me = 'Business::OnlinePayment::IPPay';
19
20 sub _info {
21   {
22     'info_version'          => '0.01',
23     'module_version'        => $VERSION,
24     'supported_types'       => [ qw( CC ECHECK ) ],
25     'supported_actions'     => { 'CC' => [
26                                      'Normal Authorization',
27                                      'Authorization Only',
28                                      'Post Authorization',
29                                      'Void',
30                                      'Credit',
31                                      'Reverse Authorization',
32                                    ],
33                                    'ECHECK' => [
34                                      'Normal Authorization',
35                                      'Void',
36                                      'Credit',
37                                    ],
38                                  },
39     'CC_void_requires_card' => 1,
40     'ECHECK_void_requires_account' => 1,
41   };
42 }
43
44 sub set_defaults {
45     my $self = shift;
46     my %opts = @_;
47
48     # standard B::OP methods/data
49     $self->server('gateway17.jetpay.com') unless $self->server;
50     $self->port('443') unless $self->port;
51     $self->path('/jetpay') unless $self->path;
52
53     $self->build_subs(qw( 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     my %_defaults = ();
64     foreach my $key (keys %opts) {
65       $key =~ /^default_(\w*)$/ or next;
66       $_defaults{$1} = $opts{$key};
67       delete $opts{$key};
68     }
69     $self->{_defaults} = \%_defaults;
70 }
71
72 sub map_fields {
73     my($self) = @_;
74
75     my %content = $self->content();
76
77     # TYPE MAP
78     my %types = ( 'visa'               => 'CC',
79                   'mastercard'         => 'CC',
80                   'american express'   => 'CC',
81                   'discover'           => 'CC',
82                   'check'              => 'ECHECK',
83                 );
84     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
85     $self->transaction_type($content{'type'});
86     
87     # ACTION MAP 
88     my $action = lc($content{'action'});
89     my %actions =
90       ( 'normal authorization'            => 'SALE',
91         'authorization only'              => 'AUTHONLY',
92         'post authorization'              => 'CAPT',
93         'reverse authorization'           => 'REVERSEAUTH',
94         'void'                            => 'VOID',
95         'credit'                          => 'CREDIT',
96       );
97     my %check_actions =
98       ( 'normal authorization'            => 'CHECK',
99         'void'                            => 'VOIDACH',
100         'credit'                          => 'REVERSAL',
101       );
102     if ($self->transaction_type eq 'CC') {
103       $content{'TransactionType'} = $actions{$action} || $action;
104     }elsif ($self->transaction_type eq 'ECHECK') {
105       $content{'TransactionType'} = $check_actions{$action} || $action;
106     }
107
108
109     # ACCOUNT TYPE MAP
110     my %account_types = ('personal checking'   => 'Checking',
111                          'personal savings'    => 'Savings',
112                          'business checking'   => 'BusinessCk',
113                         );
114     $content{'account_type'} = $account_types{lc($content{'account_type'})}
115                                || $content{'account_type'};
116
117     $content{Origin} = 'RECURRING' 
118       if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
119
120     # stuff it back into %content
121     $self->content(%content);
122
123 }
124
125 sub expdate_month {
126   my ($self, $exp) = (shift, shift);
127   my $month;
128   if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
129     $month  = sprintf( "%02d", $1 );
130   }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
131     $month  = sprintf( "%02d", $1 );
132   }
133   return $month;
134 }
135
136 sub expdate_year {
137   my ($self, $exp) = (shift, shift);
138   my $year;
139   if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
140     $year  = sprintf( "%02d", $1 );
141   }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
142     $year  = sprintf( "%02d", $1 );
143   }
144   return $year;
145 }
146
147 sub revmap_fields {
148   my $self = shift;
149   tie my(%map), 'Tie::IxHash', @_;
150   my %content = $self->content();
151   map {
152         my $value;
153         if ( ref( $map{$_} ) eq 'HASH' ) {
154           $value = $map{$_} if ( keys %{ $map{$_} } );
155         }elsif( ref( $map{$_} ) ) {
156           $value = ${ $map{$_} };
157         }elsif( exists( $content{ $map{$_} } ) ) {
158           $value = $content{ $map{$_} };
159         }
160
161         if (defined($value)) {
162           ($_ => $value);
163         }else{
164           ();
165         }
166       } (keys %map);
167 }
168
169 sub submit {
170   my($self) = @_;
171
172   $self->is_success(0);
173   $self->map_fields();
174
175   my @required_fields = qw(action login type);
176
177   my $action = lc($self->{_content}->{action});
178   my $type = $self->transaction_type();
179   if ( $action eq 'normal authorization'
180     || $action eq 'credit'
181     || $action eq 'authorization only' && $type eq 'CC')
182   {
183     push @required_fields, qw( amount );
184
185     push @required_fields, qw( card_number expiration )
186       if ($type eq "CC"); 
187         
188     push @required_fields,
189       qw( routing_code account_number name ) # account_type
190       if ($type eq "ECHECK");
191         
192   }elsif ( $action eq 'post authorization' && $type eq 'CC') {
193     push @required_fields, qw( order_number );
194   }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
195     push @required_fields, qw( order_number card_number expiration amount );
196   }elsif ( $action eq 'void') {
197     push @required_fields, qw( order_number amount );
198
199     push @required_fields, qw( authorization card_number )
200       if ($type eq "CC");
201
202     push @required_fields,
203       qw( routing_code account_number name ) # account_type
204       if ($type eq "ECHECK");
205
206   }else{
207     croak "$me can't handle transaction type: ".
208       $self->{_content}->{action}. " for ".
209       $self->transaction_type();
210   }
211
212   my %content = $self->content();
213   foreach ( keys ( %{($self->{_defaults})} ) ) {
214     $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
215   }
216   $self->content(%content);
217
218   $self->required_fields(@required_fields);
219
220   #quick validation because ippay dumps an error indecipherable to the end user
221   if (grep { /^routing_code$/ } @required_fields) {
222     unless( $content{routing_code} =~ /^\d{9}$/ ) {
223       $self->_error_response('Invalid routing code');
224       return;
225     }
226   }
227
228   if ($self->test_transaction()) {
229     $self->server('test1.jetpay.com');
230     $self->port('443');
231     $self->path('/jetpay');
232   }
233
234   my $transaction_id = $content{'order_number'};
235   unless ($transaction_id) {
236     my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
237     warn "fetched transaction id: (HTTPS response: $server_response) ".
238          "(HTTPS headers: ".
239          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
240          "(Raw HTTPS content: $page)"
241       if $DEBUG;
242     return unless $server_response=~ /^200/;
243     $transaction_id = $page;
244   }
245
246   my $cardexpmonth = $self->expdate_month($content{expiration});
247   my $cardexpyear  = $self->expdate_year($content{expiration});
248   my $cardstartmonth = $self->expdate_month($content{card_start});
249   my $cardstartyear  = $self->expdate_year($content{card_start});
250  
251   my $amount;
252   if (defined($content{amount})) {
253     $amount = sprintf("%.2f", $content{amount});
254     $amount =~ s/\.//;
255   }
256
257   my $check_number = $content{check_number} || "100"  # make one up
258     if($content{account_number});
259
260   my $terminalid = $content{login} if $type eq 'CC';
261   my $merchantid = $content{login} if $type eq 'ECHECK';
262
263   my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
264   $country  = country_code2code( $content{country},
265                                  LOCALE_CODE_ALPHA_2,
266                                  LOCALE_CODE_ALPHA_3
267                                )
268     unless $country;
269   $country = $content{country}
270     unless $country;
271   $country = uc($country) if $country;
272
273   my $ship_country =
274     country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
275   $ship_country  = country_code2code( $content{ship_country},
276                                  LOCALE_CODE_ALPHA_2,
277                                  LOCALE_CODE_ALPHA_3
278                                )
279     unless $ship_country;
280   $ship_country = $content{ship_country}
281     unless $ship_country;
282   $ship_country = uc($ship_country) if $ship_country;
283
284   tie my %ach, 'Tie::IxHash',
285     $self->revmap_fields(
286                           #AccountType         => 'account_type',
287                           AccountNumber       => 'account_number',
288                           ABA                 => 'routing_code',
289                           CheckNumber         => \$check_number,
290                         );
291
292   tie my %industryinfo, 'Tie::IxHash',
293     $self->revmap_fields(
294                           Type                => 'IndustryInfo',
295                         );
296
297   tie my %shippingaddr, 'Tie::IxHash',
298     $self->revmap_fields(
299                           Address             => 'ship_address',
300                           City                => 'ship_city',
301                           StateProv           => 'ship_state',
302                           Country             => \$ship_country,
303                           Phone               => 'ship_phone',
304                         );
305
306   unless ( $type ne 'CC' || keys %shippingaddr ) {
307     tie %shippingaddr, 'Tie::IxHash',
308       $self->revmap_fields(
309                             Address             => 'address',
310                             City                => 'city',
311                             StateProv           => 'state',
312                             Country             => \$country,
313                             Phone               => 'phone',
314                           );
315   }
316   delete $shippingaddr{Country} unless $shippingaddr{Country};
317
318   tie my %shippinginfo, 'Tie::IxHash',
319     $self->revmap_fields(
320                           CustomerPO          => 'CustomerPO',
321                           ShippingMethod      => 'ShippingMethod',
322                           ShippingName        => 'ship_name',
323                           ShippingAddr        => \%shippingaddr,
324                         );
325
326   tie my %req, 'Tie::IxHash',
327     $self->revmap_fields(
328                           TransactionType     => 'TransactionType',
329                           TerminalID          => 'login',
330 #                          TerminalID          => \$terminalid,
331 #                          MerchantID          => \$merchantid,
332                           TransactionID       => \$transaction_id,
333                           RoutingCode         => 'RoutingCode',
334                           Approval            => 'authorization',
335                           BatchID             => 'BatchID',
336                           Origin              => 'Origin',
337                           Password            => 'password',
338                           OrderNumber         => 'invoice_number',
339                           CardNum             => 'card_number',
340                           CVV2                => 'cvv2',
341                           Issue               => 'issue_number',
342                           CardExpMonth        => \$cardexpmonth,
343                           CardExpYear         => \$cardexpyear,
344                           CardStartMonth      => \$cardstartmonth,
345                           CardStartYear       => \$cardstartyear,
346                           Track1              => 'track1',
347                           Track2              => 'track2',
348                           ACH                 => \%ach,
349                           CardName            => 'name',
350                           DispositionType     => 'DispositionType',
351                           TotalAmount         => \$amount,
352                           FeeAmount           => 'FeeAmount',
353                           TaxAmount           => 'TaxAmount',
354                           BillingAddress      => 'address',
355                           BillingCity         => 'city',
356                           BillingStateProv    => 'state',
357                           BillingPostalCode   => 'zip',
358                           BillingCountry      => \$country,
359                           BillingPhone        => 'phone',
360                           Email               => 'email',
361                           UserIPAddr          => 'customer_ip',
362                           UserHost            => 'UserHost',
363                           UDField1            => 'UDField1',
364                           UDField2            => 'UDField2',
365                           UDField3            => 'UDField3',
366                           ActionCode          => 'ActionCode',
367                           IndustryInfo        => \%industryinfo,
368                           ShippingInfo        => \%shippinginfo,
369                         );
370   delete $req{BillingCountry} unless $req{BillingCountry};
371
372   my $post_data;
373   my $writer = new XML::Writer( OUTPUT      => \$post_data,
374                                 DATA_MODE   => 1,
375                                 DATA_INDENT => 1,
376                                 ENCODING    => 'us-ascii',
377                               );
378   $writer->xmlDecl();
379   $writer->startTag('JetPay');
380   foreach ( keys ( %req ) ) {
381     $self->_xmlwrite($writer, $_, $req{$_});
382   }
383   $writer->endTag('JetPay');
384   $writer->end();
385
386   warn "$post_data\n" if $DEBUG;
387
388   my ($page,$server_response,%headers) = $self->https_post($post_data);
389
390   warn "$page\n" if $DEBUG;
391
392   my $response = {};
393   if ($server_response =~ /^200/){
394     $response = XMLin($page);
395     if (  exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
396       $self->error_message($response->{ResponseText});
397     }else{
398       $self->error_message($response->{Errmsg});
399     }
400 #  }else{
401 #    $self->error_message("Server Failed");
402   }
403
404   $self->result_code($response->{ActionCode} || '');
405   $self->order_number($response->{TransactionID} || '');
406   $self->authorization($response->{Approval} || '');
407   $self->cvv2_response($response->{CVV2} || '');
408   $self->avs_code($response->{AVS} || '');
409
410   $self->is_success($self->result_code() eq '000' ? 1 : 0);
411
412   unless ($self->is_success()) {
413     unless ( $self->error_message() ) { #additional logging information
414       $self->error_message(
415         "(HTTPS response: $server_response) ".
416         "(HTTPS headers: ".
417           join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
418         "(Raw HTTPS content: $page)"
419       );
420     }
421   }
422
423 }
424
425 sub _error_response {
426   my ($self, $error_message) = (shift, shift);
427   $self->result_code('');
428   $self->order_number('');
429   $self->authorization('');
430   $self->cvv2_response('');
431   $self->avs_code('');
432   $self->is_success( 0);
433   $self->error_message($error_message);
434 }
435
436 sub _xmlwrite {
437   my ($self, $writer, $item, $value) = @_;
438   $writer->startTag($item);
439   if ( ref( $value ) eq 'HASH' ) {
440     foreach ( keys ( %$value ) ) {
441       $self->_xmlwrite($writer, $_, $value->{$_});
442     }
443   }else{
444     $writer->characters($value);
445   }
446   $writer->endTag($item);
447 }
448
449 1;
450
451 __END__
452
453 =head1 NAME
454
455 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
456
457 =head1 SYNOPSIS
458
459   use Business::OnlinePayment;
460
461   my $tx =
462     new Business::OnlinePayment( "IPPay",
463                                  'default_Origin' => 'PHONE ORDER',
464                                );
465   $tx->content(
466       type           => 'VISA',
467       login          => 'testdrive',
468       password       => '', #password 
469       action         => 'Normal Authorization',
470       description    => 'Business::OnlinePayment test',
471       amount         => '49.95',
472       customer_id    => 'tfb',
473       name           => 'Tofu Beast',
474       address        => '123 Anystreet',
475       city           => 'Anywhere',
476       state          => 'UT',
477       zip            => '84058',
478       card_number    => '4007000000027',
479       expiration     => '09/02',
480       cvv2           => '1234', #optional
481   );
482   $tx->submit();
483
484   if($tx->is_success()) {
485       print "Card processed successfully: ".$tx->authorization."\n";
486   } else {
487       print "Card was rejected: ".$tx->error_message."\n";
488   }
489
490 =head1 SUPPORTED TRANSACTION TYPES
491
492 =head2 CC, Visa, MasterCard, American Express, Discover
493
494 Content required: type, login, action, amount, card_number, expiration.
495
496 =head2 Check
497
498 Content required: type, login, action, amount, name, account_number, routing_code.
499
500 =head1 DESCRIPTION
501
502 For detailed information see L<Business::OnlinePayment>.
503
504 =head1 METHODS AND FUNCTIONS
505
506 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
507
508 =head2 result_code
509
510 Returns the response error code.
511
512 =head2 error_message
513
514 Returns the response error description text.
515
516 =head2 server_response
517
518 Returns the complete response from the server.
519
520 =head1 Handling of content(%content) data:
521
522 =head2 action
523
524 The following actions are valid
525
526   normal authorization
527   authorization only
528   reverse authorization
529   post authorization
530   credit
531   void
532
533 =head1 Setting IPPay parameters from content(%content)
534
535 The following rules are applied to map data to IPPay parameters
536 from content(%content):
537
538       # param => $content{<key>}
539       TransactionType     => 'TransactionType',
540       TerminalID          => 'login',
541       TransactionID       => 'order_number',
542       RoutingCode         => 'RoutingCode',
543       Approval            => 'authorization',
544       BatchID             => 'BatchID',
545       Origin              => 'Origin',
546       Password            => 'password',
547       OrderNumber         => 'invoice_number',
548       CardNum             => 'card_number',
549       CVV2                => 'cvv2',
550       Issue               => 'issue_number',
551       CardExpMonth        => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
552       CardExpYear         => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
553       CardStartMonth      => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
554       CardStartYear       => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
555       Track1              => 'track1',
556       Track2              => 'track2',
557       ACH
558         AccountNumber       => 'account_number',
559         ABA                 => 'routing_code',
560         CheckNumber         => 'check_number',
561       CardName            => 'name',
562       DispositionType     => 'DispositionType',
563       TotalAmount         => 'amount' reformatted into cents
564       FeeAmount           => 'FeeAmount',
565       TaxAmount           => 'TaxAmount',
566       BillingAddress      => 'address',
567       BillingCity         => 'city',
568       BillingStateProv    => 'state',
569       BillingPostalCode   => 'zip',
570       BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
571       BillingPhone        => 'phone',
572       Email               => 'email',
573       UserIPAddr          => 'customer_ip',
574       UserHost            => 'UserHost',
575       UDField1            => 'UDField1',
576       UDField2            => 'UDField2',
577       UDField3            => 'UDField3',
578       ActionCode          => 'ActionCode',
579       IndustryInfo
580         Type                => 'IndustryInfo',
581       ShippingInfo
582         CustomerPO          => 'CustomerPO',
583         ShippingMethod      => 'ShippingMethod',
584         ShippingName        => 'ship_name',
585         ShippingAddr
586           Address             => 'ship_address',
587           City                => 'ship_city',
588           StateProv           => 'ship_state',
589           Country             => 'ship_country',  # forced to ISO-3166-alpha-3
590           Phone               => 'ship_phone',
591
592 =head1 NOTE
593
594 =head1 COMPATIBILITY
595
596 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
597 1.1.2.
598
599 See http://www.ippay.com/ for more information.
600
601 =head1 AUTHORS
602
603 Original author: Jeff Finucane
604
605 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
606
607 Reverse Authorization patch from dougforpres
608
609 =head1 SEE ALSO
610
611 perl(1). L<Business::OnlinePayment>.
612
613 =cut
614