c0bf8382fa72abeedd1a3b17eb26961211c64583
[Business-OnlinePayment-Skipjack.git] / Skipjack.pm
1 ## Business::OnlinePayment::Skipjack
2 ##
3 ## Original Skipjack.pm developed by New York Connect Net (http://nyct.net)
4 ## Michael Bacarella <mbac@nyct.net>
5 ##
6 ## Modified for GetCareer.com by Slipstream.com
7 ## Troy Davis <troy@slipstream.com>
8 ##
9 ## 'Adapted' (completely rewritten) for Business::OnlinePayment 
10 ## by Fire2Wire Internet Services (http://www.fire2wire.com)
11 ## Mark Wells <mark@pc-intouch.com>
12 ## Kristian Hoffmann <khoff@pc-intouch.com>
13 ## James Switzer <jamess@fire2wire.com>
14
15 ## Required packages:
16 ## Net::SSLeay
17 ## Text::CSV
18 ## Business::OnlinePayment
19
20
21 package Business::OnlinePayment::Skipjack;
22
23 use strict;
24 use Carp;
25 use Business::OnlinePayment 3;
26 use Business::OnlinePayment::HTTPS;
27 use Text::CSV_XS;
28 use vars qw( @ISA $VERSION $DEBUG );
29
30 $VERSION = "0.2";
31 $DEBUG = 0;
32
33 @ISA = qw( Business::OnlinePayment::HTTPS );
34
35 my %CC_ERRORS = (
36         '-1'    =>      'Invalid length (-1)',
37         '-35'   =>      'Invalid credit card number (-35)',
38         '-37'   =>      'Failed communication (-37)',
39         '-39'   =>      'Serial number is too short (-39)',
40         '-51'   =>      'The zip code is invalid',
41         '-52'   =>      'The shipto zip code is invalid',
42         '-53'   =>      'Length of expiration date (-53)',
43         '-54'   =>      'Length of account number date (-54)',
44         '-55'   =>      'Length of street address (-55)',
45         '-56'   =>      'Length of shipto street address (-56)',
46         '-57'   =>      'Length of transaction amount (-57)',
47         '-58'   =>      'Length of name (-58)',
48         '-59'   =>      'Length of location (-59)',
49         '-60'   =>      'Length of state (-60)',
50         '-61'   =>      'Length of shipto state (-61)',
51         '-62'   =>      'Length of order string (-62)',
52         '-64'   =>      'Invalid phone number (-64)',
53         '-65'   =>      'Empty name (-65)', 
54         '-66'   =>      'Empty email (-66)',
55         '-67'   =>      'Empty street address (-66)',
56         '-68'   =>      'Empty city (-68)',
57         '-69'   =>      'Empty state (-69)',
58         '-70'   =>      'Empty zip code (-70)',
59         '-71'   =>      'Empty order number (-71)',
60         '-72'   =>      'Empty account number (-72)',
61         '-73'   =>      'Empty expiration month (-73)',
62         '-74'   =>      'Empty expiration year (-74)',
63         '-75'   =>      'Empty serial number (-75)',
64         '-76'   =>      'Empty transaction amount (-76)',
65         '-79'   =>      'Length of customer name (-79)',
66         '-80'   =>      'Length of shipto customer name (-80)',
67         '-81'   =>      'Length of customer location (-81)',
68         '-82'   =>      'Length of customer state (-82)',
69         '-83'   =>      'Length of shipto phone (-83)',
70         '-84'   =>      'Pos Error duplicate ordernumber (-84)',
71         '-91'   =>      'Pos Error CVV2 (-91)',
72         '-92'   =>      'Pos Error Approval Code (-92)',
73         '-93'   =>      'Pos Error Blind Credits Not Allowed (-93)',
74         '-94'   =>      'Pos Error Blind Credits Failed (-94)',
75         '-95'   =>      'Pos Error Voice Authorizations Not Allowed (-95)',
76         );
77
78 my %AVS_CODES = (
79         'X' => 'Exact match, 9 digit zip', 
80         'Y' => 'Exact match, 5 digit zip', 
81         'A' => 'Address match only', 
82         'W' => '9 digit match only', 
83         'Z' => '5 digit match only', 
84         'N' => 'No address or zip match', 
85         'U' => 'Address unavailable', 
86         'R' => 'Issuer system unavailable', 
87         'E' => 'Not a mail/phone order', 
88         'S' => 'Service not supported' 
89         );
90
91 my %FIELDS = (
92         name    => 'sjname',
93         email   => 'Email',
94         address => 'Streetaddress',
95         city    => 'City',
96         state   => 'State',
97         zip     => 'Zipcode',
98         order_number    => 'Ordernumber',
99         card_number     => 'Accountnumber',
100         exp_month       => 'Month',
101         exp_year        => 'Year',
102         amount  => 'Transactionamount',
103         orderstring     => 'Orderstring',
104         phone   => 'Shiptophone',
105         login   => 'Serialnumber',
106         );
107
108 my %CHANGE_STATUS_FIELDS = (
109         login        => 'szSerialNumber',
110         password     => 'szDeveloperSerialNumber',
111         order_number => 'szOrderNumber',
112         # => 'szTransactionId',
113         amount       => 'szAmount',
114 );
115
116 my @CHANGE_STATUS_RESPONSE = (
117   'Serial Number',
118   'Error Code',
119   'NumRecs',
120   #'Reserved',
121   #'Reserved',
122   #'Reserved',
123   #'Reserved',
124   #'Reserved',
125   #'Reserved',
126   #'Reserved',
127   #'Reserved',
128 );
129
130 my @CHANGE_STATUS_RESPONSE_RECORD = (
131   'Serial Number (Record)',
132   'Amount',
133   'Desired Status',
134   'Status Response',
135   'Status Response Message',
136   'Order Number',
137   'Transaction Id'
138 );
139
140 my %CHANGE_STATUS_ERROR_CODES = (
141     '0' => 'Success',
142    '-1' => 'Invalid Command',
143    '-2' => 'Parameter Missing',
144    '-3' => 'Failed retrieving response',
145    '-4' => 'Invalid Status',
146    '-5' => 'Failed reading security flags',
147    '-6' => 'Developer serial number not found',
148    '-7' => 'Invalid Serial Number',
149    '-8' => 'Expiration year not four characters',
150    '-9' => 'Credit card expired',
151   '-10' => 'Invalid starting date (recurring payment)',
152   '-11' => 'Failed adding recurring payment',
153   '-12' => 'Invalid frequency (recurring payment)',
154 );
155
156 my %GET_STATUS_FIELDS = (
157   login        => 'szSerialNumber',
158   password     => 'szDeveloperSerialNumber',
159   order_number => 'szOrderNumber',
160   #date         => 'szDate', # would probably need some massaging
161                              # and parse_SJAPI_TransactionStatusRequest would
162                              # need to handle multiple records...
163 );
164
165 my @GET_STATUS_RESPONSE = (
166   'Serial Number',
167   'Error Code',
168   'NumRecs',
169   #'Reserved',
170   #'Reserved',
171   #'Reserved',
172   #'Reserved',
173   #'Reserved',
174   #'Reserved',
175   #'Reserved',
176   #'Reserved',
177 );
178
179 my @GET_STATUS_RESPONSE_RECORD = (
180   'Serial Number (Record)',
181   'Amount',
182   'Transaction Status Code',
183   'Transaction Status Message',
184   'Order Number',
185   'Transaction Date',
186   'Transaction Id',
187   'Approval Code',
188   'Batch Number',
189 );
190
191 my %GET_STATUS_ERROR_CODES = (
192    '0' => 'Success',
193   '-1' => 'Invalid Command',
194   '-2' => 'Parameter Missing',
195   '-3' => 'Failed retrieving response',
196   '-4' => 'Invalid Status',
197   '-5' => 'Failed reading security flags',
198   '-6' => 'Developer serial number not found',
199   '-7' => 'Invalid Serial Number',
200   '-8' => 'Expiration year not four characters',
201   '-9' => 'Credit card expired',
202 );
203
204 my %CUR_STATUS_CODES = (
205   '0' => 'Idle',
206   '1' => 'Authorized',
207   '2' => 'Denied',
208   '3' => 'Settled',
209   '4' => 'Credited',
210   '5' => 'Deleted',
211   '6' => 'Archived',
212   '7' => 'Pre-Auth',
213 );
214
215 my %PEND_STATUS_CODES = (
216   '0' => 'Idle',
217   '1' => 'Pending Credit',
218   '2' => 'Pending Settlement ',
219   '3' => 'Pending Delete',
220   '4' => 'Pending Authorization',
221   '5' => 'Pending Settle Force (for Manual Accts)',
222   '6' => 'Pending Recurring',
223 );
224
225 sub _gen_ordernum { return int(rand(4000000000)); }
226
227 sub set_defaults
228 {
229   my $self = shift;
230
231   # For production
232   $self->server('www.skipjackic.com');
233
234   $self->port(443);
235
236   return;
237 }
238
239
240 sub submit
241 {
242   my $self = shift;
243   my %c = $self->content;
244   my (%input, %output);
245
246   unless ( $c{type} =~ /(cc|visa|mastercard|american express|discover)/i ) {
247     croak 'Business::OnlinePayment::Skipjack does not support "' . 
248           $c{type}. '" transactions';
249   }
250
251   # skipjack kicks out "Length of transaction amount (-57)" or "Invalid amount"
252   # if the amount is missing .XX
253   $c{amount} = sprintf('%.2f', $c{amount})
254     if defined($c{amount}) && length($c{amount});
255
256   if ( lc($c{action}) eq 'normal authorization' ) {
257     $self->{_action} = 'normal authorization';
258     $self->path('/scripts/evolvcc.dll?AuthorizeAPI');
259
260     $c{expiration} =~ /(\d\d?)\D*(\d\d?)/; # Slightly less crude way to extract the exp date.
261     $c{exp_month} = sprintf('%02d',$1);
262     $c{exp_year} = sprintf('%02d',$2);
263
264     $c{order_number} = _gen_ordernum unless $c{order_number};
265
266     $c{orderstring} = '0~'.$c{description}.'~'.$c{amount}.'~1~N~||'
267         unless $c{orderstring};
268
269     %input = map { ($FIELDS{$_} || $_), $c{$_} } keys(%c);
270
271   } elsif ( $c{action} =~ /^(credit|void|post authorization)$/i ) {
272
273     $self->path('/scripts/evolvcc.dll?SJAPI_TransactionChangeStatusRequest');
274
275     %input = map { ($CHANGE_STATUS_FIELDS{$_} || $_), $c{$_} } keys %c;
276
277     if ( lc($c{action} ) eq 'credit' ) {
278       $self->{_action} = 'credit';
279       $input{szDesiredStatus} = 'CREDIT';
280     } elsif ( lc($c{action} ) eq 'void' ) {
281       $self->{_action} = 'void';
282       $input{szDesiredStatus} = 'DELETE';
283     } elsif ( lc($c{action} ) eq 'post authorization' ) {
284       $self->{_action} = 'postauth';
285       $input{szDesiredStatus} = 'SETTLE';
286     } else {
287       die "fatal: $c{action} is not credit or void!";
288     }
289
290   } elsif ( lc($c{action}) eq 'status' ) {
291
292     $self->{_action} = 'status';
293     $self->path('/scripts/evolvcc.dll?SJAPI_TransactionStatusRequest');
294     %input = map { ($GET_STATUS_FIELDS{$_} || $_), $c{$_} } keys(%c);
295
296   } else {
297
298     croak 'Business::OnlinePayment::Skipjack does not support "'.
299           $c{action}. '" actions';
300
301   }
302
303   $self->server('developer.skipjackic.com') # test mode
304     if $self->test_transaction();
305
306   my( $page, $response ) = $self->https_post( %input );
307   warn "\n$page\n" if $DEBUG;
308
309   if ( $self->{_action} eq 'normal authorization' ) {
310     %output = parse_Authorize_API($page);
311   } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
312     %output = parse_SJAPI_TransactionChangeStatusRequest($page);
313   } elsif ( $self->{_action} eq 'status' ) {
314     %output = parse_SJAPI_TransactionStatusRequest($page);
315   } else {
316     die "fatal: unknown action: ". $self->{_action};
317   }
318
319   $self->{_result} = \%output;
320   $self->authorization($output{'AUTHCODE'});
321   return;
322 }
323
324 sub is_success
325 {
326   my $self = shift;
327
328   if ( $self->{_action} eq 'normal authorization' ) {
329
330     return( $self->{_result}->{'szIsApproved'} == 1 );
331
332   } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
333
334     return(       $self->{_result}{'Error Code'}       eq '0' # == 0 matches ''
335             && uc($self->{_result}{'Status Response'}) eq 'SUCCESSFUL'
336           );
337
338   } elsif ( $self->{_action} eq 'status' ) {
339
340     return( $self->{_result}{'Error Code'} eq '0' ); # == 0 matches ''
341
342   } else {
343     die "fatal: unknown action: ". $self->{_action};
344   }
345
346 }
347
348 sub error_message
349 {
350   my $self = shift;
351   my $r;
352
353   if($self->is_success) { return ''; }
354
355   if ( $self->{_action} eq 'normal authorization' ) {
356
357     if(($r = $self->{_result}->{'szReturnCode'}) < 0) { return $CC_ERRORS{$r}; }
358     if($r = $self->{_result}->{'szAVSResponseMessage'}) { return $r; }
359     if($r = $self->{_result}->{'szAuthorizationDeclinedMessage'}) { return $r; }
360
361   } elsif ( $self->{_action} =~ /^(credit|void|postauth)$/ ) {
362
363     if ( ( $r = $self->{_result}{'Error Code'} ) < 0 ) {
364       return $CHANGE_STATUS_ERROR_CODES{$r};
365     } else {
366       return $self->{_result}{'Status Response Message'};
367     }
368
369   } elsif ( $self->{_action} eq 'status' ) {
370
371     if ( ( $r = $self->{_result}{'Error Code'} ) < 0 ) {
372       return $CHANGE_STATUS_ERROR_CODES{$r};
373     } else {
374       return $self->{_result}{'Status Response Message'};
375     }
376
377   } else {
378     die "fatal: unknown action: ". $self->{_action};
379   }
380
381 }
382
383
384 #sub result_code   { shift->{_result}->{'ezIsApproved'};              }
385 sub authorization { shift->{_result}{'szAuthorizationResponseCode'}; }
386 sub avs_code      { shift->{_result}{'szAVSResponseCode'};           }
387 sub order_number  { shift->{_result}{'szOrderNumber'};               }
388 sub cvv2_response { shift->{_result}{'szCVV2ResponseCode'};          } 
389 sub cavv_response { shift->{_result}{'szCAVVResponseCode'};          } 
390
391 sub status {
392   my $self = shift;
393   $CUR_STATUS_CODES{
394     substr( $self->{_result}{'Transaction Status Code'}, 0, 1 )
395   };
396 }
397
398 sub pending_status {
399   my $self = shift;
400   $PEND_STATUS_CODES{
401     substr( $self->{_result}{'Transaction Status Code'}, 1, 2 )
402   };
403 }
404
405 sub parse_Authorize_API
406 {
407
408   my $page = shift;
409   my %output;
410   my $csv_keys = new Text::CSV_XS;
411   my $csv_values = new Text::CSV_XS;
412
413   my ($keystring, $valuestring) = split(/\r\n/, $page);
414   $csv_keys->parse($keystring);
415   $csv_values->parse($valuestring);
416   @output{$csv_keys->fields()} = $csv_values->fields();
417
418   return %output;
419
420 }
421
422 sub parse_SJAPI_TransactionChangeStatusRequest
423 {
424   my $page = shift;
425
426   my $csv = new Text::CSV_XS;
427
428   my %output;
429
430   my @records = split(/\r\n/, $page);
431
432   $csv->parse(shift @records) or die $csv->error;
433   @output{@CHANGE_STATUS_RESPONSE} = $csv->fields();
434
435   # we only handle a single record reponse, as that's all this module will
436   #  currently submit...
437   $csv->parse(shift @records) or die $csv->error;
438   @output{@CHANGE_STATUS_RESPONSE_RECORD} = $csv->fields();
439
440   return %output;
441
442 }
443
444 sub parse_SJAPI_TransactionStatusRequest
445 {
446   my $page = shift;
447
448   my $csv = new Text::CSV_XS;
449
450   my %output;
451
452   my @records = split(/\r\n/, $page);
453
454   $csv->parse(shift @records) or die $csv->error;
455   @output{@GET_STATUS_RESPONSE} = $csv->fields();
456
457   # we only handle a single record reponse, as that's all this module will
458   #  currently submit...
459   $csv->parse(shift @records) or die $csv->error;
460   @output{@GET_STATUS_RESPONSE_RECORD} = $csv->fields();
461
462   return %output;
463
464 }
465
466 1;
467
468 __END__
469
470 =head1 NAME
471
472 Business::OnlinePayment::Skipjack - Skipjack backend module for Business::OnlinePayment
473
474 =head1 SYNOPSIS
475
476   use Business::OnlinePayment;
477
478   ####
479   # One step transaction, the simple case.
480   ####
481
482   my $tx = new Business::OnlinePayment("Skipjack");
483   $tx->content(
484       type           => 'VISA',
485       login          => '000178101827', # "HTML serial number"
486       action         => 'Normal Authorization',
487       description    => 'Business::OnlinePayment test',
488       amount         => '49.95',
489       invoice_number => '100100',
490       customer_id    => 'jsk',
491       first_name     => 'Jason',
492       last_name      => 'Kohles',
493       address        => '123 Anystreet',
494       city           => 'Anywhere',
495       state          => 'UT',
496       zip            => '84058',
497       card_number    => '4007000000027',
498       expiration     => '09/02',
499       cvv2           => '1234', #optional
500       #referer        => 'http://valid.referer.url/',
501   );
502   $tx->submit();
503
504   if($tx->is_success()) {
505       print "Card processed successfully: ".$tx->authorization."\n";
506   } else {
507       print "Card was rejected: ".$tx->error_message."\n";
508   }
509
510   ###
511   # Process a credit...
512   ###
513
514   my $tx = new Business::OnlinePayment( "Skipjack" );
515
516   $tx->content(
517       type           => 'VISA',
518       login          => '000178101827', # "HTML serial number"
519       password       => '100594217288', # "developer serial number"
520       action         => 'Normal Authorization',
521       description    => 'Business::OnlinePayment test',
522       amount         => '49.95',
523       invoice_number => '100100',
524       customer_id    => 'jsk',
525       first_name     => 'Jason',
526       last_name      => 'Kohles',
527       address        => '123 Anystreet',
528       city           => 'Anywhere',
529       state          => 'UT',
530       zip            => '84058',
531       card_number    => '4007000000027',
532       expiration     => '09/02',
533       cvv2           => '1234', #optional
534       #referer        => 'http://valid.referer.url/',
535   );
536   $tx->submit();
537
538   if($tx->is_success()) {
539       print "Card credited successfully: ".$tx->authorization."\n";
540   } else {
541       print "Credit was rejected: ".$tx->error_message."\n";
542   }
543
544
545 =head1 SUPPORTED TRANSACTION TYPES
546
547 =head2 CC, Visa, MasterCard, American Express, Discover
548
549 Content required for Normal Authorization : login, action, amount, card_number,
550 expiration, name, address, city, state, zip, phone, email
551
552 Content required for Void or Credit: login, password, action, order_number
553
554 =head1 DESCRIPTION
555
556 For detailed information see L<Business::OnlinePayment>
557
558 =head1 PREREQUISITES
559
560 Net::SSLeay _or_ ( Crypt::SSLeay and LWP )
561
562 =head1 NOTE ON CREDITS
563
564 If you want to process credits, you must have your developer serial number
565 applied to your production account.  See
566 http://www.skipjack.com/resources/Education/serialnumbers.htm
567
568 =head1 STATUS
569
570 This modules supports a non-standard "status" action that corresponds to
571 Skipjack's TransactionStatusRequest.  It should be documented.
572
573 =head1 AUTHOR
574
575 Inspiried by (but no longer contains) code from:
576
577   Original Skipjack.pm developed by New York Connect Net (http://nyct.net)
578   Michael Bacarella <mbac@nyct.net>
579
580   Modified for GetCareer.com by Slipstream.com
581   Troy Davis <troy@slipstream.com>
582
583 'Adapted' (completely rewritten) for Business::OnlinePayment 
584 by Fire2Wire Internet Services (http://www.fire2wire.com)
585 Mark Wells <mark@pc-intouch.com>
586 Kristian Hoffmann <khoff@pc-intouch.com>
587 James Switzer <jamess@fire2wire.com>
588
589 Boring 0.2 update by Ivan Kohler <ivan-skipjack@420.am>
590
591 =head1 COPYRIGHT
592
593 Copyright (c) 2006 Fire2Wire Internet Services (http://www.fire2wire.com)
594 All rights reserved.  This program is free software; you can redistribute it
595 and/or modify it under the same terms as Perl itself.
596
597 Inspiried by (but no longer contains) code from:
598
599   Original Skipjack.pm developed by New York Connect Net (http://nyct.net)
600   Michael Bacarella <mbac@nyct.net>
601
602   Modified for GetCareer.com by Slipstream.com
603   Troy Davis <troy@slipstream.com>
604
605 =head1 SEE ALSO
606
607 L<Business::OnlinePayment>
608
609 =cut