package Business::OnlinePayment::vSecureProcessing;

use strict;
use vars qw($VERSION $DEBUG @ISA);
use Carp;
use XML::Writer;
use XML::Simple;
use Data::Dumper;
use Business::OnlinePayment;
use Business::OnlinePayment::HTTPS;

@ISA = qw(Business::OnlinePayment::HTTPS);
$DEBUG = 0;
$VERSION = '0.09';

sub _info {
  'info_compat'       => '0.01',
  'gateway_name'      => 'vSecure Processing',
  'gateway_url'       => 'http://www.vsecureprocessing.com/',
  'module_version'    => $VERSION,
  'supported_types'   => [qw( CC )],
  'token_support'     => 0,
  'test_transaction'  => 1,
  'partial_auth'      => 1,
  'supported_actions' => [
                           'Normal Authorization',
                           #'Authorization Only',
                           #'Post Authorization',
                           'Reverse Authorization',
                           'Void',
                           'Credit',
                         ],
}

# mapping out all possible endpoints
# but this version will only be building out "charge", "void", & "credit"
my %payment_actions = (
    'charge' => {
        path      => '/vsg2/processpayment',
        process   => 'ProcessPayment',
        fields    => [qw/
          Amount Trk1 Trk2 TypeOfSale Cf1 Cf2 Cf3 AccountNumber
          ExpirationMonth ExpirationYear Cvv
          CardHolderFirstName CardHolderLastName AvsZip AvsStreet
          IndustryType ApplicationId Recurring
        /],
    },
    'void' => {
        path      => '/vsg2/processvoid',
        process   => 'ProcessVoid',
        fields    => [qw(
          Amount Cf1 Cf2 Cf3 AccountNumber
          ExpirationMonth ExpirationYear ReferenceNumber
          TransactionDate IndustryType ApplicationId
        )],
    },
    'refund' => {
        path      => '/vsg2/processrefund',
        process   => 'ProcessRefund',
        fields    => [qw(
          Amount Cf1 Cf2 Cf3 AccountNumber
          ExpirationMonth ExpirationYear ApplicationId
        )],
    },
    'authorize' => {
        path      => '/vsg2/processauth',
    },
    'authorize_cancel' => {
        path      => '/vsg2/processauthcancel',
    },
    'capture' => {
        path      => '/vsg2/processcaptureonly',
    },
    'create_token' => {
        path      => '/vsg2/createtoken',
    },
    'delete_token' => {
        path      => '/vsg2/deletetoken',
    },
    'query_token' => {
        path      => '/vsg2/querytoken',
    },
    'update_exp_date' => {
        path      => '/vsg2/updateexpiration',
    },
    'update_token' => {
        path      => '/vsg2/updatetoken',
    },

);

my %action_mapping = (
    'normal authorization'  => 'charge',
    'credit'                => 'refund',
    'authorization only'    => 'authorize',
    'post authorization'    => 'capture',
    'reverse authorization' => 'authorize_cancel'
    # void => void
);

sub set_defaults {
    my $self = shift;
    my %options = @_;
    
    # inistialize standard B::OP attributes
    $self->is_success(0);
    $self->$_( '' ) for qw/authorization
                           result_code
                           error_message
                           server
                           path
                           server_response/;
                           
    # B::OP creates the following accessors:
    #     server, path, test_transaction, transaction_type,
    #     server_response, is_success, authorization,
    #     result_code, error_message,
    
    $self->build_subs(qw/
            platform tid appid
            action reference_number cvv2_response avs_code response_code
            risk_score txn_amount txn_date partial_auth_amount
    /);
    
    $DEBUG = exists($options{debug}) ? $options{debug} : $DEBUG;
    
    $self->server('svr1.vsecureprocessing.com');
    
    $self->tid($options{'tid'});
    
    $self->platform($options{'platform'});
    
    $self->appid($options{'appid'});
    
    $self->port(443);
    
}

sub clean_content {
    my ($self,$content) = @_;
    my %content = $self->content();
    
    {
        no warnings 'uninitialized';
        
        # strip non-digits from card number
        my $card_number = '';
        if ( $content{card_number} ) {
            $content{card_number} =~ s/\D//g;
        }
        
        if ($content{'description'} && length($content{'description'}) >20) {
            $content{'description'} = substr($content{'description'},0,20);
        }
        
        # separate month and year values for expiry_date
        if ( $content{expiration} ) {
            ($content{exp_month}, $content{exp_year}) =
              split /\//, $content{expiration};
            $content{exp_month} = sprintf "%02d", $content{exp_month};
            $content{exp_year}  = substr($content{exp_year},0,2)
              if ($content{exp_year} > 99);
        }
        
        if (    !$content{'first_name'}
             || !$content{'last_name'} && $content{'name'}
           )
        {
            ($content{'first_name'}, $content{'last_name'}) =
              split(' ', $content{'name'}, 2);
        }
        
        if ($content{'address'} =~ m/[\D ]*(\d+)\D/) {
            $content{'street_number'} = $1;
        }
    }
    warn "Content after cleaning:\n".Dumper(\%content)."\n" if ($DEBUG >2);
    $self->content(%content);
}

sub process_content {
    my $self = shift;
    $self->clean_content();
    my %content = $self->content();
    $self->action( ($action_mapping{lc $content{'action'}})
                     ? $action_mapping{lc $content{'action'}}
                     : lc $content{'action'}
                 );
    $self->path($payment_actions{ $self->action }{path})
      unless length($self->path);
    $self->appid($content{appid}) if (!$self->appid && $content{appid});
}

sub submit {
    my $self = shift;
    
    # inistialize standard B::OP attributes
    $self->is_success(0);
    $self->$_( '' ) for qw/authorization
                           result_code
                           error_message
                           server_response/;
                           
    # clean and process the $self->content info
    $self->process_content();
    my %content = $self->content;
    my $action = $self->action();

    if ( $self->test_transaction ) {
      $self->server('dvrotsos2.kattare.com');
    }
    
    my @acceptable_actions = ('charge', 'refund', 'void');
    
    unless ( grep { $action eq $_ } @acceptable_actions ) {
        croak "'$action' is not supported at this time.";
    }
    
    # fill in the xml vars
    my $xml_vars = {
        auth => {
            Platform    => $self->platform,
            UserId      => $content{'login'},
            GID         => $content{'password'},
            Tid         => $self->tid || '01',
        },
        
        payment => {
            Amount          => $content{'amount'},
            Trk1            => ($content{'track1'}) ? $content{'track1'} : '',
            Trk2            => ($content{'track2'}) ? $content{'track2'} : '',
            TypeOfSale      => ($content{'description'}) ? $content{'description'} : '',
            Cf1             => ($content{'invoice_number'}) ? $content{'invoice_number'} : '',
            Cf2             => ($content{'customer_id'}) ? $content{'customer_id'} : '',
            Cf3             => '',
            AccountNumber   => ($content{'card_number'}) ? $content{'card_number'} : '',
            ExpirationMonth => $content{'exp_month'},
            ExpirationYear  => $content{'exp_year'},
            Cvv             => ($content{'cvv'}) ? $content{'cvv'} : ($content{'cvv2'}) ? $content{'cvv2'} : '',
            CardHolderFirstName => ($content{'first_name'}) ? $content{'first_name'} : '',
            CardHolderLastName => ($content{'last_name'}) ? $content{'last_name'} : '',
            AvsZip          => ($content{'zip'}) ? $content{'zip'} : '',
            AvsStreet       => ($content{'street_number'}) ? $content{'street_number'} : '',
#            IndustryType    =>  { 
#                                IndType => ($content{'IndustryInfo'} && lc($content{'IndustryInfo'}) eq 'ecommerce') ? 'ecom_3' : '',
#                                IndInvoice => ($content{'invoice_number'}) ? $content{'invoice_number'} : ''
#                                },
            ApplicationId   => $self->appid(),
            Recurring       => ($content{'recurring_billing'} && $content{'recurring_billing'} eq 'YES' ) ? 1 : 0,
            ReferenceNumber => ($content{'ref_num'}) ? $content{'ref_num'} : '',
            Token           => ($content{'token'}) ? $content{'token'} : '',
            Receipt         => ($content{'receipt'}) ? $content{'receipt'} : '',
            TransactionDate => ($content{'txn_date'}) ? $content{'txn_date'} : ''
        }
        # we won't be using level2 nor level3.  So I'm leaving them out for now.
    };
  
    # create the list of required fields based on the action
    my @required_fields = qw/ Amount /;
    if ($action eq 'charge') {
        push @required_fields, $_
          foreach (qw/ AccountNumber ExpirationMonth ExpirationYear /);
    }elsif ($action eq 'void') {
        push @required_fields, $_
          foreach (qw/ ReferenceNumber /);
    }elsif ($action eq 'refund') {
        push @required_fields, $_
          foreach (qw/ Amount AccountNumber ExpirationMonth ExpirationYear /);
    }
    
    # check the requirements are met.
    my @missing_fields;
    foreach my $field (@required_fields) {
        push(@missing_fields, $field) if (!$xml_vars->{payment}{$field});
    }
    if (scalar(@missing_fields)) {
        croak "Missing required fields: ".join(', ', @missing_fields);
    }
    
    my $process_action = $action;
    $process_action =~ s/\b([a-z])/\u$1/g;
    $process_action = 'Process'.$process_action;
    my $xml_data;
    my $writer = new XML::Writer( OUTPUT      => \$xml_data,
                                  DATA_MODE   => 0,
                                  DATA_INDENT => 0,
                                  ENCODING    => 'utf-8',
                                );
    $writer->xmlDecl();
    $writer->startTag('Request');
    $writer->startTag('MerchantData');
    foreach my $key ( keys ( %{$xml_vars->{auth}} ) ) {
        $writer->dataElement( $key, $xml_vars->{auth}{$key} );
    }
    $writer->endTag('MerchantData');
    $writer->startTag($payment_actions{ $self->action }{process});
    foreach my $key ( @{$payment_actions{ $self->action }{fields}} ) {
        next if (!$xml_vars->{payment}{$key});
        if (ref $xml_vars->{payment}{$key} eq '') {
            $writer->dataElement( $key, $xml_vars->{payment}{$key}); 
        }else {
            $writer->startTag($key);
            foreach my $key2 (keys %{$xml_vars->{payment}{$key}}) {
              $writer->dataElement( $key2, $xml_vars->{payment}{$key}{$key2} ); 
            }
            $writer->endTag($key);
        }
    }
    $writer->endTag($payment_actions{ $self->action }{process});
    $writer->endTag('Request');
    $writer->end();
    
    warn "XML:\n$xml_data\n" if $DEBUG > 2;
    
    my $boundary = sprintf('FormBoundary%06d', int(rand(1000000)));
    # opts for B:OP:HTTPS::https_post
    my $opts = { headers => {}};
    $opts->{'Content-Type'} =
    $opts->{headers}->{'Content-Type'} =
        "multipart/form-data, boundary=$boundary";

    my $content =
      "--$boundary\n".
     "Content-Disposition: form-data; name=\"param\"\n\n".
     $xml_data."\n".
     "--$boundary--\n";

    # conform to RFC standards
    $content =~ s/\n/\r\n/gs;

    my ( $page, $server_response, %headers ) =
      $self->https_post( $opts, $content );
  
    # store the server response.
    $self->server_response($server_response);
    # parse the result page.
    $self->parse_response($page);

    if ( $self->is_success && $self->response_code == 10 ) { #partial auth

      if ( $content{'partial_auth'} ) {

        $self->partial_auth_amount( $self->txn_amount );

      } else {
 
        #XXX reverse auth if i was an auth only...
        my $void = new Business::OnlinePayment(
                         'vSecureProcessing',
                         map { $_ -> $self->$_() } qw( platform appid tid )
                   );

        $void->content(
          'action'           => 'Void',
          'amount'           => $self->txn_amount,
          'test_transaction' => $self->test_transaction,
          'authorization'    => $self->authorization,
          map { $_ => $content{$_} } qw( login password card_number expiration )
        );
      
        $void->submit;

        if ( !$void->is_success ) {
          #XXX now what??? at least this is better than return is_success 0 or 1
          die "Couldn't void partial auth";
        } else {
          $self->is_success(0);
        }

      }

    }
    
    if (!$self->is_success() && !$self->error_message() ) {
        if ( $DEBUG ) {
            #additional logging information, possibly too sensitive for an error
            $self->error_message(
              "(HTTPS response: ".$server_response.") ".
              "(HTTPS headers: ".
            join(", ", map { "$_ => ". $headers{$_} } keys %headers ). ") ".
              "(Raw HTTPS content: ".$page.")"
            );
        } else {
            my $response_code = $self->response_code() || '';
            if ($response_code) {
                $self->error_message(qq|Error code ${response_code} was returned by vSecureProcessing. (enable debugging for raw HTTPS response)|);
            }else {
                $self->error_message('No error information was returned by vSecureProcessing (enable debugging for raw HTTPS response)');
            }
        }
    }
    
}

# read $self->server_response and decipher any errors
sub parse_response {
    my $self = shift;
    my $page = shift;

    if ($self->server_response =~ /^200/) {
        my $response = XMLin($page);
        warn "Response:\n".Dumper($response)."\n" if $DEBUG > 2;
        $self->result_code($response->{Status}); # 0 /1
        $self->response_code($response->{ResponseCode}); # see documentation for translation
        $self->avs_code($response->{AvsResponse}); # Y / N

        #weird (missing?) gateway responses turn into a hashref screwing up Card Fortress
        $self->cvv2_response( $response->{CvvResponse} =~ /^\w$/
                                ? $response->{CvvResponse}
                                : ''
                            );

        $self->txn_date($response->{TransactionDate}); # MMDDhhmmss
        $self->txn_amount($response->{TransactionAmount} / 100); # 00000003500 / 100
        $self->reference_number($response->{ReferenceNumber});
        
        $self->is_success($self->result_code() eq '0' ? 1 : 0);
        if ($self->is_success()) {
            $self->authorization($response->{ReferenceNumber});
        } else { # fill in error_message if there is is an error
            $self->error_message( 'Error '.$response->{ResponseCode}.': '.
                                    (    $response->{AdditionalResponseData}
                                      || $response->{Receipt}
                                      || $response->{ResultCode}
                                    )
                                );
        }
        
    } else {
        die 'Error communicating with vSecureProcessing server (server sent response: '. $self->server_response. ')';
    }
    
}

1;
__END__


=head1 NAME

Business::OnlinePayment::vSecureProcessing - vSecureProcessing backend for Business::OnlinePayment

=head1 SYNOPSIS

  use Business::OnlinePayment;
  my %processor_info = (
    platform    => 'vsecure_platform',
    appid       => 'vsecure_appid',
    tid         => '54', #optional, defaults to 01
  );
  my $tx =
    new Business::OnlinePayment( "vSecureProcessing", %processor_info);
  $tx->content(
      login          => 'vsecure@user.id',
      password       => '12345678901234567890', #vsecure gid

      type           => 'CC',
      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";
  }

=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
  credit
  void

=head1 Setting vSecureProcessing parameters from content(%content)

The following rules are applied to map data to vSecureProcessing parameters
from content(%content):

      # param => $content{<key>}
      AccountNumber       => 'card_number',
      Cvv                 => 'cvv2',
      ExpirationMonth     => \( $month ), # MM from MM/YY of 'expiration'
      ExpirationYear      => \( $year ), # YY from MM/YY of 'expiration'
      Trk1                => 'track1',
      Trk2                => 'track2',
      CardHolderFirstName => 'first_name',
      CardHolderLastName  => 'last_name',
      Amount              => 'amount'
      AvsStreet           => 'address',
      AvsZip              => 'zip',
      Cf1                 => 'invoice_number',
      Cf2                 => 'customer_id',
      IndustryType        => 'IndustryInfo',

=head1 NOTE

=head1 COMPATIBILITY

Business::OnlinePayment::vSecureProcessing uses vSecureProcessing XML Document
Version: 140901 (September 1, 2014).

See http://www.vsecureprocessing.com/ for more information.

=head1 AUTHORS

Original author: Alex Brelsfoard

Current maintainer: Ivan Kohler <ivan-vsecureprocessing@freeside.biz>

=head1 COPYRIGHT

Copyright (c) 2015 Freeside Internet Services, Inc.

All rights reserved.

This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=head1 ADVERTISEMENT

Need a complete, open-source back-office and customer self-service solution?
The Freeside software includes support for credit card and electronic check
processing with vSecureProcessing and over 60 other gateways, invoicing,
integrated trouble ticketing, and customer signup and self-service web
interfaces.

http://freeside.biz/freeside/

=head1 SEE ALSO

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

=cut

