Initial import tag
authorjeff <jeff>
Mon, 24 Mar 2008 01:32:41 +0000 (01:32 +0000)
committerjeff <jeff>
Mon, 24 Mar 2008 01:32:41 +0000 (01:32 +0000)
13 files changed:
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
META.yml [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
Vanco.pm [new file with mode: 0644]
t/00load.t [new file with mode: 0644]
t/bop.t [new file with mode: 0644]
t/card_once.t [new file with mode: 0644]
t/card_recurring.t [new file with mode: 0644]
t/check.t [new file with mode: 0644]
t/lib/test_account.pl [new file with mode: 0644]
t/test_account.eg [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..846431c
--- /dev/null
+++ b/Changes
@@ -0,0 +1,5 @@
+Revision history for Perl extension Business::OnlinePayment::Vanco.
+
+0.01  Fri Feb 29 22:34:17 2008
+       - original version;
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..0ad9d8c
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,13 @@
+Vanco.pm
+Changes
+MANIFEST
+Makefile.PL
+README
+t/00load.t
+t/card_once.t
+t/card_recurring.t
+t/check.t
+t/bop.t
+t/test_account.eg
+t/lib/test_account.pl
+META.yml                                 Module meta-data (added by MakeMaker)
diff --git a/META.yml b/META.yml
new file mode 100644 (file)
index 0000000..4dc2f18
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,21 @@
+# http://module-build.sourceforge.net/META-spec.html
+#XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
+name:         Business-OnlinePayment-Vanco
+version:      0.01
+version_from: Vanco.pm
+installdirs:  site
+requires:
+    Business::OnlinePayment:       3
+    Crypt::SSLeay:                 0
+    Date::Calc:                    0
+    HTTP::Request:                 0
+    HTTP::Request::Common:         0
+    LWP::UserAgent:                0
+    Test::More:                    0.42
+    Text::CSV_XS:                  0
+    Tie::IxHash:                   0
+    XML::Simple:                   0
+    XML::Writer:                   0
+
+distribution_type: module
+generated_by: ExtUtils::MakeMaker version 6.17
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..14fca19
--- /dev/null
@@ -0,0 +1,19 @@
+use ExtUtils::MakeMaker;
+WriteMakefile(
+    'NAME'         => 'Business::OnlinePayment::Vanco',
+    'VERSION_FROM' => 'Vanco.pm', # finds $VERSION
+    'AUTHOR'       => 'Jeff Finucane <vanco@weasellips.com>',
+    'PREREQ_PM'    => { 'Business::OnlinePayment' => 3,
+#                        'Business::OnlinePayment::HTTPS' => 0,
+                        'Crypt::SSLeay'           => 0,
+                        'Date::Calc'              => 0,
+                        'HTTP::Request'           => 0,
+                        'HTTP::Request::Common'   => 0,
+                        'LWP::UserAgent'          => 0,
+                        'Text::CSV_XS' => 0,
+                       'Test::More'              => 0.42,
+                        'Tie::IxHash'             => 0,
+                        'XML::Simple'             => 0,
+                        'XML::Writer'             => 0,
+                      },
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..dde3093
--- /dev/null
+++ b/README
@@ -0,0 +1,22 @@
+Copyright (c) 1999 Jason Kohles.
+Copyright (c) 2002-2003 Ivan Kohler
+Copyright (c) 2008 Jeff Finucane
+All rights reserved. This program is free software; you can redistribute it
+and/or modify it under the same terms as Perl itself.
+
+This is Business::OnlinePayment::Vanco, a Business::OnlinePayment backend
+module for Vanco.  It is only useful if you have a merchant account with
+Vanco Services: http://www.vancoservices.com
+
+This module implements the Vanco Standeard Web Services XML API as of
+February 29, 2008.
+
+For testing edit t/test_account, then run make test.
+
+Jeff Finucane is the original author.  Please send patches as unified
+diffs (diff -u).  Mr. Kohles and Mr. Kohler provided material for cribbing.
+
+Business::OnlinePayment is a generic interface for processing payments through
+online credit card processors, online check acceptance houses, etc.  (If you
+like buzzwords, call it an "multiplatform ecommerce-enabling middleware
+solution").
diff --git a/Vanco.pm b/Vanco.pm
new file mode 100644 (file)
index 0000000..290f97b
--- /dev/null
+++ b/Vanco.pm
@@ -0,0 +1,643 @@
+package Business::OnlinePayment::Vanco;
+
+use strict;
+use Carp;
+use Tie::IxHash;
+use XML::Simple;
+use XML::Writer;
+use LWP::UserAgent;
+use HTTP::Request;
+use HTTP::Request::Common qw (POST);
+use Date::Calc qw(Add_Delta_YM Add_Delta_Days);
+use Business::OnlinePayment;
+#use Business::OnlinePayment::HTTPS;
+use vars qw($VERSION $DEBUG @ISA $me);
+
+@ISA = qw(Business::OnlinePayment);  # Business::OnlinePayment::HTTPS 
+$VERSION = '0.01';
+$DEBUG = 1;
+$me = 'Business::OnlinePayment::Vanco';
+
+sub set_defaults {
+    my $self = shift;
+    my %opts = @_;
+
+    # standard B::OP methods/data
+    $self->server('www.vancoservices.com') unless $self->server;
+    $self->port('443') unless $self->port;
+    $self->path('/cgi-bin/ws.vps') unless $self->path;
+
+    $self->build_subs(qw( order_number avs_code cvv2_response
+                          response_page response_code response_headers
+                     ));
+
+    # module specific data
+    foreach (qw( ClientID ProductID )) {
+      $self->build_subs($_);
+
+      if ( $opts{$_} ) {
+          $self->$_( $opts{$_} );
+          delete $opts{$_};
+      }
+    }
+
+}
+
+sub map_fields {
+    my($self) = @_;
+
+    my %content = $self->content();
+    my $action = lc($content{'action'});
+
+    # ACTION MAP 
+    my %actions =
+      ( 'normal authorization'            => 'EFTAddCompleteTransaction',
+        'recurring authorization'         => 'EFTAddCompleteTransaction',
+        'cancel recurring authorization'  => 'EFTDeleteTransaction',
+      );
+    $content{'RequestType'} = $actions{$action} || $action;
+
+    # TYPE MAP
+    my %types = ( 'visa'               => 'CC',
+                  'mastercard'         => 'CC',
+                  'american express'   => 'CC',
+                  'discover'           => 'CC',
+                  'check'              => 'ECHECK',
+                );
+    $content{'type'} = $types{lc($content{'type'})} || $content{'type'};
+    $self->transaction_type($content{'type'});
+    
+    # CHECK/TRANSACTION TYPE MAP
+    $content{'TransactionTypeCode'} = $content{'check_type'} || 'PPD'
+      unless ( $content{'TransactionTypeCode'} 
+            || $content{'RequestType'} eq 'EFTDeleteTransaction'); # kludgy
+
+    # let FrequencyCode, StartDate, and EndDate be specified directly;
+    unless($content{FrequencyCode}){
+      my ($length,$unit) =
+        ($self->{_content}->{interval} or '') =~
+          /^\s*(\d+)\s+(day|month)s?\s*$/;
+
+      my %daily   = (  '7' => 'W',
+                      '14' => 'BW',
+                    );
+       
+      my %monthly = (  '1' => 'M',
+                       '3' => 'Q',
+                      '12' => 'A',
+                    );
+       
+      if ($length && $unit) {
+        $content{'FrequencyCode'} = $daily{$length}
+          if ($unit eq 'day');
+
+        $content{'FrequencyCode'} = $monthly{$length}
+          if ($unit eq 'month');
+      }
+    }
+
+    unless($content{StartDate}){
+      $content{'StartDate'} = $content{'start'};
+    }
+
+    unless($content{EndDate}){
+      my ($year,$month,$day) =
+        $content{StartDate} =~ /^\s*(\d{4})-(\d{1,2})-(\d{1,2})\s*$/
+        if $content{StartDate};
+
+      my ($periods) = $content{periods} =~/^\s*(\d+)\s*$/
+        if $content{periods};
+
+      my %daily   = (  'W' => '7',
+                      'BW' => '14',
+                    );
+       
+      my %monthly = (  'M' => '1',
+                       'Q' => '3',
+                       'A' => '12',
+                    );
+
+      if ($year && $month && $day && $periods) {
+        if ($daily{$content{FrequencyCode}}) {
+          my $days = ($periods - 1) * $daily{$content{FrequencyCode}};
+          ($year, $month, $day) = Add_Delta_Days( $year, $month, $day, $days);
+          $content{EndDate} = sprintf("%04d-%02d-%02d", $year, $month, $day);
+        } 
+
+        if ($monthly{$content{FrequencyCode}}) {
+          my $months = ($periods - 1) * $monthly{$content{FrequencyCode}};
+          ($year, $month, $day) = Add_Delta_YM( $year, $month, $day, 0, $months);
+          $content{EndDate} = sprintf("%04d-%02d-%02d", $year, $month, $day);
+        } 
+      }
+
+    }
+
+    if ($action eq 'normal authorization'){
+      my $time = time + 86400 if $self->transaction_type() eq 'ECHECK';
+      $content{'FrequencyCode'} = 'O';
+      $content{'StartDate'} = $content{'start'} || substr(today($time),0,10);
+      $content{'EndDate'} = $content{'StartDate'};
+    }
+
+
+    # ACCOUNT TYPE MAP
+    my %account_types = ('personal checking'   => 'C',
+                         'personal savings'    => 'S',
+                         'business checking'   => 'C',
+                         'business savings'    => 'S',
+                         'checking'            => 'C',
+                         'savings'             => 'S',
+                        );
+    $content{'account_type'} = $account_types{lc($content{'account_type'})}
+                               || $content{'account_type'};
+    $content{'account_type'} = 'CC' if lc($content{'type'}) eq 'cc';
+
+    # SHIPPING INFORMATION
+    foreach (qw(name address city state zip)) {
+      $content{"ship_$_"} = $content{$_} unless $content{"ship$_"};
+    }
+
+    # stuff it back into %content
+    $self->content(%content);
+
+}
+
+sub expdate_month {
+  my ($self, $exp) = (shift, shift);
+  my $month;
+  if ( defined($exp) and $exp =~ /^(\d+)\D+\d*\d{2}$/ ) {
+    $month  = sprintf( "%02d", $1 );
+  }elsif ( defined($exp) and $exp =~ /^(\d{2})\d{2}$/ ) {
+    $month  = sprintf( "%02d", $1 );
+  }
+  return $month;
+}
+
+sub expdate_year {
+  my ($self, $exp) = (shift, shift);
+  my $year;
+  if ( defined($exp) and $exp =~ /^\d+\D+\d*(\d{2})$/ ) {
+    $year  = sprintf( "%02d", $1 );
+  }elsif ( defined($exp) and $exp =~ /^\d{2}(\d{2})$/ ) {
+    $year  = sprintf( "%02d", $1 );
+  }
+  return $year;
+}
+
+sub today {  
+  my @time = localtime($_[0] ? shift : time);
+  $time[5] += 1900;
+  $time[4]++;
+  sprintf("%04d-%02d-%02d %02d:%02d:%02d", reverse(@time[0..5]));
+}
+
+sub revmap_fields {
+  my $self = shift;
+  tie my(%map), 'Tie::IxHash', @_;
+  my %content = $self->content();
+  map {
+        my $value;
+        if ( ref( $map{$_} ) eq 'HASH' ) {
+          $value = $map{$_} if ( keys %{ $map{$_} } );
+        }elsif( ref( $map{$_} ) ) {
+          $value = ${ $map{$_} };
+        }elsif( exists( $content{ $map{$_} } ) ) {
+          $value = $content{ $map{$_} };
+        }
+
+        if (defined($value)) {
+          ($_ => $value);
+        }else{
+          ();
+        }
+      } (keys %map);
+}
+
+sub submit {
+  my($self) = @_;
+
+  $self->is_success(0);
+  unless($self->ClientID() && $self->ProductID()) {
+    croak "ClientID and ProductID are required";
+  }
+
+  my $requestid = time . sprintf("%010u", rand() * 2**32);
+  my $auth_requestid = $requestid . '0';
+  my $req_requestid  = $requestid . '1';
+
+  $self->map_fields();
+
+  my @required_fields = qw(action login password);
+
+  if ( lc($self->{_content}->{action}) eq 'normal authorization' ) {
+    push @required_fields, qw( type amount name );
+
+    push @required_fields, qw( card_number expiration )
+      if ($self->transaction_type() eq "CC"); 
+        
+    push @required_fields,
+      qw( routing_code account_number account_type )
+      if ($self->transaction_type() eq "ECHECK");
+        
+  }elsif ( lc($self->{_content}->{action}) eq 'recurring authorization' ) {
+    push @required_fields, qw( type interval start periods amount name );
+
+    push @required_fields, qw( card_number expiration )
+      if ($self->transaction_type() eq 'CC' ); 
+
+    push @required_fields,
+      qw( routing_code account_number account_type )
+      if ($self->transaction_type() eq "ECHECK");
+
+  }elsif ( lc($self->{_content}->{action}) eq 'cancel recurring authorization' ) {
+    push @required_fields, qw( subscription );
+
+  }else{
+    croak "$me can't handle transaction type: ".
+      $self->{_content}->{action}. " for ".
+      $self->transaction_type();
+  }
+
+  $self->required_fields(@required_fields);
+
+  tie my %auth, 'Tie::IxHash', (
+                                 RequestType => 'Login',
+                                 RequestID   => $auth_requestid,
+                                 RequestTime => today(),
+                               );
+
+  tie my %requestvars, 'Tie::IxHash',
+    $self->revmap_fields(
+                          UserID      => 'login',
+                          Password    => 'password',
+                        );
+  $requestvars{'ProductID'} = $self->ProductID();
+
+  tie my %req, 'Tie::IxHash',
+    $self->revmap_fields (
+                           Auth    => \%auth,
+                           Request => { RequestVars => \%requestvars },
+                         );
+
+  my $response = $self->_my_https_post(%req);
+  return if $self->result_code();
+
+  tie %auth, 'Tie::IxHash',
+    $self->revmap_fields( RequestType => 'RequestType');
+  $auth{'RequestID'}   = $req_requestid;
+  $auth{'RequestTime'} = today();
+  $auth{'SessionID'}   = $response->{Response}->{SessionID};
+
+  my $client_id = $self->ClientID();
+  my $cardexpmonth = $self->expdate_month($self->{_content}->{expiration});
+  my $cardexpyear  = $self->expdate_year($self->{_content}->{expiration});
+  my $account_number = ( defined($self->transaction_type())
+                         && $self->transaction_type() eq 'CC')
+                       ? $self->{_content}->{card_number}
+                       : $self->{_content}->{account_number}
+  ;
+
+  tie %requestvars, 'Tie::IxHash',
+    $self->revmap_fields(
+                          ClientID            => \$client_id,
+                          CustomerID          => 'customer_id',
+                          CustomerName        => 'ship_name',   # defaults to 
+                          CustomerAddress1    => 'ship_address',# values without
+                          CustomerCity        => 'ship_city',   # ship_ prefix
+                          CustomerState       => 'ship_state',  #
+                          CustomerZip         => 'ship_zip',    #
+                          CustomerPhone       => 'phone',
+                          AccountType         => 'account_type',
+                          AccountNumber       => \$account_number,
+                          RoutingNumber       => 'routing_code',
+                          CardBillingName     => 'name',
+                          CardExpMonth        => \$cardexpmonth,
+                          CardExpYear         => \$cardexpyear,
+                          CardCVV2            => 'cvv2',
+                          CardBillingAddr1    => 'address',
+                          CardBillingCity     => 'city',
+                          CardBillingState    => 'state',
+                          CardBillingZip      => 'zip',
+                          Amount              => 'amount',
+                          StartDate           => 'StartDate',
+                          EndDate             => 'EndDate',
+                          FrequencyCode       => 'FrequencyCode',
+                          TransactionTypeCode => 'TransactionTypeCode',
+                          TransactionRef      => 'subscription',
+                        );
+
+  tie %req, 'Tie::IxHash',
+    $self->revmap_fields (
+                           Auth    => \%auth,
+                           Request => { RequestVars => \%requestvars },
+                         );
+
+  $response = $self->_my_https_post(%req);
+  $self->order_number($response->{Response}->{TransactionRef});
+
+  $self->is_success(1);
+  if ($self->result_code()) {
+    $self->is_success(0);
+    unless ( $self->error_message() ) { #additional logging information
+      my %headers = %{$self->response_headers()};
+      $self->error_message(
+        "(HTTPS response: ". $self->result_code(). ") ".
+        "(HTTPS headers: ".
+          join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
+        "(Raw HTTPS content: ". $self->server_response(). ")"
+      );
+    }
+  }
+
+}
+
+sub _my_https_post {
+  my $self = shift;
+  my %req = @_;
+  my $post_data;
+  my $writer = new XML::Writer( OUTPUT      => \$post_data,
+                                DATA_MODE   => 1,
+                                DATA_INDENT => 1,
+#                                ENCODING    => 'us-ascii',
+                              );
+  $writer->xmlDecl();
+  $writer->startTag('VancoWS');
+  foreach ( keys ( %req ) ) {
+    $self->_xmlwrite($writer, $_, $req{$_});
+  }
+  $writer->endTag('VancoWS');
+  $writer->end();
+
+  if ($self->test_transaction()) {
+    $self->server('www.vancodev.com');
+    $self->port('443');
+    $self->path('/cgi-bin/wstest.vps');
+  }
+
+  my $url = "https://" . $self->server. ':';
+  $url .= $self->port || '443';
+  $url .= $self->path;
+
+  my $ua = new LWP::UserAgent;
+  my $res = $ua->request( POST( $url, 'Content_Type' => 'form-data',
+                                      'Content' => [ 'xml' => $post_data ])
+                        );
+
+  warn $post_data if $DEBUG;
+  my($page,$server_response,%headers) =  (
+    $res->content,
+    $res->code. ' ' . $res->message,
+    map { $_ => $res->header($_) } $res->header_field_names
+  );
+
+  warn $page if $DEBUG;
+
+  my $response;
+  my $error;
+  if ($server_response =~ /200/){
+    $response = XMLin($page);
+    if (  exists($response->{Response})
+      && !exists($response->{Response}->{Errors})) {     # so much for docs
+      $error->{ErrorDescription} = '';
+      $error->{ErrorCode} = '';
+    }elsif (ref($response->{Response}->{Errors}) eq 'ARRAY') {
+      $error = $response->{Response}->{Errors}->[0];
+    }else{
+      $error = $response->{Response}->{Errors}->{Error};
+    }
+  }else{
+    $error->{ErrorDescription} = "Server Failed";
+    $error->{ErrorCode} = $server_response;
+  }
+
+  $self->result_code($error->{ErrorCode});
+  $self->error_message($error->{ErrorDescription});
+
+  $self->server_response($page);
+  $self->response_page($page);
+  $self->response_headers(\%headers);
+  return $response;
+}
+
+sub _xmlwrite {
+  my ($self, $writer, $item, $value) = @_;
+  $writer->startTag($item);
+  if ( ref( $value ) eq 'HASH' ) {
+    foreach ( keys ( %$value ) ) {
+      $self->_xmlwrite($writer, $_, $value->{$_});
+    }
+  }else{
+    $writer->characters($value);
+  }
+  $writer->endTag($item);
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Business::OnlinePayment::Vanco - Vanco Services backend for Business::OnlinePayment
+
+=head1 SYNOPSIS
+
+  use Business::OnlinePayment;
+
+  ####
+  # One step transaction, the simple case.
+  ####
+
+  my $tx = new Business::OnlinePayment( "Vanco",
+                                        ClientID  => 'CL1234',
+                                        ProductID => 'EFT',
+                                      );
+  $tx->content(
+      type           => 'VISA',
+      login          => 'testdrive',
+      password       => '', #password 
+      action         => 'Normal Authorization',
+      description    => 'Business::OnlinePayment test',
+      amount         => '49.95',
+      customer_id    => 'tfb',
+      name           => 'Tofu Beast',
+      address        => '123 Anystreet',
+      city           => 'Anywhere',
+      state          => 'UT',
+      zip            => '84058',
+      card_number    => '4007000000027',
+      expiration     => '09/02',
+      cvv2           => '1234', #optional
+  );
+  $tx->submit();
+
+  if($tx->is_success()) {
+      print "Card processed successfully: ".$tx->authorization."\n";
+  } else {
+      print "Card was rejected: ".$tx->error_message."\n";
+  }
+
+  ####
+  # One step subscription, the simple case.
+  ####
+
+  my $tx = new Business::OnlinePayment( "Vanco",
+                                        ClientID  => 'CL1234',
+                                        ProductID => 'EFT',
+                                      );
+  $tx->content(
+      type           => 'CC',
+      login          => 'testdrive',
+      password       => 'testpass',
+      action         => 'Recurring Authorization',
+      interval       => '7 days',
+      start          => '2008-3-10',
+      periods        => '16',
+      amount         => '99.95',
+      description    => 'Business::OnlinePayment test',
+      customer_id    => 'vip',
+      name           => 'Tofu Beast',
+      address        => '123 Anystreet',
+      city           => 'Anywhere',
+      state          => 'GA',
+      zip            => '84058',
+      card_number    => '4111111111111111',
+      expiration     => '09/02',
+  );
+  $tx->submit();
+
+  if($tx->is_success()) {
+      print "Card processed successfully: ".$tx->order_number."\n";
+  } else {
+      print "Card was rejected: ".$tx->error_message."\n";
+  }
+  my $subscription = $tx->order_number
+
+
+  ####
+  # Subscription cancellation.   It happens.
+  ####
+
+  $tx->content(
+      subscription   => '99W2D',
+      login          => 'testdrive',
+      password       => 'testpass',
+      action         => 'Cancel Recurring Authorization',
+  );
+  $tx->submit();
+
+  if($tx->is_success()) {
+      print "Cancellation processed successfully."\n";
+  } else {
+      print "Cancellation was rejected: ".$tx->error_message."\n";
+  }
+
+
+=head1 SUPPORTED TRANSACTION TYPES
+
+=head2 CC, Visa, MasterCard, American Express, Discover
+
+Content required: type, login, password, action, amount, name, card_number, expiration.
+
+=head2 Check
+
+Content required: type, login, password, action, amount, name, account_number, routing_code, account_type.
+
+=head2 Subscriptions
+
+Additional content required: interval, start, periods.
+
+=head1 DESCRIPTION
+
+For detailed information see L<Business::OnlinePayment>.
+
+=head1 METHODS AND FUNCTIONS
+
+See L<Business::OnlinePayment> for the complete list. The following methods either override the methods in L<Business::OnlinePayment> or provide additional functions.  
+
+=head2 result_code
+
+Returns the response error code.
+
+=head2 error_message
+
+Returns the response error description text.
+
+=head2 server_response
+
+Returns the complete response from the server.
+
+=head1 Handling of content(%content) data:
+
+=head2 action
+
+The following actions are valid
+
+  normal authorization
+  recurring authorization
+  cancel recurring authorization
+
+=head2 interval
+
+  Interval contains a number of digits, whitespace, and the units of days or months in either singular or plural form.
+  
+=head1 Setting Vanco parameters from content(%content)
+
+The following rules are applied to map data to AuthorizeNet ARB parameters
+from content(%content):
+
+      # param => $content{<key>}
+      Auth
+        UserId                   =>  'login',
+        Password                 =>  'password',
+      Request
+        RequestVars
+          CustomerID             => 'customer_id',
+          CustomerName           => 'ship_name',
+          CustomerAddress1       => 'ship_address',
+          CustomerCity           => 'ship_city',
+          CustomerState          => 'ship_state',
+          CustomerZip            => 'ship_zip',
+          CustomerPhone          => 'phone',
+          AccountType            => 'account_type',  # C, S, or CC
+          AccountNumber          => 'account_number' # or card_number 
+          RoutingNumber          => 'routing_code',
+          CardBillingName        => 'name',
+          CardExpMonth           => \( $month ), # YYYY-MM from 'expiration'
+          CardExpYear            => \( $year ), # YYYY-MM from 'expiration'
+          CardCVV2               => 'cvv2',
+          CardBillingAddr1       => 'address',
+          CardBillingCity        => 'city',
+          CardBillingState       => 'state',
+          CardBillingZip         => 'zip',
+          Amount                 => 'amount',
+          StartDate              => 'start',
+          EndDate                => calculated_from start, periods, interval,
+          FrequencyCode          => [O,M,W,BW,Q, or A determined from interval],
+          TransactionTypeCode    => 'check_type', # (or PPD by default)
+
+=head1 NOTE
+
+To cancel a recurring authorization transaction, submit the TransactionRef
+in the field "subscription" with the action set to "Cancel Recurring
+Authorization".  You can get the TransactionRef from the authorization by
+calling the order_number method on the object returned from the authorization.
+
+=head1 COMPATIBILITY
+
+Business::OnlinePayment::Vanco uses Vanco Services' "Standard Web Services
+XML API"  as described on February 29, 2008.  The describing documents
+are protected by a non-disclosure agreement.
+
+See http://www.vancoservices.com/ for more information.
+
+=head1 AUTHOR
+
+Jeff Finucane, vanco@weasellips.com
+
+=head1 SEE ALSO
+
+perl(1). L<Business::OnlinePayment>.
+
+=cut
+
diff --git a/t/00load.t b/t/00load.t
new file mode 100644 (file)
index 0000000..13577fc
--- /dev/null
@@ -0,0 +1,5 @@
+#!/usr/bin/perl -w
+
+use Test::More tests => 1;
+
+use_ok 'Business::OnlinePayment::Vanco';
diff --git a/t/bop.t b/t/bop.t
new file mode 100644 (file)
index 0000000..68f5f98
--- /dev/null
+++ b/t/bop.t
@@ -0,0 +1,5 @@
+#!/usr/bin/perl -w
+
+use Test::More tests => 1;
+
+use_ok 'Business::OnlinePayment';
diff --git a/t/card_once.t b/t/card_once.t
new file mode 100644 (file)
index 0000000..4e9f21e
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -w
+
+use Test::More;
+require "t/lib/test_account.pl";
+
+my($login, $password, @opts) = test_account_or_skip();
+plan tests => 2;
+  
+use_ok 'Business::OnlinePayment';
+
+my $tx = Business::OnlinePayment->new("Vanco", @opts);
+$tx->content(
+    type           => 'VISA',
+    login          => $login,
+    password       => $password,
+    action         => 'Normal Authorization',
+    description    => 'Business::OnlinePayment visa test',
+    amount         => '49.95',
+    customer_id    => 'tfb',
+    name           => 'Tofu Beast',
+    address        => '123 Anystreet',
+    city           => 'Anywhere',
+    state          => 'UT',
+    zip            => '84058',
+#    card_number    => '4007000000027',
+    card_number    => '4111111111111111',
+    expiration     => expiration_date(),
+);
+$tx->test_transaction(1); # test, dont really charge
+$tx->submit();
+
+ok($tx->is_success()) or diag $tx->error_message;
diff --git a/t/card_recurring.t b/t/card_recurring.t
new file mode 100644 (file)
index 0000000..b7dbfd6
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/perl -w
+
+use Test::More;
+require "t/lib/test_account.pl";
+
+my($login, $password, %opts) = test_account_or_skip();
+plan tests => 4;
+  
+use_ok 'Business::OnlinePayment';
+
+my $tx = Business::OnlinePayment->new("Vanco", %opts);
+$tx->content(
+    type           => 'VISA',
+    login          => $login,
+    password       => $password,
+    action         => 'Recurring Authorization',
+    description    => 'Business::OnlinePayment visa test',
+    amount         => '49.95',
+    customer_id    => 'tofu',
+    name           => 'Tofu Beast',
+    address        => '123 Anystreet',
+    city           => 'Anywhere',
+    state          => 'UT',
+    zip            => '84058',
+    card_number    => '5105105105105100',
+    expiration     => expiration_date(),
+    interval       => '1 month',
+    start          => tomorrow(),
+    periods        => '3',
+);
+$tx->test_transaction(1); # test, dont really charge
+$tx->submit();
+
+ok($tx->is_success()) or diag $tx->error_message;
+
+my $subscription = $tx->order_number();
+like($subscription, qr/^[0-9]{1,13}$/, "Get order number");
+
+SKIP: {
+
+  skip "No order number", 1 unless $subscription;
+
+  $tx->content(
+    login        => $login,
+    password     => $password,
+    action       => 'Cancel Recurring Authorization',
+    subscription => $subscription,
+  );
+  $tx->test_transaction(1);
+  $tx->submit();
+  ok($tx->is_success()) or diag $tx->error_message;
+}
diff --git a/t/check.t b/t/check.t
new file mode 100644 (file)
index 0000000..6d5b7c8
--- /dev/null
+++ b/t/check.t
@@ -0,0 +1,27 @@
+#!/usr/bin/perl -w
+
+use Test::More;
+require "t/lib/test_account.pl";
+
+my($login, $password, %opt) = test_account_or_skip();
+plan tests => 2;
+
+use_ok 'Business::OnlinePayment';
+
+my $ctx = Business::OnlinePayment->new("Vanco", %opt);
+$ctx->content(
+    type           => 'CHECK',
+    login          => $login,
+    password       => $password,
+    action         => 'Normal Authorization',
+    amount         => '49.95',
+    customer_id    => 'jsk',
+    name           => 'Tofu Beast',
+    account_number => '12345',
+    routing_code   => '111000025',  # BoA in Texas taken from Wikipedia
+    bank_name      => 'First National Test Bank',
+    account_type   => 'Checking',
+);
+$ctx->test_transaction(1); # test, dont really charge
+$ctx->submit();
+ok( $ctx->is_success() ) || diag $ctx->error_message;
diff --git a/t/lib/test_account.pl b/t/lib/test_account.pl
new file mode 100644 (file)
index 0000000..9ced583
--- /dev/null
@@ -0,0 +1,37 @@
+sub test_account_or_skip {
+    my $suffix = shift;
+    my($login, $password, @opts) = test_account($suffix);
+
+    unless( defined $login ) {
+        plan skip_all => "No test account";
+    }
+
+    return($login, $password, @opts);
+}
+
+sub test_account {
+    my $suffix = shift || '';
+    $suffix = "_$suffix" if $suffix;
+    open TEST_ACCOUNT, "t/test_account$suffix" or return;
+    my($login, $password, @opts) = <TEST_ACCOUNT>;
+    chomp $login;
+    chomp $password;
+    chomp foreach @opts;
+
+    return($login, $password, @opts);
+}
+
+sub expiration_date {
+    my($month, $year) = (localtime)[4,5];
+    $year++;       # So we expire next year.
+    $year %= 100;  # y2k?  What's that?
+
+    return sprintf("%02d/%02d", $month, $year);
+}
+
+sub tomorrow {
+    my($day, $month, $year) = (localtime(time+86400))[3..5];
+    return sprintf("%04d-%02d-%02d", $year+1900, ++$month, $day);
+}
+
+1;
diff --git a/t/test_account.eg b/t/test_account.eg
new file mode 100644 (file)
index 0000000..cf9ea21
--- /dev/null
@@ -0,0 +1,6 @@
+the user id goes here
+the password goes here
+ClientID
+the client id goes here
+ProductID
+EFT