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