s/JetPay/ippay/ per new spec
[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.11_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_compat'           => '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     $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
58
59     # module specific data
60     my %_defaults = ();
61     foreach my $key (keys %opts) {
62       $key =~ /^default_(\w*)$/ or next;
63       $_defaults{$1} = $opts{$key};
64       delete $opts{$key};
65     }
66     $self->{_defaults} = \%_defaults;
67 }
68
69 sub map_fields {
70     my($self) = @_;
71
72     my %content = $self->content();
73
74     # TYPE MAP
75     my %types = ( 'visa'               => 'CC',
76                   'mastercard'         => 'CC',
77                   'american express'   => 'CC',
78                   'discover'           => 'CC',
79                   'check'              => 'ECHECK',
80                 );
81     $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
82     $self->transaction_type($content{'type'});
83     
84     # ACTION MAP 
85     my $action = lc($content{'action'});
86     my %actions =
87       ( 'normal authorization'            => 'SALE',
88         'authorization only'              => 'AUTHONLY',
89         'post authorization'              => 'CAPT',
90         'reverse authorization'           => 'REVERSEAUTH',
91         'void'                            => 'VOID',
92         'credit'                          => 'CREDIT',
93       );
94     my %check_actions =
95       ( 'normal authorization'            => 'CHECK',
96         'void'                            => 'VOIDACH',
97         'credit'                          => 'REVERSAL',
98       );
99
100     if ($self->transaction_type eq 'CC') {
101       $content{'TransactionType'} = $actions{$action} || $action;
102     } elsif ($self->transaction_type eq 'ECHECK') {
103
104       $content{'TransactionType'} = $check_actions{$action} || $action;
105
106       # ACCOUNT TYPE MAP
107       my %account_types = ('personal checking'   => 'CHECKING',
108                            'personal savings'    => 'SAVINGS',
109                            'business checking'   => 'CHECKING',
110                            'business savings'    => 'SAVINGS',
111                            #not technically B:OP valid i guess?
112                            'checking'            => 'CHECKING',
113                            'savings'             => 'SAVINGS',
114                           );
115       $content{'account_type'} = $account_types{lc($content{'account_type'})}
116                                  || $content{'account_type'};
117     }
118
119     $content{Origin} = 'RECURRING' 
120       if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
121
122     # stuff it back into %content
123     $self->content(%content);
124
125 }
126
127 sub expdate_month {
128   my ($self, $exp) = (shift, shift);
129   my $month;
130   if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
131     $month  = sprintf( "%02d", $1 );
132   }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
133     $month  = sprintf( "%02d", $1 );
134   }
135   return $month;
136 }
137
138 sub expdate_year {
139   my ($self, $exp) = (shift, shift);
140   my $year;
141   if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
142     $year  = sprintf( "%02d", $1 );
143   }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
144     $year  = sprintf( "%02d", $1 );
145   }
146   return $year;
147 }
148
149 sub revmap_fields {
150   my $self = shift;
151   tie my(%map), 'Tie::IxHash', @_;
152   my %content = $self->content();
153   map {
154         my $value;
155         if ( ref( $map{$_} ) eq 'HASH' ) {
156           $value = $map{$_} if ( keys %{ $map{$_} } );
157         }elsif( ref( $map{$_} ) ) {
158           $value = ${ $map{$_} };
159         }elsif( exists( $content{ $map{$_} } ) ) {
160           $value = $content{ $map{$_} };
161         }
162
163         if (defined($value)) {
164           ($_ => $value);
165         }else{
166           ();
167         }
168       } (keys %map);
169 }
170
171 sub submit {
172   my($self) = @_;
173
174   $self->is_success(0);
175   $self->map_fields();
176
177   my @required_fields = qw(action login password type);
178
179   my $action = lc($self->{_content}->{action});
180   my $type = $self->transaction_type();
181   if ( $action eq 'normal authorization'
182     || $action eq 'credit'
183     || $action eq 'authorization only' && $type eq 'CC')
184   {
185     push @required_fields, qw( amount );
186
187     push @required_fields, qw( card_number expiration )
188       if ($type eq "CC"); 
189         
190     push @required_fields,
191       qw( routing_code account_number name ) # account_type
192       if ($type eq "ECHECK");
193         
194   }elsif ( $action eq 'post authorization' && $type eq 'CC') {
195     push @required_fields, qw( order_number );
196   }elsif ( $action eq 'reverse authorization' && $type eq 'CC') {
197     push @required_fields, qw( order_number card_number expiration amount );
198   }elsif ( $action eq 'void') {
199     push @required_fields, qw( order_number amount );
200
201     push @required_fields, qw( authorization card_number )
202       if ($type eq "CC");
203
204     push @required_fields,
205       qw( routing_code account_number name ) # account_type
206       if ($type eq "ECHECK");
207
208   }else{
209     croak "$me can't handle transaction type: ".
210       $self->{_content}->{action}. " for ".
211       $self->transaction_type();
212   }
213
214   my %content = $self->content();
215   foreach ( keys ( %{($self->{_defaults})} ) ) {
216     $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
217   }
218   if ($self->test_transaction()) {
219     $content{'login'} = 'TESTTERMINAL';
220     $self->server('testgtwy.ippay.com') if $self->server eq 'gtwy.ippay.com';
221   }
222   $self->content(%content);
223
224   $self->required_fields(@required_fields);
225
226   #quick validation because ippay dumps an error indecipherable to the end user
227   if (grep { /^routing_code$/ } @required_fields) {
228     unless( $content{routing_code} =~ /^\d{9}$/ ) {
229       $self->_error_response('Invalid routing code');
230       return;
231     }
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 > 1;
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                           #wtf, this is a "Type"" attribute of the ACH element,
287                           # not a child element like the others
288                           #AccountType         => 'account_type',
289                           AccountNumber       => 'account_number',
290                           ABA                 => 'routing_code',
291                           CheckNumber         => \$check_number,
292                         );
293
294   tie my %industryinfo, 'Tie::IxHash',
295     $self->revmap_fields(
296                           Type                => 'IndustryInfo',
297                         );
298
299   tie my %shippingaddr, 'Tie::IxHash',
300     $self->revmap_fields(
301                           Address             => 'ship_address',
302                           City                => 'ship_city',
303                           StateProv           => 'ship_state',
304                           Country             => \$ship_country,
305                           Phone               => 'ship_phone',
306                         );
307
308   unless ( $type ne 'CC' || keys %shippingaddr ) {
309     tie %shippingaddr, 'Tie::IxHash',
310       $self->revmap_fields(
311                             Address             => 'address',
312                             City                => 'city',
313                             StateProv           => 'state',
314                             Country             => \$country,
315                             Phone               => 'phone',
316                           );
317   }
318   delete $shippingaddr{Country} unless $shippingaddr{Country};
319
320   tie my %shippinginfo, 'Tie::IxHash',
321     $self->revmap_fields(
322                           CustomerPO          => 'CustomerPO',
323                           ShippingMethod      => 'ShippingMethod',
324                           ShippingName        => 'ship_name',
325                           ShippingAddr        => \%shippingaddr,
326                         );
327
328   tie my %req, 'Tie::IxHash',
329     $self->revmap_fields(
330                           TransactionType     => 'TransactionType',
331                           TerminalID          => 'login',
332 #                          TerminalID          => \$terminalid,
333 #                          MerchantID          => \$merchantid,
334                           TransactionID       => \$transaction_id,
335                           RoutingCode         => 'RoutingCode',
336                           Approval            => 'authorization',
337                           BatchID             => 'BatchID',
338                           Origin              => 'Origin',
339                           Password            => 'password',
340                           OrderNumber         => 'invoice_number',
341                           CardNum             => 'card_number',
342                           CVV2                => 'cvv2',
343                           Issue               => 'issue_number',
344                           CardExpMonth        => \$cardexpmonth,
345                           CardExpYear         => \$cardexpyear,
346                           CardStartMonth      => \$cardstartmonth,
347                           CardStartYear       => \$cardstartyear,
348                           Track1              => 'track1',
349                           Track2              => 'track2',
350                           ACH                 => \%ach,
351                           CardName            => 'name',
352                           DispositionType     => 'DispositionType',
353                           TotalAmount         => \$amount,
354                           FeeAmount           => 'FeeAmount',
355                           TaxAmount           => 'TaxAmount',
356                           BillingAddress      => 'address',
357                           BillingCity         => 'city',
358                           BillingStateProv    => 'state',
359                           BillingPostalCode   => 'zip',
360                           BillingCountry      => \$country,
361                           BillingPhone        => 'phone',
362                           Email               => 'email',
363                           UserIPAddress       => 'customer_ip',
364                           UserHost            => 'UserHost',
365                           UDField1            => 'UDField1',
366                           UDField2            => 'UDField2',
367                           UDField3            => \"$me $VERSION", #'UDField3',
368                           ActionCode          => 'ActionCode',
369                           IndustryInfo        => \%industryinfo,
370                           ShippingInfo        => \%shippinginfo,
371                         );
372   delete $req{BillingCountry} unless $req{BillingCountry};
373
374   my $post_data;
375   my $writer = new XML::Writer( OUTPUT      => \$post_data,
376                                 DATA_MODE   => 1,
377                                 DATA_INDENT => 1,
378                                 ENCODING    => 'us-ascii',
379                               );
380   $writer->xmlDecl();
381   $writer->startTag('ippay');
382   foreach ( keys ( %req ) ) {
383     $self->_xmlwrite($writer, $_, $req{$_});
384   }
385   $writer->endTag('ippay');
386   $writer->end();
387
388   warn "$post_data\n" if $DEBUG > 1;
389
390   my ($page,$server_response,%headers) = $self->https_post($post_data);
391
392   warn "$page\n" if $DEBUG > 1;
393
394   my $response = {};
395   if ($server_response =~ /^200/){
396     $response = XMLin($page);
397     if (  exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
398       $self->error_message($response->{ResponseText});
399     }else{
400       $self->error_message($response->{ErrMsg});
401     }
402 #  }else{
403 #    $self->error_message("Server Failed");
404   }
405
406   $self->result_code($response->{ActionCode} || '');
407   $self->order_number($response->{TransactionID} || '');
408   $self->authorization($response->{Approval} || '');
409   $self->cvv2_response($response->{CVV2} || '');
410   $self->avs_code($response->{AVS} || '');
411
412   $self->is_success($self->result_code() eq '000' ? 1 : 0);
413
414   unless ($self->is_success()) {
415     unless ( $self->error_message() ) {
416       if ( $DEBUG ) {
417         #additional logging information, possibly too sensitive for an error msg
418         # (IPPay seems to have a failure mode where they return the full
419         #  original request including card number)
420         $self->error_message(
421           "(HTTPS response: $server_response) ".
422           "(HTTPS headers: ".
423             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
424           "(Raw HTTPS content: $page)"
425         );
426       } else {
427         $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
428       }
429     }
430   }
431
432 }
433
434 sub _error_response {
435   my ($self, $error_message) = (shift, shift);
436   $self->result_code('');
437   $self->order_number('');
438   $self->authorization('');
439   $self->cvv2_response('');
440   $self->avs_code('');
441   $self->is_success( 0);
442   $self->error_message($error_message);
443 }
444
445 sub _xmlwrite {
446   my ($self, $writer, $item, $value) = @_;
447
448   my %att = ();
449   if ( $item eq 'ACH' ) {
450     $att{'Type'} = $self->{_content}->{'account_type'}
451       if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
452     $att{'SEC'}  = $self->{_content}->{'nacha_sec_code'}
453                  || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
454   }
455
456   $writer->startTag($item, %att);
457
458   if ( ref( $value ) eq 'HASH' ) {
459     foreach ( keys ( %$value ) ) {
460       $self->_xmlwrite($writer, $_, $value->{$_});
461     }
462   }else{
463     $writer->characters($value);
464   }
465
466   $writer->endTag($item);
467 }
468
469 1;
470
471 __END__
472
473 =head1 NAME
474
475 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
476
477 =head1 SYNOPSIS
478
479   use Business::OnlinePayment;
480
481   my $tx =
482     new Business::OnlinePayment( "IPPay",
483                                  'default_Origin' => 'PHONE ORDER',
484                                );
485   $tx->content(
486       type           => 'VISA',
487       login          => 'testdrive',
488       password       => '', #password 
489       action         => 'Normal Authorization',
490       description    => 'Business::OnlinePayment test',
491       amount         => '49.95',
492       customer_id    => 'tfb',
493       name           => 'Tofu Beast',
494       address        => '123 Anystreet',
495       city           => 'Anywhere',
496       state          => 'UT',
497       zip            => '84058',
498       card_number    => '4007000000027',
499       expiration     => '09/02',
500       cvv2           => '1234', #optional
501   );
502   $tx->submit();
503
504   if($tx->is_success()) {
505       print "Card processed successfully: ".$tx->authorization."\n";
506   } else {
507       print "Card was rejected: ".$tx->error_message."\n";
508   }
509
510 =head1 SUPPORTED TRANSACTION TYPES
511
512 =head2 CC, Visa, MasterCard, American Express, Discover
513
514 Content required: type, login, action, amount, card_number, expiration.
515
516 =head2 Check
517
518 Content required: type, login, action, amount, name, account_number, routing_code.
519
520 =head1 DESCRIPTION
521
522 For detailed information see L<Business::OnlinePayment>.
523
524 =head1 METHODS AND FUNCTIONS
525
526 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
527
528 =head2 result_code
529
530 Returns the response error code.
531
532 =head2 error_message
533
534 Returns the response error description text.
535
536 =head2 server_response
537
538 Returns the complete response from the server.
539
540 =head1 Handling of content(%content) data:
541
542 =head2 action
543
544 The following actions are valid
545
546   normal authorization
547   authorization only
548   reverse authorization
549   post authorization
550   credit
551   void
552
553 =head1 Setting IPPay parameters from content(%content)
554
555 The following rules are applied to map data to IPPay parameters
556 from content(%content):
557
558       # param => $content{<key>}
559       TransactionType     => 'TransactionType',
560       TerminalID          => 'login',
561       TransactionID       => 'order_number',
562       RoutingCode         => 'RoutingCode',
563       Approval            => 'authorization',
564       BatchID             => 'BatchID',
565       Origin              => 'Origin',
566       Password            => 'password',
567       OrderNumber         => 'invoice_number',
568       CardNum             => 'card_number',
569       CVV2                => 'cvv2',
570       Issue               => 'issue_number',
571       CardExpMonth        => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
572       CardExpYear         => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
573       CardStartMonth      => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
574       CardStartYear       => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
575       Track1              => 'track1',
576       Track2              => 'track2',
577       ACH
578         AccountNumber       => 'account_number',
579         ABA                 => 'routing_code',
580         CheckNumber         => 'check_number',
581       CardName            => 'name',
582       DispositionType     => 'DispositionType',
583       TotalAmount         => 'amount' reformatted into cents
584       FeeAmount           => 'FeeAmount',
585       TaxAmount           => 'TaxAmount',
586       BillingAddress      => 'address',
587       BillingCity         => 'city',
588       BillingStateProv    => 'state',
589       BillingPostalCode   => 'zip',
590       BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
591       BillingPhone        => 'phone',
592       Email               => 'email',
593       UserIPAddress        => 'customer_ip',
594       UserHost            => 'UserHost',
595       UDField1            => 'UDField1',
596       UDField2            => 'UDField2',
597       ActionCode          => 'ActionCode',
598       IndustryInfo
599         Type                => 'IndustryInfo',
600       ShippingInfo
601         CustomerPO          => 'CustomerPO',
602         ShippingMethod      => 'ShippingMethod',
603         ShippingName        => 'ship_name',
604         ShippingAddr
605           Address             => 'ship_address',
606           City                => 'ship_city',
607           StateProv           => 'ship_state',
608           Country             => 'ship_country',  # forced to ISO-3166-alpha-3
609           Phone               => 'ship_phone',
610
611 =head1 NOTE
612
613 =head1 COMPATIBILITY
614
615 Version 0.07 changes the server name and path for IPPay's late 2012 update.
616
617 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
618 1.1.2.
619
620 See http://www.ippay.com/ for more information.
621
622 =head1 AUTHORS
623
624 Original author: Jeff Finucane
625
626 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
627
628 Reverse Authorization patch from dougforpres
629
630 =head1 COPYRIGHT AND LICENSE
631
632 Copyright (c) 1999 Jason Kohles
633 Copyright (c) 2002-2003 Ivan Kohler
634 Copyright (c) 2008-2021 Freeside Internet Services, Inc.
635
636 All rights reserved. This program is free software; you can redistribute it
637 and/or modify it under the same terms as Perl itself.
638
639 =head1 ADVERTISEMENT
640
641 Need a complete, open-source back-office and customer self-service solution?
642 The Freeside software includes support for credit card and electronic check
643 processing with IPPay and over 50 other gateways, invoicing, integrated
644 trouble ticketing, and customer signup and self-service web interfaces.
645
646 http://freeside.biz/freeside/
647
648 =head1 SEE ALSO
649
650 perl(1). L<Business::OnlinePayment>.
651
652 =cut
653