From e67699e0194e4672dc97b1be43c7f6f4a98ee0dc Mon Sep 17 00:00:00 2001 From: Martin Kamerbeek Date: Wed, 21 May 2008 20:42:40 +0000 Subject: [PATCH] Recurring payment stuff for the ITransact plugin. Still needs to be tested. --- lib/WebGUI/Shop/PayDriver/ITransact.pm | 197 +++++++++++++++++++++---- lib/WebGUI/Shop/Transaction.pm | 81 +++++++++- 2 files changed, 249 insertions(+), 29 deletions(-) diff --git a/lib/WebGUI/Shop/PayDriver/ITransact.pm b/lib/WebGUI/Shop/PayDriver/ITransact.pm index 16f2a7c0f..95a4a6c1a 100644 --- a/lib/WebGUI/Shop/PayDriver/ITransact.pm +++ b/lib/WebGUI/Shop/PayDriver/ITransact.pm @@ -31,10 +31,142 @@ sub _monthYear { } #------------------------------------------------------------------- -sub cancelRecurringPayment { +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('gatewayId'); + $recurUpdate->{ RemReps } = 0; + + my $xmlStructure = { + GatewayInterface => { + VendorIdentification => $vendorIdentification, + RecurUpdate => $recurUpdate, + } + }; + + my $xml = + '' + . XMLout( $xmlStructure, + NoAttr => 1, + KeepRoot => 1, + KeyAttr => [], + ); + + return $xml; } +#------------------------------------------------------------------- + +=head2 doXmlRequest ( xml [ isAdministrative ] ) + +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( 'application/x-www-form-urlencoded' ); + $request->content( 'xml='.$xml ); + + # Do the request + my $response = $userAgent->request($request); + + return $response; +} + +#------------------------------------------------------------------- + +=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, + $transactionResult->{ Status }, + $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, "$errorMessage Category: $errorCategory" ); + } + } else { + # Connection Error + $session->errorHandler->info("Connection error"); + + return ( 0, undef, 'ConnectionError', $response->status_line ); + } +} #------------------------------------------------------------------- sub definition { @@ -129,30 +261,35 @@ sub _generatePaymentRequestXML { foreach my $item (@{ $items }) { my $sku = $item->getSku; - ####TODO: How to handle intial payment? + # 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'); + $recurringData->{ RecurTotal } = + $item->get('price') + $transaction->get('taxes') + $transaction->get('shippingPrice'); $recurringData->{ RecurDesc } = $item->get('configuredTitle'); } - - push @{ $orderItems->{ Item } }, { - Description => $item->get('configuredTitle'), - Cost => $item->get('price'), - Qty => $item->get('quantity'), + 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) { + #### TODO: Don't add this if the transaction is recurring + if ( $transaction->get('taxes') > 0 ) { push @{ $orderItems->{ Item } }, { Description => $i18n->get('taxes'), Cost => $transaction->get('taxes'), Qty => 1, }; } + #### TODO: Don't add this if the transaction is recurring if ($transaction->get('shippingPrice') > 0) { push @{ $orderItems->{ Item } }, { Description => $i18n->get('shipping'), @@ -334,30 +471,34 @@ sub processPayment { } #------------------------------------------------------------------- -sub www_confirmRecurringTransaction { +sub www_processRecurringTransactionPostback { my $self = shift; my $session = $self->session; my $form = $session->form; - # Fetch transaction - my $gatewayId = $form->process( 'orig_xid' ); -# Somehow, the lines below aren't used for nothing, but were in the original code... -# my $transaction = WebGUI::Shop::Transaction->getByGatewayTransactionId( $session, $gatewayId, $self ); -# my $itemProperties = $transaction->getItems->[0]; - - # Convert the passed timestamps to epochs - my $startEpoch = $session->datetime->setToEpoch(sprintf("%4d-%02d-%02d %02d:%02d:%02d", unpack('a4a2a2a2a2a2', $form->process("start_date")))); - my $currentEpoch = $session->datetime->setToEpoch(sprintf("%4d-%02d-%02d %02d:%02d:%02d", unpack('a4a2a2a2a2a2', $form->process("when")))); - - # Update record - $session->db->setRow( 'ITransact_recurringStatus', 'gatewayId', { - gatewayId => $gatewayId, - initDate => $startEpoch, - lastTransaction => $currentEpoch, - status => $form->process( 'status' ), - errorMessage => $form->process( 'error_message' ), - recipe => $form->process( 'recipe_name' ), + # 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 = WebGUI::Shop::Transaction->newByGatewayId( $session, $originatingXid, $self->getId ); + + # 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 ); + } } #------------------------------------------------------------------- diff --git a/lib/WebGUI/Shop/Transaction.pm b/lib/WebGUI/Shop/Transaction.pm index 8a71584be..6ad088809 100644 --- a/lib/WebGUI/Shop/Transaction.pm +++ b/lib/WebGUI/Shop/Transaction.pm @@ -181,6 +181,37 @@ sub denyPurchase { #------------------------------------------------------------------- +=head2 duplicate ( [ overrideProperties ] ) + +Creates a new transaction with identical properties to the to this transaction . + +=head3 overrideProperties + +An optional hash ref containing transaction properties you want to override. + +=cut + +sub duplicate { + my $self = shift; + my $overrideProperties = shift || {}; + my $session = $self->session; + + # Fetch the properties for the duplicate transaction and apply the overrides + my $transactionProperties = { %{ $self->get }, %{ $overrideProperties } }; + + # Create a new transactions with the duplicated properties + my $newTransaction = WebGUI::Shop::Transaction->create( $session, $transactionProperties ); + + # Copy the items in the subscription + foreach my $item ( @{ $self->getItems } ) { + $newTransaction->addItem( $item->get ); + } + + return $newTransaction; +} + +#------------------------------------------------------------------- + =head2 formatAddress ( address ) Returns a formatted address. @@ -225,7 +256,7 @@ sub formatCurrency { =head2 get ( [ property ] ) -Returns a duplicated hash reference of this objectÕs data. +Returns a duplicated hash reference of this object's data. =head3 property @@ -327,6 +358,54 @@ sub new { #------------------------------------------------------------------- +=head2 newByGatewayId ( session, gatewayId, payDriverId ) + +Constructor. Instanciates a transaction based upon a paymentDriverId and a payment gateway issued id. + +=head3 session + +A reference to the current session. + +=head3 gatewayId + +The id the payment gateway has assigned to this transaction. More specifically the value stored in the +transactionCode field. + +=head3 payDriverId + +The id of the payment driver instance that has processed the transaction. + +=cut + +sub newByGatewayId { + my $class = shift; + my $session = shift; + unless (defined $session && $session->isa("WebGUI::Session")) { + WebGUI::Error::InvalidObject->throw(expected=>"WebGUI::Session", got=>(ref $session), error=>"Need a session."); + } + my $gatewayId = shift || WebGUI::Error::InvalidParam->throw(error=>"Need a gatewayId."); + my $payDriverId = shift || WebGUI::Error::InvalidParam->throw(error=>"Need a payDriverId."); + + # Find the transactionId belonging to the gatewayId/payDriverId combo. + my $transactionId = $session->db->quickScalar( + 'select transactionId from transaction where transactionCode=? and paymentDriverId=?', + [ + $gatewayId, + $payDriverId, + ] + ); + + # Throw an error if there is no such gatewayId/payDriverId combo. + unless ( $transactionId ) { + WebGUI::Error::ObjectNotFound->throw(error=>"No such transaction.", id=>$transactionId); + } + + # We have a transactionId so instanciate it and return the object + return $class->new( $session, $transactionId ); +} + +#------------------------------------------------------------------- + =head2 update ( properties ) Sets properties in the transaction.