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.03';
+$VERSION = '0.09_03';
+$VERSION = eval $VERSION; # modperlstyle: convert the string into a number
+
$DEBUG = 0;
$me = 'Business::OnlinePayment::IPPay';
+sub _info {
+ {
+ 'info_version' => '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;
( 'normal authorization' => 'SALE',
'authorization only' => 'AUTHONLY',
'post authorization' => 'CAPT',
+ 'reverse authorization' => 'REVERSEAUTH',
'void' => 'VOID',
'credit' => 'CREDIT',
);
'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' );
$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();
}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 );
foreach ( keys ( %{($self->{_defaults})} ) ) {
$content{$_} = $self->{_defaults}->{$_} unless exists($content{$_});
}
+ if ($self->test_transaction()) {
+ $content{'login'} = 'TESTTERMINAL';
+ }
$self->content(%content);
$self->required_fields(@required_fields);
- if ($self->test_transaction()) {
- $self->server('test1.jetpay.com');
- $self->port('443');
- $self->path('/jetpay');
+ #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'};
"(HTTPS headers: ".
join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
"(Raw HTTPS content: $page)"
- if $DEBUG;
+ if $DEBUG > 1;
return unless $server_response=~ /^200/;
$transaction_id = $page;
}
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',
Address => 'ship_address',
City => 'ship_city',
StateProv => 'ship_state',
- Country => 'ship_country',
+ Country => \$ship_country,
Phone => 'ship_phone',
);
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(
BillingCity => 'city',
BillingStateProv => 'state',
BillingPostalCode => 'zip',
- BillingCountry => 'country',
+ BillingCountry => \$country,
BillingPhone => 'phone',
Email => 'email',
UserIPAddr => '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,
$writer->endTag('JetPay');
$writer->end();
- 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/){
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");
$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'} = 'PPD';
+ }
+
+ $writer->startTag($item, %att);
+
if ( ref( $value ) eq 'HASH' ) {
foreach ( keys ( %$value ) ) {
$self->_xmlwrite($writer, $_, $value->{$_});
}else{
$writer->characters($value);
}
+
$writer->endTag($item);
}
1;
+
__END__
=head1 NAME
normal authorization
authorization only
+ reverse authorization
post authorization
credit
void
BillingCity => 'city',
BillingStateProv => 'state',
BillingPostalCode => 'zip',
- BillingCountry => 'country',
+ BillingCountry => 'country', # forced to ISO-3166-alpha-3
BillingPhone => 'phone',
Email => 'email',
UserIPAddr => 'customer_ip',
UserHost => 'UserHost',
UDField1 => 'UDField1',
UDField2 => 'UDField2',
- UDField3 => 'UDField3',
ActionCode => 'ActionCode',
IndustryInfo
Type => 'IndustryInfo',
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 <ivan-ippay@freeside.biz>
-Jeff Finucane, ippay@weasellips.com
+Reverse Authorization patch from dougforpres
=head1 SEE ALSO