The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Business::OnlinePayment::AuthorizeNet::ARB;

use strict;
use Carp;
use Business::OnlinePayment::AuthorizeNet;
use Business::OnlinePayment::HTTPS;
use XML::Simple;
use XML::Writer;
use Tie::IxHash;
use vars qw($VERSION $DEBUG @ISA $me);

@ISA = qw(Business::OnlinePayment::AuthorizeNet Business::OnlinePayment::HTTPS);
$VERSION = '0.02';
$DEBUG = 0;
$me='Business::OnlinePayment::AuthorizeNet::ARB';

sub set_defaults {
    my $self = shift;

    $self->server('api.authorize.net') unless $self->server;
    $self->port('443') unless $self->port;
    $self->path('/xml/v1/request.api') unless $self->path;

    $self->build_subs(qw( order_number md5 avs_code cvv2_response
                          cavv_response
                     ));
}

sub map_fields {
    my($self) = @_;

    my %content = $self->content();

    # ACTION MAP
    my %actions = ('recurring authorization'
                      => 'ARBCreateSubscriptionRequest',
                   'modify recurring authorization'
                      => 'ARBUpdateSubscriptionRequest',
                   'cancel recurring authorization'
                      => 'ARBCancelSubscriptionRequest',
                  );
    $content{'action'} = $actions{lc($content{'action'} || '')} || $content{'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'});

    # ACCOUNT TYPE MAP
    my %account_types = ('personal checking'   => 'checking',
                         'personal savings'    => 'savings',
                         'business checking'   => 'businessChecking',
                         'business savings'    => 'savings',
                        );
    $content{'account_type'} = $account_types{lc($content{'account_type'} || '')}
                               || $content{'account_type'};

    # MASSAGE EXPIRATION 
    $content{'expdate_yyyymm'} = $self->expdate_yyyymm($content{'expiration'});

    # stuff it back into %content
    $self->content(%content);

}

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( exists( $content{ $map{$_} } ) ) {
          $value = $content{ $map{$_} };
        }

        if (defined($value)) {
          ($_ => $value);
        }else{
          ();
        }
      } (keys %map);
}

sub expdate_yyyymm {
  my $self = shift;
  my $expiration = shift;
  my $expdate_yyyymm;
  if ( defined($expiration) and $expiration =~ /^(\d{1,2})\D+(\d{2})$/ ) {
    my ( $month, $year ) = ( $1, $2 );
   $expdate_yyyymm = sprintf( "20%02d-%02d", $year, $month );
  }
  return defined($expdate_yyyymm) ? $expdate_yyyymm : $expiration;
};

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);
}

sub submit {
  my($self) = @_;

  $self->map_fields();

  my @required_fields = qw(action login password);

  if ( $self->{_content}->{action} eq 'ARBCreateSubscriptionRequest' ) {
    push @required_fields,
      qw( type interval start periods amount first_name last_name );

    if ($self->transaction_type() eq "ECHECK") {
      push @required_fields,
        qw( amount routing_code account_number account_type account_name
            check_type
          );
    } elsif ($self->transaction_type() eq 'CC' ) {
      push @required_fields, qw( card_number expiration );
    }
  }elsif ( $self->{_content}->{action} eq 'ARBUpdateSubscriptionRequest' ) {
    push @required_fields, qw( subscription );
  }elsif ( $self->{_content}->{action} eq 'ARBCancelSubscriptionRequest' ) {
    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 %merchant, 'Tie::IxHash',
    $self->revmap_fields(
                          name           => 'login',
                          transactionKey => 'password',
                        );

  my ($length,$unit) =
    ($self->{_content}->{interval} or '') =~ /^\s*(\d+)\s+(day|month)s?\s*$/;
  tie my %interval, 'Tie::IxHash', (
                                     ($length ? (length => $length)   : () ),
                                     ($unit   ? (unit   => $unit.'s') : () ),
                                   );

  tie my %schedule, 'Tie::IxHash',
    $self->revmap_fields(
                          interval         => \%interval,
                          startDate        => 'start',
                          totalOccurrences => 'periods',
                          trialOccurrences => 'trialperiods',
                        );

  tie my %account, 'Tie::IxHash', ( 
    ( defined($self->transaction_type())
      && $self->transaction_type() eq 'CC'
    ) ? $self->revmap_fields(
                              cardNumber     => 'card_number',
                              expirationDate => 'expdate_yyyymm',
                            )
      : $self->revmap_fields(
                              accountType    => 'account_type',
                              routingNumber  => 'routing_code',
                              accountNumber  => 'account_number',
                              nameOnAccount  => 'account_name',
                              echeckType     => 'check_type',
                              bankName       => 'bank_name',
                            )
  );

  tie my %payment, 'Tie::IxHash',
    $self->revmap_fields(
                           ( ( defined($self->transaction_type()) && # require?
                               $self->transaction_type() eq 'CC'
                             ) ?  'creditCard'
                               : 'bankAccount'
                           )  => \%account,
                         );

  tie my %order, 'Tie::IxHash',
    $self->revmap_fields(
                          invoiceNumber => 'invoice_number',
                          description   => 'description',
                        );

  tie my %drivers, 'Tie::IxHash',
    $self->revmap_fields(
                          number      => 'license_num',
                          state       => 'license_state',
                          dateOfBirth => 'license_dob',
                        );

  tie my %billto, 'Tie::IxHash',
    $self->revmap_fields(
                          firstName => 'first_name',
                          lastName  => 'last_name',
                          company   => 'company',
                          address   => 'address',
                          city      => 'city',
                          state     => 'state',
                          zip       => 'zip',
                          country   => 'country',
                        );

  tie my %shipto, 'Tie::IxHash',
    $self->revmap_fields(
                          firstName => 'ship_first_name',
                          lastName  => 'ship_last_name',
                          company   => 'ship_company',
                          address   => 'ship_address',
                          city      => 'ship_city',
                          state     => 'ship_state',
                          zip       => 'ship_zip',
                          country   => 'ship_country',
                        );

  tie my %customer, 'Tie::IxHash',
    $self->revmap_fields(
                          type           => 'customer_org',
                          id             => 'customer_id',
                          email          => 'email',
                          phoneNumber    => 'phone',
                          faxNumber      => 'fax',
                          driversLicense => \%drivers,
                          taxid          => 'customer_ssn',
                        );

  tie my %sub, 'Tie::IxHash',
    $self->revmap_fields(
                          name            => 'subscription_name',
                          paymentSchedule => \%schedule,
                          amount          => 'amount',
                          trialAmount     => 'trialamount',
                          payment         => \%payment,
                          order           => \%order,
                          customer        => \%customer,
                          billTo          => \%billto,
                          shipTo          => \%shipto,
                        );


  tie my %req, 'Tie::IxHash',
    $self->revmap_fields (
                           merchantAuthentication => \%merchant,
                           subscriptionId         => 'subscription',
                           subscription           => \%sub,
                         );

  my $ns = "AnetApi/xml/v1/schema/AnetApiSchema.xsd";
  my $post_data;
  my $writer = new XML::Writer( OUTPUT      => \$post_data,
                                DATA_MODE   => 1,
                                DATA_INDENT => 1,
                                ENCODING    => 'utf-8',
                              );
  $writer->xmlDecl();
  $writer->startTag($self->{_content}->{action}, 'xmlns', $ns);
  foreach ( keys ( %req ) ) {
    $self->_xmlwrite($writer, $_, $req{$_});
  }
  $writer->endTag($self->{_content}->{action});
  $writer->end();

  if ($self->test_transaction()) {
    $self->server('apitest.authorize.net');
  }

  warn $post_data if $DEBUG;
  my($page,$server_response,%headers) =
    $self->https_post( { 'Content-Type' => 'text/xml' }, $post_data);

  #trim leading (4?) characters of unknown origin not in spec
  $page =~ s/^(.*?)</</;
  my $garbage=$1;
  warn "Trimmed $garbage from response page.\n" if $DEBUG;

  warn $page if $DEBUG;

  my $response;
  my $message;
  if ($server_response =~ /200/){
    $response = XMLin($page);
    if (ref($response->{messages}->{message}) eq 'ARRAY') {
      $message = $response->{messages}->{message}->[0];
    }else{
      $message = $response->{messages}->{message};
    }
  }else{
    $response->{messages}->{resultCode} = "Server Failed";
    $message->{code} = $server_response;
  }

  $self->server_response($page);
  $self->order_number($response->{subscriptionId});
  $self->result_code($message->{code});
  $self->error_message($message->{text});

  if($response->{messages}->{resultCode} eq "Ok" ) {
      $self->is_success(1);
  } else {
      $self->is_success(0);
      unless ( $self->error_message() ) { #additional logging information
        $self->error_message(
          "(HTTPS response: $server_response) ".
          "(HTTPS headers: ".
            join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
          "(Raw HTTPS content: $page)"
        );
      }
  }
}

1;
__END__

=head1 NAME

Business::OnlinePayment::AuthorizeNet::ARB - AuthorizeNet ARB backend for Business::OnlinePayment

=head1 AUTHOR

Jeff Finucane, authorizenetarb@weasellips.com

=head1 SEE ALSO

perl(1). L<Business::OnlinePayment>.

=cut