/** * Process the response and set transaction_response properties * * @param DOMDocument $response Cleaned-up XML from the GlobalCollect API * * @throws ResponseProcessingException with code and potentially retry vars. */ public function processResponse($response) { $this->transaction_response->setCommunicationStatus($this->parseResponseCommunicationStatus($response)); $errors = $this->parseResponseErrors($response); $this->transaction_response->setErrors($errors); $data = $this->parseResponseData($response); $this->transaction_response->setData($data); //set the transaction result message $responseStatus = isset($data['STATUSID']) ? $data['STATUSID'] : ''; $this->transaction_response->setTxnMessage("Response Status: " . $responseStatus); //TODO: Translate for GC. $this->transaction_response->setGatewayTransactionId($this->getData_Unstaged_Escaped('order_id')); $retErrCode = null; $retErrMsg = ''; $retryVars = array(); // We are also curious to know if there were any recoverable errors foreach ($errors as $errCode => $errObj) { $errMsg = $errObj['message']; $messageFromProcessor = $errObj['debugInfo']; $retryOrderId = false; switch ($errCode) { case 400120: // INSERTATTEMPT PAYMENT FOR ORDER ALREADY FINAL FOR COMBINATION. $transaction = $this->getCurrentTransaction(); if ($transaction !== 'INSERT_ORDERWITHPAYMENT') { // Don't regenerate order ID if it's too late, just steam // right through and let regular error handling deal // with it. $this->logger->error('Order ID already processed, remain calm.'); $retErrCode = $errCode; $retErrMsg = $errMsg; break; } $this->logger->error('InsertAttempt on a finalized order! Starting again.'); $retryOrderId = true; break; case 400490: // INSERTATTEMPT_MAX_NR_OF_ATTEMPTS_REACHED $this->logger->error('InsertAttempt - max attempts reached! Starting again.'); $retryOrderId = true; break; case 300620: // Oh no! We've already used this order # somewhere else! Restart! $this->logger->error('Order ID collision! Starting again.'); $retryOrderId = true; break; case 430260: // wow: If we were a point of sale, we'd be calling security. // wow: If we were a point of sale, we'd be calling security. case 430349: // TRANSACTION_CANNOT_BE_COMPLETED_VIOLATION_OF_LAW (EXTERMINATE!) // TRANSACTION_CANNOT_BE_COMPLETED_VIOLATION_OF_LAW (EXTERMINATE!) case 430357: // lost or stolen card // lost or stolen card case 430410: // CHALLENGED (GC docs say fraud) // CHALLENGED (GC docs say fraud) case 430415: // Security violation // Security violation case 430418: // Stolen card // Stolen card case 430421: // Suspected fraud // Suspected fraud case 430697: // Suspected fraud // Suspected fraud case 485020: // DO_NOT_TRY_AGAIN (or else EXTERMINATE!) // DO_NOT_TRY_AGAIN (or else EXTERMINATE!) case 4360022: // ECARD_FRAUD // ECARD_FRAUD case 4360023: // ECARD_ONLINE_FRAUD // These naughty codes get all the cancel treatment below, plus some extra // IP velocity spanking. if ($this->getGlobal('EnableIPVelocityFilter')) { Gateway_Extras_CustomFilters_IP_Velocity::penalize($this); } case 430306: // Expired card. // Expired card. case 430330: // invalid card number // invalid card number case 430354: // issuer unknown // All of these should stop us from retrying at all // Null out the retry vars and throw error immediately $retryVars = null; $this->logger->info("Got error code {$errCode}, not retrying to avoid MasterCard fines."); // TODO: move forceCancel - maybe to the exception? $this->transaction_response->setForceCancel(true); $this->transaction_response->setErrors(array('internal-0003' => array('message' => $this->getErrorMapByCodeAndTranslate('internal-0003')))); throw new ResponseProcessingException("Got error code {$errCode}, not retrying to avoid MasterCard fines.", $errCode); case 430285: //most common declined cc code. //most common declined cc code. case 430396: //not authorized to cardholder, whatever that means. //not authorized to cardholder, whatever that means. case 430409: //Declined, because "referred". We're not going to call the bank to push it through. //Declined, because "referred". We're not going to call the bank to push it through. case 430424: //Declined, because "SYSTEM_MALFUNCTION". I have no words. //Declined, because "SYSTEM_MALFUNCTION". I have no words. case 430692: //cvv2 declined break; //don't need to hear about these at all. //don't need to hear about these at all. case 20001000: //REQUEST {0} NULL VALUE NOT ALLOWED FOR {1} : Validation pain. Need more. //look in the message for more clues. //Yes: That's an 8-digit error code that buckets a silly number of validation issues, some of which are legitimately ours. //The only way to tell is to search the English message. //@TODO: Refactor all 3rd party error handling for GC. This whole switch should definitely be in parseResponseErrors; It is very silly that this is here at all. $not_errors = array('/NULL VALUE NOT ALLOWED FOR EXPIRYDATE/', '/DID NOT PASS THE LUHNCHECK/'); foreach ($not_errors as $regex) { if (preg_match($regex, $errObj['debugInfo'])) { //not a system error, but definitely the end of the payment attempt. Log it to info and leave. $this->logger->info(__FUNCTION__ . ": {$errObj['debugInfo']}"); throw new ResponseProcessingException($errMsg, $errCode); } } case 21000050: //REQUEST {0} VALUE {2} OF FIELD {1} IS NOT A NUMBER WITH MINLENGTH {3}, MAXLENGTH {4} AND PRECISION {5} : More validation pain. //say something painful here. $errMsg = 'Blocking validation problems with this payment. Investigation required! ' . "Original error: '{$messageFromProcessor}'. Our data: " . $this->getLogDebugJSON(); default: $this->logger->error(__FUNCTION__ . " Error {$errCode} : {$errMsg}"); break; } if ($retryOrderId) { $retryVars[] = 'order_id'; $retErrCode = $errCode; $retErrMsg = $errMsg; } } if ($retErrCode) { throw new ResponseProcessingException($retErrMsg, $retErrCode, $retryVars); } // Unstage any data that we've whitelisted. // TODO: This should be generalized into the base class. $whitelisted_keys = $this->transaction_option('response'); if ($whitelisted_keys) { $filtered_data = array_intersect_key($data, array_flip($whitelisted_keys)); $unstaged = $this->unstageKeys($filtered_data); $this->addResponseData($unstaged); } }