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