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