package Business::OnlinePayment::MerchantCommerce;

use strict;
use Carp;
use Net::SSLeay qw/make_form post_https/;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

require Exporter;

@ISA = qw(Exporter AutoLoader Business::OnlinePayment);
@EXPORT = qw();
@EXPORT_OK = qw();
$VERSION = 0.01;

sub set_defaults {
    my ($self) = @_;
    $self->server('trans.atsbank.com');
    $self->port('443');
    $self->path('/cgi-bin/ats.cgi');
}

sub _action {
  my ($self, $action) = @_;

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

  $content{_action} = $content{action};
  delete $content{action};

  for
  (
  'normal authorization','authorization only',
  'post authorization','ns_quicksale_cc',
  'visa','mastercard','american express','discover'
  )
  {
    $content{action} = 'ns_quicksale_cc'
      if $content{_action} eq $_
      or $content{type}    eq $_;
  }

  unless ( $content{action} ) {
    for ('check','ns_quicksale_check') {
      $content{action} = 'ns_quicksale_check'
        if $content{_action} eq $_
        or $content{type}    eq $_;
    }
  }                          
 
  if ( $content{action} eq 'ns_quicksale_cc' ) {
    if ( ! $content{_action} ) {
      if ( $content{type} =~ /^(post authorization|authorization only)$/ ) {
        $content{_action} = $content{type};
      }
      else {
        $content{_action} = 'normal authorization';
      }
    }
  }
  elsif ( $content{action} eq 'ns_quicksale_check' ) {
    $content{_action} = 'check';
  }

  Carp::croak('Specified action or type not supported by Merchant Commerce')
    unless $content{action};

  $self->content(%content);
}

sub _map {
  my ($self) = @_;
  my %content = $self->content;

  my %map = (name           => $content{action} eq 'ns_quicksale_cc' ? 'ccname' : 'ckname',
             login          => 'acctid',
             description    => 'ci_memo',
             address        => 'ci_billaddr1',
             city           => 'ci_billcity',
             state          => 'ci_billstate',
             zip            => 'ci_billzip',
             country        => 'ci_billcountry',
             phone          => 'ci_phone',
             email          => 'ci_email',
             card_number    => 'ccnum',
             account_number => 'ckacct',
             routing_code   => 'ckaba');
             
  while ( my($base_identifier, $mc_identifier) = each (%map) ) {
    if (defined $content{$base_identifier}) {
      $content{$mc_identifier} = $content{$base_identifier};
    }
  }
  
  $self->content(%content);
}



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

  Carp::croak('require_avs function not supported by Merchant Commerce')
    if $self->require_avs;
  
  my %content = $self->content;
  my $test    = $self->test_transaction;
  
  if ( $test ) {
    $content{acctid} = 'TEST0';
  }

  if ( $content{action} eq 'ns_quicksale_cc' ) {
    ($content{expmon}, $content{expyear}) = $self->_date(\%content);
    if ( $test ) {
      $self->required_fields(qw~ccname~);
      $content{'ccnum'} = '5454545454545454';
    }
    else {
      $self->required_fields(qw~ccname ccnum~);
    }
  }
  
  if ( $content{_action} eq 'authorization only' ) {
    $self->build_subs('authrefcode');
    $content{authonly} = 1;
  }
  elsif ( $content{_action} eq 'post authorization' ) {
    $self->build_subs('authrefcode');
    $self->required_fields(qw~authrefcode~);
  }
  elsif ( $content{_action} eq 'check' ) {
    $self->required_fields(qw~ckname ckacct ckaba~);
  }
  
  $self->content(%content);
  $self->required_fields(qw~acctid amount~);
}

sub _date {
  my ($self, $content) = @_;

  if ( !($content->{expmon} and $content->{expyear}) and $content->{exp_date} ) {
    ($content->{expmon}, $content->{expyear}) = split /\//, $content->{exp_date};
  }
  Carp::croak('Merchant Commerce requires exp_date(mm/yyyy) or expmon(mm) and expyear(yyyy)')
    unless $content->{expmon} and $content->{expyear};

  $content->{expmon}, $content->{expyear};
}

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

  $self->_action;
  $self->_map;
  $self->_required;
  
  my %post_data = $self->get_fields(qw~ckname ckacct ckaba
                                       ccname ccnum expmon expyear authonly authrefcode
                                       acctid action amount subid usepost
                                       ci_companyname ci_phone ci_email ci_memo ci_dlnum ci_ssnum
                                       ci_billaddr1 ci_billaddr2 ci_billcity ci_billstate ci_billzip ci_billcountry
                                       ci_shipaddr1 ci_shipaddr2 ci_shipcity ci_shipstate ci_shipzip ci_shipcountry
                                       emailto emailfrom emailsubject emailtext
                                       recur_create recur_billingcycle recur_billingmax recur_start recur_amount~);

  while (my ($key,$value) = each (%post_data)) {
    delete $post_data{$key} unless defined $post_data{$key};
  }
  
  my $query  = make_form(%post_data);
  my $server = $self->server;
  my $port   = $self->port;
  my $path   = $self->path;

  $self->build_subs(qw/response_headers server_status accepted 
                       historyid orderid refcode unknown_result_code
                       unknown_response_format unknown_methods
                       declined error post_data conversation
                       server_response/);

  my ($content,$server_status,%headers) = post_https($server,$port,$path,'',$query);

  $self->post_data(\%post_data);
  $self->server_status($server_status);
  $self->server_response($content);
  $self->response_headers(\%headers);

  my @post_data       = map { "$_ => $post_data{$_}" } keys %post_data;
  my @headers         = map { "$_: $headers{$_}" }     keys %headers;
  my $conversation    = join "\n", @post_data,
                                   $server_status,
                                   @headers,
                                   $content;
  $self->conversation($conversation);  
  my $response_format = $content =~ s/^<html><body><plaintext>//;
  chomp $content;
  
  if ( $response_format ) {

    my @unknown_methods = ();
    for ( split /\n/, $content ) {
      my ($meth, $val) = split /=/, lc $_;
      unless ( $self->can($meth) )  {
        $self->build_subs($meth);
        push @unknown_methods, $meth;  
      }
      $self->$meth($val);
    }
    $self->unknown_functions(\@unknown_methods)
        if defined $unknown_methods[0];
        
    $self->result_code($self->accepted || $self->declined || $self->error);

    if ( $self->accepted ) {
      $self->is_success(1);
      $self->authorization($self->accepted);
    }
    elsif ( $self->declined || $self->error ) {
      $self->error_message($self->declined || $self->error);
    }
    else {
      $self->unknown_result_code(1);
      $self->error_message($conversation);
    };
    
  }
  else {
    $self->unknown_response_format(1);
    $self->error_message($conversation);
  }
}

1;

__END__

=head1 NAME

Business::OnlinePayment::MerchantCommerce

=head1 SYNOPSIS

  use Business::OnlinePayment;

  my $transaction = new Business::OnlinePayment("MerchantCommerce");
  $transaction->content(type        => 'VISA',
                        login       => '12345',
                        action      => 'Normal Authorization',
                        amount      => '5.00',
                        name        => 'Tim McIntyre',
                        card_number => '5454545454545454',
                        exp_date    => '12/2005');
  $transacton->submit;

  if($transaction->is_success) {
      print "Success! Authorization Code: " . $transaction->authorization."\n";
  } else {
      print "Failure!: " . $transaction->error_message."\n";
  }

=head1 DESCRIPTION

Merchant Commerce backend for Business::OnlinePayment.  Please see Buisness::OnlinePayment for details.

=head2 Supported transaction types

Visa, MasterCard, American Express, Discover, Check

=head2 Required content for all transactions

=over 4

=item * login or acctid

These keys are synonamous

=item * name

Alternatively you may use ccname for credit cards or ckname for checking withhdrawls.

=item * action or type

One of authorization only, post authorization, check, ns_quicksale_check, normal authorization, ns_quicksale_cc, visa, mastercard, american express, or discover.  

If set to authorization only MerchantCommerce will create a method(authrefcode) that will return an authrefcode for use in a post authorization.
 
If set to post authorization then authrefcode becomes a required field. 

Use check or ns_quicksale_check for checking withdrawls.  

The last six possibilities, normal authorization, ns_quicksale_cc, visa, mastercard, american express, and discover are all synonymous as far as MerchantCommerce is concerned.  

If for some reason type and action are both set action will take precidence.

=item * amount

=back

=head2 Additionally required content for credit cards

=over 4

=item * card_number or ccnum:

These keys are synonamous

=item * exp_date(mm/yyyy) or expmon(mm) and expyear(yyyy)

=back

=head2 Optional content for credit cards

=over 4

=item * authrefcode

A reference code returned from a previous authorization only action.  Only used with an action of post authorization.

=back

=head2 Additionally required content for checks

=over 4

=item * type or action

Must be set to check, or ns_quicksale_check.  

=item * account_number or ckacct

=item * routing_code or ckaba

=back

=head2 Optional content for all transactons

=over 4

=item * address or ci_billaddr1

=item * ci_billaddr2

=item * city or ci_billcity

=item * state or ci_billstate

=item * zip or ci_billzip

=item * country or ci_billcountry

=item * description or ci_memo

=item * subid

=item * usepost

=item * ci_companyname

=item * ci_phone

=item * ci_email

=item * ci_dlnum

=item * ci_ssnum 

=item * ci_shipaddr1

=item * ci_shipaddr2

=item * ci_shipcity

=item * ci_shipstate

=item * ci_shipzip

=item * ci_shipcountry

=item * emailto

=item * emailfrom

=item * emailsubject

=item * emailtext

=item * recur_create

=item * recur_billingcycle

=item * recur_billingmax

=item * recur_start

=item * recur_amount

=back

=head2 Content not supported

=over 4

=item * password

=item * bank_name

=item * invoice_number

=item * customer_id

=item * require_avs

=item * fax

=item * action => credit

=back

=head2 Methods

=over 4

=item * orderid

Can be called on any successful authorization.

=item * authrefcode

Can be called on successful authorization only.  Returns a reference code to be used in a later post authorization.

=item * historyid

Can be called on success or failure.  Usually but not always returned for unknown reasons.  Seems to be of dubious usefullness but is provided just in case.

=item * conversation

Returns a string showing the (almost) entire transaction.  The first lines are the content that was posted in the traditional hash name => value format.  After that is the server status line hopefully HTTP/1.1 200 OK.  Next come the response headers in their traditional NAME: value format.  Last but certainly not least is content of the response in the format of name=value.  The first line of the content part of the string should also contain the string <html><body><plaintext>.

=item * post_data

Returns a reference to a hash of the data that was posted to the server.

=item * server_response

Returns a string showing the content of the response from the server.  These will be in the form of name=value deliminated by \n.

=item * response_headers

Returns a reference to a hash containing the response headers.

=item * server_status

Returns the server status from the server response.

=item * accepted

On a successful transaction this method returns the same value as the authorization method.  This would also mean that is_success is true.

=item * declined

On a failed transaction this method will return the same value as error_message. This would also mean that is_success is false.

=item * error

On a failed transaction this method will return the same value as error_message. This would also mean that is_success is false.

=back

=head2 Please Note

If any of the following three methods are ever set it essentially means that you have found a bug in this module.  If you should decide to use them and you find that they are set please email me the string returned from the conversation method so that I can determine the problem.  Better yet fix it and send me that.  If you decide not to use them you should be fine.

=over 4

=item * unknown_result_code

Merchant Commerce has three known possible result codes, accepted, declined, and error.  If for some reason a different result code is returned this method will be set to true and error_message will contain the same value as the conversation method described above.  The result code should be contained on the first line of the content of the response.  That is the one containing <html><body><plaintext>.

=item * unknown_response_format
       
If the content part of the server response does not contain the <html><body><plaintext> as part of its first line this method will be set to true and error_message will contain the same value as the conversation method described above.

=item * unknown_methods

The Merchant Commerce server returns the content of its response as name=value pairs. This MerchantCommerce module parses the content and creates a method named name and sets it's value to value.  The methods orderid, refcode, and historyid are known content responses.  This method returns a reference to an array of method names that are unknown.  You could then call these methods to get the corresponding value.

=back

=head1 AUTHOR

Tim McIntyre, tmac@transport.com

=head1 DISCLAIMER

THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

=head1 SEE ALSO

perl(1) L<Business::OnlinePayment>

=cut