1 package Business::OnlinePayment::WesternACH;
5 use Business::OnlinePayment 3;
6 use Business::OnlinePayment::HTTPS;
8 use Date::Format 'time2str';
9 use Date::Parse 'str2time';
10 use vars qw($VERSION @ISA $me $DEBUG);
12 @ISA = qw(Business::OnlinePayment::HTTPS);
14 $me = 'Business::OnlinePayment::WesternACH';
22 tender_type => 'check',
27 my $required = { map { $_ => 1 } ( qw(
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.
48 password => 'password',
53 TransactionRequest => {
58 type => '_payment_type',
60 # effective date: not supported
62 type => 'tender_type',
64 InvoiceNumber => { value => 'invoice_number' },
65 AccountHolder => { value => '_full_name' },
66 Address => { value => 'address' },
67 ClientID => { value => 'customer_id' },
68 UserDefinedID => { value => 'email' },
70 routing => 'routing_code',
71 account => 'account_number',
72 check => 'check_number',
73 type => '_check_type',
74 verification => 'check_ver',
76 Authorization => { schedule => 'schedule' },
77 SECCode => { value => 'sec_code' },
84 my $returns_request = {
85 TransactionRequest => {
99 $self->server('www.webcheckexpress.com');
101 $self->path('/requester.php');
107 $Business::OnlinePayment::HTTPS::DEBUG = $DEBUG;
110 if ($self->{_content}->{command} eq 'get_returns') {
111 # Setting get_returns overrides anything else.
112 $xml_request = XMLout($self->build($returns_request), KeepRoot => 1);
115 # Error-check and prepare as a normal transaction.
118 # Return-with-error situations
119 croak "Unsupported transaction type: '" . $self->transaction_type . "'"
120 if(not $self->transaction_type =~ /^e?check$/i);
122 croak "Unsupported action: '" . $self->{_content}->{action} . "'"
123 if(!defined($self->_payment_type));
125 croak 'Test transactions not supported'
126 if($self->test_transaction());
130 $self->is_success(0);
131 $self->error_message($@);
135 $xml_request = XMLout($self->build($request), KeepRoot => 1);
138 my ($xml_reply, $response, %reply_headers) = $self->https_post({ 'Content-Type' => 'text/xml' }, $xml_request);
140 if(not $response =~ /^200/) {
141 croak "HTTPS error: '$response'";
144 $self->server_response($xml_reply);
145 my $reply = XMLin($xml_reply, KeepRoot => 1)->{TransactionResponse};
147 if(exists($reply->{Response})) {
148 $self->is_success( ( $reply->{Response}->{status} eq 'successful') ? 1 : 0);
149 $self->error_message($reply->{Response}->{ErrorMessage});
150 if(exists($reply->{Response}->{TransactionID})) {
151 # get_returns puts its results here
152 my $tid = $reply->{Response}->{TransactionID};
153 if($self->{_content}->{command} eq 'get_returns') {
154 $self->{_content}->{returns} = [ map { $_->{value} } @$tid ];
156 else { # It's not get_returns
157 $self->authorization($tid->{value});
161 elsif(exists($reply->{FatalException})) {
162 $self->is_success(0);
163 $self->error_message($reply->{FatalException});
166 $DB::single = 1 if $DEBUG;
173 my $content = $self->{_content};
174 if(exists($content->{'command'})) {
175 croak 'get_returns: command is already set on this transaction';
177 $content->{'command'} = 'get_returns';
179 if($self->is_success) {
180 if(exists($content->{'returns'})) {
181 return @{$content->{'returns'}};
188 # you need to check error_message() for details.
195 my $content = { $self->content };
198 if (ref($skel) ne 'HASH') { croak 'Failed to build non-hash' };
199 foreach my $k (keys(%$skel)) {
200 my $val = $skel->{$k};
201 # Rules for building from the skeleton:
202 # 1. If the value is a hashref, build it recursively.
203 if(ref($val) eq 'HASH') {
204 $data->{$k} = $self->build($val);
206 # 2. If the value starts with an underscore, it's treated as a method name.
207 elsif($val =~ /^_/ and $self->can($val)) {
208 $data->{$k} = $self->can($val)->($self);
210 # 3. If the value is undefined, keep it undefined.
211 elsif(!defined($val)) {
214 # 4. If the value is the name of a key in $self->content, look up that value.
215 elsif(exists($content->{$val})) {
216 $data->{$k} = $content->{$val};
218 # 5. If the value is a key in $defaults, use that value.
219 elsif(exists($defaults->{$val})) {
220 $data->{$k} = $defaults->{$val};
222 # 6. If the value is not required, use an empty string.
223 elsif(! $required->{$val}) {
228 croak "Missing request field: '$val'";
235 # For testing build().
237 return XMLout($self->build($request), KeepRoot => 1);
242 my $action = $self->{_content}->{action};
243 if(!defined($action) or $action =~ /^normal authorization$/i) {
246 elsif($action =~ /^credit$/i) {
256 my $type = $self->{_content}->{account_type};
257 return 'checking' if($type =~ /checking/i);
258 return 'savings' if($type =~ /savings/i);
259 croak "Invalid account_type: '$type'";
264 return join(' ',$self->{_content}->{first_name},$self->{_content}->{last_name});
269 my $start = time2str('%Y-%m-%d', str2time($self->{_content}->{start}));
270 croak "Invalid start date: '".$self->{_content}->{start} if !$start;
276 my $end = $self->{_content}->{end};
278 $end = time2str('%Y-%m-%d', str2time($end));
279 croak "Invalid end date: '".$self->{_content}->{end} if !$end;
283 return time2str('%Y-%m-%d', time);
292 Business::OnlinePayment::WesternACH - Western ACH backend for Business::OnlinePayment
296 use Business::OnlinePayment;
299 # Electronic check authorization. We only support
300 # 'Normal Authorization' and 'Credit'.
303 my $tx = new Business::OnlinePayment("WesternACH");
306 login => 'testdrive',
307 password => 'testpass',
308 action => 'Normal Authorization',
309 description => 'Business::OnlinePayment test',
311 invoice_number => '100100',
312 first_name => 'Jason',
313 last_name => 'Kohles',
314 address => '123 Anystreet',
318 account_type => 'personal checking',
319 account_number => '1000468551234',
320 routing_code => '707010024',
321 check_number => '1001', # optional
325 if($tx->is_success()) {
326 print "Check processed successfully: ".$tx->authorization."\n";
328 print "Check was rejected: ".$tx->error_message."\n";
331 my $tx = new Business::OnlinePayment("WesternACH");
333 login => 'testdrive',
334 password => 'testpass',
335 start => '2009-06-25', # optional; defaults to yesterday
336 end => '2009-06-26', # optional; defaults to today
341 =head1 SUPPORTED TRANSACTION TYPES
345 Content required: type, login, password|transaction_key, action, amount, first_name, last_name, account_number, routing_code, account_type.
349 For detailed information see L<Business::OnlinePayment>.
351 =head1 METHODS AND FUNCTIONS
353 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
357 Currently returns nothing; these transactions don't seem to have result codes.
361 Returns the response reason text. This can come from several locations in the response document or from certain local errors.
363 =head2 server_response
365 Returns the complete response from the server.
367 =head1 Handling of content(%content) data:
371 The following actions are valid:
378 Mark Wells <mark@freeside.biz> with advice from Ivan Kohler <ivan-westernach@freeside.biz>.
382 perl(1). L<Business::OnlinePayment>.