f653e8aba96965fe4ac4e8474fb6de7c7f7f56fc
[Business-OnlinePayment-Jety.git] / lib / Business / OnlinePayment / Jety.pm
1 package Business::OnlinePayment::Jety;
2
3 use strict;
4 use Carp 'croak';
5 use Business::OnlinePayment 3;
6 use Business::OnlinePayment::HTTPS;
7 use vars qw($VERSION @ISA $me $DEBUG);
8
9 use Date::Format;
10 use Tie::IxHash;
11
12 @ISA = qw(Business::OnlinePayment::HTTPS);
13 $VERSION = '0.06';
14 $me = 'Business::OnlinePayment::Jety';
15
16 $DEBUG = 0;
17
18 my %trans_type = (
19   'normal authorization' => 'echeck',
20   'void'                 => 'ereturn',
21   );
22
23 my %map = (
24 # 'function' will always be prepended
25 'normal authorization' => [ # note array-ness
26   'username'      => 'login',
27   'password'      => 'password',
28   'firstname'     => 'first_name',
29   'lastname'      => 'last_name',
30   'address1'      => 'address',
31   'address2'      => 'address2',
32   'city'          => 'city',
33   'state'         => 'state',
34   'zip'           => 'zip',
35   'email'         => 'email',
36   'phone'         => 'phone',
37   'programdesc'   => 'description',
38   'ref'           => sub { my %c = @_; 
39                            $c{'order_number'} || 
40                            substr( time2str('%Y%m%d%H%M%S',time). int(rand(10000)), -15 ) 
41                            },
42   'bankname'      => 'bank_name',
43   'bankcity'      => 'bank_city',
44   'bankstate'     => 'bank_state',
45   'accountaba'    => 'routing_code',
46   'accountdda'    => 'account_number',
47   'amount'        => sub { my %c = @_; sprintf("%.02f",$c{'amount'}) },
48 ],
49 'void' => [
50   'username'      => 'login',
51   'password'      => 'password',
52   'ref'           => 'order_number',
53   'accountdda'    => 'account_number',
54   'amount'        => sub { my %c = @_; sprintf("%.02f",$c{'amount'}) },
55 ],
56 );
57
58 my %defaults = ( # using the B:OP names
59   'phone'         => '111-111-1111',
60   'bank_name'     => 'unknown',
61   'bank_city'     => 'unknown',
62   'bank_state'    => 'XX',
63   );
64
65 my %required = (
66 'normal authorization' => [ qw(
67   type
68   action
69   login
70   password
71   first_name
72   last_name
73   address
74   city
75   state
76   zip
77   email
78   account_number
79   routing_code
80   amount
81   description
82 ) ],
83 'void' => [ qw(
84   type
85   action
86   login
87   password
88   order_number
89   account_number
90   amount
91 ) ],
92 );
93
94 sub set_defaults {
95   my $self = shift;
96   $self->server('api.cardservicesportal.com');
97   $self->port(443);
98   $self->path('/servlet/drafts.echeck');
99   return;
100 }
101
102 sub submit {
103   my $self = shift;
104   $Business::OnlinePayment::HTTPS::DEBUG = $DEBUG;
105   $DB::single = $DEBUG; 
106
107   # strip existent but empty fields so that required_fields works right
108   foreach(keys(%{$self->{_content}})) {
109     delete $self->{_content}->{$_} 
110       if (!defined($self->{_content}->{$_} ) or
111            $self->{_content}->{$_} eq '');
112   }
113
114   my %content = $self->content();
115   my $action = lc($content{'action'});
116
117   croak "Jety only supports ECHECK payments.\n"
118     if( lc($content{'type'}) ne 'echeck' );
119   croak "Unsupported transaction type: '$action'\n"
120     if( !exists($trans_type{$action}) );
121
122   $self->required_fields(@{ $required{$action} });
123
124   my @fields = @{ $map{$action} } ;
125   tie my %request, 'Tie::IxHash', ( 'function' => $trans_type{$action} );
126   while(@fields) {
127     my ($key, $value) = (shift (@fields), shift (@fields));
128     if( ref($value) eq 'CODE' ) {
129       $request{$key} = $value->(%content);
130     }
131     elsif (defined($content{$value}) and $content{$value} ne '') {
132       $request{$key} = $content{$value};
133     }
134     elsif (exists($defaults{$value})) {
135       $request{$key} = $defaults{$value};
136     } # else do nothing
137   }
138
139   $DB::single = $DEBUG;
140   if($self->test_transaction()) {
141     print "https://".$self->server.$self->path."\n";
142     print "$_\t".$request{$_}."\n" foreach keys(%request);
143     $self->error_message('test mode not supported');
144     $self->is_success(0);
145     return;
146   }
147   my ($reply, $response, %reply_headers) = $self->https_post(\%request);
148   
149   if(not $response =~ /^200/) {
150     croak "HTTPS error: '$response'";
151   }
152
153   # string looks like this:
154   # P1=1234&P2=General Status&P3=Specific Status
155   # P3 is not always there, though.
156   if($reply =~ /^P1=(\d+)&P2=([\w ]*)(&P3=(\S+))?/) {
157     if($1 == 0) {
158       $self->is_success(1);
159       $self->authorization($4);
160     }
161     else {
162       $self->is_success(0);
163       $self->error_message($2.($4 ? "($4)" : ''));
164     }
165   }
166   else {
167     croak "Malformed server response: '$reply'";
168   }
169
170   return;
171 }
172
173 sub get_returns {
174 # Required parameters:
175 # ftp_user, ftp_pass, ftp_host, ftp_path
176 # Optional:
177 # start, end
178   eval('use Date::Parse q!str2time!; use Net::FTP; use File::Temp q!tempdir!');
179   die $@ if $@;
180
181   my $self = shift;
182 # $self->required_fields, for processor options
183   my @missing;
184   my ($user, $pass, $host, $path) = map {
185     if($self->can($_) and $self->$_) {
186       $self->$_ ;
187     } else {
188       push @missing, $_; '';
189     } 
190   } qw(ftp_user ftp_pass ftp_host);
191   die "missing gateway option(s): ".join(', ',@missing)."\n" if @missing;
192   my $ftp_path = $self->ftp_path if $self->can('ftp_path');
193
194   my $start = $self->{_content}->{start};
195   $start &&= str2time($start);
196   $start ||= time - 86400;
197   $start = time2str('%Y%m%d',$start);
198
199   my $end = $self->{_content}->{end};
200   $end &&= str2time($end);
201   $end ||= time;
202   $end = time2str('%Y%m%d',$end);
203   
204   my $ftp = Net::FTP->new($host) 
205     or die "FTP connection to '$host' failed.\n";
206   $ftp->login($user, $pass) or die "FTP login failed: ".$ftp->message."\n";
207   $ftp->cwd($path) or die "can't chdir to $path\n" if $path;
208  
209   my $tmp = tempdir(CLEANUP => 1);
210   my @files;
211   foreach my $filename ($ftp->ls) {
212     if($filename =~ /^\w+_RET(\d{8}).csv$/ 
213       and $1 >= $start 
214       and $1 <= $end ) {
215       $ftp->get($filename, "$tmp/$1") or die "Failed to download $filename: ".$ftp->message."\n";
216       push @files, $1;
217     }
218   }
219   $ftp->close;
220
221   my @tids;
222   foreach my $filename (@files) {
223     open IN, '<', "$tmp/$filename";
224     my @fields = split ',',<IN>; #fetch header row
225     my ($i) = grep { $fields[$_] eq 'AccountToID' } 0..(scalar @fields - 1);
226     $i ||= 1;
227     while(<IN>) {
228       my @fields = split ',', $_;
229       push @tids, $fields[$i];
230     }
231     close IN;
232   }
233   return @tids;
234 }
235
236 1;
237 __END__
238
239 =head1 NAME
240
241 Business::OnlinePayment::Jety - Jety Payments ACH backend for Business::OnlinePayment
242
243 =head1 SYNOPSIS
244
245   use Business::OnlinePayment;
246
247   ####
248   # Electronic check authorization.  We only support 
249   # 'Normal Authorization'.
250   ####
251
252   my $tx = new Business::OnlinePayment("Jety");
253   $tx->content(
254       type           => 'ECHECK',
255       login          => 'testdrive',
256       password       => 'testpass',
257       action         => 'Normal Authorization',
258       description    => '111-111-1111 www.example.com',
259       amount         => '49.95',
260       invoice_number => '100100',
261       first_name     => 'Jason',
262       last_name      => 'Kohles',
263       address        => '123 Anystreet',
264       city           => 'Anywhere',
265       state          => 'UT',
266       zip            => '84058',
267       account_type   => 'personal checking',
268       account_number => '1000468551234',
269       routing_code   => '707010024',
270       check_number   => '1001', # optional
271   );
272   $tx->submit();
273
274   if($tx->is_success()) {
275       print "Check processed successfully: ".$tx->authorization."\n";
276   } else {
277       print "Check was rejected: ".$tx->error_message."\n";
278   }
279
280 =head1 SUPPORTED TRANSACTION TYPES
281
282 =head2 ECHECK
283
284 Content required: type, login, password, action, amount, first_name, last_name, account_number, routing_code, description.
285
286 description should be set in the form "111-111-1111 www.example.com"
287
288 =head1 DESCRIPTION
289
290 For detailed information see L<Business::OnlinePayment>.
291
292 =head1 METHODS AND FUNCTIONS
293
294 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
295
296 =head2 result_code
297
298 Returns the four-digit result code.
299
300 =head2 error_message
301
302 Returns a useful error message.
303
304 =head1 Handling of content(%content) data:
305
306 =head2 action
307
308 The following actions are valid:
309
310   normal authorization
311
312 =head1 AUTHOR
313
314 Mark Wells <mark@freeside.biz>
315
316 =head1 SEE ALSO
317
318 perl(1). L<Business::OnlinePayment>.
319
320 =cut
321