webgui/lib/WebGUI/Shop/PayDriver/Ogone.pm
2009-07-22 22:00:45 +00:00

578 lines
18 KiB
Perl

package WebGUI::Shop::PayDriver::Ogone;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2009 Plain Black Corporation.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
use strict;
use WebGUI::Shop::PayDriver;
use WebGUI::Exception;
use Digest::SHA qw{ sha1_hex };
use WebGUI::International;
use Data::Dumper;
use base qw{ WebGUI::Shop::PayDriver };
#-------------------------------------------------------------------
=head2 canCheckoutCart ( )
Returns whether the cart can be checked out by this plugin.
=cut
sub canCheckoutCart {
my $self = shift;
my $cart = $self->getCart;
return 0 unless $cart->readyForCheckout;
return 0 if $cart->requiresRecurringPayment;
return 1;
}
#-------------------------------------------------------------------
=head2 definition ( session, definition )
See WebGUI::Shop::PayDriver->definition.
=cut
sub definition {
my $class = shift;
my $session = shift;
my $definition = shift;
WebGUI::Error::InvalidParam->throw( error => q{Must provide a session variable} )
unless $session && ref $session eq 'WebGUI::Session';
my $i18n = WebGUI::International->new($session, 'PayDriver_Ogone');
tie my %fields, 'Tie::IxHash';
%fields = (
pspid => {
fieldType => 'text',
label => $i18n->get('psp id'),
hoverHelp => $i18n->get('psp id help'),
defaultValue => '',
},
shaSecret => {
fieldType => 'password',
label => $i18n->get('sha secret'),
hoverHelp => $i18n->get('sha secret help'),
},
postbackSecret => {
fieldType => 'password',
label => $i18n->get('postback secret'),
hoverHelp => $i18n->get('postback secret help'),
},
locale => {
fieldType => 'text',
label => $i18n->get('locale'),
hoverHelp => $i18n->get('locale help'),
defaultValue => 'en_US',
maxlength => 5,
size => 5,
},
currency => {
fieldType => 'text',
label => $i18n->get('currency'),
hoverHelp => $i18n->get('currency help'),
defaultValue => 'EUR',
maxlength => 3,
size => 3,
},
useTestMode => {
fieldType => 'yesNo',
label => $i18n->get('use test mode'),
hoverHelp => $i18n->get('use test mode help'),
defaultValue => 1,
},
);
push @{ $definition }, {
name => $i18n->get('Ogone'),
properties => \%fields,
};
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
=head2 getButton ( )
Returns the HTML for a form containing a button that, when clicked, will take the user to the checkout screen of
this plugin.
=cut
sub getButton {
my $self = shift;
my $session = $self->session;
my $i18n = WebGUI::International->new($session, 'PayDriver_Ogone');
my $payForm = WebGUI::Form::formHeader($session)
. $self->getDoFormTags('getCredentials')
. WebGUI::Form::submit($session, {value => $i18n->get('Ogone') })
. WebGUI::Form::formFooter($session);
return $payForm;
}
#-------------------------------------------------------------------
=head2 getCart
Returns the cart for either the current user or the transaction passed back by Ogone.
=cut
sub getCart {
my $self = shift;
my $cart;
if ($self->{_cartId}) {
$cart = WebGUI::Shop::Cart->new( $self->session, $self->{_cartId} );
}
return $cart || $self->SUPER::getCart;
}
#-------------------------------------------------------------------
=head2 processPayment ()
See WebGUI::Shop::PayDriver->processPayment
=cut
sub processPayment {
my $self = shift;
# Since we'll have to create a transaction before doing the actual tranasction, we let it fail
# initially with a message that it is pending.
# Unless the transaction result with _setPaymentStatus the transaction will fail.
my $success = $self->{_transactionSuccessful} || 0;
my $id = $self->{_ogoneId} || undef;
my $status = $self->{_statusCode} || undef;
my $message = $self->{_statusMessage} || 'Waiting for checkout';
return ( $success, $id, $status, $message );
return (0, undef, 1, 'Pending');
}
#-------------------------------------------------------------------
=head2 ogoneCheckoutButton ( transaction, address )
Generates a form with a submit button that, when clicked, posts the payment data for the given transaction to Ogone
and takes the user there.
=head3 transaction
The instanciated transaction that should be paid.
=head3 address
An instanciated WebGUI::Shop::Address object that contains the billing address.
=cut
sub ogoneCheckoutButton {
my $self = shift;
my $transaction = shift;
my $address = shift;
my $session = $self->session;
my $i18n = WebGUI::International->new( $session, 'PayDriver_Ogone' );
$self->{ _ogoneTransaction } = "done" ;
# Ogone needs the transaction amount in cents
my $amount = sprintf( "%.2f", $transaction->get('amount') ) * 100;
$amount =~ s/[^\d]//g; # Remove any character from amount except digits.
my $orderId = $transaction->getId;
my $description = "Transaction ID: $orderId";
my $pspId = $self->get('pspid');
my $name = join " ", $address->get( 'firstName' ), $address->get( 'lastName' );
my $email = $address->get('email');
my $currency = $self->get('currency');
# Generate sha signature the payment data
my $passphrase = join '', $orderId, $amount, $currency, $pspId, $self->get('shaSecret');
my $shaSignature = uc sha1_hex( $passphrase );
# Define the data to be sent to ogone
my %parameters = (
PSPID => $pspId,
orderID => $orderId,
amount => $amount,
currency => $currency,
language => $self->get('locale'),
CN => join( " ", $address->get('firstName'), $address->get('lastName') ),
EMAIL => $email,
ownerZIP => $address->get( 'code' ),
owneraddress => join( " ", $address->get('address1'), $address->get('address2'), $address->get('address3') ),
ownercty => $address->get('country'),
ownertown => $address->get('city'),
ownertelno => $address->get('phoneNumber'),
COMPLUS => $self->getCart->getId,
COM => $description,
SHASign => $shaSignature,
accepturl =>
$self->session->url->getSiteURL.'/?shop=pay&method=do&do=acceptTransaction&paymentGatewayId='.$self->getId,
cancelurl =>
$self->session->url->getSiteURL.'/?shop=pay&method=do&do=cancelTransaction&paymentGatewayId='.$self->getId,
declineurl =>
$self->session->url->getSiteURL.'/?shop=pay&method=do&do=declineTransaction&paymentGatewayId='.$self->getId,
exceptionurl =>
$self->session->url->getSiteURL.'/?shop=pay&method=do&do=exceptionTransaction&paymentGatewayId='.$self->getId
);
# Convert payment data to hidden input tags
my $formFields =
join "\n",
map { WebGUI::Form::hidden( $session, { name => $_, value => $parameters{ $_ } } ) }
keys %parameters;
# Construct actual checkout form
my $action = $self->get('useTestMode')
? 'https://secure.ogone.com/ncol/test/orderstandard.asp'
: 'https://secure.ogone.com/ncol/prod/orderstandard.asp'
;
my $form =
WebGUI::Form::formHeader( $session, {
action => $action,
method => 'POST',
enctype => 'application/x-www-form-urlencoded',
} )
. $formFields
. WebGUI::Form::submit( $session, { name => 'submit2', value => $i18n->get('pay') } )
. WebGUI::Form::formFooter( $session );
return $form;
}
#-------------------------------------------------------------------
=head2 www_getCredentials ( [ addressId ] )
Displays the checkout form for this plugin.
=head3 addressId
Optionally supply this variable which will set the payment address to this addressId.
=cut
sub www_getCredentials {
my ($self, $addressId) = @_;
my $session = $self->session;
my $i18n = WebGUI::International->new( $session, 'PayDriver_Ogone' );
# Process address from address book if passed
$addressId = $session->form->process( 'addressId' );
my $address;
if ( $addressId ) {
$address = eval{ $self->getAddress( $addressId ) };
}
else {
$address = $self->getCart->getShippingAddress;
}
my $billingAddressHtml = $address->getHtmlFormatted;
# Fetch transaction
my $transactionId = $session->form->process('transactionId');
my $transaction;
if ($transactionId) {
$transaction = WebGUI::Shop::Transaction->new( $session, $transactionId );
}
# Or generate a new one
unless ($transaction) {
$transaction = $self->processTransaction( $address );
}
# Set the billing address
$transaction->update( {
paymentAddress => $address,
} );
# Generate the json string that defines where the address book posts the selected address
my $callbackParams = {
url => $session->url->page,
params => [
{ name => 'shop', value => 'pay' },
{ name => 'method', value => 'do' },
{ name => 'do', value => 'getCredentials' },
{ name => 'paymentGatewayId', value => $self->getId },
],
};
my $callbackJson = JSON::to_json( $callbackParams );
# Generate 'Choose billing address' button
my $addressButton = WebGUI::Form::formHeader( $session )
. WebGUI::Form::hidden( $session, { name => 'shop', value => 'address' } )
. WebGUI::Form::hidden( $session, { name => 'method', value => 'view' } )
. WebGUI::Form::hidden( $session, { name => 'callback', value => $callbackJson } )
. WebGUI::Form::submit( $session, { value => $i18n->get('choose billing address') } )
. WebGUI::Form::formFooter( $session);
# Generate 'Proceed' button
my $proceedButton = $address
? $self->ogoneCheckoutButton( $transaction, $address )
: $i18n->get('please choose a billing address')
;
return $session->style->userStyle($addressButton.'<br />'.$billingAddressHtml.'<br />'.$proceedButton);
}
#-------------------------------------------------------------------
=head2 checkPostbackSHA ( )
Processes the postback data Ogone sends after a payment/cancelation. Figures out which transaction the data belongs
to and checks whether the data isn't tampered with by comparing SHA hashes.
If everything checks out, returns the instanciated transaction object, otherwise returns undef.
=cut
sub checkPostbackSHA {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $url = $session->url;
# Instanciate transaction
my $transactionId = $url->unescape( $form->process( 'orderID' ) );
my $transaction = WebGUI::Shop::Transaction->new( $session, $transactionId );
return undef unless $transaction;
# Fetch and format amount from transaction
my $amount = $transaction->get('amount');
$amount =~ s/\.00$//; # remove trailing .00
my $currency = $self->get('currency');
# Construct the passphrase...
my $passphrase = join '',
$transactionId, $currency, $amount,
map( { $url->unescape( $form->process( $_ ) ) } qw{ PM ACCEPTANCE STATUS CARDNO PAYID NCERROR BRAND } ),
$self->get('postbackSecret');
# and obtain its sha-1 hash in uppercase
my $shaSignature = uc sha1_hex( $passphrase );
# Return the instanciated transaction if the hash is valid, else return undef.
return $transaction if $shaSignature eq $form->process('SHASIGN');
return undef;
}
#-------------------------------------------------------------------
=head2 _setPaymentStatus ( transactionSuccessful, ogoneId, statusCode, statusMessage )
Stores the results of a postback in the object for later use by other methods.
=head3 transactionSuccessful
A boolean indicating whether or not the payment was successful.
=head3 ogoneId
The Ogone issued transaction ID.
=head3 statusCode
The Ogone issued status code.
=head3 statusMessage
The ogone issued status message.
=cut
sub _setPaymentStatus {
my $self = shift;
my ($form, $url) = $self->session->quick( 'form', 'url' );
$self->{_transactionSuccessful} = shift || 0;
$self->{_ogoneId} = shift || undef;
$self->{_statusCode} = shift || undef;
$self->{_statusMessage} = shift || undef;
$self->{_cartId} = $url->unescape( $form->process('COMPLUS') );
}
#-------------------------------------------------------------------
=head2 www_acceptTransaction ( )
The user is redirected to this screen when the payment was successful.
=cut
sub www_acceptTransaction {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $transaction = $self->checkPostbackSHA;
return $session->style->userStyle('Invalid postback data.') unless $transaction;
if ( $form->process('NCERROR') == 0 ) {
if ( !$transaction->isSuccessful ) {
$self->_setPaymentStatus( 1, $form->process('PAYID'), $form->process('STATUS'), 'Complete' );
$self->processTransaction( $transaction );
}
return $transaction->thankYou;
}
return $session->style->userStyle( 'An error occurred with your transaction.' );
}
#-------------------------------------------------------------------
=head2 www_cancelTransaction ( )
The user is redirected to this screen when the payment was canceled.
=cut
sub www_cancelTransaction {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $transaction = $self->checkPostbackSHA;
return $session->style->userStyle('Invalid postback data.') unless $transaction;
$self->_setPaymentStatus( 0, $form->process('PAYID'), $form->process('STATUS'), 'Cancelled' );
$self->processTransaction( $transaction );
$session->http->setRedirect($self->session->url->getSiteURL.'?shop=cart');
return $session->style->userStyle('Transaction cancelled');
}
#-------------------------------------------------------------------
=head2 www_declineTransaction ( )
The user is redirected to this screen when the payment was declined.
=cut
sub www_declineTransaction {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $transaction = $self->checkPostbackSHA;
return $session->style->userStyle('Invalid postback data.') unless $transaction;
$self->_setPaymentStatus( 0, $form->process('PAYID'), $form->process('STATUS'), 'Declined' );
$self->processTransaction( $transaction );
$session->http->setRedirect($self->session->url->getSiteURL.'?shop=cart');
return $session->style->userStyle('Transaction declined');
}
#-------------------------------------------------------------------
=head2 www_exceptionTransaction ( )
The user is redirected to this screen when a payment exception occurred.
=cut
sub www_exceptionTransaction {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $transaction = $self->checkPostbackSHA;
return $session->style->userStyle('Invalid postback data.') unless $transaction;
$self->_setPaymentStatus( 0, $form->process('PAYID'), $form->process('STATUS'), 'Transaction exception occurred' );
$self->processTransaction( $transaction );
$session->http->setRedirect($self->session->url->getSiteURL.'?shop=cart');
return $session->style->userStyle('A transaction exception occurred.');
}
#-------------------------------------------------------------------
=head2 www_edit ( )
Displays the properties screen.
=cut
sub www_edit {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
my $i18n = WebGUI::International->new($session, 'PayDriver_Ogone');
return $session->privilege->insufficient() unless $admin->canManage;
my $form = $self->getEditForm;
$form->submit;
my $processUrl = $self->session->url->getSiteURL.'/?shop=pay&method=do&do=processTransaction&paymentGatewayId='.$self->getId;
my $output = '<br />';
$output .= sprintf $i18n->get('ogone setup'), $processUrl, $processUrl;
return $admin->getAdminConsole->render($form->print.$output, $i18n->get('payment methods','PayDriver'));
}
#-------------------------------------------------------------------
=head2 www_processTransaction ( )
This method is called by the post sale notfication.
=cut
sub www_processTransaction {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $transaction = $self->checkPostbackSHA;
return $session->style->userStyle('Invalid postback data.') unless $transaction;
if ( $form->process('NCERROR') == 0 ) {
if ( !$transaction->isSuccessful ) {
$self->_setPaymentStatus( 1, $form->process('PAYID'), $form->process('STATUS'), 'Complete' );
}
}
else {
$self->_setPaymentStatus( 0, $form->process('PAYID'), $form->process('STATUS'), 'A payment processing error occurred' );
}
$self->processTransaction( $transaction );
return 'ok';
}
1;