webgui/lib/WebGUI/Shop/PayDriver/ITransact.pm

777 lines
27 KiB
Perl

package WebGUI::Shop::PayDriver::ITransact;
=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 XML::Simple;
use Data::Dumper;
use base qw/WebGUI::Shop::PayDriver/;
#-------------------------------------------------------------------
sub _generateCancelRecurXml {
my $self = shift;
my $transaction = shift;
# Construct xml
my $vendorIdentification;
$vendorIdentification->{ VendorId } = $self->get('vendorId');
$vendorIdentification->{ VendorPassword } = $self->get('password');
$vendorIdentification->{ HomePage } = $self->session->setting->get("companyURL");
my $recurUpdate;
$recurUpdate->{ OperationXID } = $transaction->get('transactionCode');
$recurUpdate->{ RemReps } = 0;
my $xmlStructure = {
GatewayInterface => {
VendorIdentification => $vendorIdentification,
RecurUpdate => $recurUpdate,
}
};
my $xml =
'<?xml version="1.0" standalone="yes"?>'
. XMLout( $xmlStructure,
NoAttr => 1,
KeepRoot => 1,
KeyAttr => [],
);
return $xml;
}
#-------------------------------------------------------------------
sub _generatePaymentRequestXML {
my $self = shift;
my $transaction = shift;
my $session = $self->session;
my $paymentAddress = $self->getCart->getBillingAddress->get();
my $cardData = $self->{ _cardData };
# Set up the XML.
# --- Customer data part ---
my $billingAddress;
$billingAddress->{ Address1 } = $paymentAddress->{ address1 };
# $billingAddress->{ Address2 } = $paymentAddress->{ address2 };
# $billingAddress->{ Address3 } = $paymentAddress->{ address3 };
$billingAddress->{ FirstName } = $paymentAddress->{ firstName };
$billingAddress->{ LastName } = $paymentAddress->{ lastName };
$billingAddress->{ City } = $paymentAddress->{ city };
$billingAddress->{ State } = $paymentAddress->{ state };
$billingAddress->{ Zip } = $paymentAddress->{ code };
$billingAddress->{ Country } = $paymentAddress->{ country };
$billingAddress->{ Phone } = $paymentAddress->{ phoneNumber };
my $cardInfo;
$cardInfo->{ CCNum } = $cardData->{ acct };
$cardInfo->{ CCMo } = $cardData->{ expMonth };
$cardInfo->{ CCYr } = $cardData->{ expYear };
$cardInfo->{ CVV2Number } = $cardData->{ cvv2 } if $self->get('useCVV2');
my $customerData;
$customerData->{ Email } = $paymentAddress->{ email };
$customerData->{ BillingAddress } = $billingAddress;
$customerData->{ AccountInfo }->{ CardInfo } = $cardInfo;
# --- Transaction data part ---
my $emailText;
$emailText->{ EmailTextItem } = [
$self->get('emailMessage'),
'ID: '. $transaction->getId,
];
# Process items
my ($orderItems, $recurringData);
my $items = $transaction->getItems;
# Check if recurring payments have a unique transaction
#### TODO: Throw the correct Exception Class
WebGUI::Error::InvalidParam->throw( error => 'Recurring transaction mixed with other transactions' )
if ( (scalar @{ $items } > 1) && (grep { $_->getSku->isRecurring } @{ $items }) );
foreach my $item (@{ $items }) {
my $sku = $item->getSku;
# Since recur recipes are based on intervals defined in days, the first term will payed NOW. Since the
# subscription start NOW too, we never need an initial amount for recurring payments.
if ( $sku->isRecurring ) {
$recurringData->{ RecurRecipe } = $self->_resolveRecurRecipe( $sku->getRecurInterval );
$recurringData->{ RecurReps } = 99999;
$recurringData->{ RecurTotal } =
sprintf("%.2f",$item->get('price') + $transaction->get('taxes') + $transaction->get('shippingPrice'));
$recurringData->{ RecurDesc } = $item->get('configuredTitle');
}
# else {
push @{ $orderItems->{ Item } }, {
Description => $item->get('configuredTitle'),
Cost => sprintf("%.2f", $item->get('price')),
Qty => $item->get('quantity'),
}
# }
}
# taxes, shipping, etc
my $i18n = WebGUI::International->new($session, "Shop");
if ( $transaction->get('taxes') > 0 ) {
push @{ $orderItems->{ Item } }, {
Description => $i18n->get('taxes'),
Cost => sprintf("%.2f",$transaction->get('taxes')),
Qty => 1,
};
}
if ($transaction->get('shippingPrice') > 0) {
push @{ $orderItems->{ Item } }, {
Description => $i18n->get('shipping'),
Cost => sprintf("%.2f",$transaction->get('shippingPrice')),
Qty => 1,
};
}
if ($transaction->get('shopCreditDeduction') < 0) {
push @{ $orderItems->{ Item } }, {
Description => $i18n->get('in shop credit'),
Cost => sprintf("%.2f",$transaction->get('shopCreditDeduction')),
Qty => 1,
};
}
my $vendorData;
$vendorData->{ Element }->{ Name } = 'transactionId';
$vendorData->{ Element }->{ Value } = $transaction->getId;
my $transactionData;
$transactionData->{ VendorId } = $self->get('vendorId');
$transactionData->{ VendorPassword } = $self->get('password');
$transactionData->{ VendorData } = $vendorData;
$transactionData->{ HomePage } = $self->session->setting->get("companyURL");
$transactionData->{ RecurringData } = $recurringData if $recurringData;
$transactionData->{ EmailText } = $emailText if $emailText;
$transactionData->{ OrderItems } = $orderItems if $orderItems;
# --- The XML structure ---
my $xmlStructure = {
SaleRequest => {
CustomerData => $customerData,
TransactionData => $transactionData,
}
};
my $xml =
'<?xml version="1.0" standalone="yes"?>'
. XMLout( $xmlStructure,
NoAttr => 1,
KeepRoot => 1,
KeyAttr => [],
);
return $xml;
}
#-------------------------------------------------------------------
sub _monthYear {
my $session = shift;
my $form = $session->form;
tie my %months, "Tie::IxHash";
tie my %years, "Tie::IxHash";
%months = map { sprintf( '%02d', $_ ) => sprintf( '%02d', $_ ) } 1 .. 12;
%years = map { $_ => $_ } 2004 .. 2099;
my $monthYear =
WebGUI::Form::selectBox( $session, {
name => 'expMonth',
options => \%months,
value => [ $form->process("expMonth") ]
})
. " / "
. WebGUI::Form::selectBox( $session, {
name => 'expYear',
options => \%years,
value => [ $form->process("expYear") ]
});
return $monthYear;
}
#-------------------------------------------------------------------
sub _resolveRecurRecipe {
my $self = shift;
my $duration = shift;
my %resolve = (
Weekly => 'weekly',
BiWeekly => 'biweekly',
FourWeekly => 'fourweekly',
Monthly => 'monthly',
Quarterly => 'quarterly',
HalfYearly => 'halfyearly',
Yearly => 'yearly',
);
# TODO: Throw exception
return $resolve{ $duration };
}
#-------------------------------------------------------------------
=head2 cancelRecurringPayment ( transaction )
Cancels a recurring transaction. Returns an array containing ( isSuccess, gatewayStatus, gatewayError).
=head3 transaction
The instanciated recurring transaction object.
=cut
sub cancelRecurringPayment {
my $self = shift;
my $transaction = shift;
my $session = $self->session;
#### TODO: Throw exception
# Get the payment definition XML
my $xml = $self->_generateCancelRecurXml( $transaction );
$session->errorHandler->debug("XML Request: $xml");
# Post the xml to ITransact
my $response = $self->doXmlRequest( $xml, 1 );
# Process response
if ($response->is_success) {
# We got some XML back from iTransact, now parse it.
$session->errorHandler->info('Starting request');
my $transactionResult = XMLin( $response->content );
unless (defined $transactionResult->{ RecurUpdateResponse }) {
# GatewayFailureResponse: This means the xml is invalid or has the wrong mime type
$session->errorHandler->info( "GatewayFailureResponse: result: [" . $response->content . "]" );
return(
0,
"Status: " . $transactionResult->{ Status }
." Message: " . $transactionResult->{ ErrorMessage }
." Category: " . $transactionResult->{ ErrorCategory }
);
} else {
# RecurUpdateResponse: We have succesfully sent the XML and it was correct. Note that this doesn't mean
# that the cancellation has succeeded. It only has if Status is set to OK and the remaining terms is 0.
$session->errorHandler->info( "RecurUpdateResponse: result: [" . $response->content . "]" );
my $transactionData = $transactionResult->{ RecurUpdateResponse };
my $status = $transactionData->{ Status };
my $errorMessage = $transactionData->{ ErrorMessage };
my $errorCategory = $transactionData->{ ErrorCategory };
my $remainingTerms = $transactionData->{ RecurDetails }->{ RemReps };
# Uppercase the status b/c the documentation is not clear on the case.
my $isSuccess = uc( $status ) eq 'OK' && $remainingTerms == 0;
return ( $isSuccess, "Status: $status Message: $errorMessage Category: $errorCategory" );
}
} else {
# Connection Error
$session->errorHandler->info("Connection error");
return ( 0, undef, 'ConnectionError', $response->status_line );
}
}
#-------------------------------------------------------------------
=head2 checkRecurringTransaction ( $xid, $expectedAmount )
Make an XML request back to ITransact to verify a recurring transaction. Returns 0 if
the transaction cannot be verified or is incorrect. Otherwise, it returns 1.
NOTE: THIS CODE IS NOT CALLED ANYWHERE.
=head3 $xid
Transaction ID, from ITransact.
=head3 $expectedAmount
The amount we think should be charged in this transaction.
=cut
sub checkRecurringTransaction {
my $self = shift;
my $xid = shift;
my $expectedAmount = shift;
my $session = $self->session;
my $xmlStructure = {
GatewayInterface => {
VendorIdentification => {
VendorId => $self->get('vendorId'),
VendorPassword => $self->get('password'),
HomePage => ,
},
RecurDetails => {
OperiationXID => $xid, ##BUGGO, typo?
},
}
};
my $xml =
'<?xml version="1.0" standalone="yes"?>'
. XMLout( $xmlStructure,
NoAttr => 1,
KeepRoot => 1,
KeyAttr => [],
);
my $response = $self->doXmlRequest( $xml, 1 );
if ($response->is_success) {
$session->errorHandler->info("Check recurring postback response: [".$response->content."]");
# We got some XML back from iTransact, now parse it.
my $transactionResult = XMLin( $response->content || '<empty></empty>');
unless (defined $transactionResult->{ RecurDetailsResponse }) {
# Something went wrong.
$session->errorHandler->info("Check recurring postback failed!");
return 0;
} else {
$session->errorHandler->info("Check recurring postback! Response: [".$response->content."]");
my $data = $transactionResult->{ RecurDetailsResponse };
my $status = $data->{ Status };
my $amount = $data->{ RecurDetails }->{ RecurTotal };
$session->errorHandler->info("Check recurring postback! Status: $status");
if ( $amount != $expectedAmount ) {
$session->errorHandler->info(
"Check recurring postback, received amount: $amount not equal to expected amount: $expectedAmount"
);
return 0;
}
return 1;
}
} else {
# Connection Error
$session->errorHandler->info("Connection error");
return 0;
}
}
#-------------------------------------------------------------------
sub definition {
my $class = shift;
my $session = shift;
WebGUI::Error::InvalidParam->throw(error => q{Must provide a session variable})
unless ref $session eq 'WebGUI::Session';
my $definition = shift;
my $i18n = WebGUI::International->new($session, 'PayDriver_ITransact');
tie my %fields, 'Tie::IxHash';
%fields = (
vendorId => {
fieldType => 'text',
label => $i18n->get('vendorId'),
hoverHelp => $i18n->get('vendorId help'),
},
password => {
fieldType => 'password',
label => $i18n->get('password'),
hoverHelp => $i18n->get('password help'),
},
useCVV2 => {
fieldType => 'yesNo',
label => $i18n->get('use cvv2'),
hoverHelp => $i18n->get('use cvv2 help'),
},
credentialsTemplateId => {
fieldType => 'template',
label => $i18n->get('credentials template'),
hoverHelp => $i18n->get('credentials template help'),
namespace => 'Shop/Credentials',
defaultValue => 'itransact_credentials1',
},
emailMessage => {
fieldType => 'textarea',
label => $i18n->get('emailMessage'),
hoverHelp => $i18n->get('emailMessage help'),
},
# readonly stuff from old plugin here?
);
push @{ $definition }, {
name => $i18n->get('Itransact'),
properties => \%fields,
};
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
=head2 doXmlRequest ( xml, [ isGatewayInterface ] )
Post an xml request to the ITransact backend. Returns a LWP::UserAgent response object.
=head3 xml
The xml string. Must contain a valid xml header.
=head3 isGatewayInterface
Determines what kind of request the xml is. For GatewayRequests set this value to 1. For SaleRequests set to 0 or
undef.
=cut
sub doXmlRequest {
my $self = shift;
my $xml = shift;
my $isGatewayInterface = shift;
# Figure out which cgi script we should post the XML to.
my $xmlTransactionScript = $isGatewayInterface
? 'https://secure.paymentclearing.com/cgi-bin/rc/xmltrans2.cgi'
: 'https://secure.paymentclearing.com/cgi-bin/rc/xmltrans.cgi'
;
# Set up LWP
my $userAgent = LWP::UserAgent->new;
$userAgent->env_proxy;
$userAgent->agent("WebGUI");
# Create a request and stuff the xml in it
my $request = HTTP::Request->new( POST => $xmlTransactionScript );
$request->content_type( 'text/xml' );
$request->add_content_utf8( $xml );
# Do the request
my $response = $userAgent->request($request);
return $response;
}
#-------------------------------------------------------------------
=head2 handlesRecurring
Tells the commerce system that this payment plugin can handle recurring payments.
=cut
sub handlesRecurring {
return 1;
}
#-------------------------------------------------------------------
=head2 processCredentials
Process the form where credentials (name, address, phone number and credit card information)
are entered.
=cut
sub processCredentials {
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my $i18n = WebGUI::International->new($session,'PayDriver_ITransact');
my @error;
# Check if expDate and expYear have sane values
my ($currentYear, $currentMonth) = $self->session->datetime->localtime;
my $expires = $form->integer( 'expYear' ) . sprintf '%02d', $form->integer( 'expMonth' );
my $now = $currentYear . sprintf '%02d', $currentMonth;
push @error, $i18n->get('invalid expiration date') unless $expires =~ m{^\d{6}$};
push @error, $i18n->get('expired expiration date') unless $expires >= $now;
return \@error if scalar @error;
# Everything ok process the actual data
$self->{ _cardData } = {
acct => $form->integer( 'cardNumber' ),
expMonth => $form->integer( 'expMonth' ),
expYear => $form->integer( 'expYear' ),
cvv2 => $form->integer( 'cvv2' ),
};
return;
}
#-------------------------------------------------------------------
=head2 processPayment ($transaction)
Contact ITransact and submit the payment data to them for processing.
=head3 $transaction
A WebGUI::Shop::Transaction object to pull information from.
=cut
sub processPayment {
my $self = shift;
my $transaction = shift;
my $session = $self->session;
# Get the payment definition XML
my $xml = $self->_generatePaymentRequestXML( $transaction );
# Send the xml to ITransact
my $response = $self->doXmlRequest( $xml );
# Process response
if ($response->is_success) {
# We got some XML back from iTransact, now parse it.
my $transactionResult = XMLin( $response->content, SuppressEmpty => '' );
#### TODO: More checking: price, address, etc
unless (defined $transactionResult->{ TransactionData }) {
# GatewayFailureResponse: This means the xml is invalid or has the wrong mime type
$session->errorHandler->info("GatewayFailureResponse: result: [".$response->content."]");
return(
0,
undef,
$transactionResult->{ Status },
$transactionResult->{ ErrorMessage } . ' Category: ' . $transactionResult->{ ErrorCategory }
);
} else {
# SaleResponse: We have succesfully sent the XML and it was correct. Note that this doesn't mean that
# the transaction has succeeded. It only has if Status is set to OK.
$session->errorHandler->info("SaleResponse: result: [".$response->content."]");
my $transactionData = $transactionResult->{ TransactionData };
my $status = $transactionData->{ Status };
my $errorMessage = $transactionData->{ ErrorMessage };
my $errorCategory = $transactionData->{ ErrorCategory };
my $gatewayCode = $transactionData->{ XID };
my $isSuccess = $status eq 'OK';
my $message = ($errorCategory) ? " $errorMessage Category: $errorCategory" : $errorMessage;
return ( $isSuccess, $gatewayCode, $status, $message );
}
} else {
# Connection Error
$session->errorHandler->info("Connection error");
return ( 0, undef, 'ConnectionError', $response->status_line );
}
}
#-------------------------------------------------------------------
=head2 www_edit ( )
Generates an edit form.
=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_ITransact');
return $session->privilege->insufficient() unless $admin->canManage;
my $form = $self->getEditForm;
$form->submit;
my $terminal = WebGUI::HTMLForm->new($session, action=>"https://secure.paymentclearing.com/cgi-bin/rc/sess.cgi", extras=>'target="_blank"');
$terminal->hidden(name=>"ret_addr", value=>"/cgi-bin/rc/sure/sure.cgi?sure_template_code=session_check&sure_use_session_mid=1");
$terminal->hidden(name=>"override", value=>1);
$terminal->hidden(name=>"cookie_precheck", value=>0);
$terminal->hidden(name=>"mid", value=>$self->get('vendorId'));
$terminal->hidden(name=>"pwd", value=>$self->get('password'));
$terminal->submit(value=>$i18n->get('show terminal'));
my $output = '<br />';
if ($self->get('vendorId')) {
$output .= $terminal->print.'<br />';
}
$output .= $i18n->get('extra info').'<br />'
.'<b>https://'.$session->config->get("sitename")->[0]
.'/?shop=pay;method=do;do=processRecurringTransactionPostback;paymentGatewayId='.$self->getId.'</b>';
return $admin->getAdminConsole->render($form->print.$output, $i18n->get('payment methods','PayDriver'));
}
#-------------------------------------------------------------------
=head2 www_getCredentials ( $errors )
Build a templated form for asking the user for their credentials.
=head3 $errors
An array reference of errors to show the user.
=cut
sub www_getCredentials {
my $self = shift;
my $errors = shift;
my $session = $self->session;
my $form = $session->form;
my $i18n = WebGUI::International->new($self->session, 'PayDriver_ITransact');
my $var = {};
# Process form errors
$var->{errors} = [];
if ($errors) {
$var->{error_message} = $i18n->get('error occurred message');
foreach my $error (@{ $errors} ) {
push @{ $var->{errors} }, { error => $error };
}
}
$var->{formHeader} = WebGUI::Form::formHeader($session)
. $self->getDoFormTags('pay');
$var->{formFooter} = WebGUI::Form::formFooter();
# Credit card information
$var->{cardNumberField} = WebGUI::Form::text($session, {
name => 'cardNumber',
value => $self->session->form->process("cardNumber"),
});
$var->{monthYearField} = WebGUI::Form::readOnly($session, {
value => _monthYear( $session ),
});
$var->{cvv2Field} = WebGUI::Form::integer($session, {
name => 'cvv2',
value => $self->session->form->process("cvv2"),
}) if $self->get('useCVV2');
$var->{checkoutButton} = WebGUI::Form::submit($session, {
value => $i18n->get('checkout button', 'Shop'),
extras => 'onclick="this.disabled=true;this.form.submit(); return false;"',
});
$self->appendCartVariables($var);
my $output = $self->processTemplate($self->get("credentialsTemplateId"), $var);
return $session->style->userStyle($output);
}
#-------------------------------------------------------------------
=head2 www_pay
Makes sure that the user has all the requirements for checking out, including
getting credentials, it processes the transaction and then displays a thank
you screen.
=cut
sub www_pay {
my $self = shift;
my $session = $self->session;
# Check whether the user filled in the checkout form and process those.
my $credentialsErrors = $self->processCredentials;
# Go back to checkout form if credentials are not ok
return $self->www_getCredentials( $credentialsErrors ) if $credentialsErrors;
# Payment time!
my $transaction = $self->processTransaction( );
if ($transaction->get('isSuccessful')) {
return $transaction->thankYou();
}
# Payment has failed...
return $self->displayPaymentError($transaction);
}
#-------------------------------------------------------------------
=head2 www_processRecurringTransactionPostback
Callback method for ITransact to dial up WebGUI and post the results of a
recurring transaction. This allows WebGUI to renew group memberships or
do whatever other activity a Sku purchase would allow.
=cut
sub www_processRecurringTransactionPostback {
my $self = shift;
my $session = $self->session;
$session->http->setMimeType('text/plain');
my $form = $session->form;
# Get posted data of interest
my $originatingXid = $form->process( 'orig_xid' );
my $status = $form->process( 'status' );
my $xid = $form->process( 'xid' );
my $errorMessage = $form->process( 'error_message' );
# Fetch the original transaction
my $baseTransaction = eval{WebGUI::Shop::Transaction->newByGatewayId( $session, $originatingXid, $self->getId )};
#---- Check the validity of the request -------
# First check whether the original transaction actualy exists
if (WebGUI::Error->caught || !(defined $baseTransaction) ) {
$session->errorHandler->warn("Check recurring postback: No base transction for XID: [$originatingXid]");
$session->http->setStatus('500', "No base transction for XID: [$originatingXid]");
return "Check recurring postback. No base transction for XID: [$originatingXid]";
}
# Secondly check if the postback is coming from secure.paymentclearing.com
# This will most certainly fail on mod_proxied webgui instances
# unless ( $ENV{ HTTP_HOST } eq 'secure.paymentclearing.com') {
# $session->errorHandler->info('ITransact Recurring Payment Postback is coming from host: ['.$ENV{ HTTP_HOST }.']');
# return;
# }
# Third, check if the new xid exists and if the amount is correct.
# my $expectedAmount = sprintf("%.2f",
# $baseTransaction->get('amount') + $baseTransaction->get('taxes') + $baseTransaction->get('shippingPrice') );
# unless ( $self->checkRecurringTransaction( $xid, $expectedAmount ) ) {
# $session->errorHandler->warn('Check recurring postback: transaction check failed.');
# return 'Check recurring postback: transaction check failed.';
# }
#---- Passed all test, continue ---------------
#make sure the same user is used in this transaction as the last {mostly needed for reoccurring transactions
$self->session->user({userId=>$baseTransaction->get('userId')});
# Create a new transaction for this term
my $transaction = $baseTransaction->duplicate( {
originatingTransactionId => $baseTransaction->getId,
});
# Check the transaction status and act accordingly
if ( uc $status eq 'OK' ) {
# The term was succesfully payed
$transaction->completePurchase( $xid, $status, $errorMessage );
}
else {
# The term has not been payed succesfully
$transaction->denyPurchase( $xid, $status, $errorMessage );
}
return "OK";
}
1;