/** * Called from do_transaction() in order to be able to deal with transactions that had * recoverable errors but that do require the entire transaction to be repeated. * * This function has the following extension hooks: * * pre_process_<strtolower($transaction)> * Called before the transaction is processed; intended to call setValidationAction() * if the transaction should not be performed. Anti-fraud can be performed in this * hook by calling $this->runAntifraudFilters(). * * * post_process_<strtolower($transaction)> * * @param string $transaction Name of the transaction being performed * @param &string() $retryVars Reference to an array of variables that caused the * transaction to fail. * * @return PaymentTransactionResponse * @throws UnexpectedValueException */ private final function do_transaction_internal($transaction, &$retryVars = null) { $this->debugarray[] = __FUNCTION__ . " is doing a {$transaction}."; //reset, in case this isn't our first time. $this->transaction_response = new PaymentTransactionResponse(); $this->final_status = false; $this->setValidationAction('process', true); $errCode = null; /* --- Build the transaction string for cURL --- */ try { $this->setCurrentTransaction($transaction); $this->executeIfFunctionExists('pre_process_' . $transaction); if ($this->getValidationAction() != 'process') { $this->logger->info("Failed pre-process checks for transaction type {$transaction}."); $this->transaction_response->setCommunicationStatus(false); $this->transaction_response->setMessage($this->getErrorMapByCodeAndTranslate('internal-0000')); $this->transaction_response->setErrors(array('internal-0000' => array('debugInfo' => "Failed pre-process checks for transaction type {$transaction}.", 'message' => $this->getErrorMapByCodeAndTranslate('internal-0000'), 'logLevel' => LogLevel::INFO))); return $this->transaction_response; } if (!$this->isBatchProcessor()) { // TODO: Maybe move this to the pre_process functions? $this->dataObj->saveContributionTrackingData(); } $commType = $this->getCommunicationType(); if ($commType === 'redirect') { //in the event that we have a redirect transaction that never displays the form, //save this most recent one before we leave. $this->session_pushFormName($this->getData_Unstaged_Escaped('ffname')); $this->transaction_response->setCommunicationStatus(true); // Build the redirect URL. $redirectUrl = $this->getProcessorUrl(); $redirectParams = $this->buildRequestParams(); if ($redirectParams) { // Add GET parameters, if provided. $redirectUrl .= '?' . http_build_query($redirectParams); } $this->transaction_response->setRedirect($redirectUrl); return $this->transaction_response; } elseif ($commType === 'xml') { $this->profiler->getStopwatch("buildRequestXML", true); // begin profiling $curlme = $this->buildRequestXML(); // build the XML $this->profiler->saveCommunicationStats("buildRequestXML", $transaction); // save profiling data } elseif ($commType === 'namevalue') { $this->profiler->getStopwatch("buildRequestNameValueString", true); // begin profiling $curlme = $this->buildRequestNameValueString(); // build the name/value pairs $this->profiler->saveCommunicationStats("buildRequestNameValueString", $transaction); // save profiling data } else { throw new UnexpectedValueException("Communication type of '{$commType}' unknown"); } } catch (Exception $e) { $this->logger->critical('Malformed gateway definition. Cannot continue: Aborting.\\n' . $e->getMessage()); $this->transaction_response->setCommunicationStatus(false); $this->transaction_response->setMessage($this->getErrorMapByCodeAndTranslate('internal-0001')); $this->transaction_response->setErrors(array('internal-0001' => array('debugInfo' => 'Malformed gateway definition. Cannot continue: Aborting.\\n' . $e->getMessage(), 'message' => $this->getErrorMapByCodeAndTranslate('internal-0001'), 'logLevel' => LogLevel::CRITICAL))); return $this->transaction_response; } /* --- Do the cURL request --- */ $this->profiler->getStopwatch(__FUNCTION__, true); $txn_ok = $this->curl_transaction($curlme); if ($txn_ok === true) { // We have something to slice and dice. $this->logger->info("RETURNED FROM CURL:" . print_r($this->transaction_response->getRawResponse(), true)); // Decode the response according to $this->getResponseType $formatted = $this->getFormattedResponse($this->transaction_response->getRawResponse()); // Process the formatted response. This will then drive the result action try { $this->processResponse($formatted); } catch (ResponseProcessingException $ex) { $errCode = $ex->getErrorCode(); $retryVars = $ex->getRetryVars(); $this->transaction_response->addError($errCode, array('message' => $this->getErrorMapByCodeAndTranslate('internal-0001'), 'debugInfo' => $ex->getMessage(), 'logLevel' => LogLevel::ERROR)); } } elseif ($txn_ok === false) { // nothing to process, so we have to build it manually $logMessage = 'Transaction Communication failed' . print_r($this->transaction_response, true); $this->logger->error($logMessage); $this->transaction_response->setCommunicationStatus(false); $this->transaction_response->setMessage($this->getErrorMapByCodeAndTranslate('internal-0002')); $this->transaction_response->setErrors(array('internal-0002' => array('debugInfo' => $logMessage, 'message' => $this->getErrorMapByCodeAndTranslate('internal-0002'), 'logLevel' => LogLevel::ERROR))); } // Log out how much time it took for the cURL request $this->profiler->saveCommunicationStats(__FUNCTION__, $transaction); if (!empty($retryVars)) { $this->logger->critical("{$transaction} Communication failed (errcode {$errCode}), will reattempt!"); // Set this by key so that the result object still has all the cURL data $this->transaction_response->setCommunicationStatus(false); $this->transaction_response->setMessage($this->getErrorMapByCodeAndTranslate($errCode)); $this->transaction_response->setErrors(array($errCode => array('debugInfo' => "{$transaction} Communication failed (errcode {$errCode}), will reattempt!", 'message' => $this->getErrorMapByCodeAndTranslate($errCode), 'logLevel' => LogLevel::CRITICAL))); } //if we have set errors by this point, the transaction is not okay $errors = $this->getTransactionErrors(); if (!empty($errors)) { $txn_ok = false; } // If we have any special post-process instructions for this // transaction, do 'em. // NOTE: If you want your transaction to fire off the post-process // logic, you need to run $this->postProcessDonation in a function // called // 'post_process' . strtolower($transaction) // in the appropriate gateway object. if ($txn_ok && empty($retryVars)) { $this->executeIfFunctionExists('post_process_' . $transaction); if ($this->getValidationAction() != 'process') { $this->logger->info("Failed post-process checks for transaction type {$transaction}."); $this->transaction_response->setCommunicationStatus(false); $this->transaction_response->setMessage($this->getErrorMapByCodeAndTranslate('internal-0000')); $this->transaction_response->setErrors(array('internal-0000' => array('debugInfo' => "Failed post-process checks for transaction type {$transaction}.", 'message' => $this->getErrorMapByCodeAndTranslate('internal-0000'), 'logLevel' => LogLevel::INFO))); return $this->transaction_response; } } // log that the transaction is essentially complete $this->logger->info('Transaction complete.'); if (!$this->isBatchProcessor()) { $this->debugarray[] = 'numAttempt = ' . $this->session_getData('numAttempt'); } return $this->transaction_response; }
/** * Either confirm or reject the payment * * FIXME: This function is way too complex. Unroll into new functions. * * @return PaymentTransactionResponse */ private function transactionConfirm_CreditCard() { $is_orphan = $this->isBatchProcessor(); if ($is_orphan) { // We're in orphan processing mode, so a "pending waiting for donor // input" status means that we'll never complete. Set this range // to map to "failed". $this->addCodeRange('GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 0, 70); } $cancelflag = false; //this will denote the thing we're trying to do with the donation attempt $problemflag = false; //this will get set to true, if we can't continue and need to give up and just log the hell out of it. $problemmessage = ''; //to be used in conjunction with the flag. $problemseverity = LogLevel::ERROR; //to be used also in conjunction with the flag, to route the message to the appropriate log. Urf. $original_status_code = NULL; $loopcount = $this->getGlobal('RetryLoopCount'); $loops = 0; $status_response = null; for ($loops = 0; $loops < $loopcount && !$cancelflag && !$problemflag; ++$loops) { $status_result = $this->do_transaction('GET_ORDERSTATUS'); $validationAction = $this->getValidationAction(); $cvv_result = $this->getData_Unstaged_Escaped('cvv_result'); $gotCVV = strlen($cvv_result) > 0; // TODO: This logging is redundant with the response from GET_ORDERSTATUS. $logmsg = 'CVV Result: ' . $this->getData_Unstaged_Escaped('cvv_result'); $logmsg .= ', AVS Result: ' . $this->getData_Unstaged_Escaped('avs_result'); $this->logger->info($logmsg); // FIXME: "isForceCancel"? if ($status_result->getForceCancel()) { $cancelflag = true; //don't retry or MasterCard will fine us } //we filtered if ($validationAction !== 'process') { $cancelflag = true; //don't retry: We've fraud-failed them intentionally. } elseif ($status_result->getCommunicationStatus() === false) { //can't communicate or internal error $problemflag = true; $problemmessage = "Can't communicate or internal error: " . $status_result->getMessage(); } $order_status_results = false; if (!$cancelflag && !$problemflag) { // $order_status_results = $this->getFinalStatus(); $txn_data = $this->getTransactionData(); if (isset($txn_data['STATUSID'])) { if (is_null($original_status_code)) { $original_status_code = $txn_data['STATUSID']; } $order_status_results = $this->findCodeAction('GET_ORDERSTATUS', 'STATUSID', $txn_data['STATUSID']); } if ($loops === 0 && $is_orphan && !is_null($original_status_code)) { //save stats. if (!isset($this->orphanstats) || !isset($this->orphanstats[$original_status_code])) { $this->orphanstats[$original_status_code] = 1; } else { $this->orphanstats[$original_status_code] += 1; } } if (!$order_status_results) { $problemflag = true; $problemmessage = "We don't have an order status after doing a GET_ORDERSTATUS."; } switch ($order_status_results) { case FinalStatus::FAILED: case FinalStatus::REVISED: $cancelflag = true; //makes sure we don't try to confirm. break 2; case FinalStatus::COMPLETE: $problemflag = true; //nothing to be done. $problemmessage = "GET_ORDERSTATUS reports that the payment is already complete."; $problemseverity = LogLevel::INFO; break 2; case FinalStatus::PENDING_POKE: if ($is_orphan && !$gotCVV) { $problemflag = true; $problemmessage = "Unable to retrieve orphan cvv/avs results (Communication problem?)."; } //none of this should ever execute for a transaction that doesn't use 3d secure... if ($txn_data['STATUSID'] === '200' && $loops < $loopcount - 1) { $this->logger->info("Running DO_FINISHPAYMENT ({$loops})"); $dopayment_result = $this->do_transaction('DO_FINISHPAYMENT'); $dopayment_data = $dopayment_result->getData(); //Check the txn status and result code to see if we should bother continuing if ($this->getTransactionStatus()) { $this->logger->info("DO_FINISHPAYMENT ({$loops}) returned with status ID " . $dopayment_data['STATUSID']); if ($this->findCodeAction('GET_ORDERSTATUS', 'STATUSID', $dopayment_data['STATUSID']) === FinalStatus::FAILED) { //ack and die. $problemflag = true; //nothing to be done. $problemmessage = "DO_FINISHPAYMENT says the payment failed. Giving up forever."; $this->finalizeInternalStatus(FinalStatus::FAILED); } } else { $this->logger->error("DO_FINISHPAYMENT ({$loops}) returned NOK"); } break; } if ($txn_data['STATUSID'] !== '200') { break 2; //no need to loop. } // FIXME: explicit that we want to fall through? // FIXME: explicit that we want to fall through? case FinalStatus::PENDING: // If it's really pending at this point, we need to // leave it alone. // FIXME: If we're orphan slaying, this should stay in // the queue, but we currently delete it. break 2; } } } //if we got here with no problemflag, //confirm or cancel the payment based on $cancelflag if (!$problemflag) { if (!$cancelflag) { $final = $this->do_transaction('SET_PAYMENT'); if ($final->getCommunicationStatus() === true) { $this->finalizeInternalStatus(FinalStatus::COMPLETE); //get the old status from the first txn, and add in the part where we set the payment. $this->transaction_response->setTxnMessage("Original Response Status (pre-SET_PAYMENT): " . $original_status_code); $this->postProcessDonation(); // Queueing is in here. } else { $this->finalizeInternalStatus(FinalStatus::FAILED); $problemflag = true; $problemmessage = "SET_PAYMENT couldn't communicate properly!"; } } else { if ($order_status_results === false) { //we didn't do the check, because we're going to fail the thing. /** * No need to send an explicit CANCEL_PAYMENT here, because * the payment has not been set. * In fact, GC will error out if we try to do that, and tell * us there is nothing to cancel. */ $this->finalizeInternalStatus(FinalStatus::FAILED); } else { //in case we got wiped out, set the final status to what it was before. $this->finalizeInternalStatus($order_status_results); } } } if ($problemflag || $cancelflag) { if ($cancelflag) { //cancel wins $problemmessage = "Cancelling payment"; $problemseverity = LogLevel::INFO; $errors = array('1000001' => $problemmessage); } else { $errors = array('1000000' => 'Transaction could not be processed due to an internal error.'); } //we have probably had a communication problem that could mean stranded payments. $this->logger->log($problemseverity, $problemmessage); //hurm. It would be swell if we had a message that told the user we had some kind of internal error. $ret = new PaymentTransactionResponse(); $ret->setCommunicationStatus(false); //DO NOT PREPEND $problemmessage WITH ANYTHING! //orphans.php is looking for specific things in position 0. $ret->setMessage($problemmessage); foreach ($errors as $code => $error) { $ret->addError($code, array('message' => $error, 'debugInfo' => 'Failure in transactionConfirm_CreditCard', 'logLevel' => $problemseverity)); } // TODO: should we set $this->transaction_response ? return $ret; } // return something better... if we need to! return $status_result; }