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