use vars qw($VERSION $DEBUG @ISA $me);
@ISA = qw(Business::OnlinePayment::HTTPS);
-$VERSION = '2.00';
+
+$VERSION = '2.05';
+$VERSION = eval $VERSION; # modperlstyle: convert the string into a number
+
$DEBUG = 0;
$me='Business::OnlinePayment::PaymenTech';
'Content-Transfer-Encoding' => 'text',
'Request-Number' => 1,
'Document-Type' => 'Request',
- #'Trace-Number' => 1,
'Interface-Version' => "$me $VERSION",
); # Content-Type has to be passed separately
tie my %new_order, 'Tie::IxHash', (
- OrbitalConnectionUsername => ':login',
- OrbitalConnectionPassword => ':password',
- IndustryType => 'EC', # Assume industry = Ecommerce
- MessageType => ':message_type',
- BIN => ':bin',
- MerchantID => ':merchant_id',
- TerminalID => ':terminal_id',
- CardBrand => '',
- AccountNum => ':card_number',
- Exp => ':expiration',
- CurrencyCode => ':currency_code',
- CurrencyExponent => ':currency_exp',
- CardSecValInd => ':cvvind',
- CardSecVal => ':cvv2',
-# AVSname => ':name', not needed
- AVSzip => ':zip',
- AVSaddress1 => ':address',
- AVScity => ':city',
- AVSstate => ':state',
- OrderID => ':invoice_number',
- Amount => ':amount',
- Comments => ':email', # as per B:OP:WesternACH
+ OrbitalConnectionUsername => [ ':login', 32 ],
+ OrbitalConnectionPassword => [ ':password', 32 ],
+ IndustryType => [ 'EC', 2 ],
+ MessageType => [ ':message_type', 2 ],
+ BIN => [ ':bin', 6 ],
+ MerchantID => [ ':merchant_id', 12 ],
+ TerminalID => [ ':terminal_id', 3 ],
+ CardBrand => [ '', 2 ],
+ AccountNum => [ ':card_number', 19 ],
+ Exp => [ ':expiration', 4 ],
+ CurrencyCode => [ ':currency_code', 3 ],
+ CurrencyExponent => [ ':currency_exp', 6 ],
+ CardSecValInd => [ ':cvvind', 1 ],
+ CardSecVal => [ ':cvv2', 4 ],
+ AVSzip => [ ':zip', 10 ],
+ AVSaddress1 => [ ':address', 30 ],
+ AVScity => [ ':city', 20 ],
+ AVSstate => [ ':state', 2 ],
+ AVScountryCode => [ ':country', 2 ],
+ OrderID => [ ':invoice_number', 22 ],
+ Amount => [ ':amount', 12 ],
+ Comments => [ ':email', 64 ],
+ TxRefNum => [ ':order_number', 40 ],# used only for Refund
+);
+
+tie my %mark_for_capture, 'Tie::IxHash', (
+ OrbitalConnectionUsername => [ ':login', 32 ],
+ OrbitalConnectionPassword => [ ':password', 32 ],
+ OrderID => [ ':invoice_number', 22 ],
+ Amount => [ ':amount', 12 ],
+ BIN => [ ':bin', 6 ],
+ MerchantID => [ ':merchant_id', 12 ],
+ TerminalID => [ ':terminal_id', 3 ],
+ TxRefNum => [ ':order_number', 40 ],
+);
+
+tie my %reversal, 'Tie::IxHash', (
+ OrbitalConnectionUsername => [ ':login', 32 ],
+ OrbitalConnectionPassword => [ ':password', 32 ],
+ TxRefNum => [ ':order_number', 40 ],
+ TxRefIdx => [ '0', 4 ],
+ OrderID => [ ':invoice_number', 22 ],
+ BIN => [ ':bin', 6 ],
+ MerchantID => [ ':merchant_id', 12 ],
+ TerminalID => [ ':terminal_id', 3 ],
+ OnlineReversalInd => [ 'Y', 1 ],
+# Always attempt to reverse authorization.
);
my %defaults = (
action
bin
merchant_id
- card_number
- expiration
- currency
- address
- city
- zip
invoice_number
amount
)
MXN => [484, 2],
);
+my %paymentech_countries = map { $_ => 1 } qw( US CA GB UK );
+my %failure_status = (
+ # values of the RespCode element
+ # in theory RespMsg should be set to a descriptive message, but it looks
+ # like that's not reliable
+ # XXX we should have a way to indicate other actions required by the
+ # processor, such as "honor with identification", "call for instructions",
+ # etc.
+ '00' => undef, # Approved
+ '04' => 'pickup', # Pickup
+ '33' => 'expired', # Card is Expired
+ '41' => 'stolen', # Lost/Stolen
+ '42' => 'inactive', # Account Not Active
+ '43' => 'stolen', # Lost/Stolen Card
+ '44' => 'inactive', # Account Not Active
+ #'45' duplicate transaction, should also have its own status
+ 'B7' => 'blacklisted', # Fraud
+ 'B9' => 'blacklisted', # On Negative File
+ 'BB' => 'stolen', # Possible Compromise
+ 'BG' => 'blacklisted', # Blocked Account
+ 'BQ' => 'blacklisted', # Issuer has Flagged Account as Suspected Fraud
+ 'C4' => 'nsf', # Over Credit Limit
+ 'D5' => 'blacklisted', # On Negative File
+ 'D7' => 'nsf', # Insufficient Funds
+ 'F3' => 'inactive', # Account Closed
+ 'K6' => 'nsf', # NSF
+);
+
sub set_defaults {
my $self = shift;
$self->port('443') unless $self->port;
$self->path('/authorize') unless $self->path;
- $self->build_subs(qw( TxRefNum ProcStatus ApprovalStatus StatusMsg Response ));
-
+ $self->build_subs(qw(
+ order_number
+ ));
+
+ #leaking gateway-specific anmes? need to be mapped to B:OP standards :)
+ # ProcStatus
+ # ApprovalStatus
+ # StatusMsg
+ # RespCode
+ # AuthCode
+ # AVSRespCode
+ # CVV2RespCode
+ # Response
}
sub build {
ref($skel) eq 'HASH' or die 'Tried to build non-hash';
foreach my $k (keys(%$skel)) {
my $v = $skel->{$k};
- # Not recursive like B:OP:WesternACH; Paymentech requests are only one layer deep.
+ my $l;
+ ($v, $l) = @$v if(ref $v eq 'ARRAY');
if($v =~ /^:(.*)/) {
# Get the content field with that name.
$data{$k} = $content{$1};
else {
$data{$k} = $v;
}
+ # Ruthlessly enforce field length.
+ $data{$k} = substr($data{$k}, 0, $l) if($data{$k} and $l);
}
return \%data;
}
('normal authorization' => 'AC',
'authorization only' => 'A',
'credit' => 'R',
+ 'void' => 'V',
'post authorization' => 'MFC', # for our use, doesn't go in the request
);
$content{'message_type'} = $message_type{lc($content{'action'})}
or die "unsupported action: '".$content{'action'}."'";
- if($content{'message_type'} eq 'MFC') {
- die 'MarkForCapture not implemented';
- # for later implementation
- }
foreach (keys(%defaults) ) {
$content{$_} = $defaults{$_} if !defined($content{$_});
}
- $DB::single=1;
if(length($content{merchant_id}) == 12) {
$content{bin} = '000002' # PNS
}
$content{name} = $content{first_name} . ' ' . $content{last_name};
# According to the spec, the first 8 characters of this have to be unique.
# The test server doesn't enforce this, but we comply anyway to the extent possible.
- if($content{invoice_number}) {
- # Mark it so that it's obvious that this is an invoice number
- $content{invoice_number} = 'INV '.$content{invoice_number};
- }
- else {
- # Otherwise, make something up!
+ if(! $content{invoice_number}) {
+ # Choose one arbitrarily
$content{invoice_number} ||= sprintf("%04x%04x",time % 2**16,int(rand() * 2**16));
}
- $content{expiration} =~ s/\D//g; # Because Freeside sends it as mm/yy, not mmyy.
+ # Always send as MMYY
+ $content{expiration} =~ s/\D//g;
+ $content{expiration} = sprintf('%04d',$content{expiration});
+
+ $content{country} ||= 'US';
+ $content{country} = ( $paymentech_countries{ $content{country} }
+ ? $content{country}
+ : ''
+ ),
$self->content(%content);
return;
$DB::single = $DEBUG;
$self->map_fields();
+ my %content = $self->content;
- # This will change when we add e-check support
my @required_fields = @required;
- $self->required_fields(@required_fields);
+ my $request;
+ if( $content{'message_type'} eq 'MFC' ) {
+ $request = { MarkForCapture => $self->build(\%mark_for_capture) };
+ push @required_fields, 'order_number';
+ }
+ elsif( $content{'message_type'} eq 'V' ) {
+ $request = { Reversal => $self->build(\%reversal) };
+ }
+ else {
+ $request = { NewOrder => $self->build(\%new_order) };
+ push @required_fields, qw(
+ card_number
+ expiration
+ currency
+ address
+ city
+ zip
+ );
+ }
- # This will change when we add mark-for-capture support
- my $request = { NewOrder => $self->build(\%new_order) };
+ $self->required_fields(@required_fields);
my $post_data = XMLout({ Request => $request }, KeepRoot => 1, NoAttr => 1, NoSort => 1);
warn $page if $DEBUG;
- my $response;
- my $error = '';
- if ($server_response =~ /200/){
- $response = XMLin($page, KeepRoot => 0);
- $self->Response($response);
- my ($r) = values(%$response);
- if(!exists($r->{'ProcStatus'})) {
- $error = "Malformed response: '$page'";
- }
- elsif($r->{'ProcStatus'} != 0 || $r->{'ApprovalStatus'} != 1) {
- $error = "Transaction error: '". ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'";
- }
- else {
- # success!
+ my $response = XMLin($page, KeepRoot => 0);
+ #$self->Response($response);
+
+ #use Data::Dumper;
+ #warn Dumper($response) if $DEBUG;
+
+ my ($r) = values(%$response);
+ #foreach(qw(ProcStatus RespCode AuthCode AVSRespCode CVV2RespCode)) {
+ # if(exists($r->{$_}) and
+ # !ref($r->{$_})) {
+ # $self->$_($r->{$_});
+ # }
+ #}
+
+ foreach (keys %$r) {
+
+ #turn empty hashrefs into the empty string
+ $r->{$_} = '' if ref($r->{$_}) && ! keys %{ $r->{$_} };
+
+ #turn hashrefs with content into scalars
+ $r->{$_} = $r->{$_}{'content'}
+ if ref($r->{$_}) && exists($r->{$_}{'content'});
+ }
+
+ if ($server_response !~ /^200/) {
+
+ $self->is_success(0);
+ my $error = "Server error: '$server_response'";
+ $error .= " / Transaction error: '".
+ ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'"
+ if $r->{'ProcStatus'} != 0;
+ $self->error_message($error);
+
+ } else {
+
+ if ( !exists($r->{'ProcStatus'}) ) {
+
+ $self->is_success(0);
+ $self->error_message( "Malformed response: '$page'" );
+
+ } elsif ( $r->{'ProcStatus'} != 0 or
+ # NewOrders get ApprovalStatus, Reversals don't.
+ ( exists($r->{'ApprovalStatus'}) ?
+ $r->{'ApprovalStatus'} != 1 :
+ $r->{'StatusMsg'} ne 'Approved' )
+ )
+ {
+
+ $self->failure_status( $failure_status{ $r->{RespCode} } || 'decline' );
+ $self->is_success(0);
+ $self->error_message( "Transaction error: '".
+ ($r->{'ProcStatusMsg'} || $r->{'StatusMsg'}) . "'"
+ );
+
+ } else { # success!
+
$self->is_success(1);
- $self->authorization($r->{'TxRefNum'});
+ # For credits, AuthCode is empty and gets converted to a hashref.
+ $self->authorization($r->{'AuthCode'}) if !ref($r->{'AuthCode'});
+ $self->order_number($r->{'TxRefNum'});
}
- }else{
- $error = "Server error: '$server_response'";
+
}
- $self->error_message($error);
- $self->is_success(0) if $error;
}
=head1 SYNOPSIS
-$trans = new Business::OnlinePayment('PaymenTech');
-$trans->content(
- login => "login",
- password => "password",
- merchant_id => "000111222333",
- terminal_id => "001",
- type => "CC",
- card_number => "5500000000000004",
- expiration => "0211",
- address => "123 Anystreet",
- city => "Sacramento",
- zip => "95824",
- action => "Normal Authorization",
- amount => "24.99",
-
-);
-
-$trans->submit;
-if($trans->is_approved) {
- print "Approved: ".$trans->authorization;
-
-} else {
- print "Failed: ".$trans->error_message;
-
-}
+ $trans = new Business::OnlinePayment('PaymenTech',
+ merchant_id => "000111222333",
+ terminal_id => "001",
+ currency => "USD", # CAD, MXN
+ );
+
+ $trans->content(
+ login => "login",
+ password => "password",
+ type => "CC",
+ card_number => "5500000000000004",
+ expiration => "0211",
+ address => "123 Anystreet",
+ city => "Sacramento",
+ zip => "95824",
+ action => "Normal Authorization",
+ amount => "24.99",
+ );
+
+ $trans->submit;
+ if($trans->is_approved) {
+ print "Approved: ".$trans->authorization;
+ } else {
+ print "Failed: ".$trans->error_message;
+ }
=head1 NOTES
-The only supported transaction types are Normal Authorization and Credit. Paymentech
-supports separate Authorize and Capture actions as well as recurring billing, but
-those are not yet implemented.
-
-Electronic check processing is not yet supported.
+Electronic check processing and recurring billing are not yet supported.
=head1 AUTHOR