0.03
[Business-OnlinePayment-WesternACH.git] / lib / Business / OnlinePayment / WesternACH.pm
1 package Business::OnlinePayment::WesternACH;
2
3 use strict;
4 use Carp;
5 use Business::OnlinePayment 3;
6 use Business::OnlinePayment::HTTPS;
7 use XML::Simple;
8 use Date::Format 'time2str';
9 use Date::Parse  'str2time';
10 use vars qw($VERSION @ISA $me $DEBUG);
11
12 @ISA = qw(Business::OnlinePayment::HTTPS);
13 $VERSION = '0.03';
14 $me = 'Business::OnlinePayment::WesternACH';
15
16 $DEBUG = 0;
17
18 my $defaults = {
19   command      => 'payment',
20   check_ver    => 'yes',
21   sec_code     => 'PPD',
22   tender_type  => 'check',
23   check_number => 9999,
24   schedule     => 'live',
25 };
26
27 my $required = { map { $_ => 1 } ( qw(
28   login
29   password
30   command
31   amount
32   tender_type
33   _full_name
34   routing_code
35   check_number
36   _check_type 
37 ))};
38
39
40
41 # Structure of the XML request document
42 # Right sides of the hash entries are Business::OnlinePayment 
43 # field names.  Those that start with _ are local method names.
44
45 my $auth = {
46 Authentication => {
47   username => 'login',
48   password => 'password',
49 }
50 };
51
52 my $request = {
53 TransactionRequest => {
54   %$auth,
55   Request => {
56     command => 'command',
57     Payment => {
58       type   => '_payment_type',
59       amount => 'amount',
60       # effective date: not supported
61       Tender => {
62         type   => 'tender_type',
63         amount => 'amount',
64         InvoiceNumber => { value => 'invoice_number' },
65         AccountHolder => { value => '_full_name'      },
66         Address       => { value => 'address'       },
67         ClientID      => { value => 'customer_id'    },
68         CheckDetails => {
69           routing      => 'routing_code',
70           account      => 'account_number',
71           check        => 'check_number',
72           type         => '_check_type',
73           verification => 'check_ver',
74         },
75         Authorization => { schedule => 'schedule' },
76         SECCode => { value => 'sec_code' },
77       },
78     },
79   }
80 }
81 };
82
83 my $returns_request = {
84 TransactionRequest => {
85   %$auth,
86   Request => {
87     command => 'command',
88     DateRange => {
89       start => '_start',
90       end   => '_end',
91     },
92   },
93 }
94 };
95
96 sub set_defaults {
97   my $self = shift;
98   $self->server('www.webcheckexpress.com');
99   $self->port(443);
100   $self->path('/requester.php');
101   return;
102 }
103
104 sub submit {
105   my $self = shift;
106   $Business::OnlinePayment::HTTPS::DEBUG = $DEBUG;
107   my $xml_request;
108
109   if ($self->{_content}->{command} eq 'get_returns') {
110     # Setting get_returns overrides anything else.
111     $xml_request = XMLout($self->build($returns_request), KeepRoot => 1);
112   }
113   else {
114     # Error-check and prepare as a normal transaction.
115
116       eval {
117       # Return-with-error situations
118       croak "Unsupported transaction type: '" . $self->transaction_type . "'"
119         if(not $self->transaction_type =~ /^e?check$/i);
120
121       croak "Unsupported action: '" . $self->{_content}->{action} . "'"
122         if(!defined($self->_payment_type));
123
124       croak 'Test transactions not supported'
125         if($self->test_transaction());
126     };
127
128     if($@) {
129       $self->is_success(0);
130       $self->error_message($@);
131       return;
132     }
133     
134     $xml_request = XMLout($self->build($request), KeepRoot => 1);
135   }
136   $DB::single=1;
137   my ($xml_reply, $response, %reply_headers) = $self->https_post({ 'Content-Type' => 'text/xml' }, $xml_request);
138   
139   if(not $response =~ /^200/) {
140     croak "HTTPS error: '$response'";
141   }
142
143   $self->server_response($xml_reply);
144   my $reply = XMLin($xml_reply, KeepRoot => 1)->{TransactionResponse};
145
146   if(exists($reply->{Response})) {
147     $self->is_success( ( $reply->{Response}->{status} eq 'successful') ? 1 : 0);
148     $self->error_message($reply->{Response}->{ErrorMessage});
149     if(exists($reply->{Response}->{TransactionID})) {
150       # get_returns puts its results here
151       my $tid = $reply->{Response}->{TransactionID};
152       if($self->{_content}->{command} eq 'get_returns') {
153         $self->{_content}->{returns} =  [ map { $_->{value} } @$tid ];
154       }
155       else { # It's not get_returns
156         $self->authorization($tid->{value});
157       }
158     }
159   }
160   elsif(exists($reply->{FatalException})) {
161     $self->is_success(0);
162     $self->error_message($reply->{FatalException});
163   }
164
165   $DB::single = 1 if $DEBUG;
166
167   return;
168 }
169
170 sub get_returns {
171   my $self = shift;
172   my $content = $self->{_content};
173   if(exists($content->{'command'})) {
174     croak 'get_returns: command is already set on this transaction';
175   }
176   $content->{'command'} = 'get_returns';
177   $self->submit;
178   if($self->is_success) {
179     if(exists($content->{'returns'})) {
180       return @{$content->{'returns'}};
181     }
182     else {
183       return ();
184     }
185   }
186   else {
187     # you need to check error_message() for details.
188     return ();
189   }
190 }
191
192 sub build {
193   my $self = shift;
194     my $content = { $self->content };
195     my $skel = shift;
196     my $data;
197     if (ref($skel) ne 'HASH') { croak 'Failed to build non-hash' };
198     foreach my $k (keys(%$skel)) {
199       my $val = $skel->{$k};
200       # Rules for building from the skeleton:
201       # 1. If the value is a hashref, build it recursively.
202       if(ref($val) eq 'HASH') {
203         $data->{$k} = $self->build($val);
204       }
205       # 2. If the value starts with an underscore, it's treated as a method name.
206       elsif($val =~ /^_/ and $self->can($val)) {
207         $data->{$k} = $self->can($val)->($self);
208       }
209       # 3. If the value is undefined, keep it undefined.
210       elsif(!defined($val)) {
211         $data->{$k} = undef;
212       }
213       # 4. If the value is the name of a key in $self->content, look up that value.
214     elsif(exists($content->{$val})) {
215       $data->{$k} = $content->{$val};
216     }
217     # 5. If the value is a key in $defaults, use that value.
218     elsif(exists($defaults->{$val})) {
219       $data->{$k} = $defaults->{$val};
220     }
221     # 6. If the value is not required, use an empty string.
222     elsif(! $required->{$val}) {
223       $data->{$k} = '';
224     }
225     # 7. Fail.
226     else {
227       croak "Missing request field: '$val'";
228     }
229   }
230   return $data;
231 }
232
233 sub XML {
234   # For testing build().
235   my $self = shift;
236   return XMLout($self->build($request), KeepRoot => 1);
237 }
238
239 sub _payment_type {
240   my $self = shift;
241   my $action = $self->{_content}->{action};
242   if(!defined($action) or $action =~ /^normal authorization$/i) {
243     return 'debit';
244   }
245   elsif($action =~ /^credit$/i) {
246     return 'credit';
247   }
248   else {
249     return;
250   }
251 }
252
253 sub _check_type {
254   my $self = shift;
255   my $type = $self->{_content}->{account_type};
256   return 'checking' if($type =~ /checking/i);
257   return 'savings'  if($type =~ /savings/i);
258   croak "Invalid account_type: '$type'";
259 }
260
261 sub _full_name {
262   my $self = shift;
263   return join(' ',$self->{_content}->{first_name},$self->{_content}->{last_name});
264 }
265
266 sub _start {
267   my $self = shift;
268   my $start = time2str('%Y-%m-%d', str2time($self->{_content}->{start}));
269   croak "Invalid start date: '".$self->{_content}->{start} if !$start;
270   return $start;
271 }
272
273 sub _end {
274   my $self = shift;
275   my $end = $self->{_content}->{end};
276   if($end) {
277     $end = time2str('%Y-%m-%d', str2time($end));
278     croak "Invalid end date: '".$self->{_content}->{end} if !$end;
279     return $end;
280   }
281   else {
282     return time2str('%Y-%m-%d', time);
283   }
284 }
285
286 1;
287 __END__
288
289 =head1 NAME
290
291 Business::OnlinePayment::WesternACH - Western ACH backend for Business::OnlinePayment
292
293 =head1 SYNOPSIS
294
295   use Business::OnlinePayment;
296
297   ####
298   # Electronic check authorization.  We only support 
299   # 'Normal Authorization' and 'Credit'.
300   ####
301
302   my $tx = new Business::OnlinePayment("WesternACH");
303   $tx->content(
304       type           => 'ECHECK',
305       login          => 'testdrive',
306       password       => 'testpass',
307       action         => 'Normal Authorization',
308       description    => 'Business::OnlinePayment test',
309       amount         => '49.95',
310       invoice_number => '100100',
311       first_name     => 'Jason',
312       last_name      => 'Kohles',
313       address        => '123 Anystreet',
314       city           => 'Anywhere',
315       state          => 'UT',
316       zip            => '84058',
317       account_type   => 'personal checking',
318       account_number => '1000468551234',
319       routing_code   => '707010024',
320       check_number   => '1001', # optional
321   );
322   $tx->submit();
323
324   if($tx->is_success()) {
325       print "Check processed successfully: ".$tx->authorization."\n";
326   } else {
327       print "Check was rejected: ".$tx->error_message."\n";
328   }
329
330   my $tx = new Business::OnlinePayment("WesternACH");
331   $tx->content(
332       login     => 'testdrive',
333       password  => 'testpass',
334       start     => '2009-06-25', # optional; defaults to yesterday
335       end       => '2009-06-26', # optional; defaults to today
336       );
337   $tx->get_returns;
338   
339
340 =head1 SUPPORTED TRANSACTION TYPES
341
342 =head2 ECHECK
343
344 Content required: type, login, password|transaction_key, action, amount, first_name, last_name, account_number, routing_code, account_type.
345
346 =head1 DESCRIPTION
347
348 For detailed information see L<Business::OnlinePayment>.
349
350 =head1 METHODS AND FUNCTIONS
351
352 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
353
354 =head2 result_code
355
356 Currently returns nothing; these transactions don't seem to have result codes.
357
358 =head2 error_message
359
360 Returns the response reason text.  This can come from several locations in the response document or from certain local errors.
361
362 =head2 server_response
363
364 Returns the complete response from the server.
365
366 =head1 Handling of content(%content) data:
367
368 =head2 action
369
370 The following actions are valid:
371
372   normal authorization
373   credit
374
375 =head1 AUTHOR
376
377 Mark Wells <mark@freeside.biz> with advice from Ivan Kohler <ivan-westernach@freeside.biz>.
378
379 =head1 SEE ALSO
380
381 perl(1). L<Business::OnlinePayment>.
382
383 =cut
384