Business::OnlinePayment::Litle - Litle & Co. Backend for Business::OnlinePayment


Business-OnlinePayment-Litle documentation Contained in the Business-OnlinePayment-Litle distribution.

Index


Code Index:

NAME

Top

Business::OnlinePayment::Litle - Litle & Co. Backend for Business::OnlinePayment

VERSION

Top

Version 0.910

SYNOPSIS

Top

This is a plugin for the Business::OnlinePayment interface. Please refer to that docuementation for general usage, and here for Litle specific usage.

In order to use this module, you will need to have an account set up with Litle & Co. http://www.litle.com/



  use Business::OnlinePayment;
  my $tx = Business::OnlinePayment->new(
     "Litle",
     default_Origin => 'NEW',
  );

  $tx->content(
      type           => 'CC',
      login          => 'testdrive',
      password       => '123qwe',
      action         => 'Normal Authorization',
      description    => 'FOO*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
      invoice_number => '54123',
  );
  $tx->submit();

  if($tx->is_success()) {
      print "Card processed successfully: ".$tx->authorization."\n";
  } else {
      print "Card was rejected: ".$tx->error_message."\n";
  }

METHODS AND FUNCTIONS

Top

See Business::OnlinePayment for the complete list. The following methods either override the methods in Business::OnlinePayment or provide additional functions.

result_code

Returns the response error code.

error_message

Returns the response error description text.

server_response

Returns the complete response from the server.

Handling of content(%content) data:

Top

action

The following actions are valid

  normal authorization
  authorization only
  post authorization
  credit
  void

Litle specific data

Top

Fields

Mostdata fields nto part of the BOP standard can be added to the content hash directly, and will be used

Products

Part of the enhanced data for level III Interchange rates

    products        =>  [
    {   description =>  'First Product',
        sku         =>  'sku',
        quantity    =>  1,
        units       =>  'Months'
        amount      =>  500,  ## currently I don't reformat this, $5.00
        discount    =>  0,
        code        =>  1,
        cost        =>  500,
    },
    {   description =>  'Second Product',
        sku         =>  'sku',
        quantity    =>  1,
        units       =>  'Months',
        amount      =>  1500,
        discount    =>  0,
        code        =>  2,
        cost        =>  500,
    }

    ],

SPECS

Top

Currently uses the Litle XML specifications version 7.2

TESTING

Top

In order to run the provided test suite, you will first need to apply and get your account setup with Litle. Then you can use the test account information they give you to run the test suite. The scripts will look for three environment variables to connect: BOP_USERNAME, BOP_PASSWORD, BOP_MERCHANTID

Currently the description field also uses a fixed descriptor. This will possibly need to be changed based on your arrangements with Litle.

FUNCTIONS

Top

_info

Return the introspection hash for BOP 3.x

set_defaults

map_fields

add_item

Top

A new method, not supported under BOP yet, but interface to adding multiple entries, so we can write and interface with batches

$tx->add_item( \%content );

AUTHOR

Top

Jason Hall, <jayce at lug-nut.com>

UNIMPLEMENTED

Top

Certain features are not yet implemented (no current personal business need), though the capability of support is there, and the test data for the verification suite is there.

    Force Capture
    Capture Given Auth
    3DS
    billMeLater
    Credit against non-litle transaction

BUGS

Top

Please report any bugs or feature requests to bug-business-onlinepayment-litle at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Business-OnlinePayment-Litle. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

You may also add to the code via github, at http://github.com/Jayceh/Business--OnlinePayment--Litle.git

SUPPORT

Top

You can find documentation for this module with the perldoc command.

    perldoc Business::OnlinePayment::Litle




You can also look for information at:

http://www.litle.com/

* RT: CPAN's request tracker

http://rt.cpan.org/NoAuth/Bugs.html?Dist=Business-OnlinePayment-Litle

* AnnoCPAN: Annotated CPAN documentation

http://annocpan.org/dist/Business-OnlinePayment-Litle

* CPAN Ratings

http://cpanratings.perl.org/d/Business-OnlinePayment-Litle

* Search CPAN

http://search.cpan.org/dist/Business-OnlinePayment-Litle/

ACKNOWLEDGEMENTS

Top

Heavily based on Jeff Finucane's l<Business::OnlinePayment::IPPay> because it also required dynamically writing XML formatted docs to a gateway.

COPYRIGHT & LICENSE

Top

SEE ALSO

Top

perl(1). Business::OnlinePayment


Business-OnlinePayment-Litle documentation Contained in the Business-OnlinePayment-Litle distribution.
package Business::OnlinePayment::Litle;

use warnings;
use strict;

use Business::OnlinePayment;
use Business::OnlinePayment::HTTPS;
use Business::OnlinePayment::Litle::ErrorCodes '%ERRORS';
use vars qw(@ISA $me $DEBUG $VERSION);
use XML::Writer;
use XML::Simple;
use Tie::IxHash;
use Business::CreditCard qw(cardtype);
use Data::Dumper;
use IO::String;
use Carp qw(croak);

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

sub _info {
    return {
        info_compat       => '0.01',
        gateway_name      => 'Litle',
        gateway_url       => 'http://www.litle.com',
        module_version    => $VERSION,
        supported_types   => ['CC'],
        supported_actions => {
            CC => [
                'Normal Authorization',
                'Post Authorization',
                'Authorization Only',
                'Credit',
                'Void',
                'Auth Reversal',
            ],
        },
    };
}

sub set_defaults {
    my $self = shift;
    my %opts = @_;

    $self->server('payments.litle.com') unless $self->server;

    $self->port('443')                      unless $self->port;
    $self->path('/vap/communicator/online') unless $self->path;

    if ( $opts{debug} ) {
        $self->debug( $opts{debug} );
        delete $opts{debug};
    }

    ## load in the defaults
    my %_defaults = ();
    foreach my $key ( keys %opts ) {
        $key =~ /^default_(\w*)$/ or next;
        $_defaults{$1} = $opts{$key};
        delete $opts{$key};
    }

    $self->build_subs(
        qw( order_number md5 avs_code cvv2_response
          cavv_response api_version xmlns failure_status batch_api_version
          is_prepaid prepaid_balance get_affluence
          )
    );

    $self->api_version('8.1')                   unless $self->api_version;
    $self->batch_api_version('8.1')             unless $self->batch_api_version;
    $self->xmlns('http://www.litle.com/schema') unless $self->xmlns;
}

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

    my $action  = lc( $content->{'action'} );
    my %actions = (
        'normal authorization' => 'sale',
        'authorization only'   => 'authorization',
        'post authorization'   => 'capture',
        'void'                 => 'void',
        'credit'               => 'credit',
        'auth reversal'        => 'authReversal',
        'account update'       => 'accountUpdate',

        # AVS ONLY
        # Capture Given
        # Force Capture
        #
    );
    $content->{'TransactionType'} = $actions{$action} || $action;

    $content->{'company_phone'} =~ s/\D//g if $content->{'company_phone'};

    my $type_translate = {
        'VISA card'                   => 'VI',
        'MasterCard'                  => 'MC',
        'Discover card'               => 'DI',
        'American Express card'       => 'AX',
        'Diner\'s Club/Carte Blanche' => 'DI',
        'JCB'                         => 'DI',
        'China Union Pay'             => 'DI',
    };

    $content->{'card_type'} =
         $type_translate->{ cardtype( $content->{'card_number'} ) }
      || $content->{'type'};

    if (   $content->{recurring_billing}
        && $content->{recurring_billing} eq 'YES' )
    {
        $content->{'orderSource'} = 'recurring';
    }
    else {
        $content->{'orderSource'} = 'ecommerce';
    }
    $content->{'customerType'} =
      $content->{'orderSource'} eq 'recurring'
      ? 'Existing'
      : 'New';    # new/Existing

    $content->{'expiration'} =~ s/\D+//g;

    $content->{'deliverytype'} = 'SVC';

    # stuff it back into %content
    if ( $content->{'products'} && ref( $content->{'products'} ) eq 'ARRAY' ) {
        my $count = 1;
        foreach ( @{ $content->{'products'} } ) {
            $_->{'itemSequenceNumber'} = $count++;
        }
    }
    if ( $content->{'cvv2'} && length( $content->{'cvv2'} ) > 4 ) {
        croak "CVV2 has too many characters";
    }

    if( $content->{'velocity_check'} && (
        $content->{'velocity_check'} != 0 
        && $content->{'velocity_check'} !~ m/false/i ) ) {
      $content->{'velocity_check'} = 'true';
    } else {
      $content->{'velocity_check'} = 'false';
    }

    if( $content->{'partial_auth'} && (
        $content->{'partial_auth'} != 0 
        && $content->{'partial_auth'} !~ m/false/i ) ) {
      $content->{'partial_auth'} = 'true';
    } else {
      $content->{'partial_auth'} = 'false';
    }

    $self->content( %{$content} );
    return $content;
}

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

    $self->map_fields($content);

    my $action = $content->{'TransactionType'};

    my @required_fields = qw(action type);

    $self->required_fields(@required_fields);

    # for tabbing
    # clean up the amount to the required format
    my $amount;
    if ( defined( $content->{amount} ) ) {
        $amount = sprintf( "%.2f", $content->{amount} );
        $amount =~ s/\.//g;
    }
    
    #  put in a list of constraints
    my @validate = (
      [ 'city', 35 ],
      [ 'address', 35 ],
      [ 'state', 30 ],
      [ 'name', 100 ],
    );
    foreach my $trunc ( @validate ) {
      if( defined $content->{ $trunc->[0] } ) {
        $content->{ $trunc->[0] } = substr($content->{ $trunc->[0] } , 0, $trunc->[1] );
      }
    };

    tie my %billToAddress, 'Tie::IxHash', $self->revmap_fields(
        name         => 'name',
        email        => 'email',
        addressLine1 => 'address',
        city         => 'city',
        state        => 'state',
        zip          => 'zip',
        country      => 'country'
        , #TODO: will require validation to the spec, this field wont' work as is
        phone => 'phone',
    );

    tie my %shipToAddress, 'Tie::IxHash', $self->revmap_fields(
        name         => 'ship_name',
        email        => 'ship_email',
        addressLine1 => 'ship_address',
        city         => 'ship_city',
        state        => 'ship_state',
        zip          => 'ship_zip',
        country      => 'ship_country'
        , #TODO: will require validation to the spec, this field wont' work as is
        phone => 'ship_phone',
    );

    tie my %customerinfo, 'Tie::IxHash',
      $self->revmap_fields( customerType => 'customerType', );

    my $description =
      $content->{'description'}
      ? substr( $content->{'description'}, 0, 25 )
      : '';    # schema req
    $description =~ s/[^\w\s\*\,\-\'\#\&\.]//g;

    tie my %custombilling, 'Tie::IxHash',
      $self->revmap_fields(
        phone      => 'company_phone',
        descriptor => \$description,
      );

    ## loop through product list and generate linItemData for each
    #
    my @products = ();
    if( scalar( @{ $content->{'products'} } ) < 100 ){
      foreach my $prod ( @{ $content->{'products'} } ) {
          $prod->{'description'} = substr( $prod->{'description'}, 0, 25 );
          $prod->{'code'} = substr( $prod->{'code'}, 0, 12 );
          tie my %lineitem, 'Tie::IxHash',
            $self->revmap_fields(
              content              => $prod,
              itemSequenceNumber   => 'itemSequenceNumber',
              itemDescription      => 'description',
              productCode          => 'code',
              quantity             => 'quantity',
              unitOfMeasure        => 'units',
              taxAmount            => 'tax',
              lineItemTotal        => 'amount',
              lineItemTotalWithTax => 'totalwithtax',
              itemDiscountAmount   => 'discount',
              commodityCode        => 'code',
              unitCost             => 'cost',
            );
          push @products, \%lineitem;
      }
    }

    #
    #
    tie my %enhanceddata, 'Tie::IxHash', $self->revmap_fields(
        customerReference      => 'po_number',
        salesTax               => 'salestax',
        discountAmount         => 'discount',
        shippingAmount         => 'shipping',
        dutyAmount             => 'duty',
        invoiceReferenceNumber => 'invoice_number',    ##
        orderDate              => 'orderdate',
        lineItemData           => \@products,
    );

    tie my %card, 'Tie::IxHash', $self->revmap_fields(
        type               => 'card_type',
        number             => 'card_number',
        expDate            => 'expiration',
        cardValidationNum  => 'cvv2',
        cardAuthentication => '3ds',          # is this what we want to name it?
    );

    tie my %token, 'Tie::IxHash', $self->revmap_fields(
        litleToken         => 'token',
        expDate            => 'expiration',
        cardValidationNum  => 'cvv2',
    );

    tie my %processing, 'Tie::IxHash', $self->revmap_fields(
        bypassVelocityCheck   => 'velocity_check',
    );

    tie my %cardholderauth, 'Tie::IxHash',
      $self->revmap_fields(
        authenticationValue         => '3ds',
        authenticationTransactionId => 'visaverified',
        customerIpAddress           => 'ip',
        authenticatedByMerchant     => 'authenticated',
      );

    my %req;

    if ( $action eq 'sale' ) {
        tie %req, 'Tie::IxHash', $self->revmap_fields(
            orderId       => 'invoice_number',
            amount        => \$amount,
            orderSource   => 'orderSource',
            billToAddress => \%billToAddress,
            card          => \%card,
            token         => $content->{'token'} ? \%token : {},

            #cardholderAuthentication    =>  \%cardholderauth,
            customBilling => \%custombilling,
            enhancedData  => \%enhanceddata,
            processingInstructions  =>  \%processing,
            allowPartialAuth => 'partial_auth',
        );
    }
    elsif ( $action eq 'authorization' ) {
        tie %req, 'Tie::IxHash', $self->revmap_fields(
            orderId       => 'invoice_number',
            amount        => \$amount,
            orderSource   => 'orderSource',
            billToAddress => \%billToAddress,
            card          => \%card,
            token         => $content->{'token'} ? \%token : {},
            #cardholderAuthentication    =>  \%cardholderauth,
            processingInstructions  =>  \%processing,
            customBilling => \%custombilling,
            allowPartialAuth => 'partial_auth',
        );
    }
    elsif ( $action eq 'capture' ) {
        push @required_fields, qw( order_number amount );
        tie %req, 'Tie::IxHash',
          $self->revmap_fields(
            litleTxnId   => 'order_number',
            amount       => \$amount,
            enhancedData => \%enhanceddata,
            processingInstructions  =>  \%processing,
          );
    }
    elsif ( $action eq 'credit' ) {

       # IF there is a litleTxnId, it's a normal linked credit
       if( $content->{'order_number'} ){
          push @required_fields, qw( order_number amount );
          tie %req, 'Tie::IxHash', $self->revmap_fields(
              litleTxnId    => 'order_number',
              amount        => \$amount,
              customBilling => \%custombilling,
              processingInstructions  =>  \%processing,
          );
        }
       # ELSE it's an unlinked, which requires different data
       else {
          push @required_fields, qw( invoice_number amount );
          tie %req, 'Tie::IxHash', $self->revmap_fields(
              orderId       => 'invoice_number',
              amount        => \$amount,
              orderSource   => 'orderSource',
              billToAddress => \%billToAddress,
              card          => \%card,
              token         => $content->{'token'} ? \%token : {},
              customBilling => \%custombilling,
              processingInstructions  =>  \%processing,
          );
       }
    }
    elsif ( $action eq 'void' ) {
        push @required_fields, qw( order_number );
        tie %req, 'Tie::IxHash',
          $self->revmap_fields( 
            litleTxnId              => 'order_number', 
            processingInstructions  =>  \%processing,
          );
    }
    elsif ( $action eq 'authReversal' ) {
        push @required_fields, qw( order_number amount );
        tie %req, 'Tie::IxHash',
          $self->revmap_fields(
            litleTxnId => 'order_number',
            amount     => \$amount,
          );
    }
    elsif ( $action eq 'accountUpdate' ) {
        push @required_fields, qw( card_number expiration );
        tie %req, 'Tie::IxHash',
          $self->revmap_fields(
            orderId => 'customer_id',
            card    => \%card,
          );
    }

    $self->required_fields(@required_fields);
    return \%req;
}

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

    if ( $self->test_transaction() ) {
        $self->server('cert.litle.com');    ## alternate host for processing
    }
    $self->is_success(0);

    my %content = $self->content();
    my $req     = $self->map_request( \%content );
    my $post_data;

    my $writer = new XML::Writer(
        OUTPUT      => \$post_data,
        DATA_MODE   => 1,
        DATA_INDENT => 2,
        ENCODING    => 'utf8',
    );

    ## set the authentication data
    tie my %authentication, 'Tie::IxHash',
      $self->revmap_fields(
        user     => 'login',
        password => 'password',
      );

    warn Dumper($req) if $DEBUG;
    ## Start the XML Document, parent tag
    $writer->xmlDecl();
    $writer->startTag(
        "litleOnlineRequest",
        version    => $self->api_version,
        xmlns      => $self->xmlns,
        merchantId => $content{'merchantid'},
    );

    $self->_xmlwrite( $writer, 'authentication', \%authentication );

    ## partial capture modifier, odd location, because it modifies the start tag :(
    my %extra;
    if ($content{'TransactionType'} eq 'capture'){
        $extra{'partial'} = $content{'partial'} ? 'true' : 'false';
    }

    $writer->startTag(
        $content{'TransactionType'},
        id          => $content{'invoice_number'},
        reportGroup => $content{'report_group'} || 'BOP',
        customerId  => $content{'customer_id'} || 1,
        %extra,
    );
    foreach ( keys( %{$req} ) ) {
        $self->_xmlwrite( $writer, $_, $req->{$_} );
    }

    $writer->endTag( $content{'TransactionType'} );
    $writer->endTag("litleOnlineRequest");
    $writer->end();
    ## END XML Generation

    my ( $page, $server_response, %headers ) = $self->https_post($post_data);
    $self->{'_post_data'} = $post_data;
    warn $self->{'_post_data'} if $DEBUG;

    warn Dumper $page, $server_response, \%headers if $DEBUG;

    my $response = {};
    if ( $server_response =~ /^200/ ) {
        $response = XMLin($page);
        if ( exists( $response->{'response'} ) && $response->{'response'} == 1 )
        {
            ## parse error type error
            warn Dumper( $response, $self->{'_post_data'} );
            $self->error_message( $response->{'message'} );
            return;
        }
        else {
            $self->error_message(
                $response->{ $content{'TransactionType'} . 'Response' }
                  ->{'message'} );
        }
    }
    else {
        die "CONNECTION FAILURE: $server_response";
    }
    $self->{_response} = $response;

    warn Dumper($response) if $DEBUG;

    ## Set up the data:
    my $resp = $response->{ $content{'TransactionType'} . 'Response' };
    $self->{_response} = $resp;
    $self->order_number( $resp->{'litleTxnId'} || '' );
    $self->result_code( $resp->{'response'}    || '' );
    $resp->{'authCode'} =~ s/\D//g if $resp->{'authCode'};
    $self->authorization( $resp->{'authCode'} || '' );
    $self->cvv2_response( $resp->{'fraudResult'}->{'cardValidationResult'}
          || '' );
    $self->avs_code( $resp->{'fraudResult'}->{'avsResult'} || '' );
    if( $resp->{enhancedAuthResponse}
        && $resp->{enhancedAuthResponse}->{fundingSource} 
        && $resp->{enhancedAuthResponse}->{fundingSource}->{type} eq 'PREPAID' ) {

      $self->is_prepaid(1);
      $self->prepaid_balance( $resp->{enhancedAuthResponse}->{fundingSource}->{availableBalance} );
    } else {
      $self->is_prepaid(0);
    }

    #$self->is_dupe( $resp->{'duplicate'} ? 1 : 0 );

    if( $resp->{enhancedAuthResponse}
        && $resp->{enhancedAuthResponse}->{affluence} 
      ){
      $self->get_affluence( $resp->{enhancedAuthResponse}->{affluence} );
    }
    $self->is_success( $self->result_code() eq '000' ? 1 : 0 );
    if( $self->result_code() eq '010' ) {
      # Partial approval, if they chose that option
      $self->is_success(1);
    }

    ##Failure Status for 3.0 users
    if ( !$self->is_success ) {
        my $f_status =
            $ERRORS{ $self->result_code }->{'failure'}
          ? $ERRORS{ $self->result_code }->{'failure'}
          : 'decline';
        $self->failure_status($f_status);
    }

    unless ( $self->is_success() ) {
        unless ( $self->error_message() ) {
            $self->error_message( "(HTTPS response: $server_response) "
                  . "(HTTPS headers: "
                  . join( ", ", map { "$_ => " . $headers{$_} } keys %headers )
                  . ") "
                  . "(Raw HTTPS content: $page)" );
        }
    }

}

sub parse_batch_response {
    my ( $self, $args ) = @_;
    my @results;
    my $resp = $self->{'batch_response'};
    $self->order_number( $resp->{'litleBatchId'} );

    #$self->invoice_number( $resp->{'id'} );
    my @result_types =
      grep { $_ =~ m/Response$/ }
      keys %{$resp};    ## get a list of result types in this batch
    return {
        'account_update' => $self->get_update_response,
        ## do the other response types now
    };
}

sub add_item {
    my $self = shift;
    ## do we want to render it now, or later?
    push @{ $self->{'batch_entries'} }, shift;
}

sub create_batch {
    my ( $self, %opts ) = @_;
    if ( $self->test_transaction() ) {
        $self->server('cert.litle.com');    ## alternate host for processing
    }
    $self->is_success(0);

    if ( scalar( @{ $self->{'batch_entries'} } ) < 1 ) {
        $self->error('Cannot create an empty batch');
        return;
    }

    my $post_data;

    my $writer = new XML::Writer(
        OUTPUT      => \$post_data,
        DATA_MODE   => 1,
        DATA_INDENT => 2,
        ENCODING    => 'utf8',
    );
    ## set the authentication data
    tie my %authentication, 'Tie::IxHash',
      $self->revmap_fields(
        content  => \%opts,
        user     => 'login',
        password => 'password',
      );

    ## Start the XML Document, parent tag
    $writer->xmlDecl();
    $writer->startTag(
        "litleRequest",
        version => $self->batch_api_version,
        xmlns   => $self->xmlns,
        id      => $opts{'batch_id'} || time,
        numBatchRequests => 1,  #hardcoded for now, not doing multiple merchants
    );

    ## authentication
    $self->_xmlwrite( $writer, 'authentication', \%authentication );
    ## batch Request tag
    $writer->startTag(
        'batchRequest',
        id => $opts{'batch_id'} || time,
        numAccountUpdates => scalar( @{ $self->{'batch_entries'} } ),
        merchantId        => $opts{'merchantid'},
    );
    foreach my $entry ( @{ $self->{'batch_entries'} } ) {
        $self->content( %{$entry} );
        my %content = $self->content;
        my $req     = $self->map_request( \%content );
        $writer->startTag(
            $content{'TransactionType'},
            id          => $content{'invoice_number'},
            reportGroup => $content{'report_group'} || 'BOP',
            customerId  => $content{'customer_id'} || 1,
        );
        foreach ( keys( %{$req} ) ) {
            $self->_xmlwrite( $writer, $_, $req->{$_} );
        }
        $writer->endTag( $content{'TransactionType'} );
        ## need to also handle the action tag here, and custid info
    }
    $writer->endTag("batchRequest");
    $writer->endTag("litleRequest");
    $writer->end();
    ## END XML Generation

    #----- Send it
    if ( $opts{'method'} && $opts{'method'} eq 'sftp' ) {    #FTP
        require Net::SFTP::Foreign;
        my $sftp = Net::SFTP::Foreign->new(
            $self->server(),
            user     => $opts{'ftp_username'},
            password => $opts{'ftp_password'},
        );
        $sftp->error and die "SSH connection failed: " . $sftp->error;

        $sftp->setcwd("inbound")
          or die "Cannot change working directory ", $sftp->error;
        ## save the file out, can't put directly from var, and is multibyte, so issues from filehandle
        my $io = IO::String->new($post_data);
        tie *IO, 'IO::String';

        my $filename = $opts{'batch_id'} || $opts{'login'} . "_" . time;
        $sftp->put( $io, "$filename.prg" )
          or die "Cannot PUT $filename", $sftp->error;
        $sftp->rename( "$filename.prg",
            "$filename.asc" )    #once complete, you rename it, for pickup
          or die "Cannot RENAME file", $sftp->message;
        $self->is_success(1);
    }
    elsif ( $opts{'method'} && $opts{'method'} eq 'https' ) {    #https post
        $self->port('15000');
        $self->path('/');
        if ( $self->test_transaction() ) {
            $self->server('cert.litle.com');    ## alternate host for processing
        }
        my ( $page, $server_response, %headers ) =
          $self->https_post($post_data);
        $self->{'_post_data'} = $post_data;
        warn $self->{'_post_data'} if $DEBUG;

        warn Dumper [ $page, $server_response, \%headers ] if $DEBUG;

        my $response = {};
        if ( $server_response =~ /^200/ ) {
            $response = XMLin($page);
            if ( exists( $response->{'response'} )
                && $response->{'response'} == 1 )
            {
                ## parse error type error
                warn Dumper( $response, $self->{'_post_data'} );
                $self->error_message( $response->{'message'} );
                return;
            }
            else {
                $self->error_message(
                    $response->{'batchResponse'}->{'message'} );
            }
        }
        else {
            die "CONNECTION FAILURE: $server_response";
        }
        $self->{_response} = $response;

        ##parse out the batch info as our general status
        my $resp = $response->{'batchResponse'};
        $self->order_number( $resp->{'litleSessionId'} );
        $self->result_code( $response->{'response'} );
        $self->is_success( $response->{'response'} eq '0' ? 1 : 0 );

        warn Dumper($response) if $DEBUG;
        unless ( $self->is_success() ) {
            unless ( $self->error_message() ) {
                $self->error_message(
                        "(HTTPS response: $server_response) " 
                      . "(HTTPS headers: "
                      . join( ", ",
                        map { "$_ => " . $headers{$_} } keys %headers )
                      . ") "
                      . "(Raw HTTPS content: $page)"
                );
            }
        }
        if ( $self->is_success() ) {
            $self->{'batch_response'} = $resp;
        }
    }

}

sub send_rfr {
    my ( $self, $args ) = @_;
    my $post_data;

    $self->is_success(0);
    my $writer = new XML::Writer(
        OUTPUT      => \$post_data,
        DATA_MODE   => 1,
        DATA_INDENT => 2,
        ENCODING    => 'utf8',
    );
    ## set the authentication data
    tie my %authentication, 'Tie::IxHash',
      $self->revmap_fields(
        content  => $args,
        user     => 'login',
        password => 'password',
      );

    ## Start the XML Document, parent tag
    $writer->xmlDecl();
    $writer->startTag(
        "litleRequest",
        version          => $self->batch_api_version,
        xmlns            => $self->xmlns,
        numBatchRequests => 0,
    );

    ## authentication
    $self->_xmlwrite( $writer, 'authentication', \%authentication );
    ## batch Request tag
    $writer->startTag('RFRRequest');
    $writer->startTag('accountUpdateFileRequestData');
    $writer->startTag('merchantId');
    $writer->characters( $args->{'merchantid'} );
    $writer->endTag('merchantId');
    $writer->startTag('postDay');
    $writer->characters( $args->{'date'} );
    $writer->endTag('postDay');
    $writer->endTag('accountUpdateFileRequestData');
    $writer->endTag("RFRRequest");
    $writer->endTag("litleRequest");
    $writer->end();
    ## END XML Generation
    #
    $self->port('15000');
    $self->path('/');
    if ( $self->test_transaction() ) {
        $self->server('cert.litle.com');    ## alternate host for processing
    }
    my ( $page, $server_response, %headers ) = $self->https_post($post_data);
    $self->{'_post_data'} = $post_data;
    warn $self->{'_post_data'} if $DEBUG;

    warn Dumper [ $page, $server_response, \%headers ] if $DEBUG;

    my $response = {};
    if ( $server_response =~ /^200/ ) {
        $response = XMLin($page);
        if ( exists( $response->{'response'} ) && $response->{'response'} == 1 )
        {
            ## parse error type error
            warn Dumper( $response, $self->{'_post_data'} );
            $self->error_message( $response->{'message'} );
            return;
        }
        else {
            $self->error_message( $response->{'RFRResponse'}->{'message'} );
        }
    }
    else {
        die "CONNECTION FAILURE: $server_response";
    }
    $self->{_response} = $response;
    if ( $response->{'RFRResponse'} ) {
        ## litle returns an 'error' if the file is not done.  So it's not ready yet.
        $self->result_code( $response->{'RFRResponse'}->{'response'} );
        return;
    }
    else {

      #if processed, it returns as a batch, so, success, and let get the details
        my $resp = $response->{'batchResponse'};
        $self->is_success( $resp->{'response'} eq '000' ? 1 : 0 );
        $self->{'batch_response'} = $resp;
        $self->parse_batch_response;
    }
}

sub retrieve_batch {
    my ( $self, %opts ) = @_;
    croak "Missing filename" if !$opts{'batch_id'};
    my $post_data;
    if ( $opts{'batch_return'} ) {
        ## passed in data structure
        $post_data = $opts{'batch_return'};
    }
    else {
        ## go download a batch
        require Net::SFTP::Foreign;
        my $sftp = Net::SFTP::Foreign->new(
            $self->server(),
            user     => $opts{'ftp_username'},
            password => $opts{'ftp_password'},
        );
        $sftp->error and die "SSH connection failed: " . $sftp->error;

        $sftp->setcwd("outbound")
          or die "Cannot change working directory ", $sftp->error;
        ## save the file out, can't put directly from var, and is multibyte, so issues from filehandle
        my $io = IO::String->new($post_data);
        tie *IO, 'IO::String';

        my $filename = $opts{'batch_id'};
        $sftp->get( "$filename.asc", $io )
          or die "Cannot GET $filename", $sftp->error;
        $self->is_success(1);
        warn $post_data if $DEBUG;
    }

    my $response = {};
    $response = XMLin($post_data);
    if ( exists( $response->{'response'} ) && $response->{'response'} == 1 ) {
        ## parse error type error
        warn Dumper( $response, $self->{'_post_data'} );
        $self->error_message( $response->{'message'} );
        return;
    }
    else {
        $self->error_message( $response->{'batchResponse'}->{'message'} );
    }

    $self->{_response} = $response;
    my $resp = $response->{'batchResponse'};
    $self->order_number( $resp->{'litleSessionId'} );
    $self->result_code( $response->{'response'} );
    $self->is_success( $response->{'response'} eq '0' ? 1 : 0 );
    if ( $self->is_success() ) {
        $self->{'batch_response'} = $resp;
        return $self->parse_batch_response;
    }
}

sub get_update_response {
    my $self = shift;
    require Business::OnlinePayment::Litle::UpdaterResponse;
    my @response;
    foreach
      my $item ( @{ $self->{'batch_response'}->{'accountUpdateResponse'} } )
    {
        push @response,
          Business::OnlinePayment::Litle::UpdaterResponse->new( $item );
    }
    return \@response;
}

sub revmap_fields {
    my $self = shift;
    tie my (%map), 'Tie::IxHash', @_;
    my %content;
    if ( $map{'content'} && ref( $map{'content'} ) eq 'HASH' ) {
        %content = %{ delete( $map{'content'} ) };
    }
    else {
        %content = $self->content();
    }

    map {
        my $value;
        if ( ref( $map{$_} ) eq 'HASH' ) {
            $value = $map{$_} if ( keys %{ $map{$_} } );
        }
        elsif ( ref( $map{$_} ) eq 'ARRAY' ) {
            $value = $map{$_};
        }
        elsif ( ref( $map{$_} ) ) {
            $value = ${ $map{$_} };
        }
        elsif ( exists( $content{ $map{$_} } ) ) {
            $value = $content{ $map{$_} };
        }

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

sub _xmlwrite {
    my ( $self, $writer, $item, $value ) = @_;
    if ( ref($value) eq 'HASH' ) {
        my $attr = $value->{'attr'} ? $value->{'attr'} : {};
        $writer->startTag( $item, %{$attr} );
        foreach ( keys(%$value) ) {
            next if $_ eq 'attr';
            $self->_xmlwrite( $writer, $_, $value->{$_} );
        }
        $writer->endTag($item);
    }
    elsif ( ref($value) eq 'ARRAY' ) {
        foreach ( @{$value} ) {
            $self->_xmlwrite( $writer, $item, $_ );
        }
    }
    else {
        $writer->startTag($item);
        $writer->characters($value);
        $writer->endTag($item);
    }
}

1;    # End of Business::OnlinePayment::Litle