initial checkin
[Business-OnlinePayment-ElavonVirtualMerchant.git] / ElavonVirtualMerchant.pm
1 package Business::OnlinePayment::ElavonVirtualMerchant;
2 use base qw(Business::OnlinePayment::viaKLIX);
3
4 use strict;
5 use vars qw( $VERSION %maxlength );
6
7 $VERSION = '0.03';
8 $VERSION = eval $VERSION;
9
10 =head1 NAME
11
12 Business::OnlinePayment::ElavonVirtualMerchant - Elavon Virtual Merchant backend for Business::OnlinePayment
13
14 =head1 SYNOPSIS
15
16   use Business::OnlinePayment::ElavonVirtualMerchant;
17
18   my $tx = new Business::OnlinePayment("ElavonVirtualMerchant", { default_ssl_userid => 'whatever' });
19     $tx->content(
20         type           => 'VISA',
21         login          => 'testdrive',
22         password       => '', #password or transaction key
23         action         => 'Normal Authorization',
24         description    => 'Business::OnlinePayment test',
25         amount         => '49.95',
26         invoice_number => '100100',
27         customer_id    => 'jsk',
28         first_name     => 'Jason',
29         last_name      => 'Kohles',
30         address        => '123 Anystreet',
31         city           => 'Anywhere',
32         state          => 'UT',
33         zip            => '84058',
34         card_number    => '4007000000027',
35         expiration     => '09/02',
36         cvv2           => '1234', #optional
37     );
38     $tx->submit();
39
40     if($tx->is_success()) {
41         print "Card processed successfully: ".$tx->authorization."\n";
42     } else {
43         print "Card was rejected: ".$tx->error_message."\n";
44     }
45
46 =head1 DESCRIPTION
47
48 This module lets you use the Elavon (formerly Nova Information Systems) Virtual Merchant real-time payment gateway, a successor to viaKlix, from an application that uses the Business::OnlinePayment interface.
49
50 You need an account with Elavon.  Elavon uses a three-part set of credentials to allow you to configure multiple 'virtual terminals'.  Since Business::OnlinePayment only passes a login and password with each transaction, you must pass the third item, the user_id, to the constructor.
51
52 Elavon offers a number of transaction types, including electronic gift card operations and 'PINless debit'.  Of these, only credit card transactions fit the Business::OnlinePayment model.
53
54 Since the Virtual Merchant API is just a newer version of the viaKlix API, this module subclasses Business::OnlinePayment::viaKlix.
55
56 This module does not use Elavon's XML encoding as this doesn't appear to offer any benefit over the standard encoding.
57
58 =head1 SUBROUTINES
59
60 =head2 set_defaults
61
62 Sets defaults for the Virtual Merchant gateway URL.
63
64 =cut
65
66 sub set_defaults {
67     my $self = shift;
68     my %opts = @_;
69
70     $self->SUPER::set_defaults(%opts);
71     # standard B::OP methods/data
72     $self->server("www.myvirtualmerchant.com");
73     $self->port("443");
74     $self->path("/VirtualMerchant/process.do");
75
76 }
77
78 =head2 _map_fields
79
80 Converts credit card types and transaction types from the Business::OnlinePayment values to Elavon's.
81
82 =cut
83
84 sub _map_fields {
85     my ($self) = @_;
86
87     my %content = $self->content();
88
89     #ACTION MAP
90     my %actions = (
91         'normal authorization' => 'CCSALE',  # Authorization/Settle transaction
92         'credit'               => 'CCCREDIT', # Credit (refund)
93     );
94
95     $content{'ssl_transaction_type'} = $actions{ lc( $content{'action'} ) }
96       || $content{'action'};
97
98     # TYPE MAP
99     my %types = (
100         'visa'             => 'CC',
101         'mastercard'       => 'CC',
102         'american express' => 'CC',
103         'discover'         => 'CC',
104         'cc'               => 'CC',
105     );
106
107     $content{'type'} = $types{ lc( $content{'type'} ) } || $content{'type'};
108
109     $self->transaction_type( $content{'type'} );
110
111     # stuff it back into %content
112     $self->content(%content);
113 }
114
115 =head2 submit
116
117 Maps data from Business::OnlinePayment name space to Elavon's, checks that all required fields
118 for the transaction type are present, and submits the transaction.  Saves the results.
119
120 =cut
121
122 %maxlength = (
123         ssl_description        => 255,
124         ssl_invoice_number     => 25,
125         ssl_customer_code      => 17,
126
127         ssl_first_name         => 20,
128         ssl_last_name          => 30,
129         ssl_company            => 50,
130         ssl_avs_address        => 30,
131         ssl_city               => 30,
132         ssl_phone              => 20,
133
134         ssl_ship_to_first_name => 20,
135         ssl_ship_to_last_name  => 30,
136         ssl_ship_to_company    => 50,
137         ssl_ship_to_address1   => 30,
138         ssl_ship_to_city       => 30,
139         ssl_ship_to_phone      => 20, #though we don't map anything to this...
140 );
141
142 sub submit {
143     my ($self) = @_;
144
145     $self->_map_fields();
146
147     my %content = $self->content;
148
149     my %required;
150     $required{CC_CCSALE} =  [ qw( ssl_transaction_type ssl_merchant_id ssl_pin
151                                 ssl_amount ssl_card_number ssl_exp_date
152                                 ssl_cvv2cvc2_indicator 
153                               ) ];
154     $required{CC_CCCREDIT} = $required{CC_CCSALE};
155     my %optional;
156     $optional{CC_CCSALE} =  [ qw( ssl_user_id ssl_salestax ssl_cvv2cvc2
157                                 ssl_description ssl_invoice_number
158                                 ssl_customer_code ssl_company ssl_first_name
159                                 ssl_last_name ssl_avs_address ssl_address2
160                                 ssl_city ssl_state ssl_avs_zip ssl_country
161                                 ssl_phone ssl_email ssl_ship_to_company
162                                 ssl_ship_to_first_name ssl_ship_to_last_name
163                                 ssl_ship_to_address1 ssl_ship_to_city
164                                 ssl_ship_to_state ssl_ship_to_zip
165                                 ssl_ship_to_country
166                               ) ];
167     $optional{CC_CCCREDIT} = $optional{CC_CCSALE};
168
169     my $type_action = $self->transaction_type(). '_'. $content{ssl_transaction_type};
170     unless ( exists($required{$type_action}) ) {
171       $self->error_message("Elavon can't handle transaction type: ".
172         "$content{action} on " . $self->transaction_type() );
173       $self->is_success(0);
174       return;
175     }
176
177     my $expdate_mmyy = $self->expdate_mmyy( $content{"expiration"} );
178     my $zip          = $content{'zip'};
179     $zip =~ s/[^[:alnum:]]//g;
180
181     my $cvv2indicator = $content{"cvv2"} ? 1 : 9; # 1 = Present, 9 = Not Present
182
183     $self->_revmap_fields(
184
185         ssl_merchant_id        => 'login',
186         ssl_pin                => 'password',
187
188         ssl_amount             => 'amount',
189         ssl_card_number        => 'card_number',
190         ssl_exp_date           => \$expdate_mmyy,    # MMYY from 'expiration'
191         ssl_cvv2cvc2_indicator => \$cvv2indicator,
192         ssl_cvv2cvc2           => 'cvv2',
193         ssl_description        => 'description',
194         ssl_invoice_number     => 'invoice_number',
195         ssl_customer_code      => 'customer_id',
196
197         ssl_first_name         => 'first_name',
198         ssl_last_name          => 'last_name',
199         ssl_company            => 'company',
200         ssl_avs_address        => 'address',
201         ssl_city               => 'city',
202         ssl_state              => 'state',
203         ssl_avs_zip            => \$zip,          # 'zip' with non-alnums removed
204         ssl_country            => 'country',
205         ssl_phone              => 'phone',
206         ssl_email              => 'email',
207
208         ssl_ship_to_first_name => 'ship_first_name',
209         ssl_ship_to_last_name  => 'ship_last_name',
210         ssl_ship_to_company    => 'ship_company',
211         ssl_ship_to_address1   => 'ship_address',
212         ssl_ship_to_city       => 'ship_city',
213         ssl_ship_to_state      => 'ship_state',
214         ssl_ship_to_zip        => 'ship_zip',
215         ssl_ship_to_country    => 'ship_country',
216
217     );
218
219     my %params = $self->get_fields( @{$required{$type_action}},
220                                     @{$optional{$type_action}},
221                                   );
222
223     $params{$_} = substr($params{$_},0,$maxlength{$_})
224       foreach grep exists($maxlength{$_}), keys %params;
225
226     foreach ( keys ( %{($self->{_defaults})} ) ) {
227       $params{$_} = $self->{_defaults}->{$_} unless exists($params{$_});
228     }
229
230     $params{ssl_test_mode}='true' if $self->test_transaction;
231     
232     $params{ssl_show_form}='false';
233     $params{ssl_result_format}='ASCII';
234
235     $self->required_fields(@{$required{$type_action}});
236     
237     warn join("\n", map{ "$_ => $params{$_}" } keys(%params)) if $self->debug > 1;
238     my ( $page, $resp, %resp_headers ) = 
239       $self->https_post( %params );
240
241     $self->response_code( $resp );
242     $self->response_page( $page );
243     $self->response_headers( \%resp_headers );
244
245     warn "$page\n" if $self->debug > 1;
246     # $page should contain key/value pairs
247
248     my $status ='';
249     my %results = map { s/\s*$//; split '=', $_, 2 } grep { /=/ } split '^', $page;
250
251     # AVS and CVS values may be set on success or failure
252     $self->avs_code( $results{ssl_avs_response} );
253     $self->cvv2_response( $results{ ssl_cvv2_response } );
254     $self->result_code( $status = $results{ errorCode } || $results{ ssl_result } );
255     $self->order_number( $results{ ssl_txn_id } );
256     $self->authorization( $results{ ssl_approval_code } );
257     $self->error_message( $results{ errorMessage } || $results{ ssl_result_message } );
258
259
260     if ( $resp =~ /^(HTTP\S+ )?200/ && $status eq "0" ) {
261         $self->is_success(1);
262     } else {
263         $self->is_success(0);
264     }
265 }
266
267 1;
268 __END__
269
270 =head1 SEE ALSO
271
272 Business::OnlinePayment, Business::OnlinePayment::viaKlix, Elavon Virtual Merchant Developers' Guide
273
274 =head1 AUTHOR
275
276 Richard Siddall, E<lt>elavon@elirion.netE<gt>
277
278 =head1 BUGS
279
280 Duplicates code to handle deprecated 'type' codes.
281
282 Method for passing raw card track data is not documented by Elavon.
283
284 =head1 COPYRIGHT AND LICENSE
285
286 Copyright (C) 2009 by Richard Siddall.  This module is largely based on Business::OnlinePayment::viaKlix by Jeff Finucane.
287
288 This library is free software; you can redistribute it and/or modify
289 it under the same terms as Perl itself, either Perl version 5.8.8 or,
290 at your option, any later version of Perl 5 you may have available.
291
292 =cut
293