orderFactory = $orderFactory; $this->storeManager = $storeManager; $this->quoteRepository = $quoteRepository; $this->response = $responseFactory->create(); $this->orderSender = $orderSender; $this->transactionRepository = $transactionRepository; $this->_code = static::METHOD_CODE; $this->paymentFailures = $paymentFailures ? : ObjectManager::getInstance() ->get(\Magento\Sales\Api\PaymentFailuresInterface::class); parent::__construct( $context, $registry, $extensionFactory, $customAttributeFactory, $paymentData, $scopeConfig, $logger, $moduleList, $localeDate, $dataHelper, $requestFactory, $responseFactory, $transactionService, $httpClientFactory, $resource, $resourceCollection, $data ); } /** * Set data helper * * @param \Magento\Authorizenet\Helper\Data $dataHelper * @return void */ public function setDataHelper(\Magento\Authorizenet\Helper\Data $dataHelper) { $this->dataHelper = $dataHelper; } /** * Do not validate payment form using server methods * * @return bool */ public function validate() { return true; } /** * Send authorize request to gateway * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @param float $amount * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount) { $payment->setAdditionalInformation('payment_type', $this->getConfigData('payment_action')); } /** * Send capture request to gateway * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount) { if ($amount <= 0) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid amount for capture.')); } $payment->setAmount($amount); if ($payment->getParentTransactionId()) { $payment->setAnetTransType(self::REQUEST_TYPE_PRIOR_AUTH_CAPTURE); $payment->setXTransId($this->getRealParentTransactionId($payment)); } else { $payment->setAnetTransType(self::REQUEST_TYPE_AUTH_CAPTURE); } $result = $this->getResponse(); if (empty($result->getData())) { $request = $this->buildRequest($payment); $result = $this->postRequest($request); } return $this->processCapture($result, $payment); } /** * Process capture request * * @param \Magento\Authorizenet\Model\Directpost\Response $result * @param \Magento\Payment\Model\InfoInterface $payment * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ protected function processCapture($result, $payment) { switch ($result->getXResponseCode()) { case self::RESPONSE_CODE_APPROVED: case self::RESPONSE_CODE_HELD: if (in_array( $result->getXResponseReasonCode(), [ self::RESPONSE_REASON_CODE_APPROVED, self::RESPONSE_REASON_CODE_PENDING_REVIEW, self::RESPONSE_REASON_CODE_PENDING_REVIEW_AUTHORIZED ] ) ) { if (!$payment->getParentTransactionId() || $result->getXTransId() != $payment->getParentTransactionId() ) { $payment->setTransactionId($result->getXTransId()); } $payment->setIsTransactionClosed(0) ->setTransactionAdditionalInfo( self::REAL_TRANSACTION_ID_KEY, $result->getXTransId() ); return $this; } throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); default: throw new \Magento\Framework\Exception\LocalizedException(__('Payment capturing error.')); } } /** * Void the payment through gateway * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ public function void(\Magento\Payment\Model\InfoInterface $payment) { if (!$payment->getParentTransactionId()) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid transaction ID.')); } $payment->setAnetTransType(self::REQUEST_TYPE_VOID); $payment->setXTransId($this->getRealParentTransactionId($payment)); $request = $this->buildRequest($payment); $result = $this->postRequest($request); switch ($result->getXResponseCode()) { case self::RESPONSE_CODE_APPROVED: if ($result->getXResponseReasonCode() == self::RESPONSE_REASON_CODE_APPROVED) { if ($result->getXTransId() != $payment->getParentTransactionId()) { $payment->setTransactionId($result->getXTransId()); } $payment->setIsTransactionClosed(1) ->setShouldCloseParentTransaction(1) ->setTransactionAdditionalInfo(self::REAL_TRANSACTION_ID_KEY, $result->getXTransId()); return $this; } throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); default: throw new \Magento\Framework\Exception\LocalizedException(__('Payment voiding error.')); } } /** * Refund the amount need to decode last 4 digits for request. * * @param \Magento\Framework\DataObject|\Magento\Payment\Model\InfoInterface $payment * @param float $amount * @return $this * @throws \Exception */ public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount) { $last4 = $payment->getCcLast4(); $payment->setCcLast4($payment->decrypt($last4)); try { $this->processRefund($payment, $amount); } catch (\Exception $e) { $payment->setCcLast4($last4); throw $e; } $payment->setCcLast4($last4); return $this; } /** * Refund the amount with transaction id * * @param \Magento\Framework\DataObject $payment * @param float $amount * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ protected function processRefund(\Magento\Framework\DataObject $payment, $amount) { if ($amount <= 0) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid amount for refund.')); } if (!$payment->getParentTransactionId()) { throw new \Magento\Framework\Exception\LocalizedException(__('Invalid transaction ID.')); } $payment->setAnetTransType(self::REQUEST_TYPE_CREDIT); $payment->setAmount($amount); $payment->setXTransId($this->getRealParentTransactionId($payment)); $request = $this->buildRequest($payment); $result = $this->postRequest($request); switch ($result->getXResponseCode()) { case self::RESPONSE_CODE_APPROVED: if ($result->getXResponseReasonCode() == self::RESPONSE_REASON_CODE_APPROVED) { if ($result->getXTransId() != $payment->getParentTransactionId()) { $payment->setTransactionId($result->getXTransId()); } $payment->setIsTransactionClosed(true) ->setTransactionAdditionalInfo(self::REAL_TRANSACTION_ID_KEY, $result->getXTransId()); return $this; } throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: throw new \Magento\Framework\Exception\LocalizedException( $this->dataHelper->wrapGatewayError($result->getXResponseReasonText()) ); default: throw new \Magento\Framework\Exception\LocalizedException(__('Payment refunding error.')); } } /** * Get CGI url * * @return string */ public function getCgiUrl() { $uri = $this->getConfigData('cgi_url'); return $uri ? $uri : self::CGI_URL; } /** * Return URL on which Authorize.net server will return payment result data in hidden request. * * @param int $storeId * @return string */ public function getRelayUrl($storeId = null) { if ($storeId == null && $this->getStore()) { $storeId = $this->getStore(); } return $this->dataHelper->getRelayUrl($storeId); } /** * Return response. * * @return \Magento\Authorizenet\Model\Directpost\Response */ public function getResponse() { return $this->response; } /** * Instantiate state and set it to state object * * @param string $paymentAction * @param \Magento\Framework\DataObject $stateObject * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function initialize($paymentAction, $stateObject) { $requestType = null; switch ($paymentAction) { case self::ACTION_AUTHORIZE: $requestType = self::REQUEST_TYPE_AUTH_ONLY; //intentional case self::ACTION_AUTHORIZE_CAPTURE: $requestType = $requestType ?: self::REQUEST_TYPE_AUTH_CAPTURE; $payment = $this->getInfoInstance(); $order = $payment->getOrder(); $order->setCanSendNewEmailFlag(false); $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); $payment->setAmountAuthorized($order->getTotalDue()); $payment->setAnetTransType($requestType); break; default: break; } } /** * Generate request object and fill its fields from Quote or Order object * * @param \Magento\Sales\Model\Order $order Quote or order object. * @return \Magento\Authorizenet\Model\Directpost\Request */ public function generateRequestFromOrder(\Magento\Sales\Model\Order $order) { $request = $this->requestFactory->create() ->setConstantData($this) ->setDataFromOrder($order, $this) ->signRequestData(); $this->_debug(['request' => $request->getData()]); return $request; } /** * Fill response with data. * * @param array $postData * @return $this */ public function setResponseData(array $postData) { $this->getResponse()->setData($postData); return $this; } /** * Validate response data. Needed in controllers. * * @return bool true in case of validation success. * @throws \Magento\Framework\Exception\LocalizedException In case of validation error */ public function validateResponse() { $response = $this->getResponse(); $hashConfigKey = !empty($response->getData('x_SHA2_Hash')) ? 'signature_key' : 'trans_md5'; //hash check if (!$response->isValidHash($this->getConfigData($hashConfigKey), $this->getConfigData('login')) ) { throw new \Magento\Framework\Exception\LocalizedException( __('The transaction was declined because the response hash validation failed.') ); } return true; } /** * Operate with order using data from $_POST which came from authorize.net by Relay URL. * * @param array $responseData data from Authorize.net from $_POST * @return void * @throws \Magento\Framework\Exception\LocalizedException In case of validation error or order creation error */ public function process(array $responseData) { $this->_debug(['response' => $responseData]); $this->setResponseData($responseData); //check MD5 error or others response errors //throws exception on false. $this->validateResponse(); $response = $this->getResponse(); $responseText = $this->dataHelper->wrapGatewayError($response->getXResponseReasonText()); $isError = false; if ($this->getOrderIncrementId()) { $order = $this->getOrderFromResponse(); //check payment method $payment = $order->getPayment(); if (!$payment || $payment->getMethod() != $this->getCode()) { throw new \Magento\Framework\Exception\LocalizedException( __('This payment didn\'t work out because we can\'t find this order.') ); } if ($order->getId()) { //operate with order $this->processOrder($order); } else { $isError = true; } } else { $isError = true; } if ($isError) { $responseText = $responseText && !$response->isApproved() ? $responseText : __('This payment didn\'t work out because we can\'t find this order.'); throw new \Magento\Framework\Exception\LocalizedException($responseText); } } /** * Fill payment with credit card data from response from Authorize.net. * * @param \Magento\Framework\DataObject $payment * @return void */ protected function fillPaymentByResponse(\Magento\Framework\DataObject $payment) { $response = $this->getResponse(); $payment->setTransactionId($response->getXTransId()) ->setParentTransactionId(null) ->setIsTransactionClosed(0) ->setTransactionAdditionalInfo(self::REAL_TRANSACTION_ID_KEY, $response->getXTransId()); if ($response->getXMethod() == self::REQUEST_METHOD_CC) { $payment->setCcAvsStatus($response->getXAvsCode()) ->setCcLast4($payment->encrypt(substr($response->getXAccountNumber(), -4))); } if ($response->getXResponseCode() == self::RESPONSE_CODE_HELD) { $payment->setIsTransactionPending(true) ->setIsFraudDetected(true); } $additionalInformationKeys = explode(',', $this->getValue('paymentInfoKeys')); foreach ($additionalInformationKeys as $paymentInfoKey) { $paymentInfoValue = $response->getDataByKey($paymentInfoKey); if ($paymentInfoValue !== null) { $payment->setAdditionalInformation($paymentInfoKey, $paymentInfoValue); } } } /** * Check response code came from Authorize.net. * * @return true in case of Approved response * @throws \Magento\Framework\Exception\LocalizedException In case of Declined or Error response from Authorize.net */ public function checkResponseCode() { switch ($this->getResponse()->getXResponseCode()) { case self::RESPONSE_CODE_APPROVED: case self::RESPONSE_CODE_HELD: return true; case self::RESPONSE_CODE_DECLINED: case self::RESPONSE_CODE_ERROR: $errorMessage = $this->dataHelper->wrapGatewayError($this->getResponse()->getXResponseReasonText()); $order = $this->getOrderFromResponse(); $this->paymentFailures->handle((int)$order->getQuoteId(), (string)$errorMessage); throw new \Magento\Framework\Exception\LocalizedException($errorMessage); default: throw new \Magento\Framework\Exception\LocalizedException( __('There was a payment authorization error.') ); } } /** * Check transaction id came from Authorize.net * * @return true in case of right transaction id * @throws \Magento\Framework\Exception\LocalizedException In case of bad transaction id. */ public function checkTransId() { if (!$this->getResponse()->getXTransId()) { throw new \Magento\Framework\Exception\LocalizedException( __('Please enter a transaction ID to authorize this payment.') ); } return true; } /** * Compare amount with amount from the response from Authorize.net. * * @param float $amount * @return bool */ protected function matchAmount($amount) { return sprintf('%.2F', $amount) == sprintf('%.2F', $this->getResponse()->getXAmount()); } /** * Operate with order using information from Authorize.net. * * Authorize order or authorize and capture it. * * @param \Magento\Sales\Model\Order $order * @return void * @throws \Magento\Framework\Exception\LocalizedException * @throws \Exception * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function processOrder(\Magento\Sales\Model\Order $order) { try { $this->checkResponseCode(); $this->checkTransId(); } catch (\Exception $e) { //decline the order (in case of wrong response code) but don't return money to customer. $message = $e->getMessage(); $this->declineOrder($order, $message, false); throw $e; } $response = $this->getResponse(); //create transaction. need for void if amount will not match. $payment = $order->getPayment(); $this->fillPaymentByResponse($payment); $payment->getMethodInstance()->setIsInitializeNeeded(false); $payment->getMethodInstance()->setResponseData($response->getData()); $this->processPaymentFraudStatus($payment); $payment->place(); $this->addStatusComment($payment); $order->save(); //match amounts. should be equals for authorization. //decline the order if amount does not match. if (!$this->matchAmount($payment->getBaseAmountAuthorized())) { $message = __( 'Something went wrong: the paid amount doesn\'t match the order amount.' . ' Please correct this and try again.' ); $this->declineOrder($order, $message, true); throw new \Magento\Framework\Exception\LocalizedException($message); } try { if (!$response->hasOrderSendConfirmation() || $response->getOrderSendConfirmation()) { $this->orderSender->send($order); } $quote = $this->quoteRepository->get($order->getQuoteId())->setIsActive(false); $this->quoteRepository->save($quote); } catch (\Exception $e) { // do not cancel order if we couldn't send email } } /** * Process fraud status * * @param \Magento\Sales\Model\Order\Payment $payment * @return $this */ protected function processPaymentFraudStatus(\Magento\Sales\Model\Order\Payment $payment) { try { $fraudDetailsResponse = $payment->getMethodInstance() ->fetchTransactionFraudDetails($this->getResponse()->getXTransId()); $fraudData = $fraudDetailsResponse->getData(); if (empty($fraudData)) { $payment->setIsFraudDetected(false); return $this; } $fdsFilterAction = (string)$fraudDetailsResponse->getFdsFilterAction(); if ($this->fdsFilterActionIsReportOnly($fdsFilterAction) === false) { $payment->setIsFraudDetected(true); } $payment->setAdditionalInformation('fraud_details', $fraudData); } catch (\Exception $e) { //this request is optional } return $this; } /** * Add status comment to history * * @param \Magento\Sales\Model\Order\Payment $payment * @return $this */ protected function addStatusComment(\Magento\Sales\Model\Order\Payment $payment) { try { $transactionId = $this->getResponse()->getXTransId(); $data = $this->transactionService->getTransactionDetails($this, $transactionId); $transactionStatus = (string)$data->transaction->transactionStatus; $fdsFilterAction = (string)$data->transaction->FDSFilterAction; if ($payment->getIsTransactionPending()) { $message = 'Amount of %1 is pending approval on the gateway.
' . 'Transaction "%2" status is "%3".
' . 'Transaction FDS Filter Action is "%4"'; $message = __( $message, $payment->getOrder()->getBaseCurrency()->formatTxt($this->getResponse()->getXAmount()), $transactionId, $this->dataHelper->getTransactionStatusLabel($transactionStatus), $this->dataHelper->getFdsFilterActionLabel($fdsFilterAction) ); $payment->getOrder()->addStatusHistoryComment($message); } } catch (\Exception $e) { $this->getPsrLogger()->critical($e); //this request is optional } return $this; } /** * Register order cancellation. Return money to customer if needed. * * @param \Magento\Sales\Model\Order $order * @param string $message * @param bool $voidPayment * @return void */ protected function declineOrder(\Magento\Sales\Model\Order $order, $message = '', $voidPayment = true) { try { $response = $this->getResponse(); if ($voidPayment && $response->getXTransId() && strtoupper($response->getXType()) == self::REQUEST_TYPE_AUTH_ONLY ) { $order->getPayment() ->setTransactionId(null) ->setParentTransactionId($response->getXTransId()) ->void($response); } $order->registerCancellation($message)->save(); $this->_eventManager->dispatch('order_cancel_after', ['order' => $order ]); } catch (\Exception $e) { //quiet decline $this->getPsrLogger()->critical($e); } } /** * Return additional information`s transaction_id value of parent transaction model * * @param \Magento\Sales\Model\Order\Payment $payment * @return string */ protected function getRealParentTransactionId($payment) { $transaction = $this->transactionRepository->getByTransactionId( $payment->getParentTransactionId(), $payment->getId(), $payment->getOrder()->getId() ); return $transaction->getAdditionalInformation(self::REAL_TRANSACTION_ID_KEY); } /** * {inheritdoc} */ public function getConfigInterface() { return $this; } /** * Getter for specified value according to set payment method code * * @param mixed $key * @param mixed $storeId * @return mixed */ public function getValue($key, $storeId = null) { return $this->getConfigData($key, $storeId); } /** * Set initialization requirement state * * @param bool $isInitializeNeeded * @return void */ public function setIsInitializeNeeded($isInitializeNeeded = true) { $this->_isInitializeNeeded = (bool)$isInitializeNeeded; } /** * Get whether it is possible to capture * * @return bool */ public function canCapture() { return !$this->isGatewayActionsLocked($this->getInfoInstance()); } /** * Fetch transaction details info * * Update transaction info if there is one placing transaction only * * @param \Magento\Payment\Model\InfoInterface $payment * @param string $transactionId * @return array */ public function fetchTransactionInfo(\Magento\Payment\Model\InfoInterface $payment, $transactionId) { $transaction = $this->transactionRepository->getByTransactionId( $transactionId, $payment->getId(), $payment->getOrder()->getId() ); $response = $this->getTransactionResponse($transactionId); if ($response->getXResponseCode() == self::RESPONSE_CODE_APPROVED) { if ($response->getTransactionStatus() == 'voided') { $payment->setIsTransactionDenied(true); $payment->setIsTransactionClosed(true); $transaction->close(); } else { $transaction->setAdditionalInformation(self::TRANSACTION_FRAUD_STATE_KEY, false); $payment->setIsTransactionApproved(true); } } elseif ($response->getXResponseReasonCode() == self::RESPONSE_REASON_CODE_PENDING_REVIEW_DECLINED) { $payment->setIsTransactionDenied(true); } $this->addStatusCommentOnUpdate($payment, $response, $transactionId); return $response->getData(); } /** * Add status comment on update * * @param \Magento\Sales\Model\Order\Payment $payment * @param \Magento\Framework\DataObject $response * @param string $transactionId * @return $this */ protected function addStatusCommentOnUpdate( \Magento\Sales\Model\Order\Payment $payment, \Magento\Framework\DataObject $response, $transactionId ) { if ($payment->getIsTransactionApproved()) { $message = __( 'Transaction %1 has been approved. Amount %2. Transaction status is "%3"', $transactionId, $payment->getOrder()->getBaseCurrency()->formatTxt($payment->getAmountAuthorized()), $this->dataHelper->getTransactionStatusLabel($response->getTransactionStatus()) ); $payment->getOrder()->addStatusHistoryComment($message); } elseif ($payment->getIsTransactionDenied()) { $message = __( 'Transaction %1 has been voided/declined. Transaction status is "%2". Amount %3.', $transactionId, $this->dataHelper->getTransactionStatusLabel($response->getTransactionStatus()), $payment->getOrder()->getBaseCurrency()->formatTxt($payment->getAmountAuthorized()) ); $payment->getOrder()->addStatusHistoryComment($message); } return $this; } /** * Sets method code * * @param string $methodCode * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return void */ public function setMethodCode($methodCode) { } /** * Sets path pattern * * @param string $pathPattern * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return void */ public function setPathPattern($pathPattern) { } /** * This function returns full transaction details for a specified transaction ID. * * @param string $transactionId * @return \Magento\Framework\DataObject * @throws \Magento\Framework\Exception\LocalizedException * @link http://www.authorize.net/support/ReportingGuide_XML.pdf * @link http://developer.authorize.net/api/transaction_details/ */ protected function getTransactionResponse($transactionId) { $responseXmlDocument = $this->transactionService->getTransactionDetails($this, $transactionId); $response = new \Magento\Framework\DataObject(); $response->setXResponseCode((string)$responseXmlDocument->transaction->responseCode) ->setXResponseReasonCode((string)$responseXmlDocument->transaction->responseReasonCode) ->setTransactionStatus((string)$responseXmlDocument->transaction->transactionStatus); return $response; } /** * Get psr logger. * * @return \Psr\Log\LoggerInterface * @deprecated 100.1.0 */ private function getPsrLogger() { if (null === $this->psrLogger) { $this->psrLogger = ObjectManager::getInstance() ->get(\Psr\Log\LoggerInterface::class); } return $this->psrLogger; } /** * Fetch order by increment id from response. * * @return \Magento\Sales\Model\Order */ private function getOrderFromResponse(): \Magento\Sales\Model\Order { if (!$this->order) { $this->order = $this->orderFactory->create(); if ($incrementId = $this->getOrderIncrementId()) { $this->order = $this->order->loadByIncrementId($incrementId); } } return $this->order; } /** * Fetch order increment id from response. * * @return string */ private function getOrderIncrementId(): string { return $this->getResponse()->getXInvoiceNum(); } /** * Checks if filter action is Report Only. * * Transactions that trigger this filter are processed as normal, * but are also reported in the Merchant Interface as triggering this filter. * * @param string $fdsFilterAction * @return bool */ private function fdsFilterActionIsReportOnly($fdsFilterAction) { return $fdsFilterAction === (string)$this->dataHelper->getFdsFilterActionLabel('report'); } }