/**
  * 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;
 }