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