Business-OnlinePayment-Ogone

 view release on metacpan or  search on metacpan

lib/Business/OnlinePayment/Ogone.pm  view on Meta::CPAN

use MIME::Base64;

# ABSTRACT: Online payment processing via Ogone
our $VERSION = 0.2;
our $API_VERSION = 4.9;

# Ogone config defaults and info ######################################################################################

our %defaults = (
    server => 'secure.ogone.com',
    port => 443,
);

our %info = (
    'info_compat'           => '0.01', # always 0.01 for now,
    'gateway_name'          => 'Ogone',
    'gateway_url'           => 'http://www.ogone.com/',
    'module_version'        => $VERSION,
    'supported_types'       => [ qw( CC ) ],
    'token_support'         => 0, #card storage/tokenization support
    'test_transaction'      => 1, #set true if ->test_transaction(1) works
    'supported_actions'     => [
        'Authorization Only',
        'Post Authorization',
        'Query',
        'Credit',
    ]
);

# Methods #############################################################################################################

sub _info { 
    return \%info;
}

sub set_defaults {
    my $self = shift;
    my %data = @_;
    $self->{$_} = $defaults{$_} for keys %defaults;

    $self->build_subs(qw/http_args result_xml/);
}


sub submit {
    my $self = shift;
    my %data = $self->content();

    # do not allow submitting same object twice
    croak 'submitting same object twice is not allowed' if $self->{_dirty}; $self->{_dirty} = 1;

    # Turn the data into a format usable by the online processor
    croak 'no action parameter defined in content' unless exists $self->{_content}->{action};

    # Default currency to Euro
    $self->{_content}->{currency} ||= 'EUR';

    # Table to translate from Business::OnlinePayment::Ogone args to Ogone API args
    # The values of this hash are also used as a list of allowed args for the HTTP POST request, thus preventing information leakage
    my %ogone_api_args = (
        # credentials
        login   => 'USERID',
        password => 'PSWD',
        PSPID   => 'PSPID',
        
        # primary identifier
        invoice_number => 'orderID',
        
        # transaction identifiers (action = query)
        payid => 'PAYID',
        payidsub => 'PAYIDSUB',
        
        # credit card data
        card_number => 'CARDNO', 
        cvc => 'CVC',
        expiration => 'ED',
        alias => 'ALIAS',
        
        # financial data
        currency => 'Currency',
        amount => 'amount',
        
        # Ogone specific arguments
        operation => 'Operation',       # REN, DEL, DES, SAL, SAS, RFD, RFS
        eci => 'ECI',                   # defaults 7: e-commerce with ssl (9: recurring e-commerce)
        accepturl => 'accepturl',
        declineurl => 'declineurl',
        exceptionurl => 'exceptionurl',
        paramplus => 'paramplus',
        complus => 'complus',
        language => 'LANGUAGE',

        # Business::OnlinePayment common
        description => 'COM',
        name => 'CN',
        email => 'EMAIL',
        address => 'Owneraddress',
        zip => 'OwnerZip',
        city => 'ownertown',
        country => 'ownercty',
        phone => 'ownertelno',

        # client authentication (not used directly, only here as valid HTTP POST arg)
        SHASign => 'SHASign',           # see sha_key, sha_type
        
        # 3d secure arguments
        flag3d => 'FLAG3D',
        win3ds => 'win3ds',
        http_accept => 'HTTP_ACCEPT',
        http_user_agent => 'HTTP_USER_AGENT',

        # recurrent fields
        subscription_id => 'SUBSCRIPTION_ID',
        subscription_orderid => 'SUB_ORDERID',
        subscription_status => 'SUBSCRIPTION_STATUS',
        startdate => 'SUB_STARTDATE',
        enddate => 'SUB_ENDDATE',
        status => 'SUB_STATUS',
        period_unit => 'SUB_PERIOD_UNIT', # 'd', 'ww', 'm' (yes two 'w's) for resp daily weekly monthly
        period_moment => 'SUB_PERIOD_MOMENT', # Integer, the moment in time on which the payment is (0-7 when period_unit is ww, 1-31 for d, 1-12 for m?)
        period_number => 'SUB_PERIOD_NUMBER', 

lib/Business/OnlinePayment/Ogone.pm  view on Meta::CPAN

        $http_req_args{SUB_AMOUNT} = $http_req_args{amount};
        $http_req_args{SUB_ORDERID} = $http_req_args{orderID};
    }

    # PSPID might be entered in lowercase (as per old documentation)
    $http_req_args{PSPID} = $self->{_content}{pspid} if defined $self->{_content}{pspid};

    # Calculate sha1 by default, but has to be enabled in the Ogone backend to have any effect
    my ($sha_type)  = ($self->{_content}->{sha_type} =~ m/^(1|256|512)$/);

    # Create a reference to the correct sha${sha_type}_hex function, default to SHA-1
    my $sha_hex = sub { my $type = shift; no strict; &{"sha".($type || 1)."_hex"}(@_); use strict; };

    # Algo: make a list of "KEY=value$passphrase" sort alphabetically
    my $signature = join('',
                         sort map { uc($_) . "=" . $http_req_args{$_} . ($self->{_content}{sha_key} || '') }
                         keys %http_req_args);

    $http_req_args{SHASign} = $sha_hex->($sha_type,$signature);

    # Construct the URL to query, taking into account the action and test_transaction values
    my %action_file = (
        qr/normal authorization/i  => 'orderdirect.asp',
        qr/authorization only/i    => 'orderdirect.asp',
        qr/recurrent authorization/i    => 'orderdirect.asp',
        qr/post authorization/i    => 'maintenancedirect.asp',
        qr/query/i                 => 'querydirect.asp',
    );

    my $uri_dir  = $self->test_transaction() ? 'test' : 'prod';
    my ($uri_file) =  map { $action_file{$_} }
                      grep { $self->{_content}->{action} =~ $_ }
                      keys %action_file;

    croak 'unable to determine URI path, is the action parameter one of ( authorization only | normal authorization | post authorization | query | recueent authorization)' unless $uri_file;
    
    # Construct the path to be used in https_post
    $self->{path} = '/ncol/'.$uri_dir.'/'.$uri_file;

    # Save the http args for later inspection
    $self->http_args(\%http_req_args);

    # Submit the transaction to the processor and collect a response.
    my ($page, $response_code, %reply_headers) = $self->https_post(%http_req_args);

    # Call server_response() with a copy of the entire unprocessed response
    $self->server_response([$response_code, \%reply_headers, $page]);

    my $xml = XMLin($page, ForceArray => [], KeyAttr => [] );

    # Store the result xml for later inspection
    $self->result_xml($xml);

    croak 'Ogone refused SHA digest' if $xml->{NCERRORPLUS} =~ m#^unknown order/1/s#;

    # Call is_success() with either a true or false value, indicating if the transaction was successful or not.
    if ( $response_code =~ m/^200/ ) {
        $self->is_success(0); # defaults to fail


        # croak 'incorrect credentials. WARNING: continuing with bad credentials will block your account s: '.$xml->{STATUS}.'{}'.$xml->{NCERROR} if $xml->{NCERROR} eq '50001119';

        if ( $xml->{STATUS} == 46 ) { $self->is_success(1) } # identification required
        if ( $xml->{STATUS} == 5  ) { $self->is_success(1) } # authorization accepted
        if ( $xml->{STATUS} == 9  ) { $self->is_success(1) } # payment accepted
        if ( $xml->{STATUS} == 91 ) { $self->is_success(1) } # partial payment accepted
        if ( $xml->{STATUS} == 61 ) { $self->is_success(1) } # Author. deletion waiting
        if ( $xml->{STATUS} == 2 ) { $self->failure_status('refused') } # authorization refused
        if ( $xml->{STATUS} == 0 && $xml->{NCERROR} eq '50001134' ) { $self->failure_status('declined') } # 3d secure wrong identification
        if ( $xml->{STATUS} == 0 && $xml->{NCERRORPLUS} =~ m/status \(91\)/ ) { 
            $self->failure_status('declined');
            $self->error_message('Operation only allowed on fully completed transactions (status may not be 91)'); }

    } else { 
        warn "remote server did not respond with HTTP 200 status code";
        $self->is_success(0) 
    }

    # Extract the base64 encoded HTML part
    if ( $xml->{STATUS} == 46 ) {
        my $html = decode_base64($xml->{HTML_ANSWER});
           # remove sillyness
           $html =~ s/
//g; 
           $html =~ s/�//g;

        # TODO: parse
        #open my $fh, '>', '/tmp/ogone_'.$self->{_content}->{win3ds}.'.html';
        #print $fh $html;
    }
  
    # Call result_code() with the servers result code
    $self->result_code($xml->{NCERROR});

    # If the transaction was successful, call authorization() with the authorization code the processor provided.
    if ( $self->is_success() ) {
        $self->authorization($xml->{PAYID});
    }

    # If the transaction was not successful, call error_message() with either the processor provided error message, or some error message to indicate why it failed.
    if ( not $self->is_success() and $xml->{NCERRORPLUS} ne '!' ) { # '!' == no errorplus
        $self->error_message($xml->{NCERRORPLUS});
    }
}

42;
__END__

=head1 NAME

Business::OnlinePayment::Ogone - Online payment processing via Ogone

=head1 SYNOPSYS

    use common::sense;
    use Data::Dumper;
    use Business::OnlinePayment;

    my $tx = new Business::OnlinePayment('Ogone', pspid => 'fred', 
                                                  login => 'bedrock_api',
                                                  password => 'fl1nst0ne' );

lib/Business/OnlinePayment/Ogone.pm  view on Meta::CPAN


=head2 Features

=over 4

=item * code is documented and hopefully quite readable

=item * prevents submitting your transaction twice

=item * accepts EUR and US currencies (defaults to Euro)

=item * allows creation of VISA Alias mitigating the need to store them temporary (RES -> SAL)

=item * allows creation of Recurrent VISA bills (Subscriptions)

=item * close coupling to native API, easy to crossreference

=item * allows Ogone's native operations: RES, REN, DEL, DES, SAL, SAS, RFD, RFS

=item * uses SHASign to protect your request's integrity

=item * implements SHA-1, SHA-256 and SHA-512 signing

=item * allows switching between test and production environments

=item * low-fat is_success() check, Ogone::Status has more details

=item * no dependency on Ogone::Status if memory is a constraint

=back

=head1 PROGRAM FLOW

You have a choice when implementing credit card processing. You can pocess the money transfer in one or more steps.
If you choose to go with one step, customers will be billed directly.

=over 4

=item B<action> => 'Normal Authorization' (with B<operation> => 'SAL' (default))

=item B<action> => 'Authorization Only' (with B<operation> => 'RES' (default)) then B<action> => 'Post Authorization' (with B<operation> => 'SAS' (default))

=back

=head1 METHODS

=head2 new()

Not used directly. Creates a new instance. It's possible to pass credential infomation in the constructor.

    my $tx = new Business::OnlinePayment('Ogone', pspid => 'fred', login => 'bedrock_api', password => 'fl1nst0ne');

=head2 content()

This method takes a lot of parameters to prepare the transaction to be submitted. Depending on these parameters the
payment processor will act on them in different ways, you can consider it a sort of dispatch table. The main actors are
C<action>, C<alias>, C<win3ds>.

=head3 content() internal parameter mappings

        # credentials
        login   => 'USERID',
        password => 'PSWD',
        PSPID   => 'PSPID',
        
        # primary identifier
        invoice_number => 'orderID',
        
        # transaction identifiers (action = query)
        payid => 'PAYID',
        payidsub => 'PAYIDSUB',
        
        # credit card data
        card_number => 'CARDNO', 
        cvc => 'CVC',
        expiration => 'ED',
        alias => 'ALIAS',
        
        # financial data
        currency => 'Currency',
        amount => 'amount',
        
        # Ogone specific arguments
        operation => 'Operation',       # REN, DEL, DES, SAL, SAS, RFD, RFS
        eci => 'ECI',                   # defaults 7: e-commerce with ssl (9: recurring e-commerce)
        accepturl => 'accepturl',
        declineurl => 'declineurl',
        exceptionurl => 'exceptionurl',
        paramplus => 'paramplus',
        complus => 'complus',
        language => 'LANGUAGE',

        # Business::OnlinePayment common
        description => 'COM',
        name => 'CN',
        email => 'EMAIL',
        address => 'Owneraddress',
        zip => 'OwnerZip',
        city => 'ownertown',
        country => 'ownercty',
        phone => 'ownertelno',

        # client authentication (not used directly, only here as valid HTTP POST arg)
        SHASign => 'SHASign',           # see sha_key, sha_type
        
        # 3d secure arguments
        flag3d => 'FLAG3D',
        win3ds => 'win3ds',
        http_accept => 'HTTP_ACCEPT',
        http_user_agent => 'HTTP_USER_AGENT',

        # recurrent fields
        subscription_id => 'SUBSCRIPTION_ID',
        subscription_orderid => 'SUB_ORDERID',
        subscription_status => 'SUBSCRIPTION_STATUS',
        startdate => 'SUB_STARTDATE',
        enddate => 'SUB_ENDDATE',
        status => 'SUB_STATUS',
        period_unit => 'SUB_PERIOD_UNIT', # 'd', 'ww', 'm' (yes two 'w's) for resp daily weekly monthly
        period_moment => 'SUB_PERIOD_MOMENT', # Integer, the moment in time on which the payment is (0-7 when period_unit is ww, 1-31 for d, 1-12 for m?)
        period_number => 'SUB_PERIOD_NUMBER', 

lib/Business/OnlinePayment/Ogone.pm  view on Meta::CPAN


=item B<1> some time later, you wish to receive/transfer the money to you. You issue a C<post authorize> request (defaults to C<SAL>)

=item B<2> STATUS=91 indicates the payment is being processed. PAYID and PAYIDSUB are references that identify the current operation on the transaction.

=item B<3> Ogone handles processing with your bank

=item B<4> money has been put into your account. STATUS is set to 9

=back
 
=head2 Refund

         Client                                 Ogone HTTPS      Bank
      ------------------------------------------------------------------------

      1 .->+---|Query| orderID or PAYID,PAYIDSUB= -->. 
        |                                            |       
        |  .<----------------------------------------'                   
        |  |
      2 STATUS == 9   
           |
      3    `---|Refund| orderID or PAYID,PAYIDSUB= ->. [RFD]
                                                     |
      4    *<-- STATUS=81 ---------------------------+ (processing)
                                                     |
      5                                              `----------->.
                                                                  | (processed) -$
      6                                     STATUS=8 .<-----------'


=over 4

=item B<1> We want to refund a transaction. To check the transaction is refundable, we must first query it.

=item B<2> Refunds are only possible once a transaction is completed (STATUS = 9) (e.g. not while it is processing = 91), thus loop until so.

=item B<3> Request refund using orderID or PAYID and PAYISUB to identify refundable operation.

=item B<4> STATUS=81 indicates the refund is being processed

=item B<5> Ogone handles processing with your bank

=item B<6> Money has been taken from your account. STATUS is set to 8

=back


=head1 TODO

=over 4

=item * Parse 3d-secure HTML 

=item * use SHA1 passwd hashing see: L<https://secure.ogone.com/ncol/test/hash_pswd.aspi>

=back

=head1 TESTING

To test this module you will need to set your credentials in the environment. Put the following in a file in your hoe directory e.g. F<~/.ogone>
The password is not the same as the PSPID password, you will need to enter the API users' password.

    export OGONE_PSPID=bob
    export OGONE_USERID=bob_api
    export OGONE_PSWD=foobar
    
Limit access to the F<~/.ogone> file

    chmod 600 ~/.ogone

Then load this file into your env en perform the testcases:

    source ~/.ogone
    perl -I lib/ t/*.t


=head1 INTERACTIVE SESSION

    [fred@triceratops ~/business-online-payment-ogone] $ curl -L http://cpanmin.us | perl - --self-upgrade
    [fred@triceratops ~/business-online-payment-ogone] $ cpanm -S Devel::REPL
    [fred@triceratops ~/business-online-payment-ogone] $ re.pl

    001:0> use lib './lib';
    002:0> use Business::OnlinePayment::Ogone;
    003:0> my $tx = new Business::OnlinePayment('Ogone', pspid => 'fred', login => 'bedrock_api', password => 'fl1nst0ne');

    bless( {
      login => "bedrock_api",
      passwprd => "fl1nst0ne",
      port => 443,
      processor => "Ogone",
      pspid => "fred",
      server => "secure.ogone.com"
    }, 'Business::OnlinePayment::Ogone' )

    004:0> 



=head1 REMOTE SIDE DOCUMENTATION

=head2 Backend

=over 4

=item Ogone Test Backend L<https://secure.ogone.com/ncol/test/frame_ogone.asp> 

=item Ogone Prod Backend L<https://secure.ogone.com/ncol/prod/frame_ogone.asp> 

=back

=head2 Online Documentation

=over 4

=item Homepage L<http://www.ogone.com>

=item Ogone DirectLink integration guide L<https://secure.ogone.com/ncol/Ogone_DirectLink_EN.pdf>

=item Ogone Alias Manager Option integration guide L<https://secure.ogone.com/ncol/Ogone_Alias_EN.pdf>



( run in 2.022 seconds using v1.01-cache-2.11-cpan-364913b4093 )