package WebGUI::Shop::PayDriver::ITransact;
use strict;
use XML::Simple;
use Data::Dumper;
use base qw/WebGUI::Shop::PayDriver/;
#-------------------------------------------------------------------
=head2 _generateCancelRecurXml ( transaction )
Generates the XML that cancels a recurring payment. Includes the xml header.
=head3 transaction
The instantiated WebGUI::Shop::Transaction object for the transaction the recurring payment should be stopped.
=cut
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 =
''
. XMLout( $xmlStructure,
NoAttr => 1,
KeepRoot => 1,
KeyAttr => [],
);
return $xml;
}
#-------------------------------------------------------------------
=head2 _generatePaymentRequestXML ( transaction )
Generates the XML that will perform the payment transaction.
=head3 transaction
The instantiated WebGUI::Shop::Transaction object for the transaction that should be payed.
=cut
sub _generatePaymentRequestXML {
my $self = shift;
my $transaction = shift;
my $session = $self->session;
my $paymentAddress = $self->{ _billingAddress };
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 } =
$item->get('price') + $transaction->get('taxes') + $transaction->get('shippingPrice');
$recurringData->{ RecurDesc } = $item->get('configuredTitle');
}
# else {
push @{ $orderItems->{ Item } }, {
Description => $item->get('configuredTitle'),
Cost => $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 => $transaction->get('taxes'),
Qty => 1,
};
}
if ($transaction->get('shippingPrice') > 0) {
push @{ $orderItems->{ Item } }, {
Description => $i18n->get('shipping'),
Cost => $transaction->get('shippingPrice'),
Qty => 1,
};
}
if ($transaction->get('shopCreditDeduction') < 0) {
push @{ $orderItems->{ Item } }, {
Description => $i18n->get('in shop credit'),
Cost => $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 =
''
. XMLout( $xmlStructure,
NoAttr => 1,
KeepRoot => 1,
KeyAttr => [],
);
return $xml;
}
#-------------------------------------------------------------------
=head _monthYear ()
Returns the HTML for month/year combo box as is used for the credit card expiration date in the checkout form.
=cut
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;
}
#-------------------------------------------------------------------
=head2 _resolveRecurRecipe ( duration )
Returns the ITransact recipe name tied to one of the allowed recurring payment term durations as used within the
commerce system.
=head3 duration
The idenntifier for the term of the recurring transaction. May be either 'Weekly', 'BiWeekly', 'FourWeekly',
'Monthly', 'BiMonthly', 'Quarterly', 'HalfYearly' or 'Yearly'.
=cut
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->info("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 )
Does a request to ITransact to check the amount tied to a transaction. This is maily used to check whether a
postback result is an actual post back from ITransact and not a falsified post back by a malicious user.
=head3 xid
The id ITranscat to the transaction. Make sure not to use the orig_xid.
=head3 expectedAmount
The amount the transaction should be.
=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,
},
}
};
my $xml =
''
. 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 || '
'
);
$f->readOnly(
-value => ''.$i18n->get('show terminal').''
) if $self->get('vendorId');
$f->readOnly(
-value => '
'
);
$f->readOnly(
-value =>
$i18n->get('extra info')
.'
'
.'https://'.$session->config->get("sitename")->[0]
.'/?shop=pay;method=do;do=processRecurringTransactionPostback;paymentGatewayId='.$self->getId.''
);
return $f;
}
#-------------------------------------------------------------------
=head2 handlesRecurring
Tells the commerce system that this payment plugin can handle recurring payments.
=cut
sub handlesRecurring {
return 1;
}
#-------------------------------------------------------------------
=head2 processCredentials ( )
Checks and processes the data submitted by the user to the checkout form. Returns an array ref containing error messages if an
error occurred. If everything is okay, undef will be returned. Since this method stores the redentials in this
object instance you must execute this method before attempting a payment request.
=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 address data
push @error, $i18n->get( 'invalid firstName' ) unless $form->process( 'firstName' );
push @error, $i18n->get( 'invalid lastName' ) unless $form->process( 'lastName' );
push @error, $i18n->get( 'invalid address' ) unless $form->process( 'address' );
push @error, $i18n->get( 'invalid city' ) unless $form->process( 'city' );
push @error, $i18n->get( 'invalid email' ) unless $form->email ( 'email' );
push @error, $i18n->get( 'invalid zip' )
if ( !$form->zipcode( 'zipcode' ) && $form->process( 'country' ) eq 'United States' );
# Check credit card data
push @error, $i18n->get( 'invalid card number' ) unless $form->integer('cardNumber');
push @error, $i18n->get( 'invalid cvv2' ) if ($self->get('useCVV2') && !$form->integer('cvv2'));
# 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;
# Everything ok process the actual data
unless (@error) {
$self->{ _cardData } = {
acct => $form->integer( 'cardNumber' ),
expMonth => $form->integer( 'expMonth' ),
expYear => $form->integer( 'expYear' ),
cvv2 => $form->integer( 'cvv2' ),
};
$self->{ _billingAddress } = {
address1 => $form->process( 'address' ),
code => $form->zipcode( 'zipcode' ),
city => $form->process( 'city' ),
firstName => $form->process( 'firstName' ),
lastName => $form->process( 'lastName' ),
email => $form->email ( 'email' ),
state => $form->process( 'state' ),
country => $form->process( 'country' ),
phoneNumber => $form->process( 'phone' ),
};
return;
}
return \@error;
}
#-------------------------------------------------------------------
=head2 processPayment ( transaction )
Sends a payment request to ITransact, parses the result and depending on the outcome returns either:
On a succesfull request:
(1, GatewayCode, Status, Message)
On a failed request:
(0, GatewayCode, Status, Message)
Note that in the former case Message can be empty, while in the latter case GatewayCode may not be available (for
instance on a connection error) and wil be undef.
See also WebGUI::Shop::PayDriver->processPayment.
=cut
sub processPayment {
my $self = shift;
my $transaction = shift;
my $session = $self->session;
# Get the payment definition XML
my $xml = $self->_generatePaymentRequestXML( $transaction );
$session->errorHandler->info("XML Request: $xml");
# 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_getCredentials ( errors )
Displays the checkout form where users who want to pay can enter their address and credit card data.
=head3 errors
Arrayref containing error messages for errors that the user made in entering his data.
=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 $u = WebGUI::User->new($self->session,$self->session->user->userId);
# Process address from address book if passed
my $addressId = $session->form->process('addressId');
my $addressData = {};
if ( $addressId ) {
$addressData = eval{ $self->getAddress( $addressId )->get() } || {};
}
my $output;
# Process form errors
if ( $errors ) {
$output .= $i18n->get('error occurred message')
. '