1 package Business::OnlinePayment::IPPay;
9 use Business::OnlinePayment;
10 use Business::OnlinePayment::HTTPS;
11 use vars qw($VERSION $DEBUG @ISA $me);
13 @ISA = qw(Business::OnlinePayment::HTTPS);
15 $VERSION = eval $VERSION; # modperlstyle: convert the string into a number
18 $me = 'Business::OnlinePayment::IPPay';
22 'info_compat' => '0.01',
23 'module_version' => $VERSION,
24 'supported_types' => [ qw( CC ECHECK ) ],
25 'supported_actions' => { 'CC' => [
26 'Normal Authorization',
31 'Reverse Authorization',
34 'Normal Authorization',
39 'CC_void_requires_card' => 1,
40 'ECHECK_void_requires_account' => 1,
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;
53 $self->build_subs(qw( order_number avs_code cvv2_response
54 response_page response_code response_headers
57 $DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
59 # module specific data
61 foreach my $key (keys %opts) {
62 $key =~ /^default_(\w*)$/ or next;
63 $_defaults{$1} = $opts{$key};
66 $self->{_defaults} = \%_defaults;
72 my %content = $self->content();
75 my %types = ( 'visa' => 'CC',
77 'american express' => 'CC',
81 $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
82 $self->transaction_type($content{'type'});
85 my $action = lc($content{'action'});
87 ( 'normal authorization' => 'SALE',
88 'authorization only' => 'AUTHONLY',
89 'post authorization' => 'CAPT',
90 'reverse authorization' => 'REVERSEAUTH',
95 ( 'normal authorization' => 'CHECK',
97 'credit' => 'REVERSAL',
100 if ($self->transaction_type eq 'CC') {
101 $content{'TransactionType'} = $actions{$action} || $action;
102 } elsif ($self->transaction_type eq 'ECHECK') {
104 $content{'TransactionType'} = $check_actions{$action} || $action;
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',
115 $content{'account_type'} = $account_types{lc($content{'account_type'})}
116 || $content{'account_type'};
119 $content{Origin} = 'RECURRING'
120 if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' );
122 # stuff it back into %content
123 $self->content(%content);
128 my ($self, $exp) = (shift, shift);
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 );
139 my ($self, $exp) = (shift, shift);
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 );
151 tie my(%map), 'Tie::IxHash', @_;
152 my %content = $self->content();
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{$_} };
163 if (defined($value)) {
174 $self->is_success(0);
177 my @required_fields = qw(action login password type);
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')
185 push @required_fields, qw( amount );
187 push @required_fields, qw( card_number expiration )
190 push @required_fields,
191 qw( routing_code account_number name ) # account_type
192 if ($type eq "ECHECK");
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 );
201 push @required_fields, qw( authorization card_number )
204 push @required_fields,
205 qw( routing_code account_number name ) # account_type
206 if ($type eq "ECHECK");
209 croak "$me can't handle transaction type: ".
210 $self->{_content}->{action}. " for ".
211 $self->transaction_type();
214 my %content = $self->content();
215 foreach ( keys ( %{($self->{_defaults})} ) ) {
216 $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
218 if ($self->test_transaction()) {
219 $content{'login'} = 'TESTTERMINAL';
220 $self->server('testgtwy.ippay.com') if $self->server eq 'gtwy.ippay.com';
222 $self->content(%content);
224 $self->required_fields(@required_fields);
226 #quick validation because ippay dumps an error indecipherable to the end user
227 if (grep { /^routing_code$/ } @required_fields) {
228 unless( $content{routing_code} =~ /^\d{9}$/ ) {
229 $self->_error_response('Invalid routing code');
234 my $transaction_id = $content{'order_number'};
235 unless ($transaction_id) {
236 my ($page, $server_response, %headers) = $self->https_get('dummy' => 1);
237 warn "fetched transaction id: (HTTPS response: $server_response) ".
239 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
240 "(Raw HTTPS content: $page)"
242 return unless $server_response=~ /^200/;
243 $transaction_id = $page;
246 my $cardexpmonth = $self->expdate_month($content{expiration});
247 my $cardexpyear = $self->expdate_year($content{expiration});
248 my $cardstartmonth = $self->expdate_month($content{card_start});
249 my $cardstartyear = $self->expdate_year($content{card_start});
252 if (defined($content{amount})) {
253 $amount = sprintf("%.2f", $content{amount});
257 my $check_number = $content{check_number} || "100" # make one up
258 if($content{account_number});
260 my $terminalid = $content{login} if $type eq 'CC';
261 my $merchantid = $content{login} if $type eq 'ECHECK';
263 my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 );
264 $country = country_code2code( $content{country},
269 $country = $content{country}
271 $country = uc($country) if $country;
274 country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 );
275 $ship_country = country_code2code( $content{ship_country},
279 unless $ship_country;
280 $ship_country = $content{ship_country}
281 unless $ship_country;
282 $ship_country = uc($ship_country) if $ship_country;
284 tie my %ach, 'Tie::IxHash',
285 $self->revmap_fields(
286 #wtf, this is a "Type"" attribute of the ACH element,
287 # not a child element like the others
288 #AccountType => 'account_type',
289 AccountNumber => 'account_number',
290 ABA => 'routing_code',
291 CheckNumber => \$check_number,
294 tie my %industryinfo, 'Tie::IxHash',
295 $self->revmap_fields(
296 Type => 'IndustryInfo',
299 tie my %shippingaddr, 'Tie::IxHash',
300 $self->revmap_fields(
301 Address => 'ship_address',
303 StateProv => 'ship_state',
304 Country => \$ship_country,
305 Phone => 'ship_phone',
308 unless ( $type ne 'CC' || keys %shippingaddr ) {
309 tie %shippingaddr, 'Tie::IxHash',
310 $self->revmap_fields(
311 Address => 'address',
313 StateProv => 'state',
314 Country => \$country,
318 delete $shippingaddr{Country} unless $shippingaddr{Country};
320 tie my %shippinginfo, 'Tie::IxHash',
321 $self->revmap_fields(
322 CustomerPO => 'CustomerPO',
323 ShippingMethod => 'ShippingMethod',
324 ShippingName => 'ship_name',
325 ShippingAddr => \%shippingaddr,
328 tie my %req, 'Tie::IxHash',
329 $self->revmap_fields(
330 TransactionType => 'TransactionType',
331 TerminalID => 'login',
332 # TerminalID => \$terminalid,
333 # MerchantID => \$merchantid,
334 TransactionID => \$transaction_id,
335 RoutingCode => 'RoutingCode',
336 Approval => 'authorization',
337 BatchID => 'BatchID',
339 Password => 'password',
340 OrderNumber => 'invoice_number',
341 CardNum => 'card_number',
343 Issue => 'issue_number',
344 CardExpMonth => \$cardexpmonth,
345 CardExpYear => \$cardexpyear,
346 CardStartMonth => \$cardstartmonth,
347 CardStartYear => \$cardstartyear,
352 DispositionType => 'DispositionType',
353 TotalAmount => \$amount,
354 FeeAmount => 'FeeAmount',
355 TaxAmount => 'TaxAmount',
356 BillingAddress => 'address',
357 BillingCity => 'city',
358 BillingStateProv => 'state',
359 BillingPostalCode => 'zip',
360 BillingCountry => \$country,
361 BillingPhone => 'phone',
363 UserIPAddress => 'customer_ip',
364 UserHost => 'UserHost',
365 UDField1 => 'UDField1',
366 UDField2 => 'UDField2',
367 UDField3 => \"$me $VERSION", #'UDField3',
368 ActionCode => 'ActionCode',
369 IndustryInfo => \%industryinfo,
370 ShippingInfo => \%shippinginfo,
372 delete $req{BillingCountry} unless $req{BillingCountry};
375 my $writer = new XML::Writer( OUTPUT => \$post_data,
378 ENCODING => 'us-ascii',
381 $writer->startTag('JetPay');
382 foreach ( keys ( %req ) ) {
383 $self->_xmlwrite($writer, $_, $req{$_});
385 $writer->endTag('JetPay');
388 warn "$post_data\n" if $DEBUG > 1;
390 my ($page,$server_response,%headers) = $self->https_post($post_data);
392 warn "$page\n" if $DEBUG > 1;
395 if ($server_response =~ /^200/){
396 $response = XMLin($page);
397 if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
398 $self->error_message($response->{ResponseText});
400 $self->error_message($response->{ErrMsg});
403 # $self->error_message("Server Failed");
406 $self->result_code($response->{ActionCode} || '');
407 $self->order_number($response->{TransactionID} || '');
408 $self->authorization($response->{Approval} || '');
409 $self->cvv2_response($response->{CVV2} || '');
410 $self->avs_code($response->{AVS} || '');
412 $self->is_success($self->result_code() eq '000' ? 1 : 0);
414 unless ($self->is_success()) {
415 unless ( $self->error_message() ) {
417 #additional logging information, possibly too sensitive for an error msg
418 # (IPPay seems to have a failure mode where they return the full
419 # original request including card number)
420 $self->error_message(
421 "(HTTPS response: $server_response) ".
423 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
424 "(Raw HTTPS content: $page)"
427 $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)');
434 sub _error_response {
435 my ($self, $error_message) = (shift, shift);
436 $self->result_code('');
437 $self->order_number('');
438 $self->authorization('');
439 $self->cvv2_response('');
441 $self->is_success( 0);
442 $self->error_message($error_message);
446 my ($self, $writer, $item, $value) = @_;
449 if ( $item eq 'ACH' ) {
450 $att{'Type'} = $self->{_content}->{'account_type'}
451 if $self->{_content}->{'account_type'}; #necessary so we don't pass empty?
452 $att{'SEC'} = $self->{_content}->{'nacha_sec_code'}
453 || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' );
456 $writer->startTag($item, %att);
458 if ( ref( $value ) eq 'HASH' ) {
459 foreach ( keys ( %$value ) ) {
460 $self->_xmlwrite($writer, $_, $value->{$_});
463 $writer->characters($value);
466 $writer->endTag($item);
475 Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
479 use Business::OnlinePayment;
482 new Business::OnlinePayment( "IPPay",
483 'default_Origin' => 'PHONE ORDER',
487 login => 'testdrive',
488 password => '', #password
489 action => 'Normal Authorization',
490 description => 'Business::OnlinePayment test',
492 customer_id => 'tfb',
493 name => 'Tofu Beast',
494 address => '123 Anystreet',
498 card_number => '4007000000027',
499 expiration => '09/02',
500 cvv2 => '1234', #optional
504 if($tx->is_success()) {
505 print "Card processed successfully: ".$tx->authorization."\n";
507 print "Card was rejected: ".$tx->error_message."\n";
510 =head1 SUPPORTED TRANSACTION TYPES
512 =head2 CC, Visa, MasterCard, American Express, Discover
514 Content required: type, login, action, amount, card_number, expiration.
518 Content required: type, login, action, amount, name, account_number, routing_code.
522 For detailed information see L<Business::OnlinePayment>.
524 =head1 METHODS AND FUNCTIONS
526 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
530 Returns the response error code.
534 Returns the response error description text.
536 =head2 server_response
538 Returns the complete response from the server.
540 =head1 Handling of content(%content) data:
544 The following actions are valid
548 reverse authorization
553 =head1 Setting IPPay parameters from content(%content)
555 The following rules are applied to map data to IPPay parameters
556 from content(%content):
558 # param => $content{<key>}
559 TransactionType => 'TransactionType',
560 TerminalID => 'login',
561 TransactionID => 'order_number',
562 RoutingCode => 'RoutingCode',
563 Approval => 'authorization',
564 BatchID => 'BatchID',
566 Password => 'password',
567 OrderNumber => 'invoice_number',
568 CardNum => 'card_number',
570 Issue => 'issue_number',
571 CardExpMonth => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
572 CardExpYear => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
573 CardStartMonth => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
574 CardStartYear => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
578 AccountNumber => 'account_number',
579 ABA => 'routing_code',
580 CheckNumber => 'check_number',
582 DispositionType => 'DispositionType',
583 TotalAmount => 'amount' reformatted into cents
584 FeeAmount => 'FeeAmount',
585 TaxAmount => 'TaxAmount',
586 BillingAddress => 'address',
587 BillingCity => 'city',
588 BillingStateProv => 'state',
589 BillingPostalCode => 'zip',
590 BillingCountry => 'country', # forced to ISO-3166-alpha-3
591 BillingPhone => 'phone',
593 UserIPAddress => 'customer_ip',
594 UserHost => 'UserHost',
595 UDField1 => 'UDField1',
596 UDField2 => 'UDField2',
597 ActionCode => 'ActionCode',
599 Type => 'IndustryInfo',
601 CustomerPO => 'CustomerPO',
602 ShippingMethod => 'ShippingMethod',
603 ShippingName => 'ship_name',
605 Address => 'ship_address',
607 StateProv => 'ship_state',
608 Country => 'ship_country', # forced to ISO-3166-alpha-3
609 Phone => 'ship_phone',
615 Version 0.07 changes the server name and path for IPPay's late 2012 update.
617 Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version
620 See http://www.ippay.com/ for more information.
624 Original author: Jeff Finucane
626 Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
628 Reverse Authorization patch from dougforpres
630 =head1 COPYRIGHT AND LICENSE
632 Copyright (c) 1999 Jason Kohles
633 Copyright (c) 2002-2003 Ivan Kohler
634 Copyright (c) 2008-2021 Freeside Internet Services, Inc.
636 All rights reserved. This program is free software; you can redistribute it
637 and/or modify it under the same terms as Perl itself.
641 Need a complete, open-source back-office and customer self-service solution?
642 The Freeside software includes support for credit card and electronic check
643 processing with IPPay and over 50 other gateways, invoicing, integrated
644 trouble ticketing, and customer signup and self-service web interfaces.
646 http://freeside.biz/freeside/
650 perl(1). L<Business::OnlinePayment>.