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