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.10';
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   }
221   $self->content(%content);
222
223   $self->required_fields(@required_fields);
224
225   #quick validation because ippay dumps an error indecipherable to the end user
226   if (grep { /^routing_code$/ } @required_fields) {
227     unless( $content{routing_code} =~ /^\d{9}$/ ) {
228       $self->_error_response('Invalid routing code');
229       return;
230     }
231   }
232
233   my $transaction_id = $content{'order_number'};
234   unless ($transaction_id) {
235     my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
236     warn "fetched transaction id: (HTTPS response: $server_response) ".
237          "(HTTPS headers: ".
238          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
239          "(Raw HTTPS content: $page)"
240       if $DEBUG > 1;
241     return unless $server_response=~ /^200/;
242     $transaction_id = $page;
243   }
244
245   my $cardexpmonth = $self->expdate_month($content{expiration});
246   my $cardexpyear  = $self->expdate_year($content{expiration});
247   my $cardstartmonth = $self->expdate_month($content{card_start});
248   my $cardstartyear  = $self->expdate_year($content{card_start});
249  
250   my $amount;
251   if (defined($content{amount})) {
252     $amount = sprintf("%.2f", $content{amount});
253     $amount =~ s/\.//;
254   }
255
256   my $check_number = $content{check_number} || "100"  # make one up
257     if($content{account_number});
258
259   my $terminalid = $content{login} if $type eq 'CC';
260   my $merchantid = $content{login} if $type eq 'ECHECK';
261
262   my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
263   $country  = country_code2code( $content{country},
264                                  LOCALE_CODE_ALPHA_2,
265                                  LOCALE_CODE_ALPHA_3
266                                )
267     unless $country;
268   $country = $content{country}
269     unless $country;
270   $country = uc($country) if $country;
271
272   my $ship_country =
273     country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
274   $ship_country  = country_code2code( $content{ship_country},
275                                  LOCALE_CODE_ALPHA_2,
276                                  LOCALE_CODE_ALPHA_3
277                                )
278     unless $ship_country;
279   $ship_country = $content{ship_country}
280     unless $ship_country;
281   $ship_country = uc($ship_country) if $ship_country;
282
283   tie my %ach, 'Tie::IxHash',
284     $self->revmap_fields(
285                           #wtf, this is a "Type"" attribute of the ACH element,
286                           # not a child element like the others
287                           #AccountType         => 'account_type',
288                           AccountNumber       => 'account_number',
289                           ABA                 => 'routing_code',
290                           CheckNumber         => \$check_number,
291                         );
292
293   tie my %industryinfo, 'Tie::IxHash',
294     $self->revmap_fields(
295                           Type                => 'IndustryInfo',
296                         );
297
298   tie my %shippingaddr, 'Tie::IxHash',
299     $self->revmap_fields(
300                           Address             => 'ship_address',
301                           City                => 'ship_city',
302                           StateProv           => 'ship_state',
303                           Country             => \$ship_country,
304                           Phone               => 'ship_phone',
305                         );
306
307   unless ( $type ne 'CC' || keys %shippingaddr ) {
308     tie %shippingaddr, 'Tie::IxHash',
309       $self->revmap_fields(
310                             Address             => 'address',
311                             City                => 'city',
312                             StateProv           => 'state',
313                             Country             => \$country,
314                             Phone               => 'phone',
315                           );
316   }
317   delete $shippingaddr{Country} unless $shippingaddr{Country};
318
319   tie my %shippinginfo, 'Tie::IxHash',
320     $self->revmap_fields(
321                           CustomerPO          => 'CustomerPO',
322                           ShippingMethod      => 'ShippingMethod',
323                           ShippingName        => 'ship_name',
324                           ShippingAddr        => \%shippingaddr,
325                         );
326
327   tie my %req, 'Tie::IxHash',
328     $self->revmap_fields(
329                           TransactionType     => 'TransactionType',
330                           TerminalID          => 'login',
331 #                          TerminalID          => \$terminalid,
332 #                          MerchantID          => \$merchantid,
333                           TransactionID       => \$transaction_id,
334                           RoutingCode         => 'RoutingCode',
335                           Approval            => 'authorization',
336                           BatchID             => 'BatchID',
337                           Origin              => 'Origin',
338                           Password            => 'password',
339                           OrderNumber         => 'invoice_number',
340                           CardNum             => 'card_number',
341                           CVV2                => 'cvv2',
342                           Issue               => 'issue_number',
343                           CardExpMonth        => \$cardexpmonth,
344                           CardExpYear         => \$cardexpyear,
345                           CardStartMonth      => \$cardstartmonth,
346                           CardStartYear       => \$cardstartyear,
347                           Track1              => 'track1',
348                           Track2              => 'track2',
349                           ACH                 => \%ach,
350                           CardName            => 'name',
351                           DispositionType     => 'DispositionType',
352                           TotalAmount         => \$amount,
353                           FeeAmount           => 'FeeAmount',
354                           TaxAmount           => 'TaxAmount',
355                           BillingAddress      => 'address',
356                           BillingCity         => 'city',
357                           BillingStateProv    => 'state',
358                           BillingPostalCode   => 'zip',
359                           BillingCountry      => \$country,
360                           BillingPhone        => 'phone',
361                           Email               => 'email',
362                           UserIPAddress       => 'customer_ip',
363                           UserHost            => 'UserHost',
364                           UDField1            => 'UDField1',
365                           UDField2            => 'UDField2',
366                           UDField3            => \"$me $VERSION", #'UDField3',
367                           ActionCode          => 'ActionCode',
368                           IndustryInfo        => \%industryinfo,
369                           ShippingInfo        => \%shippinginfo,
370                         );
371   delete $req{BillingCountry} unless $req{BillingCountry};
372
373   my $post_data;
374   my $writer = new XML::Writer( OUTPUT      => \$post_data,
375                                 DATA_MODE   => 1,
376                                 DATA_INDENT => 1,
377                                 ENCODING    => 'us-ascii',
378                               );
379   $writer->xmlDecl();
380   $writer->startTag('JetPay');
381   foreach ( keys ( %req ) ) {
382     $self->_xmlwrite($writer, $_, $req{$_});
383   }
384   $writer->endTag('JetPay');
385   $writer->end();
386
387   warn "$post_data\n" if $DEBUG > 1;
388
389   my ($page,$server_response,%headers) = $self->https_post($post_data);
390
391   warn "$page\n" if $DEBUG > 1;
392
393   my $response = {};
394   if ($server_response =~ /^200/){
395     $response = XMLin($page);
396     if (  exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
397       $self->error_message($response->{ResponseText});
398     }else{
399       $self->error_message($response->{ErrMsg});
400     }
401 #  }else{
402 #    $self->error_message("Server Failed");
403   }
404
405   $self->result_code($response->{ActionCode} || '');
406   $self->order_number($response->{TransactionID} || '');
407   $self->authorization($response->{Approval} || '');
408   $self->cvv2_response($response->{CVV2} || '');
409   $self->avs_code($response->{AVS} || '');
410
411   $self->is_success($self->result_code() eq '000' ? 1 : 0);
412
413   unless ($self->is_success()) {
414     unless ( $self->error_message() ) {
415       if ( $DEBUG ) {
416         #additional logging information, possibly too sensitive for an error msg
417         # (IPPay seems to have a failure mode where they return the full
418         #  original request including card number)
419         $self->error_message(
420           "(HTTPS response: $server_response) ".
421           "(HTTPS headers: ".
422             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
423           "(Raw HTTPS content: $page)"
424         );
425       } else {
426         $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
427       }
428     }
429   }
430
431 }
432
433 sub _error_response {
434   my ($self, $error_message) = (shift, shift);
435   $self->result_code('');
436   $self->order_number('');
437   $self->authorization('');
438   $self->cvv2_response('');
439   $self->avs_code('');
440   $self->is_success( 0);
441   $self->error_message($error_message);
442 }
443
444 sub _xmlwrite {
445   my ($self, $writer, $item, $value) = @_;
446
447   my %att = ();
448   if ( $item eq 'ACH' ) {
449     $att{'Type'} = $self->{_content}->{'account_type'}
450       if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
451     $att{'SEC'}  = 'PPD';
452   }
453
454   $writer->startTag($item, %att);
455
456   if ( ref( $value ) eq 'HASH' ) {
457     foreach ( keys ( %$value ) ) {
458       $self->_xmlwrite($writer, $_, $value->{$_});
459     }
460   }else{
461     $writer->characters($value);
462   }
463
464   $writer->endTag($item);
465 }
466
467 1;
468
469 __END__
470
471 =head1 NAME
472
473 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
474
475 =head1 SYNOPSIS
476
477   use Business::OnlinePayment;
478
479   my $tx =
480     new Business::OnlinePayment( "IPPay",
481                                  'default_Origin' => 'PHONE ORDER',
482                                );
483   $tx->content(
484       type           => 'VISA',
485       login          => 'testdrive',
486       password       => '', #password 
487       action         => 'Normal Authorization',
488       description    => 'Business::OnlinePayment test',
489       amount         => '49.95',
490       customer_id    => 'tfb',
491       name           => 'Tofu Beast',
492       address        => '123 Anystreet',
493       city           => 'Anywhere',
494       state          => 'UT',
495       zip            => '84058',
496       card_number    => '4007000000027',
497       expiration     => '09/02',
498       cvv2           => '1234', #optional
499   );
500   $tx->submit();
501
502   if($tx->is_success()) {
503       print "Card processed successfully: ".$tx->authorization."\n";
504   } else {
505       print "Card was rejected: ".$tx->error_message."\n";
506   }
507
508 =head1 SUPPORTED TRANSACTION TYPES
509
510 =head2 CC, Visa, MasterCard, American Express, Discover
511
512 Content required: type, login, action, amount, card_number, expiration.
513
514 =head2 Check
515
516 Content required: type, login, action, amount, name, account_number, routing_code.
517
518 =head1 DESCRIPTION
519
520 For detailed information see L<Business::OnlinePayment>.
521
522 =head1 METHODS AND FUNCTIONS
523
524 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
525
526 =head2 result_code
527
528 Returns the response error code.
529
530 =head2 error_message
531
532 Returns the response error description text.
533
534 =head2 server_response
535
536 Returns the complete response from the server.
537
538 =head1 Handling of content(%content) data:
539
540 =head2 action
541
542 The following actions are valid
543
544   normal authorization
545   authorization only
546   reverse authorization
547   post authorization
548   credit
549   void
550
551 =head1 Setting IPPay parameters from content(%content)
552
553 The following rules are applied to map data to IPPay parameters
554 from content(%content):
555
556       # param => $content{<key>}
557       TransactionType     => 'TransactionType',
558       TerminalID          => 'login',
559       TransactionID       => 'order_number',
560       RoutingCode         => 'RoutingCode',
561       Approval            => 'authorization',
562       BatchID             => 'BatchID',
563       Origin              => 'Origin',
564       Password            => 'password',
565       OrderNumber         => 'invoice_number',
566       CardNum             => 'card_number',
567       CVV2                => 'cvv2',
568       Issue               => 'issue_number',
569       CardExpMonth        => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
570       CardExpYear         => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
571       CardStartMonth      => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
572       CardStartYear       => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
573       Track1              => 'track1',
574       Track2              => 'track2',
575       ACH
576         AccountNumber       => 'account_number',
577         ABA                 => 'routing_code',
578         CheckNumber         => 'check_number',
579       CardName            => 'name',
580       DispositionType     => 'DispositionType',
581       TotalAmount         => 'amount' reformatted into cents
582       FeeAmount           => 'FeeAmount',
583       TaxAmount           => 'TaxAmount',
584       BillingAddress      => 'address',
585       BillingCity         => 'city',
586       BillingStateProv    => 'state',
587       BillingPostalCode   => 'zip',
588       BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
589       BillingPhone        => 'phone',
590       Email               => 'email',
591       UserIPAddress        => 'customer_ip',
592       UserHost            => 'UserHost',
593       UDField1            => 'UDField1',
594       UDField2            => 'UDField2',
595       ActionCode          => 'ActionCode',
596       IndustryInfo
597         Type                => 'IndustryInfo',
598       ShippingInfo
599         CustomerPO          => 'CustomerPO',
600         ShippingMethod      => 'ShippingMethod',
601         ShippingName        => 'ship_name',
602         ShippingAddr
603           Address             => 'ship_address',
604           City                => 'ship_city',
605           StateProv           => 'ship_state',
606           Country             => 'ship_country',  # forced to ISO-3166-alpha-3
607           Phone               => 'ship_phone',
608
609 =head1 NOTE
610
611 =head1 COMPATIBILITY
612
613 Version 0.07 changes the server name and path for IPPay's late 2012 update.
614
615 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
616 1.1.2.
617
618 See http://www.ippay.com/ for more information.
619
620 =head1 AUTHORS
621
622 Original author: Jeff Finucane
623
624 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
625
626 Reverse Authorization patch from dougforpres
627
628 =head1 ADVERTISEMENT
629
630 Need a complete, open-source back-office and customer self-service solution?
631 The Freeside software includes support for credit card and electronic check
632 processing with IPPay and over 50 other gateways, invoicing, integrated
633 trouble ticketing, and customer signup and self-service web interfaces.
634
635 http://freeside.biz/freeside/
636
637 =head1 SEE ALSO
638
639 perl(1). L<Business::OnlinePayment>.
640
641 =cut
642