File Coverage

blib/lib/Business/OnlinePayment/Vanco.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Business::OnlinePayment::Vanco;
2              
3 1     1   959 use strict;
  1         2  
  1         62  
4 1     1   5 use Carp;
  1         2  
  1         74  
5 1     1   913 use Tie::IxHash;
  1         5222  
  1         35  
6 1     1   482 use XML::Simple;
  0            
  0            
7             use XML::Writer;
8             use LWP::UserAgent;
9             use HTTP::Request;
10             use HTTP::Request::Common qw (POST);
11             use Date::Calc qw(Add_Delta_YM Add_Delta_Days);
12             use Business::OnlinePayment;
13             #use Business::OnlinePayment::HTTPS;
14             use vars qw($VERSION $DEBUG @ISA $me);
15              
16             @ISA = qw(Business::OnlinePayment); # Business::OnlinePayment::HTTPS
17             $VERSION = '0.02';
18             $DEBUG = 0;
19             $me = 'Business::OnlinePayment::Vanco';
20              
21             sub set_defaults {
22             my $self = shift;
23             my %opts = @_;
24              
25             # standard B::OP methods/data
26             $self->server('www.vancoservices.com') unless $self->server;
27             $self->port('443') unless $self->port;
28             $self->path('/cgi-bin/ws.vps') unless $self->path;
29              
30             $self->build_subs(qw( order_number avs_code cvv2_response
31             response_page response_code response_headers
32             ));
33              
34             # module specific data
35             foreach (qw( ClientID ProductID )) {
36             $self->build_subs($_);
37              
38             if ( $opts{$_} ) {
39             $self->$_( $opts{$_} );
40             delete $opts{$_};
41             }
42             }
43              
44             }
45              
46             sub map_fields {
47             my($self) = @_;
48              
49             my %content = $self->content();
50             my $action = lc($content{'action'});
51              
52             # ACTION MAP
53             my %actions =
54             ( 'normal authorization' => 'EFTAddCompleteTransaction',
55             'recurring authorization' => 'EFTAddCompleteTransaction',
56             'cancel recurring authorization' => 'EFTDeleteTransaction',
57             );
58             $content{'RequestType'} = $actions{$action} || $action;
59              
60             # TYPE MAP
61             my %types = ( 'visa' => 'CC',
62             'mastercard' => 'CC',
63             'american express' => 'CC',
64             'discover' => 'CC',
65             'check' => 'ECHECK',
66             );
67             $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
68             $self->transaction_type($content{'type'});
69            
70             # CHECK/TRANSACTION TYPE MAP
71             $content{'TransactionTypeCode'} = $content{'check_type'} || 'PPD'
72             unless ( $content{'TransactionTypeCode'}
73             || $content{'RequestType'} eq 'EFTDeleteTransaction'); # kludgy
74              
75             # let FrequencyCode, StartDate, and EndDate be specified directly;
76             unless($content{FrequencyCode}){
77             my ($length,$unit) =
78             ($self->{_content}->{interval} or '') =~
79             /^\s*(\d+)\s+(day|month)s?\s*$/;
80              
81             my %daily = ( '7' => 'W',
82             '14' => 'BW',
83             );
84            
85             my %monthly = ( '1' => 'M',
86             '3' => 'Q',
87             '12' => 'A',
88             );
89            
90             if ($length && $unit) {
91             $content{'FrequencyCode'} = $daily{$length}
92             if ($unit eq 'day');
93              
94             $content{'FrequencyCode'} = $monthly{$length}
95             if ($unit eq 'month');
96             }
97             }
98              
99             unless($content{StartDate}){
100             $content{'StartDate'} = $content{'start'};
101             }
102              
103             unless($content{EndDate}){
104             my ($year,$month,$day) =
105             $content{StartDate} =~ /^\s*(\d{4})-(\d{1,2})-(\d{1,2})\s*$/
106             if $content{StartDate};
107              
108             my ($periods) = $content{periods} =~/^\s*(\d+)\s*$/
109             if $content{periods};
110              
111             my %daily = ( 'W' => '7',
112             'BW' => '14',
113             );
114            
115             my %monthly = ( 'M' => '1',
116             'Q' => '3',
117             'A' => '12',
118             );
119              
120             if ($year && $month && $day && $periods) {
121             if ($daily{$content{FrequencyCode}}) {
122             my $days = ($periods - 1) * $daily{$content{FrequencyCode}};
123             ($year, $month, $day) = Add_Delta_Days( $year, $month, $day, $days);
124             $content{EndDate} = sprintf("%04d-%02d-%02d", $year, $month, $day);
125             }
126              
127             if ($monthly{$content{FrequencyCode}}) {
128             my $months = ($periods - 1) * $monthly{$content{FrequencyCode}};
129             ($year, $month, $day) = Add_Delta_YM( $year, $month, $day, 0, $months);
130             $content{EndDate} = sprintf("%04d-%02d-%02d", $year, $month, $day);
131             }
132             }
133              
134             }
135              
136             if ($action eq 'normal authorization'){
137             my $time = time + 86400 if $self->transaction_type() eq 'ECHECK';
138             $content{'FrequencyCode'} = 'O';
139             $content{'StartDate'} = $content{'start'} || substr(today($time),0,10);
140             $content{'EndDate'} = $content{'StartDate'};
141             }
142              
143              
144             # ACCOUNT TYPE MAP
145             my %account_types = ('personal checking' => 'C',
146             'personal savings' => 'S',
147             'business checking' => 'C',
148             'business savings' => 'S',
149             'checking' => 'C',
150             'savings' => 'S',
151             );
152             $content{'account_type'} = $account_types{lc($content{'account_type'})}
153             || $content{'account_type'};
154             $content{'account_type'} = 'CC' if lc($content{'type'}) eq 'cc';
155              
156             # SHIPPING INFORMATION
157             foreach (qw(name address city state zip)) {
158             $content{"ship_$_"} = $content{$_} unless $content{"ship$_"};
159             }
160              
161             # stuff it back into %content
162             $self->content(%content);
163              
164             }
165              
166             sub expdate_month {
167             my ($self, $exp) = (shift, shift);
168             my $month;
169             if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
170             $month = sprintf( "%02d", $1 );
171             }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
172             $month = sprintf( "%02d", $1 );
173             }
174             return $month;
175             }
176              
177             sub expdate_year {
178             my ($self, $exp) = (shift, shift);
179             my $year;
180             if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
181             $year = sprintf( "%02d", $1 );
182             }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
183             $year = sprintf( "%02d", $1 );
184             }
185             return $year;
186             }
187              
188             sub today {
189             my @time = localtime($_[0] ? shift : time);
190             $time[5] += 1900;
191             $time[4]++;
192             sprintf("%04d-%02d-%02d %02d:%02d:%02d", reverse(@time[0..5]));
193             }
194              
195             sub revmap_fields {
196             my $self = shift;
197             tie my(%map), 'Tie::IxHash', @_;
198             my %content = $self->content();
199             map {
200             my $value;
201             if ( ref( $map{$_} ) eq 'HASH' ) {
202             $value = $map{$_} if ( keys %{ $map{$_} } );
203             }elsif( ref( $map{$_} ) ) {
204             $value = ${ $map{$_} };
205             }elsif( exists( $content{ $map{$_} } ) ) {
206             $value = $content{ $map{$_} };
207             }
208              
209             if (defined($value)) {
210             ($_ => $value);
211             }else{
212             ();
213             }
214             } (keys %map);
215             }
216              
217             sub submit {
218             my($self) = @_;
219              
220             $self->is_success(0);
221             unless($self->ClientID() && $self->ProductID()) {
222             croak "ClientID and ProductID are required";
223             }
224              
225             my $requestid = time . sprintf("%010u", rand() * 2**32);
226             my $auth_requestid = $requestid . '0';
227             my $req_requestid = $requestid . '1';
228              
229             $self->map_fields();
230              
231             my @required_fields = qw(action login password);
232              
233             if ( lc($self->{_content}->{action}) eq 'normal authorization' ) {
234             push @required_fields, qw( type amount name );
235              
236             push @required_fields, qw( card_number expiration )
237             if ($self->transaction_type() eq "CC");
238            
239             push @required_fields,
240             qw( routing_code account_number account_type )
241             if ($self->transaction_type() eq "ECHECK");
242            
243             }elsif ( lc($self->{_content}->{action}) eq 'recurring authorization' ) {
244             push @required_fields, qw( type interval start periods amount name );
245              
246             push @required_fields, qw( card_number expiration )
247             if ($self->transaction_type() eq 'CC' );
248              
249             push @required_fields,
250             qw( routing_code account_number account_type )
251             if ($self->transaction_type() eq "ECHECK");
252              
253             }elsif ( lc($self->{_content}->{action}) eq 'cancel recurring authorization' ) {
254             push @required_fields, qw( subscription );
255              
256             }else{
257             croak "$me can't handle transaction type: ".
258             $self->{_content}->{action}. " for ".
259             $self->transaction_type();
260             }
261              
262             $self->required_fields(@required_fields);
263              
264             tie my %auth, 'Tie::IxHash', (
265             RequestType => 'Login',
266             RequestID => $auth_requestid,
267             RequestTime => today(),
268             );
269              
270             tie my %requestvars, 'Tie::IxHash',
271             $self->revmap_fields(
272             UserID => 'login',
273             Password => 'password',
274             );
275             $requestvars{'ProductID'} = $self->ProductID();
276              
277             tie my %req, 'Tie::IxHash',
278             $self->revmap_fields (
279             Auth => \%auth,
280             Request => { RequestVars => \%requestvars },
281             );
282              
283             my $response = $self->_my_https_post(%req);
284             return if $self->result_code();
285              
286             tie %auth, 'Tie::IxHash',
287             $self->revmap_fields( RequestType => 'RequestType');
288             $auth{'RequestID'} = $req_requestid;
289             $auth{'RequestTime'} = today();
290             $auth{'SessionID'} = $response->{Response}->{SessionID};
291              
292             my $client_id = $self->ClientID();
293             my $cardexpmonth = $self->expdate_month($self->{_content}->{expiration});
294             my $cardexpyear = $self->expdate_year($self->{_content}->{expiration});
295             my $account_number = ( defined($self->transaction_type())
296             && $self->transaction_type() eq 'CC')
297             ? $self->{_content}->{card_number}
298             : $self->{_content}->{account_number}
299             ;
300              
301             tie %requestvars, 'Tie::IxHash',
302             $self->revmap_fields(
303             ClientID => \$client_id,
304             CustomerID => 'customer_id',
305             CustomerName => 'ship_name', # defaults to
306             CustomerAddress1 => 'ship_address',# values without
307             CustomerCity => 'ship_city', # ship_ prefix
308             CustomerState => 'ship_state', #
309             CustomerZip => 'ship_zip', #
310             CustomerPhone => 'phone',
311             AccountType => 'account_type',
312             AccountNumber => \$account_number,
313             RoutingNumber => 'routing_code',
314             CardBillingName => 'name',
315             CardExpMonth => \$cardexpmonth,
316             CardExpYear => \$cardexpyear,
317             CardCVV2 => 'cvv2',
318             CardBillingAddr1 => 'address',
319             CardBillingCity => 'city',
320             CardBillingState => 'state',
321             CardBillingZip => 'zip',
322             Amount => 'amount',
323             StartDate => 'StartDate',
324             EndDate => 'EndDate',
325             FrequencyCode => 'FrequencyCode',
326             TransactionTypeCode => 'TransactionTypeCode',
327             TransactionRef => 'subscription',
328             );
329              
330             tie %req, 'Tie::IxHash',
331             $self->revmap_fields (
332             Auth => \%auth,
333             Request => { RequestVars => \%requestvars },
334             );
335              
336             $response = $self->_my_https_post(%req);
337             $self->order_number($response->{Response}->{TransactionRef});
338              
339             $self->is_success(1);
340             if ($self->result_code()) {
341             $self->is_success(0);
342             unless ( $self->error_message() ) { #additional logging information
343             my %headers = %{$self->response_headers()};
344             $self->error_message(
345             "(HTTPS response: ". $self->result_code(). ") ".
346             "(HTTPS headers: ".
347             join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
348             "(Raw HTTPS content: ". $self->server_response(). ")"
349             );
350             }
351             }
352              
353             }
354              
355             sub _my_https_post {
356             my $self = shift;
357             my %req = @_;
358             my $post_data;
359             my $writer = new XML::Writer( OUTPUT => \$post_data,
360             DATA_MODE => 1,
361             DATA_INDENT => 1,
362             # ENCODING => 'us-ascii',
363             );
364             $writer->xmlDecl();
365             $writer->startTag('VancoWS');
366             foreach ( keys ( %req ) ) {
367             $self->_xmlwrite($writer, $_, $req{$_});
368             }
369             $writer->endTag('VancoWS');
370             $writer->end();
371              
372             if ($self->test_transaction()) {
373             $self->server('www.vancodev.com');
374             $self->port('443');
375             $self->path('/cgi-bin/wstest.vps');
376             }
377              
378             my $url = "https://" . $self->server. ':';
379             $url .= $self->port || '443';
380             $url .= $self->path;
381              
382             my $ua = new LWP::UserAgent;
383             my $res = $ua->request( POST( $url, 'Content_Type' => 'form-data',
384             'Content' => [ 'xml' => $post_data ])
385             );
386              
387             warn $post_data if $DEBUG;
388             my($page,$server_response,%headers) = (
389             $res->content,
390             $res->code. ' ' . $res->message,
391             map { $_ => $res->header($_) } $res->header_field_names
392             );
393              
394             warn $page if $DEBUG;
395              
396             my $response;
397             my $error;
398             if ($server_response =~ /200/){
399             $response = XMLin($page);
400             if ( exists($response->{Response})
401             && !exists($response->{Response}->{Errors})) { # so much for docs
402             $error->{ErrorDescription} = '';
403             $error->{ErrorCode} = '';
404             }elsif (ref($response->{Response}->{Errors}) eq 'ARRAY') {
405             $error = $response->{Response}->{Errors}->[0];
406             }else{
407             $error = $response->{Response}->{Errors}->{Error};
408             }
409             }else{
410             $error->{ErrorDescription} = "Server Failed";
411             $error->{ErrorCode} = $server_response;
412             }
413              
414             $self->result_code($error->{ErrorCode});
415             $self->error_message($error->{ErrorDescription});
416              
417             $self->server_response($page);
418             $self->response_page($page);
419             $self->response_headers(\%headers);
420             return $response;
421             }
422              
423             sub _xmlwrite {
424             my ($self, $writer, $item, $value) = @_;
425             $writer->startTag($item);
426             if ( ref( $value ) eq 'HASH' ) {
427             foreach ( keys ( %$value ) ) {
428             $self->_xmlwrite($writer, $_, $value->{$_});
429             }
430             }else{
431             $writer->characters($value);
432             }
433             $writer->endTag($item);
434             }
435              
436             1;
437             __END__