File Coverage

blib/lib/Business/OnlinePayment/IPPay.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::IPPay;
2              
3 1     1   1133 use strict;
  1         4  
  1         47  
4 1     1   6 use Carp;
  1         2  
  1         93  
5 1     1   1126 use Tie::IxHash;
  1         5880  
  1         32  
6 1     1   530 use XML::Simple;
  0            
  0            
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.09';
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             UserIPAddr => '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'} = 'PPD';
452             }
453              
454             $writer->startTag($item, %att);
455              
456             if ( ref( $value ) eq 'HASH' ) {
457             foreach ( keys ( %$value ) ) {
458             $self->_xmlwrite($writer, $_, $value->{$_});
459             }
460             }else{
461             $writer->characters($value);
462             }
463              
464             $writer->endTag($item);
465             }
466              
467             1;
468              
469             __END__