/** * FIXME: this synthesized 'TransactionResponse' is increasingly silly * Maybe make this adapter more normal by adding an 'SDK' communication type * that just creates an array of $data, then overriding curl_transaction * to use the PwaClient. * @param ResponseProcessingException $exception * @param PaymentTransactionResponse $resultData */ public function handleErrors($exception, $resultData) { $errorCode = $exception->getErrorCode(); $resultData->addError($errorCode, $this->getErrorMapByCodeAndTranslate($errorCode)); if (array_search($errorCode, $this->retry_errors) === false) { // Fail on anything we don't recognize as retry-able. For example: // These two may show up if we start doing asynchronous authorization // 'AmazonClosed', // 'AmazonRejected', // For synchronous authorization, timeouts usually indicate that the // donor's account is under scrutiny, so letting them choose a different // card would likely just time out again // 'TransactionTimedOut', // These seem potentially fraudy - let's pay attention to them $this->logger->error('Heinous status returned from Amazon: ' . $errorCode); $this->finalizeInternalStatus(FinalStatus::FAILED); } }
/** * Show the special page */ protected function handleRequest() { $req = $this->getRequest(); // TODO: Don't do that. $fake = $req->getBool('fake'); $fail = $req->getBool('fail'); if ($fake) { if ($fail) { $this->displayFailPage(); return; } else { $go = ResultPages::getThankYouPage($this->adapter); } $this->getOutput()->addHTML("<br>Redirecting to page {$go}"); $this->getOutput()->redirect($go); return; } $forbidden = false; $this->qs_oid = $req->getText('order_id', ''); $this->qs_ref = $req->getText('REF', ''); if ($this->qs_oid === '' && $this->qs_ref === '') { $forbidden = true; $f_message = 'No order ID in the Querystring.'; } else { $result = $this->popout_if_iframe(); if ($result) { return; } } $session_oid = $this->adapter->session_getData('Donor', 'order_id'); if (is_null($session_oid) || $this->qs_oid !== $session_oid && strpos($this->qs_ref, (string) $session_oid) === false) { $forbidden = true; $f_message = "Requested order id not present in the session. (session_oid = '{$session_oid}')"; } if ($forbidden) { $this->logger->error($this->qs_oid . " Resultswitcher: forbidden for reason: {$f_message}"); wfHttpError(403, 'Forbidden', wfMessage('donate_interface-error-http-403')->text()); return; } $this->setHeaders(); $this->logger->info("Resultswitcher: OK to process Order ID: " . $this->qs_oid); // dispatch forms/handling if ($this->adapter->checkTokens()) { // Display form for the first time //this next block is for credit card coming back from GC. Only that. Nothing else, ever. if ($this->adapter->getData_Unstaged_Escaped('payment_method') === 'cc') { $sessionOrders = $req->getSessionData('order_status'); if (!is_array($sessionOrders) || !isset($sessionOrders[$this->qs_oid]) || !is_array($sessionOrders[$this->qs_oid])) { $result = $this->adapter->do_transaction('Confirm_CreditCard'); $session_info = array('data' => $result->getData(), 'message' => $result->getMessage(), 'errors' => $result->getErrors()); $sessionOrders[$this->qs_oid] = $session_info; $sessionOrders[$this->qs_oid]['data']['count'] = 0; } else { $sessionOrders = $req->getSessionData('order_status'); $sessionOrders[$this->qs_oid]['data']['count'] = $sessionOrders[$this->qs_oid]['data']['count'] + 1; $this->logger->error("Resultswitcher: Multiple attempts to process. " . $sessionOrders[$this->qs_oid]['data']['count']); $result = new PaymentTransactionResponse(); $result->setData($sessionOrders[$this->qs_oid]['data']); $result->setMessage($sessionOrders[$this->qs_oid]['message']); $result->setErrors($sessionOrders[$this->qs_oid]['errors']); } $req->setSessionData('order_status', $sessionOrders); $this->displayResultsForDebug($result); //do the switching between the... stuff. $status = $this->adapter->getFinalStatus(); if ($status) { switch ($status) { case FinalStatus::COMPLETE: case FinalStatus::PENDING: case FinalStatus::PENDING_POKE: $this->logger->info("Displaying thank you page for final status {$status}"); $go = ResultPages::getThankYouPage($this->adapter); break; case FinalStatus::FAILED: $this->logger->info('Displaying fail page for final status failed.'); $this->displayFailPage(); return; } if ($go) { $this->getOutput()->addHTML("<br>Redirecting to page {$go}"); $this->getOutput()->redirect($go); return; } else { $this->logger->error("Resultswitcher: No redirect defined. Order ID: {$this->qs_oid}"); } } else { $this->logger->error("Resultswitcher: No FinalStatus. Order ID: {$this->qs_oid}"); } } else { $this->logger->error("Resultswitcher: Payment method is not cc. Order ID: {$this->qs_oid}"); } } else { $this->logger->error("Resultswitcher: Token Check Failed. Order ID: {$this->qs_oid}"); } $this->displayFailPage(); }
/** * Build a PaymentResult object from adapter results * * @param PaymentTransactionResponse $response processed response object * @param string $finalStatus final transaction status. * * @return PaymentResult */ public static function fromResults(PaymentTransactionResponse $response, $finalStatus) { if ($finalStatus === FinalStatus::FAILED) { return PaymentResult::newFailure($response->getErrors()); } if (!$response) { return PaymentResult::newEmpty(); } if ($response->getErrors()) { // TODO: We will probably want the ability to refresh to a new form // as well and display errors at the same time. return PaymentResult::newRefresh($response->getErrors()); } if ($response->getRedirect()) { return PaymentResult::newRedirect($response->getRedirect()); } return PaymentResult::newSuccess(); }
/** * Returns an array of errors, in the format $error_code => $error_message. * This should be an empty array on transaction success. * * @deprecated * * @return array */ public function getTransactionErrors() { if ($this->transaction_response && $this->transaction_response->getErrors()) { $simplify = function ($error) { return $error['message']; }; return array_map($simplify, $this->transaction_response->getErrors()); } else { return array(); } }
/** * 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; }