## Business::OnlinePayment::Skipjack ## ## Original Skipjack.pm developed by New York Connect Net (http://nyct.net) ## Michael Bacarella ## ## Modified for GetCareer.com by Slipstream.com ## Troy Davis ## ## 'Adapted' (completely rewritten) for Business::OnlinePayment ## by Fire2Wire Internet Services (http://www.fire2wire.com) ## Mark Wells ## Kristian Hoffmann ## James Switzer ## Required packages: ## Net::SSLeay ## Text::CSV ## Business::OnlinePayment package Business::OnlinePayment::Skipjack; use strict; use Carp; use Business::OnlinePayment 3; use Business::OnlinePayment::HTTPS; use Text::CSV_XS; use vars qw( @ISA $VERSION $DEBUG ); $VERSION = "0.2"; $DEBUG = 0; @ISA = qw( Business::OnlinePayment::HTTPS ); my %CC_ERRORS = ( '-1' => 'Invalid length (-1)', '-35' => 'Invalid credit card number (-35)', '-37' => 'Failed communication (-37)', '-39' => 'Serial number is too short (-39)', '-51' => 'The zip code is invalid', '-52' => 'The shipto zip code is invalid', '-53' => 'Length of expiration date (-53)', '-54' => 'Length of account number date (-54)', '-55' => 'Length of street address (-55)', '-56' => 'Length of shipto street address (-56)', '-57' => 'Length of transaction amount (-57)', '-58' => 'Length of name (-58)', '-59' => 'Length of location (-59)', '-60' => 'Length of state (-60)', '-61' => 'Length of shipto state (-61)', '-62' => 'Length of order string (-62)', '-64' => 'Invalid phone number (-64)', '-65' => 'Empty name (-65)', '-66' => 'Empty email (-66)', '-67' => 'Empty street address (-66)', '-68' => 'Empty city (-68)', '-69' => 'Empty state (-69)', '-70' => 'Empty zip code (-70)', '-71' => 'Empty order number (-71)', '-72' => 'Empty account number (-72)', '-73' => 'Empty expiration month (-73)', '-74' => 'Empty expiration year (-74)', '-75' => 'Empty serial number (-75)', '-76' => 'Empty transaction amount (-76)', '-79' => 'Length of customer name (-79)', '-80' => 'Length of shipto customer name (-80)', '-81' => 'Length of customer location (-81)', '-82' => 'Length of customer state (-82)', '-83' => 'Length of shipto phone (-83)', '-84' => 'Pos Error duplicate ordernumber (-84)', '-91' => 'Pos Error CVV2 (-91)', '-92' => 'Pos Error Approval Code (-92)', '-93' => 'Pos Error Blind Credits Not Allowed (-93)', '-94' => 'Pos Error Blind Credits Failed (-94)', '-95' => 'Pos Error Voice Authorizations Not Allowed (-95)', ); my %AVS_CODES = ( 'X' => 'Exact match, 9 digit zip', 'Y' => 'Exact match, 5 digit zip', 'A' => 'Address match only', 'W' => '9 digit match only', 'Z' => '5 digit match only', 'N' => 'No address or zip match', 'U' => 'Address unavailable', 'R' => 'Issuer system unavailable', 'E' => 'Not a mail/phone order', 'S' => 'Service not supported' ); my %FIELDS = ( name => 'sjname', email => 'Email', address => 'Streetaddress', city => 'City', state => 'State', zip => 'Zipcode', order_number => 'Ordernumber', card_number => 'Accountnumber', exp_month => 'Month', exp_year => 'Year', amount => 'Transactionamount', orderstring => 'Orderstring', phone => 'Shiptophone', 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)); } sub set_defaults { my $self = shift; # For production $self->server('www.skipjackic.com'); $self->port(443); return; } sub submit { my $self = shift; my %c = $self->content; my (%input, %output); unless ( $c{type} =~ /(cc|visa|mastercard|american express|discover)/i ) { croak 'Business::OnlinePayment::Skipjack does not support "' . $c{type}. '" transactions'; } # 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}; %input = map { ($FIELDS{$_} || $_), $c{$_} } keys(%c); } elsif ( $c{action} =~ /^(credit|void|post authorization)$/i ) { $self->path('/scripts/evolvcc.dll?SJAPI_TransactionChangeStatusRequest'); %input = map { ($CHANGE_STATUS_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!"; } } elsif ( lc($c{action}) eq 'status' ) { $self->{_action} = 'status'; $self->path('/scripts/evolvcc.dll?SJAPI_TransactionStatusRequest'); %input = map { ($GET_STATUS_FIELDS{$_} || $_), $c{$_} } keys(%c); } 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'}); return; } sub is_success { my $self = shift; 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 $self = shift; my $r; if($self->is_success) { return ''; } 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_XS; my $csv_values = new Text::CSV_XS; my ($keystring, $valuestring) = split(/\r\n/, $page); $csv_keys->parse($keystring); $csv_values->parse($valuestring); @output{$csv_keys->fields()} = $csv_values->fields(); return %output; } 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 =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 Modified for GetCareer.com by Slipstream.com Troy Davis 'Adapted' (completely rewritten) for Business::OnlinePayment by Fire2Wire Internet Services (http://www.fire2wire.com) Mark Wells Kristian Hoffmann James Switzer Boring 0.2 update by Ivan Kohler =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 Modified for GetCareer.com by Slipstream.com Troy Davis =head1 SEE ALSO L =cut