RT#7180: fix refund problems
[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.07';
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{'authorization'} || 
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'           => 'authorization',
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   authorization 
89   account_number
90   amount
91 ) ],
92 );
93
94 sub _info {
95   {
96     info_compat             => '0.01',
97     gateway_name            => 'Jety',
98     gateway_url             => 'http://www.jetypay.com',
99     module_version          => $VERSION,
100     supported_types         => [ 'ECHECK' ],
101     supported_actions       => [ 'Normal Authorization', 'Void' ],
102     ECHECK_void_requires_account => 1,
103   }
104 }
105
106 sub set_defaults {
107   my $self = shift;
108   $self->server('api.cardservicesportal.com');
109   $self->port(443);
110   $self->path('/servlet/drafts.echeck');
111   return;
112 }
113
114 sub submit {
115   my $self = shift;
116   $Business::OnlinePayment::HTTPS::DEBUG = $DEBUG;
117   $DB::single = $DEBUG; 
118
119   # strip existent but empty fields so that required_fields works right
120   foreach(keys(%{$self->{_content}})) {
121     delete $self->{_content}->{$_} 
122       if (!defined($self->{_content}->{$_} ) or
123            $self->{_content}->{$_} eq '');
124   }
125
126   my %content = $self->content();
127   my $action = lc($content{'action'});
128
129   croak "Jety only supports ECHECK payments.\n"
130     if( lc($content{'type'}) ne 'echeck' );
131   croak "Unsupported transaction type: '$action'\n"
132     if( !exists($trans_type{$action}) );
133
134   $self->required_fields(@{ $required{$action} });
135
136   my @fields = @{ $map{$action} } ;
137   tie my %request, 'Tie::IxHash', ( 'function' => $trans_type{$action} );
138   while(@fields) {
139     my ($key, $value) = (shift (@fields), shift (@fields));
140     if( ref($value) eq 'CODE' ) {
141       $request{$key} = $value->(%content);
142     }
143     elsif (defined($content{$value}) and $content{$value} ne '') {
144       $request{$key} = $content{$value};
145     }
146     elsif (exists($defaults{$value})) {
147       $request{$key} = $defaults{$value};
148     } # else do nothing
149   }
150
151   $DB::single = $DEBUG;
152   if($self->test_transaction()) {
153     print "https://".$self->server.$self->path."\n";
154     print "$_\t".$request{$_}."\n" foreach keys(%request);
155     $self->error_message('test mode not supported');
156     $self->is_success(0);
157     return;
158   }
159   my ($reply, $response, %reply_headers) = $self->https_post(\%request);
160   
161   if(not $response =~ /^200/) {
162     croak "HTTPS error: '$response'";
163   }
164
165   # string looks like this:
166   # P1=1234&P2=General Status&P3=Specific Status
167   # P3 is not always there, though.
168   if($reply =~ /^P1=(\d+)&P2=([\w ]*)(&P3=(\S+))?/) {
169     if($1 == 0) {
170       $self->is_success(1);
171       $self->authorization($4);
172     }
173     else {
174       $self->is_success(0);
175       $self->error_message($2.($4 ? "($4)" : ''));
176     }
177   }
178   else {
179     croak "Malformed server response: '$reply'";
180   }
181
182   return;
183 }
184
185 sub get_returns {
186 # Required parameters:
187 # ftp_user, ftp_pass, ftp_host, ftp_path
188 # Optional:
189 # start, end
190   eval('use Date::Parse q!str2time!; use Net::FTP; use File::Temp q!tempdir!');
191   die $@ if $@;
192
193   my $self = shift;
194 # $self->required_fields, for processor options
195   my @missing;
196   my ($user, $pass, $host, $path) = map {
197     if($self->can($_) and $self->$_) {
198       $self->$_ ;
199     } else {
200       push @missing, $_; '';
201     } 
202   } qw(ftp_user ftp_pass ftp_host);
203   die "missing gateway option(s): ".join(', ',@missing)."\n" if @missing;
204   my $ftp_path = $self->ftp_path if $self->can('ftp_path');
205
206   my $start = $self->{_content}->{start};
207   $start &&= str2time($start);
208   $start ||= time - 86400;
209   $start = time2str('%Y%m%d',$start);
210
211   my $end = $self->{_content}->{end};
212   $end &&= str2time($end);
213   $end ||= time;
214   $end = time2str('%Y%m%d',$end);
215   
216   my $ftp = Net::FTP->new($host) 
217     or die "FTP connection to '$host' failed.\n";
218   $ftp->login($user, $pass) or die "FTP login failed: ".$ftp->message."\n";
219   $ftp->cwd($path) or die "can't chdir to $path\n" if $path;
220  
221   my $tmp = tempdir(CLEANUP => 1);
222   my @files;
223   foreach my $filename ($ftp->ls) {
224     if($filename =~ /^\w+_RET(\d{8}).csv$/ 
225       and $1 >= $start 
226       and $1 <= $end ) {
227       $ftp->get($filename, "$tmp/$1") or die "Failed to download $filename: ".$ftp->message."\n";
228       push @files, $1;
229     }
230   }
231   $ftp->close;
232
233   my @tids;
234   foreach my $filename (@files) {
235     open IN, '<', "$tmp/$filename";
236     my @fields = split ',',<IN>; #fetch header row
237     my ($i) = grep { $fields[$_] eq 'AccountToID' } 0..(scalar @fields - 1);
238     $i ||= 1;
239     while(<IN>) {
240       my @fields = split ',', $_;
241       push @tids, $fields[$i];
242     }
243     close IN;
244   }
245   return @tids;
246 }
247
248 1;
249 __END__
250
251 =head1 NAME
252
253 Business::OnlinePayment::Jety - Jety Payments ACH backend for Business::OnlinePayment
254
255 =head1 SYNOPSIS
256
257   use Business::OnlinePayment;
258
259   ####
260   # Electronic check authorization.  We only support 
261   # 'Normal Authorization'.
262   ####
263
264   my $tx = new Business::OnlinePayment("Jety");
265   $tx->content(
266       type           => 'ECHECK',
267       login          => 'testdrive',
268       password       => 'testpass',
269       action         => 'Normal Authorization',
270       description    => '111-111-1111 www.example.com',
271       amount         => '49.95',
272       invoice_number => '100100',
273       first_name     => 'Jason',
274       last_name      => 'Kohles',
275       address        => '123 Anystreet',
276       city           => 'Anywhere',
277       state          => 'UT',
278       zip            => '84058',
279       account_type   => 'personal checking',
280       account_number => '1000468551234',
281       routing_code   => '707010024',
282       check_number   => '1001', # optional
283   );
284   $tx->submit();
285
286   if($tx->is_success()) {
287       print "Check processed successfully: ".$tx->authorization."\n";
288   } else {
289       print "Check was rejected: ".$tx->error_message."\n";
290   }
291
292 =head1 SUPPORTED TRANSACTION TYPES
293
294 =head2 ECHECK
295
296 Content required: type, login, password, action, amount, first_name, last_name, account_number, routing_code, description.
297
298 description should be set in the form "111-111-1111 www.example.com"
299
300 =head1 DESCRIPTION
301
302 For detailed information see L<Business::OnlinePayment>.
303
304 =head1 METHODS AND FUNCTIONS
305
306 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
307
308 =head2 result_code
309
310 Returns the four-digit result code.
311
312 =head2 error_message
313
314 Returns a useful error message.
315
316 =head1 Handling of content(%content) data:
317
318 =head2 action
319
320 The following actions are valid:
321
322   normal authorization
323
324 =head1 AUTHOR
325
326 Mark Wells <mark@freeside.biz>
327
328 =head1 SEE ALSO
329
330 perl(1). L<Business::OnlinePayment>.
331
332 =cut
333