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