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