package Business::OnlinePayment::IPPay;
use strict;
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.12';
$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('gtwy.ippay.com') unless $self->server;
$self->port('443') unless $self->port;
$self->path('/ippay') unless $self->path;
$self->build_subs(qw( order_number avs_code cvv2_response
response_page response_code response_headers
));
$DEBUG = exists($opts{debug}) ? $opts{debug} : 0;
# module specific data
my %_defaults = ();
foreach my $key (keys %opts) {
$key =~ /^default_(\w*)$/ or next;
$_defaults{$1} = $opts{$key};
delete $opts{$key};
}
$self->{_defaults} = \%_defaults;
}
sub map_fields {
my($self) = @_;
my %content = $self->content();
# TYPE MAP
my %types = ( 'visa' => 'CC',
'mastercard' => 'CC',
'american express' => 'CC',
'discover' => 'CC',
'check' => 'ECHECK',
);
$content{'type'} = $types{lc($content{'type'})} || $content{'type'};
$self->transaction_type($content{'type'});
# ACTION MAP
my $action = lc($content{'action'});
my %actions =
( 'normal authorization' => 'SALE',
'authorization only' => 'AUTHONLY',
'post authorization' => 'CAPT',
'reverse authorization' => 'REVERSEAUTH',
'void' => 'VOID',
'credit' => 'CREDIT',
);
my %check_actions =
( 'normal authorization' => 'CHECK',
'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;
# 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' );
# stuff it back into %content
$self->content(%content);
}
sub expdate_month {
my ($self, $exp) = (shift, shift);
my $month;
if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
$month = sprintf( "%02d", $1 );
}elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
$month = sprintf( "%02d", $1 );
}
return $month;
}
sub expdate_year {
my ($self, $exp) = (shift, shift);
my $year;
if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
$year = sprintf( "%02d", $1 );
}elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
$year = sprintf( "%02d", $1 );
}
return $year;
}
sub revmap_fields {
my $self = shift;
tie my(%map), 'Tie::IxHash', @_;
my %content = $self->content();
map {
my $value;
if ( ref( $map{$_} ) eq 'HASH' ) {
$value = $map{$_} if ( keys %{ $map{$_} } );
}elsif( ref( $map{$_} ) ) {
$value = ${ $map{$_} };
}elsif( exists( $content{ $map{$_} } ) ) {
$value = $content{ $map{$_} };
}
if (defined($value)) {
($_ => $value);
}else{
();
}
} (keys %map);
}
sub submit {
my($self) = @_;
$self->is_success(0);
$self->map_fields();
my @required_fields = qw(action login password type);
my $action = lc($self->{_content}->{action});
my $type = $self->transaction_type();
if ( $action eq 'normal authorization'
|| $action eq 'credit'
|| $action eq 'authorization only' && $type eq 'CC')
{
push @required_fields, qw( amount );
push @required_fields, qw( card_number expiration )
if ($type eq "CC");
push @required_fields,
qw( routing_code account_number name ) # account_type
if ($type eq "ECHECK");
}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 );
push @required_fields, qw( authorization card_number )
if ($type eq "CC");
push @required_fields,
qw( routing_code account_number name ) # account_type
if ($type eq "ECHECK");
}else{
croak "$me can't handle transaction type: ".
$self->{_content}->{action}. " for ".
$self->transaction_type();
}
my %content = $self->content();
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;
}
my $cardexpmonth = $self->expdate_month($content{expiration});
my $cardexpyear = $self->expdate_year($content{expiration});
my $cardstartmonth = $self->expdate_month($content{card_start});
my $cardstartyear = $self->expdate_year($content{card_start});
my $amount;
if (defined($content{amount})) {
$amount = sprintf("%.2f", $content{amount});
$amount =~ s/\.//;
}
my $name = length($content{name})
? $content{name}
: $content{first_name}. ' '. $content{last_name};
my $check_number = $content{check_number} || "100" # make one up
if($content{account_number});
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_name =
length($content{ship_name})
? $content{ship_name}
: (length($content{ship_first_name}) || length($content{ship_last_name}))
? $content{ship_first_name}. ' '. $content{ship_last_name}
: '';
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',
CheckNumber => \$check_number,
);
tie my %industryinfo, 'Tie::IxHash',
$self->revmap_fields(
Type => 'IndustryInfo',
);
tie my %shippingaddr, 'Tie::IxHash',
$self->revmap_fields(
Address => 'ship_address',
City => 'ship_city',
StateProv => 'ship_state',
Country => \$ship_country,
Phone => 'ship_phone',
);
unless ( $type ne 'CC' || keys %shippingaddr ) {
tie %shippingaddr, 'Tie::IxHash',
$self->revmap_fields(
Address => 'address',
City => 'city',
StateProv => 'state',
Country => \$country,
Phone => 'phone',
);
}
delete $shippingaddr{Country} unless $shippingaddr{Country};
tie my %shippinginfo, 'Tie::IxHash',
$self->revmap_fields(
CustomerPO => 'CustomerPO',
ShippingMethod => 'ShippingMethod',
ShippingName => \$ship_name,
ShippingAddr => \%shippingaddr,
);
tie my %req, 'Tie::IxHash',
$self->revmap_fields(
TransactionType => 'TransactionType',
TerminalID => 'login',
# TerminalID => \$terminalid,
# MerchantID => \$merchantid,
TransactionID => \$transaction_id,
RoutingCode => 'RoutingCode',
Approval => 'authorization',
BatchID => 'BatchID',
Origin => 'Origin',
Password => 'password',
OrderNumber => 'invoice_number',
CardNum => 'card_number',
CVV2 => 'cvv2',
Issue => 'issue_number',
CardExpMonth => \$cardexpmonth,
CardExpYear => \$cardexpyear,
CardStartMonth => \$cardstartmonth,
CardStartYear => \$cardstartyear,
Track1 => 'track1',
Track2 => 'track2',
ACH => \%ach,
CardName => \$name,
DispositionType => 'DispositionType',
TotalAmount => \$amount,
FeeAmount => 'FeeAmount',
TaxAmount => 'TaxAmount',
BillingAddress => 'address',
BillingCity => 'city',
BillingStateProv => 'state',
BillingPostalCode => 'zip',
BillingCountry => \$country,
BillingPhone => 'phone',
Email => 'email',
UserIPAddress => 'customer_ip',
UserHost => 'UserHost',
UDField1 => 'UDField1',
UDField2 => 'UDField2',
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,
DATA_MODE => 1,
DATA_INDENT => 1,
ENCODING => 'us-ascii',
);
$writer->xmlDecl();
$writer->startTag('ippay');
foreach ( keys ( %req ) ) {
$self->_xmlwrite($writer, $_, $req{$_});
}
$writer->endTag('ippay');
$writer->end();
warn "$post_data\n" if $DEBUG > 1;
my ($page,$server_response,%headers) = $self->https_post($post_data);
warn "$page\n" if $DEBUG > 1;
my $response = {};
if ($server_response =~ /^200/){
$response = XMLin($page);
if ( exists($response->{ActionCode}) && !exists($response->{ErrMsg})) {
$self->error_message($response->{ResponseText});
}else{
$self->error_message($response->{ErrMsg});
}
# }else{
# $self->error_message("Server Failed");
}
$self->result_code($response->{ActionCode} || '');
$self->order_number($response->{TransactionID} || '');
$self->authorization($response->{Approval} || '');
$self->cvv2_response($response->{CVV2} || '');
$self->avs_code($response->{AVS} || '');
$self->is_success($self->result_code() eq '000' ? 1 : 0);
unless ($self->is_success()) {
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) = @_;
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->{$_});
}
}else{
$writer->characters($value);
}
$writer->endTag($item);
}
1;
__END__
=head1 NAME
Business::OnlinePayment::IPPay - IPPay backend for Business::OnlinePayment
=head1 SYNOPSIS
use Business::OnlinePayment;
my $tx =
new Business::OnlinePayment( "IPPay",
'default_Origin' => 'PHONE ORDER',
);
$tx->content(
type => 'VISA',
login => 'testdrive',
password => '', #password
action => 'Normal Authorization',
description => 'Business::OnlinePayment test',
amount => '49.95',
customer_id => 'tfb',
name => 'Tofu Beast',
address => '123 Anystreet',
city => 'Anywhere',
state => 'UT',
zip => '84058',
card_number => '4007000000027',
expiration => '09/02',
cvv2 => '1234', #optional
);
$tx->submit();
if($tx->is_success()) {
print "Card processed successfully: ".$tx->authorization."\n";
} else {
print "Card was rejected: ".$tx->error_message."\n";
}
=head1 SUPPORTED TRANSACTION TYPES
=head2 CC, Visa, MasterCard, American Express, Discover
Content required: type, login, action, amount, card_number, expiration.
=head2 Check
Content required: type, login, action, amount, name, account_number, routing_code.
=head1 DESCRIPTION
For detailed information see L<Business::OnlinePayment>.
=head1 METHODS AND FUNCTIONS
See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
=head2 result_code
Returns the response error code.
=head2 error_message
Returns the response error description text.
=head2 server_response
Returns the complete response from the server.
=head1 Handling of content(%content) data:
=head2 action
The following actions are valid
normal authorization
authorization only
reverse authorization
post authorization
credit
void
=head1 Setting IPPay parameters from content(%content)
The following rules are applied to map data to IPPay parameters
from content(%content):
# param => $content{<key>}
TransactionType => 'TransactionType',
TerminalID => 'login',
TransactionID => 'order_number',
RoutingCode => 'RoutingCode',
Approval => 'authorization',
BatchID => 'BatchID',
Origin => 'Origin',
Password => 'password',
OrderNumber => 'invoice_number',
CardNum => 'card_number',
CVV2 => 'cvv2',
Issue => 'issue_number',
CardExpMonth => \( $month ), # MM from MM(-)YY(YY) of 'expiration'
CardExpYear => \( $year ), # YY from MM(-)YY(YY) of 'expiration'
CardStartMonth => \( $month ), # MM from MM(-)YY(YY) of 'card_start'
CardStartYear => \( $year ), # YY from MM(-)YY(YY) of 'card_start'
Track1 => 'track1',
Track2 => 'track2',
ACH
AccountNumber => 'account_number',
ABA => 'routing_code',
CheckNumber => 'check_number',
CardName => 'name',
DispositionType => 'DispositionType',
TotalAmount => 'amount' reformatted into cents
FeeAmount => 'FeeAmount',
TaxAmount => 'TaxAmount',
BillingAddress => 'address',
BillingCity => 'city',
BillingStateProv => 'state',
BillingPostalCode => 'zip',
BillingCountry => 'country', # forced to ISO-3166-alpha-3
BillingPhone => 'phone',
Email => 'email',
UserIPAddress => 'customer_ip',
UserHost => 'UserHost',
UDField1 => 'UDField1',
UDField2 => 'UDField2',
ActionCode => 'ActionCode',
IndustryInfo
Type => 'IndustryInfo',
ShippingInfo
CustomerPO => 'CustomerPO',
ShippingMethod => 'ShippingMethod',
ShippingName => 'ship_name',
ShippingAddr
Address => 'ship_address',
City => 'ship_city',
StateProv => 'ship_state',
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 AUTHORS
Original author: Jeff Finucane
Current maintainer: Ivan Kohler <ivan-ippay@freeside.biz>
Reverse Authorization patch from dougforpres
=head1 COPYRIGHT AND LICENSE
Copyright (c) 1999 Jason Kohles
Copyright (c) 2002-2003 Ivan Kohler
Copyright (c) 2008-2022 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.
http://freeside.biz/freeside/
=head1 SEE ALSO
perl(1). L<Business::OnlinePayment>.
=cut