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 = '' . 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 = '' . 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 = '' . 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 || ''); 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 = '
'; if ($self->get('vendorId')) { $output .= $terminal->print.'
'; } $output .= $i18n->get('extra info').'
' .'https://'.$session->config->get("sitename")->[0] .'/?shop=pay;method=do;do=processRecurringTransactionPostback;paymentGatewayId='.$self->getId.''; 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;