package Business::OnlinePayment::Skipjack;
use strict;
-use Business::OnlinePayment;
-use Net::SSLeay qw(post_https make_form);
-use Text::CSV;
-use vars qw(@ISA @EXPORT @EXPORT_OK);
+use Carp;
+use Business::OnlinePayment 3;
+use Business::OnlinePayment::HTTPS;
+use Text::CSV_XS;
+use vars qw( @ISA $VERSION $DEBUG );
-#$VERSION="0.1";
+$VERSION = "0.5";
+$DEBUG = 0;
-require Exporter;
-
-@ISA = qw(Exporter AutoLoader Business::OnlinePayment);
-# Items to export into callers namespace by default. Note: do not export
-# names by default without a very good reason. Use EXPORT_OK instead.
-# Do not simply export all your public functions/methods/constants.
-@EXPORT = qw();
-@EXPORT_OK = qw();
+@ISA = qw( Business::OnlinePayment::HTTPS );
my %CC_ERRORS = (
'-1' => 'Invalid length (-1)',
'-61' => 'Length of shipto state (-61)',
'-62' => 'Length of order string (-62)',
'-64' => 'Invalid phone number (-64)',
- '-65' => 'Empty name (-65)',
+ '-65' => 'Empty name (-65)',
'-66' => 'Empty email (-66)',
'-67' => 'Empty street address (-66)',
'-68' => 'Empty city (-68)',
city => 'City',
state => 'State',
zip => 'Zipcode',
- invoice_number => 'Ordernumber',
+ order_number => 'Ordernumber',
card_number => 'Accountnumber',
exp_month => 'Month',
exp_year => 'Year',
login => 'Serialnumber',
);
+my %CHANGE_STATUS_FIELDS = (
+ login => 'szSerialNumber',
+ password => 'szDeveloperSerialNumber',
+ order_number => 'szOrderNumber',
+ # => 'szTransactionId',
+ amount => 'szAmount',
+);
+
+my @CHANGE_STATUS_RESPONSE = (
+ 'Serial Number',
+ 'Error Code',
+ 'NumRecs',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+);
+
+my @CHANGE_STATUS_RESPONSE_RECORD = (
+ 'Serial Number (Record)',
+ 'Amount',
+ 'Desired Status',
+ 'Status Response',
+ 'Status Response Message',
+ 'Order Number',
+ 'Transaction Id'
+);
+
+my %CHANGE_STATUS_ERROR_CODES = (
+ '0' => 'Success',
+ '-1' => 'Invalid Command',
+ '-2' => 'Parameter Missing',
+ '-3' => 'Failed retrieving response',
+ '-4' => 'Invalid Status',
+ '-5' => 'Failed reading security flags',
+ '-6' => 'Developer serial number not found',
+ '-7' => 'Invalid Serial Number',
+ '-8' => 'Expiration year not four characters',
+ '-9' => 'Credit card expired',
+ '-10' => 'Invalid starting date (recurring payment)',
+ '-11' => 'Failed adding recurring payment',
+ '-12' => 'Invalid frequency (recurring payment)',
+);
+
+my %GET_STATUS_FIELDS = (
+ login => 'szSerialNumber',
+ password => 'szDeveloperSerialNumber',
+ order_number => 'szOrderNumber',
+ #date => 'szDate', # would probably need some massaging
+ # and parse_SJAPI_TransactionStatusRequest would
+ # need to handle multiple records...
+);
+
+my @GET_STATUS_RESPONSE = (
+ 'Serial Number',
+ 'Error Code',
+ 'NumRecs',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+ #'Reserved',
+);
+
+my @GET_STATUS_RESPONSE_RECORD = (
+ 'Serial Number (Record)',
+ 'Amount',
+ 'Transaction Status Code',
+ 'Transaction Status Message',
+ 'Order Number',
+ 'Transaction Date',
+ 'Transaction Id',
+ 'Approval Code',
+ 'Batch Number',
+);
+
+my %GET_STATUS_ERROR_CODES = (
+ '0' => 'Success',
+ '-1' => 'Invalid Command',
+ '-2' => 'Parameter Missing',
+ '-3' => 'Failed retrieving response',
+ '-4' => 'Invalid Status',
+ '-5' => 'Failed reading security flags',
+ '-6' => 'Developer serial number not found',
+ '-7' => 'Invalid Serial Number',
+ '-8' => 'Expiration year not four characters',
+ '-9' => 'Credit card expired',
+);
+
+my %CUR_STATUS_CODES = (
+ '0' => 'Idle',
+ '1' => 'Authorized',
+ '2' => 'Denied',
+ '3' => 'Settled',
+ '4' => 'Credited',
+ '5' => 'Deleted',
+ '6' => 'Archived',
+ '7' => 'Pre-Auth',
+);
+
+my %PEND_STATUS_CODES = (
+ '0' => 'Idle',
+ '1' => 'Pending Credit',
+ '2' => 'Pending Settlement ',
+ '3' => 'Pending Delete',
+ '4' => 'Pending Authorization',
+ '5' => 'Pending Settle Force (for Manual Accts)',
+ '6' => 'Pending Recurring',
+);
sub _gen_ordernum { return int(rand(4000000000)); }
# For production
$self->server('www.skipjackic.com');
- $self->path('/scripts/evolvcc.dll?AuthorizeAPI');
-
- # For auth testing
- #$self->server('developer.skipjackic.com'); # test mode
- #$self->path('/scripts/evolvcc.dll?AuthorizeAPI');
-
- # For comms testing
- #$self->server('www.skipjackic.com');
- #$self->path('/secure/echo.asp');
$self->port(443);
my $self = shift;
my %c = $self->content;
my (%input, %output);
- my ($page, $response, %reply_headers);
-# if($c{type} and $c{type} ne 'normal authorization') {
- if($c{type} eq 'credit') {
- warn 'Business::OnlinePayment::Skipjack does not support "' .
- $c{type}. '"transactions';
- return;
+ unless ( $c{type} =~ /(cc|visa|mastercard|american express|discover)/i ) {
+ croak 'Business::OnlinePayment::Skipjack does not support "' .
+ $c{type}. '" transactions';
}
- # FIXME!
- # This should be set from %processor_info.
+ # skipjack kicks out "Length of transaction amount (-57)" or "Invalid amount"
+ # if the amount is missing .XX
+ $c{amount} = sprintf('%.2f', $c{amount})
+ if defined($c{amount}) && length($c{amount});
+
+ if ( lc($c{action}) eq 'normal authorization' ) {
+ $self->{_action} = 'normal authorization';
+ $self->path('/scripts/evolvcc.dll?AuthorizeAPI');
+
+ $c{expiration} =~ /(\d\d?)\D*(\d\d?)/; # Slightly less crude way to extract the exp date.
+ $c{exp_month} = sprintf('%02d',$1);
+ $c{exp_year} = sprintf('%02d',$2);
+
+ $c{order_number} = _gen_ordernum unless $c{order_number};
+
+ $c{orderstring} = '0~'.$c{description}.'~'.$c{amount}.'~1~N~||'
+ unless $c{orderstring};
- # Or, if we want, from $c{login}.
+ %input = map { ($FIELDS{$_} || $_), $c{$_} } keys(%c);
- $c{expiration} =~ /(\d\d?).*(\d\d?)/; # extremely crude way to split the month and year
- $c{exp_month} = sprintf('%02d',$1);
- $c{exp_year} = sprintf('%02d',$2);
+ } elsif ( $c{action} =~ /^(credit|void|post authorization)$/i ) {
- $c{invoice_number} = _gen_ordernum unless $c{invoice_number};
+ $self->path('/scripts/evolvcc.dll?SJAPI_TransactionChangeStatusRequest');
- $c{orderstring} = '0~'.$c{description}.'~'.$c{amount}.'~1~N~||'
- unless $c{orderstring};
+ %input = map { ($CHANGE_STATUS_FIELDS{$_} || $_), $c{$_} } keys %c;
- %input = map { ($FIELDS{$_} || $_), $c{$_} } keys(%c);
+ if ( lc($c{action} ) eq 'credit' ) {
+ $self->{_action} = 'credit';
+ $input{szDesiredStatus} = 'CREDIT';
+ } elsif ( lc($c{action} ) eq 'void' ) {
+ $self->{_action} = 'void';
+ $input{szDesiredStatus} = 'DELETE';
+ } elsif ( lc($c{action} ) eq 'post authorization' ) {
+ $self->{_action} = 'postauth';
+ $input{szDesiredStatus} = 'SETTLE';
+ } else {
+ die "fatal: $c{action} is not credit or void!";
+ }
- ($page, $response, %reply_headers) =
- post_https($self->server,
- $self->port,
- $self->path,
- '',
- make_form(%input));
+ } elsif ( lc($c{action}) eq 'status' ) {
+ $self->{_action} = 'status';
+ $self->path('/scripts/evolvcc.dll?SJAPI_TransactionStatusRequest');
+ %input = map { ($GET_STATUS_FIELDS{$_} || $_), $c{$_} } keys(%c);
- %output = parse_Authorize_API($page);
+ } else {
+
+ croak 'Business::OnlinePayment::Skipjack does not support "'.
+ $c{action}. '" actions';
+
+ }
+
+ $self->server('developer.skipjackic.com') # test mode
+ if $self->test_transaction();
+
+ my( $page, $response ) = $self->https_post( %input );
+ warn "\n$page\n" if $DEBUG;
+
+ if ( $self->{_action} eq 'normal authorization' ) {
+ %output = parse_Authorize_API($page);
+ } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
+ %output = parse_SJAPI_TransactionChangeStatusRequest($page);
+ } elsif ( $self->{_action} eq 'status' ) {
+ %output = parse_SJAPI_TransactionStatusRequest($page);
+ } else {
+ die "fatal: unknown action: ". $self->{_action};
+ }
$self->{_result} = \%output;
$self->authorization($output{'AUTHCODE'});
{
my $self = shift;
- return ($self->{_result}->{'szIsApproved'} == 1);
+ if ( $self->{_action} eq 'normal authorization' ) {
+
+ return( $self->{_result}->{'szIsApproved'} == 1 );
+
+ } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
+
+ return( $self->{_result}{'Error Code'} eq '0' # == 0 matches ''
+ && uc($self->{_result}{'Status Response'}) eq 'SUCCESSFUL'
+ );
+
+ } elsif ( $self->{_action} eq 'status' ) {
+
+ return( $self->{_result}{'Error Code'} eq '0' ); # == 0 matches ''
+
+ } else {
+ die "fatal: unknown action: ". $self->{_action};
+ }
+
}
sub error_message
my $r;
if($self->is_success) { return ''; }
- if(($r = $self->{_result}->{'szReturnCode'}) < 0) { return $CC_ERRORS{$r}; }
- if($r = $self->{_result}->{'szAVSResponseMessage'}) { return $r; }
- if($r = $self->{_result}->{'szAuthorizationDeclinedMessage'}) { return $r; }
+
+ if ( $self->{_action} eq 'normal authorization' ) {
+
+ if(($r = $self->{_result}->{'szReturnCode'}) < 0) { return $CC_ERRORS{$r}; }
+ if($r = $self->{_result}->{'szAVSResponseMessage'}) { return $r; }
+ if($r = $self->{_result}->{'szAuthorizationDeclinedMessage'}) { return $r; }
+
+ } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
+
+ if ( ( $r = $self->{_result}{'Error Code'} ) < 0 ) {
+ return $CHANGE_STATUS_ERROR_CODES{$r};
+ } else {
+ return $self->{_result}{'Status Response Message'};
+ }
+
+ } elsif ( $self->{_action} eq 'status' ) {
+
+ if ( ( $r = $self->{_result}{'Error Code'} ) < 0 ) {
+ return $CHANGE_STATUS_ERROR_CODES{$r};
+ } else {
+ return $self->{_result}{'Status Response Message'};
+ }
+
+ } else {
+ die "fatal: unknown action: ". $self->{_action};
+ }
+
+}
+
+
+#sub result_code { shift->{_result}->{'ezIsApproved'}; }
+sub authorization { shift->{_result}{'szAuthorizationResponseCode'}; }
+sub avs_code { shift->{_result}{'szAVSResponseCode'}; }
+sub order_number { shift->{_result}{'szOrderNumber'}; }
+sub cvv2_response { shift->{_result}{'szCVV2ResponseCode'}; }
+sub cavv_response { shift->{_result}{'szCAVVResponseCode'}; }
+
+sub status {
+ my $self = shift;
+ $CUR_STATUS_CODES{
+ substr( $self->{_result}{'Transaction Status Code'}, 0, 1 )
+ };
+}
+
+sub pending_status {
+ my $self = shift;
+ $PEND_STATUS_CODES{
+ substr( $self->{_result}{'Transaction Status Code'}, 1, 2 )
+ };
}
sub parse_Authorize_API
my $page = shift;
my %output;
- my $csv_keys = new Text::CSV;
- my $csv_values = new Text::CSV;
+ my $csv_keys = new Text::CSV_XS;
+ my $csv_values = new Text::CSV_XS;
my ($keystring, $valuestring) = split(/\r\n/, $page);
$csv_keys->parse($keystring);
}
-__END__
+sub parse_SJAPI_TransactionChangeStatusRequest
+{
+ my $page = shift;
+
+ my $csv = new Text::CSV_XS;
+
+ my %output;
+
+ my @records = split(/\r\n/, $page);
+
+ $csv->parse(shift @records)
+ or die "CSV parse failed on " . $csv->error_input;
+ @output{@CHANGE_STATUS_RESPONSE} = $csv->fields();
+
+ # we only handle a single record reponse, as that's all this module will
+ # currently submit...
+ $csv->parse(shift @records)
+ or die "CSV parse failed on " . $csv->error_input;
+ @output{@CHANGE_STATUS_RESPONSE_RECORD} = $csv->fields();
+
+ return %output;
+
+}
+
+sub parse_SJAPI_TransactionStatusRequest
+{
+ my $page = shift;
+
+ my $csv = new Text::CSV_XS;
+
+ my %output;
+
+ my @records = split(/\r\n/, $page);
+
+ #$csv->parse(shift @records)
+ $csv->parse(shift @records)
+ or die "CSV parse failed on " . $csv->error_input;
+ @output{@GET_STATUS_RESPONSE} = $csv->fields();
+
+ # we only handle a single record reponse, as that's all this module will
+ # currently submit...
+ $csv->parse(shift @records)
+ or die "CSV parse failed on " . $csv->error_input;
+ @output{@GET_STATUS_RESPONSE_RECORD} = $csv->fields();
+
+ return %output;
+
+}
1;
+__END__
+
+=head1 NAME
+
+Business::OnlinePayment::Skipjack - Skipjack backend module for Business::OnlinePayment
+
+=head1 SYNOPSIS
+
+ use Business::OnlinePayment;
+
+ ####
+ # One step transaction, the simple case.
+ ####
+
+ my $tx = new Business::OnlinePayment("Skipjack");
+ $tx->content(
+ type => 'VISA',
+ login => '000178101827', # "HTML serial number"
+ action => 'Normal Authorization',
+ description => 'Business::OnlinePayment test',
+ amount => '49.95',
+ invoice_number => '100100',
+ customer_id => 'jsk',
+ first_name => 'Jason',
+ last_name => 'Kohles',
+ address => '123 Anystreet',
+ city => 'Anywhere',
+ state => 'UT',
+ zip => '84058',
+ card_number => '4007000000027',
+ expiration => '09/02',
+ cvv2 => '1234', #optional
+ #referer => 'http://valid.referer.url/',
+ );
+ $tx->submit();
+
+ if($tx->is_success()) {
+ print "Card processed successfully: ".$tx->authorization."\n";
+ } else {
+ print "Card was rejected: ".$tx->error_message."\n";
+ }
+
+ ###
+ # Process a credit...
+ ###
+
+ my $tx = new Business::OnlinePayment( "Skipjack" );
+
+ $tx->content(
+ type => 'VISA',
+ login => '000178101827', # "HTML serial number"
+ password => '100594217288', # "developer serial number"
+ action => 'Normal Authorization',
+ description => 'Business::OnlinePayment test',
+ amount => '49.95',
+ invoice_number => '100100',
+ customer_id => 'jsk',
+ first_name => 'Jason',
+ last_name => 'Kohles',
+ address => '123 Anystreet',
+ city => 'Anywhere',
+ state => 'UT',
+ zip => '84058',
+ card_number => '4007000000027',
+ expiration => '09/02',
+ cvv2 => '1234', #optional
+ #referer => 'http://valid.referer.url/',
+ );
+ $tx->submit();
+
+ if($tx->is_success()) {
+ print "Card credited successfully: ".$tx->authorization."\n";
+ } else {
+ print "Credit was rejected: ".$tx->error_message."\n";
+ }
+
+
+=head1 SUPPORTED TRANSACTION TYPES
+
+=head2 CC, Visa, MasterCard, American Express, Discover
+
+Content required for Normal Authorization : login, action, amount, card_number,
+expiration, name, address, city, state, zip, phone, email
+
+Content required for Void or Credit: login, password, action, order_number
+
+=head1 DESCRIPTION
+
+For detailed information see L<Business::OnlinePayment>
+
+=head1 PREREQUISITES
+
+Net::SSLeay _or_ ( Crypt::SSLeay and LWP )
+
+=head1 NOTE ON CREDITS
+
+If you want to process credits, you must have your developer serial number
+applied to your production account. See
+http://www.skipjack.com/resources/Education/serialnumbers.htm
+
+=head1 STATUS
+
+This modules supports a non-standard "status" action that corresponds to
+Skipjack's TransactionStatusRequest. It should be documented.
+
+=head1 AUTHOR
+
+Inspiried by (but no longer contains) code from:
+
+ Original Skipjack.pm developed by New York Connect Net (http://nyct.net)
+ Michael Bacarella <mbac@nyct.net>
+
+ Modified for GetCareer.com by Slipstream.com
+ Troy Davis <troy@slipstream.com>
+
+'Adapted' (completely rewritten) for Business::OnlinePayment
+by Fire2Wire Internet Services (http://www.fire2wire.com)
+Mark Wells <mark@pc-intouch.com>
+Kristian Hoffmann <khoff@pc-intouch.com>
+James Switzer <jamess@fire2wire.com>
+
+Boring 0.2 update by Ivan Kohler <ivan-skipjack@420.am>
+
+=head1 COPYRIGHT
+
+Copyright (c) 2006 Fire2Wire Internet Services (http://www.fire2wire.com)
+All rights reserved. This program is free software; you can redistribute it
+and/or modify it under the same terms as Perl itself.
+
+Inspiried by (but no longer contains) code from:
+
+ Original Skipjack.pm developed by New York Connect Net (http://nyct.net)
+ Michael Bacarella <mbac@nyct.net>
+
+ Modified for GetCareer.com by Slipstream.com
+ Troy Davis <troy@slipstream.com>
+
+=head1 SEE ALSO
+
+L<Business::OnlinePayment>
+
+=cut