1.02 version can now parse BofA's new, incompatible settlement return
[Business-OnlinePayment-BankOfAmerica.git] / BankOfAmerica.pm
1 package Business::OnlinePayment::BankOfAmerica;
2
3 # $Id: BankOfAmerica.pm,v 1.3 2002-11-19 23:41:24 ivan Exp $
4
5 use strict;
6 use Carp qw(croak);
7 use Business::OnlinePayment;
8 use Net::SSLeay qw/make_form post_https make_headers/;
9 #use Text::CSV;
10 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
11
12 require Exporter;
13
14 @ISA = qw(Exporter AutoLoader Business::OnlinePayment);
15 @EXPORT = qw();
16 @EXPORT_OK = qw();
17 $VERSION = '1.02';
18
19 sub set_defaults {
20     my $self = shift;
21
22     $self->server('cart.bamart.com');
23     $self->port('443');
24
25 }
26
27 sub revmap_fields {
28     my($self, %map) = @_;
29     my %content = $self->content();
30     foreach(keys %map) {
31 #    warn "$_ = ". ( ref($map{$_})
32 #                         ? ${ $map{$_} }
33 #                         : $content{$map{$_}} ). "\n";
34         $content{$_} = ref($map{$_})
35                          ? ${ $map{$_} }
36                          : $content{$map{$_}};
37     }
38     $self->content(%content);
39 }
40
41 sub get_fields {
42     my($self,@fields) = @_;
43
44     my %content = $self->content();
45     my %new = ();
46     foreach( grep defined $content{$_}, @fields) { $new{$_} = $content{$_}; }
47     return %new;
48 }
49
50 sub order_number {
51     my $self = shift;
52     if (@_) {
53       $self->{order_number} = shift;
54     }
55     return $self->{order_number};
56 }
57
58 sub submit {
59     my($self) = @_;
60
61     my %content = $self->content;
62
63     my $action = lc($content{'action'});
64
65     die 'Normal Authorization not supported'
66       if $action eq 'normal authorization';
67
68     my @fields;
69     my $ioc_indicator = '';
70
71     if ( $action eq 'authorization only' ) {
72       $self->path('/payment.mart');
73       @fields = qw(
74           ioc_merchant_id ioc_order_total_amount ioc_merchant_shopper_id
75           ioc_merchant_order_id ecom_billto_postal_name_first
76           ecom_billto_postal_name_last ecom_billto_postal_street_line1
77           ecom_billto_postal_street_line2 ecom_billto_postal_city
78           ecom_billto_postal_stateprov ecom_billto_postal_postalcode
79           ecom_billto_postal_countrycode ecom_billto_telecom_phone_number
80           ecom_billto_online_email ecom_payment_card_name
81           ecom_payment_card_number ecom_payment_card_expdate_month
82           ecom_payment_card_expdate_year
83       );
84     } elsif ( $action eq 'credit' ) {
85       $self->path('/Settlement.mart');
86       $ioc_indicator = 'R';
87       @fields = qw(
88           ioc_handshake_id ioc_merchant_id ioc_user_name ioc_password
89           ioc_order_number ioc_indicator ioc_settlement_amount
90           ioc_authorization_code ioc_email_flag
91       );
92       #    ioc_email_flag ioc_close_flag ioc_invoice_notes ioc_email_notes_flag
93     } elsif ( $action eq 'post authorization' ) {
94       $self->path('/Settlement.mart');
95       $ioc_indicator = 'S';
96       @fields = qw(
97           ioc_handshake_id ioc_merchant_id ioc_user_name ioc_password
98           ioc_order_number ioc_indicator ioc_settlement_amount
99           ioc_authorization_code ioc_email_flag
100       );
101       #    ioc_email_flag ioc_close_flag ioc_invoice_notes ioc_email_notes_flag
102     } else {
103       die "unknown action $action";
104     }
105
106 #        $self->required_fields(qw/type login password action amount last_name
107 #                                  first_name card_number expiration/);
108
109     my($month, $year);
110     unless ( $action eq 'post authorization' ) {
111
112         if (  $self->transaction_type() =~
113                 /^(cc|visa|mastercard|american express|discover)$/i
114            ) {
115         } else {
116             Carp::croak("BankOfAmerica can't handle transaction type: ".
117                         $self->transaction_type());
118         }
119
120       $content{'expiration'} =~ /^(\d+)\D+(\d+)$/
121         or croak "unparsable expiration $content{expiration}";
122
123       ( $month, $year ) = ( $1, $2 );
124       $year += 2000 if $year < 2000; #not y4k safe, oh shit
125
126     }
127
128     $self->revmap_fields(
129         ioc_merchant_id                  => \($self->merchant_id()),
130         ioc_user_name                    => 'login',
131         ioc_password                     => 'password',
132         ioc_invoice_notes                => 'description',
133         ioc_order_total_amount           => 'amount',
134         ioc_settlement_amount            => 'amount',
135         ioc_merchant_order_id            => 'invoice_number',
136         ioc_order_number                 => 'order_number',
137         ioc_merchant_shopper_id          => 'customer_id',
138         ecom_billto_postal_name_last     => 'last_name',
139         ecom_billto_postal_name_first    => 'first_name',
140         ecom_billto_postal_street_line1  => 'address',
141 #!!!        ecom_billto_postal_street_line2 => 'address',
142         ecom_billto_postal_city          => 'city',
143         ecom_billto_postal_stateprov     => 'state',
144         ecom_billto_postal_postalcode    => 'zip',
145         ecom_payment_card_number         => 'card_number',
146         ecom_billto_postal_countrycode   => 'country',
147         ecom_billto_telecom_phone_number => 'phone',
148         ecom_billto_online_email         => 'email',
149
150         ecom_payment_card_name =>
151           \( $content{'name'} || "$content{first_name} $content{last_name}" ),
152
153         ecom_payment_card_expdate_month => \$month,
154         ecom_payment_card_expdate_year  => \$year,
155
156         ioc_authorization_code           => 'authorization',
157         ioc_indicator                    => \$ioc_indicator,
158         ioc_handshake_id                 => 'order_number',
159         ioc_email_flag                   => \'No',
160
161     );
162
163     my %post_data = $self->get_fields( @fields );
164
165 #    warn "$_ => $post_data{$_}\n" for keys %post_data;
166 #    warn "\n";
167     my $pd = make_form(%post_data);
168     my $s = $self->server();
169     my $p = $self->port();
170     my $t = $self->path();
171     my $headers = make_headers('Referer' => $content{'referer'} )
172       unless $action eq 'post authorization';
173     my($page,$server_response,%headers) = post_https($s,$p,$t,$headers,$pd);
174
175     my %response;
176     if ( $action eq 'post authorization' ) {
177 #      warn $page;
178       #$page =~ s/<HTML>.*//s;
179       #$page =~ s/\n+$//g;
180       $page =~ s/\r//g;
181       %response =
182         map { /^(\w+)\=(.*)$/ or /^()()$/ or die "unparsable response: $_";
183               lc($1) => $2 }
184           #split(/\r/, $page);
185           split(/\n/, $page);
186     } else {
187       %response =
188         map { /^(\w+)\=(.*)$/ or die "unparsable response: $_";
189               lc($1) => $2 }
190           split(/\<BR\>/i, $page);
191     }
192
193     #warn "$_ => $response{$_}\n" for keys %response;
194
195     $self->server_response($page);
196
197     if ( $response{'ioc_response_code'} eq '0' ) {
198         $self->is_success(1);
199         $self->result_code($response{'ioc_response_code'});
200         $self->authorization($response{'ioc_authorization_code'});
201         $self->order_number($response{'ioc_order_id'});
202     } else {
203         $self->is_success(0);
204         $self->result_code($response{'ioc_response_code'});
205         my $error =
206           $action eq 'post authorization'
207             ? $response{'ioc_response_desc'}
208             : $response{'ioc_reject_description'};
209         $error .= ': '. $response{'ioc_missing_fields'}
210           if $response{'ioc_missing_fields'};
211         $error .= ': '. $response{'ioc_invalid_fields'}
212           if $response{'ioc_invalid_fields'};
213         $self->error_message($error);
214     }
215
216 }
217
218 1;
219 __END__
220
221 =head1 NAME
222
223 Business::OnlinePayment::BankOfAmerica - Bank of America backend for Business::OnlinePayment
224
225 =head1 SYNOPSIS
226
227   use Business::OnlinePayment;
228
229   my $tx = new Business::OnlinePayment("BankOfAmerica", 'merchant_id' => 'YOURMERCHANTID');
230   $tx->content(
231       type           => 'VISA',
232       action         => 'Authorization Only',
233       description    => 'Business::OnlinePayment test',
234       amount         => '49.95',
235       invoice_number => '100100',
236       customer_id    => 'jsk',
237       first_name     => 'Jason',
238       last_name      => 'Kohles',
239       address        => '123 Anystreet',
240       city           => 'Anywhere',
241       state          => 'UT',
242       zip            => '84058',
243       email          => 'ivan-bofa@420.am',
244       card_number    => '4007000000027',
245       expiration     => '09/99',
246       referer        => 'http://cleanwhisker.420.am/',
247   );
248   $tx->submit();
249
250   if($tx->is_success()) {
251       print "Card processed successfully: ".$tx->authorization."\n";
252   } else {
253       print "Card was rejected: ".$tx->error_message."\n";
254   }
255
256  if($tx->is_success()) {
257
258       $auth = $tx->authorization;
259       $ordernum = $tx->order_number;
260
261       my $capture = new Business::OnlinePayment("BankOfAmerica", 'merchant_id' => 'YOURMERCHANTID' );
262
263       $capture->content(
264           action         => 'Post Authorization',
265           login          => 'YOURLOGIN
266           password       => 'YOURPASSWORD',
267           order_number   => $ordernum,
268           amount         => '0.01',
269           authorization  => $auth,
270           description    => 'Business::OnlinePayment::BankOfAmerica visa test',
271       );
272
273       $capture->submit();
274
275       if($capture->is_success()) { 
276           print "Card captured successfully: ".$capture->authorization."\n";
277       } else {
278           print "Card was rejected: ".$capture->error_message."\n";
279       }
280
281   }
282
283 =head1 SUPPORTED TRANSACTION TYPES
284
285 =head2 Visa, MasterCard, American Express, JCB, Discover/Novus, Carte blanche/Diners Club
286
287 Content required for `Authorization Only': type, action, amount,
288 invoice_number, customer_id, first_name, last_name, address, city, state, zip,
289 email, card_number, expiration, referer
290
291 Content required for `Post Authorization': action, login, password,
292 order_number, amount, authorization, description
293
294 `Normal Authorization' is not supported by the Bank of America gateway.
295
296 `Credit' is untested.
297
298 =head1 DESCRIPTION
299
300 For detailed information see L<Business::OnlinePayment>.
301
302 =head1 NOTE
303
304 Unlike Business::OnlinePayment or early verisons of
305 Business::OnlinePayment::AuthorizeNet, Business::OnlinePayment::BankOfAmerica
306 requires separate I<first_name> and I<last_name> fields.
307
308 An additional I<name> field is optional.  By default the I<first_name> and
309 I<last_name> fields will be concatenated.
310
311 =head1 NOTE
312
313 Business::OnlinePayment::BankOfAmerica does not support the
314 B<Normal Authorization> mode which combines authorization and capture into a
315 single tranaction.  You must use the B<Authorization Only> mode followed by the
316 B<Post Authorization> mode.  The B<Credit> mode is supported.
317
318 =head1 COMPATIBILITY
319
320 This module implements the interface documented at
321 http://www.bankofamerica.com/merchantservices/index.cfm?template=merch_ic_estores_developer.cfm
322
323 The settlement API is documented at
324 https://manager.bamart.com/welcome/SettlementAPI.pdf
325
326 =head1 BUGS
327
328 No login and password are required for B<Authorization Only> mode.  Access
329 is restricted only by the merchant id (available in any public store webpage
330 which passes off to the backend system) and HTTP referer header.
331
332 There is no way to run test transactions against the settlement API.
333
334 =head1 AUTHOR
335
336 Ivan Kohler <ivan-bofa@420.am>
337
338 Based on Business::OnlinePayment::AuthorizeNet written by Jason Kohles.
339
340 =head1 SEE ALSO
341
342 perl(1). L<Business::OnlinePayment>.
343
344 =cut
345