1 package Business::OnlinePayment::Vanco;
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);
16 @ISA = qw(Business::OnlinePayment); # Business::OnlinePayment::HTTPS
19 $me = 'Business::OnlinePayment::Vanco';
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;
30 $self->build_subs(qw( order_number avs_code cvv2_response
31 response_page response_code response_headers
34 # module specific data
35 foreach (qw( ClientID ProductID )) {
36 $self->build_subs($_);
39 $self->$_( $opts{$_} );
49 my %content = $self->content();
50 my $action = lc($content{'action'});
54 ( 'normal authorization' => 'EFTAddCompleteTransaction',
55 'recurring authorization' => 'EFTAddCompleteTransaction',
56 'cancel recurring authorization' => 'EFTDeleteTransaction',
58 $content{'RequestType'} = $actions{$action} || $action;
61 my %types = ( 'visa' => 'CC',
63 'american express' => 'CC',
67 $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
68 $self->transaction_type($content{'type'});
70 # CHECK/TRANSACTION TYPE MAP
71 $content{'TransactionTypeCode'} = $content{'check_type'} || 'PPD'
72 unless ( $content{'TransactionTypeCode'}
73 || $content{'RequestType'} eq 'EFTDeleteTransaction'); # kludgy
75 # let FrequencyCode, StartDate, and EndDate be specified directly;
76 unless($content{FrequencyCode}){
78 ($self->{_content}->{interval} or '') =~
79 /^\s*(\d+)\s+(day|month)s?\s*$/;
81 my %daily = ( '7' => 'W',
85 my %monthly = ( '1' => 'M',
90 if ($length && $unit) {
91 $content{'FrequencyCode'} = $daily{$length}
94 $content{'FrequencyCode'} = $monthly{$length}
95 if ($unit eq 'month');
99 unless($content{StartDate}){
100 $content{'StartDate'} = $content{'start'};
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};
108 my ($periods) = $content{periods} =~/^\s*(\d+)\s*$/
109 if $content{periods};
111 my %daily = ( 'W' => '7',
115 my %monthly = ( 'M' => '1',
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);
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);
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'};
145 my %account_types = ('personal checking' => 'C',
146 'personal savings' => 'S',
147 'business checking' => 'C',
148 'business savings' => 'S',
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';
156 # SHIPPING INFORMATION
157 foreach (qw(name address city state zip)) {
158 $content{"ship_$_"} = $content{$_} unless $content{"ship$_"};
161 # stuff it back into %content
162 $self->content(%content);
167 my ($self, $exp) = (shift, shift);
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 );
178 my ($self, $exp) = (shift, shift);
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 );
189 my @time = localtime($_[0] ? shift : time);
192 sprintf("%04d-%02d-%02d %02d:%02d:%02d", reverse(@time[0..5]));
197 tie my(%map), 'Tie::IxHash', @_;
198 my %content = $self->content();
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{$_} };
209 if (defined($value)) {
220 $self->is_success(0);
221 unless($self->ClientID() && $self->ProductID()) {
222 croak "ClientID and ProductID are required";
225 my $requestid = time . sprintf("%010u", rand() * 2**32);
226 my $auth_requestid = $requestid . '0';
227 my $req_requestid = $requestid . '1';
231 my @required_fields = qw(action login password);
233 if ( lc($self->{_content}->{action}) eq 'normal authorization' ) {
234 push @required_fields, qw( type amount name );
236 push @required_fields, qw( card_number expiration )
237 if ($self->transaction_type() eq "CC");
239 push @required_fields,
240 qw( routing_code account_number account_type )
241 if ($self->transaction_type() eq "ECHECK");
243 }elsif ( lc($self->{_content}->{action}) eq 'recurring authorization' ) {
244 push @required_fields, qw( type interval start periods amount name );
246 push @required_fields, qw( card_number expiration )
247 if ($self->transaction_type() eq 'CC' );
249 push @required_fields,
250 qw( routing_code account_number account_type )
251 if ($self->transaction_type() eq "ECHECK");
253 }elsif ( lc($self->{_content}->{action}) eq 'cancel recurring authorization' ) {
254 push @required_fields, qw( subscription );
257 croak "$me can't handle transaction type: ".
258 $self->{_content}->{action}. " for ".
259 $self->transaction_type();
262 $self->required_fields(@required_fields);
264 tie my %auth, 'Tie::IxHash', (
265 RequestType => 'Login',
266 RequestID => $auth_requestid,
267 RequestTime => today(),
270 tie my %requestvars, 'Tie::IxHash',
271 $self->revmap_fields(
273 Password => 'password',
275 $requestvars{'ProductID'} = $self->ProductID();
277 tie my %req, 'Tie::IxHash',
278 $self->revmap_fields (
280 Request => { RequestVars => \%requestvars },
283 my $response = $self->_my_https_post(%req);
284 return if $self->result_code();
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};
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}
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,
318 CardBillingAddr1 => 'address',
319 CardBillingCity => 'city',
320 CardBillingState => 'state',
321 CardBillingZip => 'zip',
323 StartDate => 'StartDate',
324 EndDate => 'EndDate',
325 FrequencyCode => 'FrequencyCode',
326 TransactionTypeCode => 'TransactionTypeCode',
327 TransactionRef => 'subscription',
330 tie %req, 'Tie::IxHash',
331 $self->revmap_fields (
333 Request => { RequestVars => \%requestvars },
336 $response = $self->_my_https_post(%req);
337 $self->order_number($response->{Response}->{TransactionRef});
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(). ") ".
347 join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
348 "(Raw HTTPS content: ". $self->server_response(). ")"
359 my $writer = new XML::Writer( OUTPUT => \$post_data,
362 # ENCODING => 'us-ascii',
365 $writer->startTag('VancoWS');
366 foreach ( keys ( %req ) ) {
367 $self->_xmlwrite($writer, $_, $req{$_});
369 $writer->endTag('VancoWS');
372 if ($self->test_transaction()) {
373 $self->server('www.vancodev.com');
375 $self->path('/cgi-bin/wstest.vps');
378 my $url = "https://" . $self->server. ':';
379 $url .= $self->port || '443';
382 my $ua = new LWP::UserAgent;
383 my $res = $ua->request( POST( $url, 'Content_Type' => 'form-data',
384 'Content' => [ 'xml' => $post_data ])
387 warn $post_data if $DEBUG;
388 my($page,$server_response,%headers) = (
390 $res->code. ' ' . $res->message,
391 map { $_ => $res->header($_) } $res->header_field_names
394 warn $page if $DEBUG;
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];
407 $error = $response->{Response}->{Errors}->{Error};
410 $error->{ErrorDescription} = "Server Failed";
411 $error->{ErrorCode} = $server_response;
414 $self->result_code($error->{ErrorCode});
415 $self->error_message($error->{ErrorDescription});
417 $self->server_response($page);
418 $self->response_page($page);
419 $self->response_headers(\%headers);
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->{$_});
431 $writer->characters($value);
433 $writer->endTag($item);
441 Business::OnlinePayment::Vanco - Vanco Services backend for Business::OnlinePayment
445 use Business::OnlinePayment;
448 # One step transaction, the simple case.
451 my $tx = new Business::OnlinePayment( "Vanco",
452 ClientID => 'CL1234',
457 login => 'testdrive',
458 password => '', #password
459 action => 'Normal Authorization',
460 description => 'Business::OnlinePayment test',
462 customer_id => 'tfb',
463 name => 'Tofu Beast',
464 address => '123 Anystreet',
468 card_number => '4007000000027',
469 expiration => '09/02',
470 cvv2 => '1234', #optional
474 if($tx->is_success()) {
475 print "Card processed successfully: ".$tx->authorization."\n";
477 print "Card was rejected: ".$tx->error_message."\n";
481 # One step subscription, the simple case.
484 my $tx = new Business::OnlinePayment( "Vanco",
485 ClientID => 'CL1234',
490 login => 'testdrive',
491 password => 'testpass',
492 action => 'Recurring Authorization',
493 interval => '7 days',
494 start => '2008-3-10',
497 description => 'Business::OnlinePayment test',
498 customer_id => 'vip',
499 name => 'Tofu Beast',
500 address => '123 Anystreet',
504 card_number => '4111111111111111',
505 expiration => '09/02',
509 if($tx->is_success()) {
510 print "Card processed successfully: ".$tx->order_number."\n";
512 print "Card was rejected: ".$tx->error_message."\n";
514 my $subscription = $tx->order_number
518 # Subscription cancellation. It happens.
522 subscription => '99W2D',
523 login => 'testdrive',
524 password => 'testpass',
525 action => 'Cancel Recurring Authorization',
529 if($tx->is_success()) {
530 print "Cancellation processed successfully."\n";
532 print "Cancellation was rejected: ".$tx->error_message."\n";
536 =head1 SUPPORTED TRANSACTION TYPES
538 =head2 CC, Visa, MasterCard, American Express, Discover
540 Content required: type, login, password, action, amount, name, card_number, expiration.
544 Content required: type, login, password, action, amount, name, account_number, routing_code, account_type.
548 Additional content required: interval, start, periods.
552 For detailed information see L<Business::OnlinePayment>.
554 =head1 METHODS AND FUNCTIONS
556 See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.
560 Returns the response error code.
564 Returns the response error description text.
566 =head2 server_response
568 Returns the complete response from the server.
570 =head1 Handling of content(%content) data:
574 The following actions are valid
577 recurring authorization
578 cancel recurring authorization
582 Interval contains a number of digits, whitespace, and the units of days or months in either singular or plural form.
584 =head1 Setting Vanco parameters from content(%content)
586 The following rules are applied to map data to AuthorizeNet ARB parameters
587 from content(%content):
589 # param => $content{<key>}
592 Password => 'password',
595 CustomerID => 'customer_id',
596 CustomerName => 'ship_name',
597 CustomerAddress1 => 'ship_address',
598 CustomerCity => 'ship_city',
599 CustomerState => 'ship_state',
600 CustomerZip => 'ship_zip',
601 CustomerPhone => 'phone',
602 AccountType => 'account_type', # C, S, or CC
603 AccountNumber => 'account_number' # or card_number
604 RoutingNumber => 'routing_code',
605 CardBillingName => 'name',
606 CardExpMonth => \( $month ), # YYYY-MM from 'expiration'
607 CardExpYear => \( $year ), # YYYY-MM from 'expiration'
609 CardBillingAddr1 => 'address',
610 CardBillingCity => 'city',
611 CardBillingState => 'state',
612 CardBillingZip => 'zip',
614 StartDate => 'start',
615 EndDate => calculated_from start, periods, interval,
616 FrequencyCode => [O,M,W,BW,Q, or A determined from interval],
617 TransactionTypeCode => 'check_type', # (or PPD by default)
621 To cancel a recurring authorization transaction, submit the TransactionRef
622 in the field "subscription" with the action set to "Cancel Recurring
623 Authorization". You can get the TransactionRef from the authorization by
624 calling the order_number method on the object returned from the authorization.
628 Business::OnlinePayment::Vanco uses Vanco Services' "Standard Web Services
629 XML API" as described on February 29, 2008. The describing documents
630 are protected by a non-disclosure agreement.
632 See http://www.vancoservices.com/ for more information.
636 Jeff Finucane, vanco@weasellips.com
640 perl(1). L<Business::OnlinePayment>.