33577031ca6cb0e14718d46cbd62c7508423eaa6
[Business-OnlinePayment-eSelectPlus.git] / eSelectPlus.pm
1 package Business::OnlinePayment::eSelectPlus;
2
3 use strict;
4 use Carp;
5 use Tie::IxHash;
6 use Business::OnlinePayment 3;
7 use Business::OnlinePayment::HTTPS 0.03;
8 use vars qw($VERSION $DEBUG @ISA);
9
10 @ISA = qw(Business::OnlinePayment::HTTPS);
11 $VERSION = '0.06';
12 $DEBUG = 0;
13
14 sub set_defaults {
15     my $self = shift;
16
17     #USD
18     #$self->server('esplusqa.moneris.com');  # development
19     $self->server('esplus.moneris.com');   # production
20     $self->path('/gateway_us/servlet/MpgRequest');
21
22     ##CAD
23     ##$self->server('esqa.moneris.com');  # development
24     #$self->server('www3.moneris.com');   # production
25     #$self->path('/gateway2/servlet/MpgRequest');
26
27     $self->port('443');
28
29     $self->build_subs(qw( order_number avs_code ));
30     # avs_code order_type md5 cvv2_response cavv_response
31 }
32
33 sub submit {
34     my($self) = @_;
35
36     if ( defined( $self->{_content}{'currency'} )
37               &&  $self->{_content}{'currency'} eq 'CAD' ) {
38       $self->server('www3.moneris.com');
39       $self->path('/gateway2/servlet/MpgRequest');
40     } else { #sorry, default to USD
41       $self->server('esplus.moneris.com');
42       $self->path('/gateway_us/servlet/MpgRequest');
43     }
44
45     if ($self->test_transaction)  {
46        if ( defined( $self->{_content}{'currency'} )
47                  &&  $self->{_content}{'currency'} eq 'CAD' ) {
48          $self->server('esqa.moneris.com');
49          $self->{_content}{'login'} = 'store2';   # store[123]
50          $self->{_content}{'password'} = 'yesguy';
51        } else { #sorry, default to USD
52          $self->server('esplusqa.moneris.com');
53          $self->{_content}{'login'} = 'monusqa002';   # monusqa00[123]
54          $self->{_content}{'password'} = 'qatoken';
55        }
56     }
57
58     # BOP field => eSelectPlus field
59     #$self->map_fields();
60     $self->remap_fields(
61         #                => 'order_type',
62         #                => 'transaction_type',
63         #login            => 'store_id',
64         #password         => 'api_token',
65         #authorization   => 
66         #customer_ip     =>
67         #name            =>
68         #first_name      =>
69         #last_name       =>
70         #company         =>
71         #address         => 
72         #city            => 
73         #state           => 
74         #zip             => 
75         #country         =>
76         phone            => 
77         #fax             =>
78         email            =>
79         card_number      => 'pan',
80         #expiration        =>
81         #                => 'expdate',
82
83         'amount'         => 'amount',
84         invoice_number   => 'cust_id',
85         #customer_id      => 'cust_id',
86         order_number     => 'order_id',   # must be unique number
87         authorization    => 'txn_number'  # reference to previous trans
88
89         #cvv2              =>
90     );
91
92     my $action = $self->{_content}{'action'};
93     if ( $self->{_content}{'action'} =~ /^\s*normal\s*authorization\s*$/i ) {
94       $action = 'purchase';
95     } elsif ( $self->{_content}{'action'} =~ /^\s*authorization\s*only\s*$/i ) {
96       $action = 'preauth';
97     } elsif ( $self->{_content}{'action'} =~ /^\s*post\s*authorization\s*$/i ) {
98       $action = 'completion';
99     } elsif ( $self->{_content}{'action'} =~ /^\s*void\s*$/i ) {
100       $action = 'purchasecorrection';
101     } elsif ( $self->{_content}{'action'} =~ /^\s*credit\s*$/i ) {
102       if ( $self->{_content}{'authorization'} ) {
103         $action = 'refund';
104       } else {
105         $action = 'ind_refund';
106       }
107     }
108
109     if ( $action =~ /^(purchase|preauth|ind_refund)$/ ) {
110
111       $self->required_fields(qw(
112         login password amount card_number expiration
113       ));
114
115       #cardexpiremonth & cardexpireyear
116       $self->{_content}{'expiration'} =~ /^(\d+)\D+\d*(\d{2})$/
117         or croak "unparsable expiration ". $self->{_content}{expiration};
118       my( $month, $year ) = ( $1, $2 );
119       $month = '0'. $month if $month =~ /^\d$/;
120       $self->{_content}{expdate} = $year.$month;
121
122       $self->generate_order_id;
123
124       $self->{_content}{amount} = sprintf('%.2f', $self->{_content}{amount} );
125
126     } elsif ( $action =~ /^(completion|purchasecorrection|refund)$/ ) {
127
128       $self->required_fields(qw(
129         login password order_number authorization
130       ));
131
132       if ( $action eq 'completion' ) {
133         $self->{_content}{comp_amount} = delete $self->{_content}{amount};
134       } elsif ( $action eq 'purchasecorrection' ) {
135         delete $self->{_content}{amount};
136       #} elsif ( $action eq 'refund' ) {
137       } 
138
139     }
140
141     # E-Commerce Indicator (see eSelectPlus docs)
142     $self->{_content}{'crypt_type'} ||= 7;
143
144     $action = "us_$action"
145       unless defined( $self->{_content}{'currency'} )
146                    && $self->{_content}{'currency'} eq 'CAD';
147
148     #no, values aren't escaped for XML.  their "mpgClasses.pl" example doesn't
149     #appear to do so, i dunno
150     tie my %fields, 'Tie::IxHash', $self->get_fields( $self->fields );
151     my $post_data =
152       '<?xml version="1.0"?>'.
153       '<request>'.
154       '<store_id>'.  $self->{_content}{'login'}. '</store_id>'.
155       '<api_token>'. $self->{_content}{'password'}. '</api_token>'.
156       "<$action>".
157       join('', map "<$_>$fields{$_}</$_>", keys %fields ).
158       "</$action>".
159       '</request>';
160
161     warn "POSTING: ".$post_data if $DEBUG > 1;
162
163     my( $page, $response, @reply_headers) = $self->https_post( $post_data );
164
165     if ($DEBUG > 1) {
166       my %reply_headers = @reply_headers;
167       warn join('', map { "  $_ => $reply_headers{$_}\n" } keys %reply_headers)
168     }
169
170     if ($response !~ /^200/)  {
171         # Connection error
172         $response =~ s/[\r\n]+/ /g;  # ensure single line
173         $self->is_success(0);
174         my $diag_message = $response || "connection error";
175         die $diag_message;
176     }
177
178     # avs_code - eSELECTplus_Perl_IG.pdf Appendix F
179     my %avsTable = ('A' => 'A',
180                     'B' => 'A',
181                     'C' => 'E',
182                     'D' => 'Y',
183                     'G' => '',
184                     'I' => '',
185                     'M' => 'Y',
186                     'N' => 'N',
187                     'P' => 'Z',
188                     'R' => 'R',
189                     'S' => '',
190                     'U' => 'E',
191                     'W' => 'Z',
192                     'X' => 'Y',
193                     'Y' => 'Y',
194                     'Z' => 'Z',
195                     );
196     my $AvsResultCode = $self->GetXMLProp($page, 'AvsResultCode');
197     $self->avs_code( defined($AvsResultCode) && exists $avsTable{$AvsResultCode}
198                          ?  $avsTable{$AvsResultCode}
199                          :  $AvsResultCode
200                    );
201
202     #md5 cvv2_response cavv_response ...?
203
204     $self->server_response($page);
205
206     my $result = $self->GetXMLProp($page, 'ResponseCode');
207
208     die "gateway error: ". $self->GetXMLProp( $page, 'Message' )
209       if $result =~ /^null$/i;
210
211     # Original order_id supplied to the gateway
212     $self->order_number($self->GetXMLProp($page, 'ReceiptId'));
213
214     # We (Whizman & DonorWare) do not have enough info about "ISO"
215     # response codes to make use of them.
216     # There may be good reasons why the ISO codes could be preferable,
217     # but we would need more information.  For now, the ResponseCode.
218     # $self->result_code( $self->GetXMLProp( $page, 'ISO' ) );
219     $self->result_code( $result );
220
221     if ( $result =~ /^\d+$/ && $result < 50 ) {
222         $self->is_success(1);
223         $self->authorization($self->GetXMLProp($page, 'TransID'));
224     } elsif ( $result =~ /^\d+$/ ) {
225         $self->is_success(0);
226         my $tmp_msg = $self->GetXMLProp( $page, 'Message' );
227         $tmp_msg =~ s/\s{2,}//g;
228         $tmp_msg =~ s/[\*\=]//g;
229         $self->error_message( $tmp_msg );
230     } else {
231         die "unparsable response received from gateway (response $result)".
232             ( $DEBUG ? ": $page" : '' );
233     }
234
235 }
236
237 use vars qw(@oidset);
238 @oidset = ( 'A'..'Z', '0'..'9' );
239 sub generate_order_id {
240     my $self = shift;
241     #generate an order_id if order_number not passed
242     unless (    exists ($self->{_content}{order_id})
243              && defined($self->{_content}{order_id})
244              && length ($self->{_content}{order_id})
245            ) {
246       $self->{_content}{'order_id'} =
247         join('', map { $oidset[int(rand(scalar(@oidset)))] } (1..23) );
248     }
249 }
250
251 sub fields {
252         my $self = shift;
253
254         #order is important to this processor
255         qw(
256           order_id
257           cust_id
258           amount
259           comp_amount
260           txn_number
261           pan
262           expdate
263           crypt_type
264           cavv
265         );
266 }
267
268 sub GetXMLProp {
269         my( $self, $raw, $prop ) = @_;
270         local $^W=0;
271
272         my $data;
273         ($data) = $raw =~ m"<$prop>(.*?)</$prop>"gsi;
274         #$data =~ s/<.*?>/ /gs;
275         chomp $data;
276         return $data;
277 }
278
279 1;
280
281 __END__
282
283 =head1 NAME
284
285 Business::OnlinePayment::eSelectPlus - Moneris eSelect Plus backend module for Business::OnlinePayment
286
287 =head1 SYNOPSIS
288
289   use Business::OnlinePayment;
290
291   ####
292   # One step transaction, the simple case.
293   ####
294
295   my $tx = new Business::OnlinePayment("eSelectPlus");
296   $tx->content(
297       type           => 'VISA',
298       login          => 'eSelect Store ID,
299       password       => 'eSelect API Token',
300       action         => 'Normal Authorization',
301       description    => 'Business::OnlinePayment test',
302       amount         => '49.95',
303       currency       => 'USD', #or CAD for compatibility with previous releases
304       name           => 'Tofu Beast',
305       address        => '123 Anystreet',
306       city           => 'Anywhere',
307       state          => 'UT',
308       zip            => '84058',
309       phone          => '420-867-5309',
310       email          => 'tofu.beast@example.com',
311       card_number    => '4005550000000019',
312       expiration     => '08/06',
313       cvv2           => '1234', #optional
314   );
315   $tx->submit();
316
317   if($tx->is_success()) {
318       print "Card processed successfully: ".$tx->authorization."\n";
319   } else {
320       print "Card was rejected: ".$tx->error_message."\n";
321   }
322   print "AVS code: ". $tx->avs_code. "\n"; # Y - Address and ZIP match
323                                            # A - Address matches but not ZIP
324                                            # Z - ZIP matches but not address
325                                            # N - no match
326                                            # E - AVS error or unsupported
327                                            # R - Retry (timeout)
328                                            # (empty) - not verified
329
330 =head1 SUPPORTED TRANSACTION TYPES
331
332 =head2 CC, Visa, MasterCard, American Express, Discover
333
334 Content required: type, login, password, action, amount, card_number, expiration.
335
336 =head1 PREREQUISITES
337
338   URI::Escape
339   Tie::IxHash
340
341   Net::SSLeay _or_ ( Crypt::SSLeay and LWP )
342
343 =head1 DESCRIPTION
344
345 For detailed information see L<Business::OnlinePayment>.
346
347 =head1 NOTES
348
349 =head2 Note for Canadian merchants upgrading to 0.03
350
351 As of version 0.03, this module now defaults to the US Moneris.  Make sure to
352 pass currency=>'CAD' for Canadian transactions.
353
354 =head2 Note for upgrading to 0.05
355
356 As of version 0.05, the bank authorization code is discarded (AuthCode),
357 so that authorization() and order_number() can return the 2 fields needed
358 for capture.  See also
359 cpansearch.perl.org/src/IVAN/Business-OnlinePayment-3.02/notes_for_module_writers_v3
360
361 =head1 AUTHOR
362
363 Ivan Kohler <ivan-eselectplus@420.am>
364 Randall Whitman L<whizman.com|http://whizman.com>
365
366 =head1 SEE ALSO
367
368 perl(1). L<Business::OnlinePayment>.
369
370 =cut
371