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