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