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