RT#42364: Elavon error when processing credit card
[Business-OnlinePayment-ElavonVirtualMerchant.git] / ElavonVirtualMerchant.pm
1 package Business::OnlinePayment::ElavonVirtualMerchant;
2 use base qw(Business::OnlinePayment::HTTPS);
3
4 use strict;
5 use vars qw( $VERSION $DEBUG %maxlength );
6 use Carp;
7
8 $VERSION = '0.04';
9 $DEBUG   = 0;
10
11 sub _info {
12   return {
13     'info_compat'           => '0.01',
14     'gateway_name'          => 'ElavonVirtualMerchant',
15     'gateway_url'           => 'http://www.myvirtualmerchant.com/',
16     'module_version'        => $VERSION,
17     'supported_types'       => [ qw( CC ECHECK ) ],
18     'token_support'         => 0,
19     'test_transaction'      => 1,
20     'supported_actions'     => [
21                                  'Normal Authorization',
22                                  #'Authorization Only',
23                                  #'Post Authorization',
24                                  #'Void',
25                                  'Credit',
26                                ],
27   };
28 }
29
30
31 =head1 NAME
32
33 Business::OnlinePayment::ElavonVirtualMerchant - Elavon Virtual Merchant backend for Business::OnlinePayment
34
35 =head1 SYNOPSIS
36
37   use Business::OnlinePayment::ElavonVirtualMerchant;
38
39   my $tx = new Business::OnlinePayment("ElavonVirtualMerchant", { default_ssl_user_id => 'whatever' });
40     $tx->content(
41         type           => 'CC',
42         login          => 'testdrive',
43         password       => '', #password or transaction key
44         action         => 'Normal Authorization',
45         description    => 'Business::OnlinePayment test',
46         amount         => '49.95',
47         invoice_number => '100100',
48         customer_id    => 'jsk',
49         first_name     => 'Jason',
50         last_name      => 'Kohles',
51         address        => '123 Anystreet',
52         city           => 'Anywhere',
53         state          => 'UT',
54         zip            => '84058',
55         card_number    => '4007000000027',
56         expiration     => '09/02',
57         cvv2           => '1234',
58     );
59     $tx->submit();
60
61     if($tx->is_success()) {
62         print "Card processed successfully: ".$tx->authorization."\n";
63     } else {
64         print "Card was rejected: ".$tx->error_message."\n";
65     }
66
67 =head1 DESCRIPTION
68
69 This module lets you use the Elavon (formerly Nova Information Systems) Converge
70 (formerly Virtual Merchant, a successor of viaKlix) real-time payment gateway 
71 from an application that uses the Business::OnlinePayment interface.
72
73 You need an account with Elavon.  Elavon uses a three-part set of credentials to 
74 allow you to configure multiple 'virtual terminals'.  Since Business::OnlinePayment 
75 only passes a login and password with each transaction, you must pass the third item,
76 default_ssl_user_id, to the constructor.  You may pass defaults for other Converge 
77 request fields to the constructor by prepending the field names with default_.
78
79 Converge offers a number of transaction types.  Of these, only credit card sale
80 (ccsale), credit card refund (cccredit) and echeck sale (ecspurchase) transactions 
81 are currently supported.
82
83 =head1 SUBROUTINES
84
85 =cut
86
87 =head2 debug LEVEL
88
89 Get/set debug level
90
91 =cut
92
93 sub debug {
94     my $self = shift;
95
96     if (@_) {
97         my $level = shift || 0;
98         if ( ref($self) ) {
99             $self->{"__DEBUG"} = $level;
100         }
101         else {
102             $DEBUG = $level;
103         }
104         $Business::OnlinePayment::HTTPS::DEBUG = $level;
105     }
106     return ref($self) ? ( $self->{"__DEBUG"} || $DEBUG ) : $DEBUG;
107 }
108
109 =head2 set_defaults
110
111 Sets defaults for the Converge gateway URL
112 and initializes internal data structures.
113
114 =cut
115
116 sub set_defaults {
117     my $self = shift;
118     my %opts = @_;
119
120     # standard B::OP methods/data
121     $self->server("www.myvirtualmerchant.com");
122     $self->port("443");
123     $self->path("/VirtualMerchant/process.do");
124
125     $self->build_subs(qw( 
126                           order_number avs_code cvv2_response
127                           response_page response_code response_headers
128                      ));
129
130     # module specific data
131     if ( $opts{debug} ) {
132         $self->debug( $opts{debug} );
133         delete $opts{debug};
134     }
135
136     my %_defaults = ();
137     foreach my $key (keys %opts) {
138       $key =~ /^default_(\w*)$/ or next;
139       $_defaults{$1} = $opts{$key};
140       delete $opts{$key};
141     }
142     $self->{_defaults} = \%_defaults;
143
144 }
145
146 =head2 _map_fields
147
148 Converts credit card types and transaction types from the Business::OnlinePayment values to Elavon's.
149
150 =cut
151
152 sub _map_fields {
153     my ($self) = @_;
154
155     my %content = $self->content();
156
157     if (uc($self->transaction_type) eq 'ECHECK') {
158
159       $content{'ssl_transaction_type'} = 'ECSPURCHASE';
160
161     } else { # or credit card, or non-supported type (support checked during submit)
162
163       #ACTION MAP
164       my %actions = (
165           'normal authorization' => 'CCSALE',  # Authorization/Settle transaction
166           'credit'               => 'CCCREDIT', # Credit (refund)
167       );
168
169       $content{'ssl_transaction_type'} = $actions{ lc( $content{'action'} ) }
170         || $content{'action'};
171
172       # TYPE MAP
173       my %types = (
174           'visa'             => 'CC',
175           'mastercard'       => 'CC',
176           'american express' => 'CC',
177           'discover'         => 'CC',
178           'cc'               => 'CC',
179       );
180
181       $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
182
183       $self->transaction_type( $content{'type'} );
184
185     } # end credit card
186
187     # stuff it back into %content
188     $self->content(%content);
189 }
190
191 =head2 _revmap_fields
192
193 Accepts I<%map> and sets the content field specified
194 by map keys to be the value of the content field
195 specified by map values, e.g.
196
197         ssl_merchant_id => 'login'
198
199 will set ssl_merchant_id to the current value of login.
200
201 Values may also be references to strings, e.g.
202
203         ssl_exp_date => \$expdate_mmyy,
204
205 will set ssl_exp_date to the value of $expdate_mmyy.
206
207 =cut
208
209 sub _revmap_fields {
210     my ( $self, %map ) = @_;
211     my %content = $self->content();
212     foreach ( keys %map ) {
213         $content{$_} =
214           ref( $map{$_} )
215           ? ${ $map{$_} }
216           : $content{ $map{$_} };
217     }
218     $self->content(%content);
219 }
220
221 =head2 expdate_mmyy
222
223 Accepts I<$expiration>.  Returns mmyy normalized value,
224 or original value if it couldn't be normalized.
225
226 =cut
227
228 sub expdate_mmyy {
229     my $self       = shift;
230     my $expiration = shift;
231     my $expdate_mmyy;
232     if ( defined($expiration) and $expiration =~ /^(\d+)\D+\d*(\d{2})$/ ) {
233         my ( $month, $year ) = ( $1, $2 );
234         $expdate_mmyy = sprintf( "%02d", $month ) . $year;
235     }
236     return defined($expdate_mmyy) ? $expdate_mmyy : $expiration;
237 }
238
239 =head2 required_fields
240
241 Accepts I<@fields> and makes sure each of those fields
242 have been set in content.
243
244 =cut
245
246 sub required_fields {
247     my($self,@fields) = @_;
248
249     my @missing;
250     my %content = $self->content();
251     foreach(@fields) {
252       next
253         if (exists $content{$_} && defined $content{$_} && $content{$_}=~/\S+/);
254       push(@missing, $_);
255     }
256
257     Carp::croak("missing required field(s): " . join(", ", @missing) . "\n")
258       if(@missing);
259
260 }
261
262 =head2 submit
263
264 Maps data from Business::OnlinePayment name space to Elavon's, checks that all required fields
265 for the transaction type are present, and submits the transaction.  Saves the results.
266
267 =cut
268
269 %maxlength = (
270         ssl_description        => 255,
271         ssl_invoice_number     => 25,
272         ssl_customer_code      => 17,
273
274         ssl_first_name         => 20,
275         ssl_last_name          => 30,
276         ssl_company            => 50,
277         ssl_avs_address        => 30,
278         ssl_city               => 30,
279         ssl_phone              => 20,
280
281         ssl_ship_to_first_name => 20,
282         ssl_ship_to_last_name  => 30,
283         ssl_ship_to_company    => 50,
284         ssl_ship_to_address1   => 30,
285         ssl_ship_to_city       => 30,
286         ssl_ship_to_phone      => 20, #though we don't map anything to this...
287 );
288
289 sub submit {
290     my ($self) = @_;
291
292     if ($self->test_transaction) {
293       $self->server("demo.myvirtualmerchant.com");
294       $self->path("/VirtualMerchantDemo/process.do");
295     }
296
297     $self->_map_fields();
298
299     my %content = $self->content;
300     warn "INITIAL PARAMETERS:\n" . join("\n", map{ "$_ => $content{$_}" } keys(%content)) if $self->debug;
301
302     my %required;
303     my @alwaysrequired = qw(
304       ssl_transaction_type
305       ssl_merchant_id
306       ssl_pin
307       ssl_amount
308     );
309     $required{CC_CCSALE} =  [ @alwaysrequired, qw(
310                                 ssl_card_number
311                                 ssl_exp_date
312                                 ssl_cvv2cvc2_indicator
313                               ),
314                             ];
315     $required{CC_CCCREDIT} = $required{CC_CCSALE};
316     $required{ECHECK_ECSPURCHASE} = [ @alwaysrequired,
317                                       qw(
318                                         ssl_aba_number
319                                         ssl_bank_account_number
320                                         ssl_bank_account_type
321                                         ssl_agree
322                                       ),
323                                     ];
324     my %optional;
325     # these are actually each sometimes required, depending on account type & settings,
326     # but we can let converge handle error messages for that
327     #   Regarding ssl_user_id...all Elavon docs say this is required,
328     #   but apparently CardFortress previously worked without it
329     my @alwaysoptional = qw(
330       ssl_user_id
331       ssl_first_name
332       ssl_last_name
333       ssl_company
334       ssl_email
335     );
336     $optional{CC_CCSALE} =  [ @alwaysoptional, qw( ssl_salestax ssl_cvv2cvc2
337                                 ssl_description ssl_invoice_number
338                                 ssl_customer_code
339                                 ssl_avs_address ssl_address2
340                                 ssl_city ssl_state ssl_avs_zip ssl_country
341                                 ssl_phone ssl_ship_to_company
342                                 ssl_ship_to_first_name ssl_ship_to_last_name
343                                 ssl_ship_to_address1 ssl_ship_to_city
344                                 ssl_ship_to_state ssl_ship_to_zip
345                                 ssl_ship_to_country
346                               ) ];
347     $optional{CC_CCCREDIT} = $optional{CC_CCSALE};
348     $optional{ECHECK_ECSPURCHASE} = [ @alwaysoptional ];
349
350     my $type_action = $self->transaction_type(). '_'. $content{ssl_transaction_type};
351     unless ( exists($required{$type_action}) ) {
352       $self->error_message("Elavon can't handle transaction type: ".
353         "$content{action} on " . $self->transaction_type() );
354       $self->is_success(0);
355       return;
356     }
357
358     $self->_revmap_fields(
359       ssl_merchant_id => 'login',
360       ssl_pin         => 'password',
361       ssl_amount      => 'amount',
362       ssl_first_name  => 'first_name',
363       ssl_last_name   => 'last_name',
364       ssl_company     => 'company',
365       ssl_email       => 'email',
366     );
367
368     if (uc($self->transaction_type) eq 'CC') {
369
370       my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
371       my $zip          = $content{'zip'};
372       $zip =~ s/[^[:alnum:]]//g;
373
374       my $cvv2indicator = $content{"cvv2"} ? 1 : 9; # 1 = Present, 9 = Not Present
375
376       $self->_revmap_fields(
377
378         ssl_card_number        => 'card_number',
379         ssl_exp_date           => \$expdate_mmyy,    # MMYY from 'expiration'
380         ssl_cvv2cvc2_indicator => \$cvv2indicator,
381         ssl_cvv2cvc2           => 'cvv2',
382         ssl_description        => 'description',
383         ssl_invoice_number     => 'invoice_number',
384         ssl_customer_code      => 'customer_id',
385
386         ssl_avs_address        => 'address',
387         ssl_city               => 'city',
388         ssl_state              => 'state',
389         ssl_avs_zip            => \$zip,          # 'zip' with non-alnums removed
390         ssl_country            => 'country',
391         ssl_phone              => 'phone',
392
393         ssl_ship_to_first_name => 'ship_first_name',
394         ssl_ship_to_last_name  => 'ship_last_name',
395         ssl_ship_to_company    => 'ship_company',
396         ssl_ship_to_address1   => 'ship_address',
397         ssl_ship_to_city       => 'ship_city',
398         ssl_ship_to_state      => 'ship_state',
399         ssl_ship_to_zip        => 'ship_zip',
400         ssl_ship_to_country    => 'ship_country',
401
402       );
403
404     } else { # ECHECK
405
406       my $account_type;
407       if (uc($content{'account_type'}) =~ 'PERSONAL') {
408         $account_type = 0;
409       } elsif (uc($content{'account_type'}) =~ 'BUSINESS') {
410         $account_type = 1;
411       } else {
412         $self->error_message("Unrecognized account type: ".$content{'account_type'});
413         $self->is_success(0);
414         return;
415       }
416
417       $self->_revmap_fields(
418         ssl_aba_number          => 'routing_code',
419         ssl_bank_account_number => 'account_number',
420         ssl_bank_account_type   => \$account_type,
421         ssl_agree               => \'1',
422       );
423
424     }
425
426     # set defaults for anything that hasn't been set yet
427     %content = $self->content;
428     foreach ( keys ( %{($self->{_defaults})} ) ) {
429       $content{$_} ||= $self->{_defaults}->{$_};
430     }
431     $self->content(%content);
432
433     # truncate long rows & validate required fields
434     my %params = $self->get_fields( @{$required{$type_action}},
435                                     @{$optional{$type_action}},
436                                   );
437     $params{$_} = substr($params{$_},0,$maxlength{$_})
438       foreach grep exists($maxlength{$_}), keys %params;
439     $self->required_fields(@{$required{$type_action}});
440
441     # some final non-overridable parameters
442     $params{ssl_test_mode}='true' if $self->test_transaction;
443     $params{ssl_show_form}='false';
444     $params{ssl_result_format}='ASCII';
445     
446     # send request
447     warn "CONNECTING TO " . $self->server . ':' . $self->port . $self->path if $self->debug;
448     warn "POST PARAMETERS:\n" . join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $self->debug;
449     my ( $page, $resp, %resp_headers ) = 
450       $self->https_post( %params );
451
452     $self->response_code( $resp );
453     $self->response_page( $page );
454     $self->response_headers( \%resp_headers );
455
456     warn "RESPONSE FROM SERVER:\n$page\n" if $self->debug;
457     # $page should contain key/value pairs
458
459     my $status ='';
460     my %results = map { s/\s*$//; split '=', $_, 2 } grep { /=/ } split '^', $page;
461
462     if (uc($self->transaction_type) eq 'CC') {
463       # AVS and CVS values may be set on success or failure
464       $self->avs_code( $results{ssl_avs_response} );
465       $self->cvv2_response( $results{ ssl_cvv2_response } );
466     }
467     $self->result_code( $status = $results{ errorCode } || $results{ ssl_result } );
468     $self->order_number( $results{ ssl_txn_id } );
469     $self->authorization( $results{ ssl_approval_code } );
470     $self->error_message( $results{ errorMessage } || $results{ ssl_result_message } );
471
472
473     if ( $resp =~ /^(HTTP\S+ )?200/ && $status eq "0" ) {
474         $self->is_success(1);
475     } else {
476         $self->is_success(0);
477     }
478 }
479
480 1;
481 __END__
482
483 =head1 SEE ALSO
484
485 L<Business::OnlinePayment>, L<Business::OnlinePayment::HTTPS>, Elavon Converge Developers' Guide
486
487 =head1 BUGS
488
489 Duplicates code to handle deprecated 'type' codes.
490
491 Only provides a small selection of possible transaction types.
492
493 =head1 COPYRIGHT AND LICENSE
494
495 Copyright (C) 2016 Freeside Internet Services.
496
497 Based on the original ElavonVirtualMerchant module by Richard Siddall,
498 which was largely based on Business::OnlinePayment::viaKlix by Jeff Finucane.
499
500 This library is free software; you can redistribute it and/or modify
501 it under the same terms as Perl itself, either Perl version 5.8.8 or,
502 at your option, any later version of Perl 5 you may have available.
503
504 =cut
505