update copyright and maintainer information
[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   }
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'}  = $self->{_content}->{'nacha_sec_code'}
452                  || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
453   }
454
455   $writer->startTag($item, %att);
456
457   if ( ref( $value ) eq 'HASH' ) {
458     foreach ( keys ( %$value ) ) {
459       $self->_xmlwrite($writer, $_, $value->{$_});
460     }
461   }else{
462     $writer->characters($value);
463   }
464
465   $writer->endTag($item);
466 }
467
468 1;
469
470 __END__
471
472 =head1 NAME
473
474 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
475
476 =head1 SYNOPSIS
477
478   use Business::OnlinePayment;
479
480   my $tx =
481     new Business::OnlinePayment( "IPPay",
482                                  'default_Origin' => 'PHONE ORDER',
483                                );
484   $tx->content(
485       type           => 'VISA',
486       login          => 'testdrive',
487       password       => '', #password 
488       action         => 'Normal Authorization',
489       description    => 'Business::OnlinePayment test',
490       amount         => '49.95',
491       customer_id    => 'tfb',
492       name           => 'Tofu Beast',
493       address        => '123 Anystreet',
494       city           => 'Anywhere',
495       state          => 'UT',
496       zip            => '84058',
497       card_number    => '4007000000027',
498       expiration     => '09/02',
499       cvv2           => '1234', #optional
500   );
501   $tx->submit();
502
503   if($tx->is_success()) {
504       print "Card processed successfully: ".$tx->authorization."\n";
505   } else {
506       print "Card was rejected: ".$tx->error_message."\n";
507   }
508
509 =head1 SUPPORTED TRANSACTION TYPES
510
511 =head2 CC, Visa, MasterCard, American Express, Discover
512
513 Content required: type, login, action, amount, card_number, expiration.
514
515 =head2 Check
516
517 Content required: type, login, action, amount, name, account_number, routing_code.
518
519 =head1 DESCRIPTION
520
521 For detailed information see L<Business::OnlinePayment>.
522
523 =head1 METHODS AND FUNCTIONS
524
525 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
526
527 =head2 result_code
528
529 Returns the response error code.
530
531 =head2 error_message
532
533 Returns the response error description text.
534
535 =head2 server_response
536
537 Returns the complete response from the server.
538
539 =head1 Handling of content(%content) data:
540
541 =head2 action
542
543 The following actions are valid
544
545   normal authorization
546   authorization only
547   reverse authorization
548   post authorization
549   credit
550   void
551
552 =head1 Setting IPPay parameters from content(%content)
553
554 The following rules are applied to map data to IPPay parameters
555 from content(%content):
556
557       # param => $content{<key>}
558       TransactionType     => 'TransactionType',
559       TerminalID          => 'login',
560       TransactionID       => 'order_number',
561       RoutingCode         => 'RoutingCode',
562       Approval            => 'authorization',
563       BatchID             => 'BatchID',
564       Origin              => 'Origin',
565       Password            => 'password',
566       OrderNumber         => 'invoice_number',
567       CardNum             => 'card_number',
568       CVV2                => 'cvv2',
569       Issue               => 'issue_number',
570       CardExpMonth        => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
571       CardExpYear         => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
572       CardStartMonth      => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
573       CardStartYear       => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
574       Track1              => 'track1',
575       Track2              => 'track2',
576       ACH
577         AccountNumber       => 'account_number',
578         ABA                 => 'routing_code',
579         CheckNumber         => 'check_number',
580       CardName            => 'name',
581       DispositionType     => 'DispositionType',
582       TotalAmount         => 'amount' reformatted into cents
583       FeeAmount           => 'FeeAmount',
584       TaxAmount           => 'TaxAmount',
585       BillingAddress      => 'address',
586       BillingCity         => 'city',
587       BillingStateProv    => 'state',
588       BillingPostalCode   => 'zip',
589       BillingCountry      => 'country',           # forced to ISO-3166-alpha-3
590       BillingPhone        => 'phone',
591       Email               => 'email',
592       UserIPAddress        => 'customer_ip',
593       UserHost            => 'UserHost',
594       UDField1            => 'UDField1',
595       UDField2            => 'UDField2',
596       ActionCode          => 'ActionCode',
597       IndustryInfo
598         Type                => 'IndustryInfo',
599       ShippingInfo
600         CustomerPO          => 'CustomerPO',
601         ShippingMethod      => 'ShippingMethod',
602         ShippingName        => 'ship_name',
603         ShippingAddr
604           Address             => 'ship_address',
605           City                => 'ship_city',
606           StateProv           => 'ship_state',
607           Country             => 'ship_country',  # forced to ISO-3166-alpha-3
608           Phone               => 'ship_phone',
609
610 =head1 NOTE
611
612 =head1 COMPATIBILITY
613
614 Version 0.07 changes the server name and path for IPPay's late 2012 update.
615
616 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
617 1.1.2.
618
619 See http://www.ippay.com/ for more information.
620
621 =head1 AUTHORS
622
623 Original author: Jeff Finucane
624
625 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
626
627 Reverse Authorization patch from dougforpres
628
629 =head1 COPYRIGHT AND LICENSE
630
631 Copyright (c) 1999 Jason Kohles
632 Copyright (c) 2002-2003 Ivan Kohler
633 Copyright (c) 2008-2021 Freeside Internet Services, Inc.
634
635 All rights reserved. This program is free software; you can redistribute it
636 and/or modify it under the same terms as Perl itself.
637
638 =head1 ADVERTISEMENT
639
640 Need a complete, open-source back-office and customer self-service solution?
641 The Freeside software includes support for credit card and electronic check
642 processing with IPPay and over 50 other gateways, invoicing, integrated
643 trouble ticketing, and customer signup and self-service web interfaces.
644
645 http://freeside.biz/freeside/
646
647 =head1 SEE ALSO
648
649 perl(1). L<Business::OnlinePayment>.
650
651 =cut
652