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