X-Git-Url: http://git.freeside.biz/gitweb/?p=Business-OnlinePayment-IPPay.git;a=blobdiff_plain;f=IPPay.pm;h=4d9dbd5360d7db83efc5afcec12f1cc1226ebdab;hp=00dffd584be7ec2019b31e025a5c32c9fe97b29a;hb=HEAD;hpb=92884f8e5566d63d73eae0b29d1489b0a0cd2eba diff --git a/IPPay.pm b/IPPay.pm index 00dffd5..4d9dbd5 100644 --- a/IPPay.pm +++ b/IPPay.pm @@ -5,34 +5,58 @@ use Carp; use Tie::IxHash; use XML::Simple; use XML::Writer; +use Locale::Country; use Business::OnlinePayment; use Business::OnlinePayment::HTTPS; use vars qw($VERSION $DEBUG @ISA $me); @ISA = qw(Business::OnlinePayment::HTTPS); -$VERSION = '0.02'; +$VERSION = '0.11_01'; +$VERSION = eval $VERSION; # modperlstyle: convert the string into a number + $DEBUG = 0; $me = 'Business::OnlinePayment::IPPay'; +sub _info { + { + 'info_compat' => '0.01', + 'module_version' => $VERSION, + 'supported_types' => [ qw( CC ECHECK ) ], + 'supported_actions' => { 'CC' => [ + 'Normal Authorization', + 'Authorization Only', + 'Post Authorization', + 'Void', + 'Credit', + 'Reverse Authorization', + ], + 'ECHECK' => [ + 'Normal Authorization', + 'Void', + 'Credit', + ], + }, + 'CC_void_requires_card' => 1, + 'ECHECK_void_requires_account' => 1, + }; +} + sub set_defaults { my $self = shift; my %opts = @_; # standard B::OP methods/data - $self->server('gateway17.jetpay.com') unless $self->server; + $self->server('gtwy.ippay.com') unless $self->server; $self->port('443') unless $self->port; - $self->path('/jetpay') unless $self->path; + $self->path('/ippay') unless $self->path; $self->build_subs(qw( order_number avs_code cvv2_response response_page response_code response_headers )); - # module specific data - if ( $opts{debug} ) { - $self->debug( $opts{debug} ); - delete $opts{debug}; - } + $DEBUG = exists($opts{debug}) ? $opts{debug} : 0; + # module specific data my %_defaults = (); foreach my $key (keys %opts) { $key =~ /^default_(\w*)$/ or next; @@ -63,6 +87,7 @@ sub map_fields { ( 'normal authorization' => 'SALE', 'authorization only' => 'AUTHONLY', 'post authorization' => 'CAPT', + 'reverse authorization' => 'REVERSEAUTH', 'void' => 'VOID', 'credit' => 'CREDIT', ); @@ -71,20 +96,25 @@ sub map_fields { 'void' => 'VOIDACH', 'credit' => 'REVERSAL', ); + if ($self->transaction_type eq 'CC') { $content{'TransactionType'} = $actions{$action} || $action; - }elsif ($self->transaction_type eq 'ECHECK') { - $content{'TransactionType'} = $check_actions{$action} || $action; - } + } elsif ($self->transaction_type eq 'ECHECK') { + $content{'TransactionType'} = $check_actions{$action} || $action; - # ACCOUNT TYPE MAP - my %account_types = ('personal checking' => 'Checking', - 'personal savings' => 'Savings', - 'business checking' => 'BusinessCk', - ); - $content{'account_type'} = $account_types{lc($content{'account_type'})} - || $content{'account_type'}; + # ACCOUNT TYPE MAP + my %account_types = ('personal checking' => 'CHECKING', + 'personal savings' => 'SAVINGS', + 'business checking' => 'CHECKING', + 'business savings' => 'SAVINGS', + #not technically B:OP valid i guess? + 'checking' => 'CHECKING', + 'savings' => 'SAVINGS', + ); + $content{'account_type'} = $account_types{lc($content{'account_type'})} + || $content{'account_type'}; + } $content{Origin} = 'RECURRING' if ($content{recurring_billing} &&$content{recurring_billing} eq 'YES' ); @@ -144,7 +174,7 @@ sub submit { $self->is_success(0); $self->map_fields(); - my @required_fields = qw(action login type); + my @required_fields = qw(action login password type); my $action = lc($self->{_content}->{action}); my $type = $self->transaction_type(); @@ -163,6 +193,8 @@ sub submit { }elsif ( $action eq 'post authorization' && $type eq 'CC') { push @required_fields, qw( order_number ); + }elsif ( $action eq 'reverse authorization' && $type eq 'CC') { + push @required_fields, qw( order_number card_number expiration amount ); }elsif ( $action eq 'void') { push @required_fields, qw( order_number amount ); @@ -183,13 +215,30 @@ sub submit { foreach ( keys ( %{($self->{_defaults})} ) ) { $content{$_} = $self->{_defaults}->{$_} unless exists($content{$_}); } + if ($self->test_transaction()) { + $content{'login'} = 'TESTTERMINAL'; + $self->server('testgtwy.ippay.com') if $self->server eq 'gtwy.ippay.com'; + } $self->content(%content); $self->required_fields(@required_fields); + #quick validation because ippay dumps an error indecipherable to the end user + if (grep { /^routing_code$/ } @required_fields) { + unless( $content{routing_code} =~ /^\d{9}$/ ) { + $self->_error_response('Invalid routing code'); + return; + } + } + my $transaction_id = $content{'order_number'}; unless ($transaction_id) { my ($page, $server_response, %headers) = $self->https_get('dummy' => 1); + warn "fetched transaction id: (HTTPS response: $server_response) ". + "(HTTPS headers: ". + join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". + "(Raw HTTPS content: $page)" + if $DEBUG > 1; return unless $server_response=~ /^200/; $transaction_id = $page; } @@ -211,8 +260,31 @@ sub submit { my $terminalid = $content{login} if $type eq 'CC'; my $merchantid = $content{login} if $type eq 'ECHECK'; + my $country = country2code( $content{country}, LOCALE_CODE_ALPHA_3 ); + $country = country_code2code( $content{country}, + LOCALE_CODE_ALPHA_2, + LOCALE_CODE_ALPHA_3 + ) + unless $country; + $country = $content{country} + unless $country; + $country = uc($country) if $country; + + my $ship_country = + country2code( $content{ship_country}, LOCALE_CODE_ALPHA_3 ); + $ship_country = country_code2code( $content{ship_country}, + LOCALE_CODE_ALPHA_2, + LOCALE_CODE_ALPHA_3 + ) + unless $ship_country; + $ship_country = $content{ship_country} + unless $ship_country; + $ship_country = uc($ship_country) if $ship_country; + tie my %ach, 'Tie::IxHash', $self->revmap_fields( + #wtf, this is a "Type"" attribute of the ACH element, + # not a child element like the others #AccountType => 'account_type', AccountNumber => 'account_number', ABA => 'routing_code', @@ -229,7 +301,7 @@ sub submit { Address => 'ship_address', City => 'ship_city', StateProv => 'ship_state', - Country => 'ship_country', + Country => \$ship_country, Phone => 'ship_phone', ); @@ -239,10 +311,11 @@ sub submit { Address => 'address', City => 'city', StateProv => 'state', - Country => 'country', + Country => \$country, Phone => 'phone', ); } + delete $shippingaddr{Country} unless $shippingaddr{Country}; tie my %shippinginfo, 'Tie::IxHash', $self->revmap_fields( @@ -284,18 +357,19 @@ sub submit { BillingCity => 'city', BillingStateProv => 'state', BillingPostalCode => 'zip', - BillingCountry => 'country', + BillingCountry => \$country, BillingPhone => 'phone', Email => 'email', - UserIPAddr => 'customer_ip', + UserIPAddress => 'customer_ip', UserHost => 'UserHost', UDField1 => 'UDField1', UDField2 => 'UDField2', - UDField3 => 'UDField3', + UDField3 => \"$me $VERSION", #'UDField3', ActionCode => 'ActionCode', IndustryInfo => \%industryinfo, ShippingInfo => \%shippinginfo, ); + delete $req{BillingCountry} unless $req{BillingCountry}; my $post_data; my $writer = new XML::Writer( OUTPUT => \$post_data, @@ -304,24 +378,18 @@ sub submit { ENCODING => 'us-ascii', ); $writer->xmlDecl(); - $writer->startTag('JetPay'); + $writer->startTag('ippay'); foreach ( keys ( %req ) ) { $self->_xmlwrite($writer, $_, $req{$_}); } - $writer->endTag('JetPay'); + $writer->endTag('ippay'); $writer->end(); - if ($self->test_transaction()) { - $self->server('test1.jetpay.com'); - $self->port('443'); - $self->path('/jetpay'); - } - - warn "$post_data\n" if $DEBUG; + warn "$post_data\n" if $DEBUG > 1; my ($page,$server_response,%headers) = $self->https_post($post_data); - warn "$page\n" if $DEBUG; + warn "$page\n" if $DEBUG > 1; my $response = {}; if ($server_response =~ /^200/){ @@ -329,7 +397,7 @@ sub submit { if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) { $self->error_message($response->{ResponseText}); }else{ - $self->error_message($response->{Errmsg}); + $self->error_message($response->{ErrMsg}); } # }else{ # $self->error_message("Server Failed"); @@ -344,21 +412,49 @@ sub submit { $self->is_success($self->result_code() eq '000' ? 1 : 0); unless ($self->is_success()) { - unless ( $self->error_message() ) { #additional logging information - $self->error_message( - "(HTTPS response: $server_response) ". - "(HTTPS headers: ". - join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". - "(Raw HTTPS content: $page)" - ); + unless ( $self->error_message() ) { + if ( $DEBUG ) { + #additional logging information, possibly too sensitive for an error msg + # (IPPay seems to have a failure mode where they return the full + # original request including card number) + $self->error_message( + "(HTTPS response: $server_response) ". + "(HTTPS headers: ". + join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ". + "(Raw HTTPS content: $page)" + ); + } else { + $self->error_message('No ResponseText or ErrMsg was returned by IPPay (enable debugging for raw HTTPS response)'); + } } } } +sub _error_response { + my ($self, $error_message) = (shift, shift); + $self->result_code(''); + $self->order_number(''); + $self->authorization(''); + $self->cvv2_response(''); + $self->avs_code(''); + $self->is_success( 0); + $self->error_message($error_message); +} + sub _xmlwrite { my ($self, $writer, $item, $value) = @_; - $writer->startTag($item); + + my %att = (); + if ( $item eq 'ACH' ) { + $att{'Type'} = $self->{_content}->{'account_type'} + if $self->{_content}->{'account_type'}; #necessary so we don't pass empty? + $att{'SEC'} = $self->{_content}->{'nacha_sec_code'} + || ( $att{'Type'} =~ /business/i ? 'CCD' : 'PPD' ); + } + + $writer->startTag($item, %att); + if ( ref( $value ) eq 'HASH' ) { foreach ( keys ( %$value ) ) { $self->_xmlwrite($writer, $_, $value->{$_}); @@ -366,10 +462,12 @@ sub _xmlwrite { }else{ $writer->characters($value); } + $writer->endTag($item); } 1; + __END__ =head1 NAME @@ -447,6 +545,7 @@ The following actions are valid normal authorization authorization only + reverse authorization post authorization credit void @@ -488,14 +587,13 @@ from content(%content): BillingCity => 'city', BillingStateProv => 'state', BillingPostalCode => 'zip', - BillingCountry => 'country', + BillingCountry => 'country', # forced to ISO-3166-alpha-3 BillingPhone => 'phone', Email => 'email', - UserIPAddr => 'customer_ip', + UserIPAddress => 'customer_ip', UserHost => 'UserHost', UDField1 => 'UDField1', UDField2 => 'UDField2', - UDField3 => 'UDField3', ActionCode => 'ActionCode', IndustryInfo Type => 'IndustryInfo', @@ -507,21 +605,45 @@ from content(%content): Address => 'ship_address', City => 'ship_city', StateProv => 'ship_state', - Country => 'ship_country', + Country => 'ship_country', # forced to ISO-3166-alpha-3 Phone => 'ship_phone', =head1 NOTE =head1 COMPATIBILITY +Version 0.07 changes the server name and path for IPPay's late 2012 update. + Business::OnlinePayment::IPPay uses IPPay XML Product Specifications version 1.1.2. See http://www.ippay.com/ for more information. -=head1 AUTHOR +=head1 AUTHORS + +Original author: Jeff Finucane + +Current maintainer: Ivan Kohler + +Reverse Authorization patch from dougforpres + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 1999 Jason Kohles +Copyright (c) 2002-2003 Ivan Kohler +Copyright (c) 2008-2021 Freeside Internet Services, Inc. + +All rights reserved. This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + +=head1 ADVERTISEMENT + +Need a complete, open-source back-office and customer self-service solution? +The Freeside software includes support for credit card and electronic check +processing with IPPay and over 50 other gateways, invoicing, integrated +trouble ticketing, and customer signup and self-service web interfaces. -Jeff Finucane, ippay@weasellips.com +http://freeside.biz/freeside/ =head1 SEE ALSO