123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589 |
- <?php
- /**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
- namespace Magento\Paypal\Model;
- use Exception;
- use Magento\Framework\Exception\LocalizedException;
- use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender;
- use Magento\Sales\Model\Order\Email\Sender\OrderSender;
- /**
- * PayPal Instant Payment Notification processor model
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- */
- class Ipn extends \Magento\Paypal\Model\AbstractIpn implements IpnInterface
- {
- /**
- * @var \Magento\Sales\Model\Order
- */
- protected $_order;
- /**
- * @var \Magento\Sales\Model\OrderFactory
- */
- protected $_orderFactory;
- /**
- * PayPal info instance
- *
- * @var Info
- */
- protected $_paypalInfo;
- /**
- * @var OrderSender
- */
- protected $orderSender;
- /**
- * @var CreditmemoSender
- */
- protected $creditmemoSender;
- /**
- * @param \Magento\Paypal\Model\ConfigFactory $configFactory
- * @param \Psr\Log\LoggerInterface $logger
- * @param \Magento\Framework\HTTP\Adapter\CurlFactory $curlFactory
- * @param \Magento\Sales\Model\OrderFactory $orderFactory
- * @param Info $paypalInfo
- * @param OrderSender $orderSender
- * @param CreditmemoSender $creditmemoSender
- * @param array $data
- */
- public function __construct(
- \Magento\Paypal\Model\ConfigFactory $configFactory,
- \Psr\Log\LoggerInterface $logger,
- \Magento\Framework\HTTP\Adapter\CurlFactory $curlFactory,
- \Magento\Sales\Model\OrderFactory $orderFactory,
- Info $paypalInfo,
- OrderSender $orderSender,
- CreditmemoSender $creditmemoSender,
- array $data = []
- ) {
- parent::__construct($configFactory, $logger, $curlFactory, $data);
- $this->_orderFactory = $orderFactory;
- $this->_paypalInfo = $paypalInfo;
- $this->orderSender = $orderSender;
- $this->creditmemoSender = $creditmemoSender;
- }
- /**
- * Get ipn data, send verification to PayPal, run corresponding handler
- *
- * @return void
- * @throws Exception
- */
- public function processIpnRequest()
- {
- $this->_addDebugData('ipn', $this->getRequestData());
- try {
- $this->_getConfig();
- $this->_postBack();
- $this->_processOrder();
- } catch (Exception $e) {
- $this->_addDebugData('exception', $e->getMessage());
- $this->_debug();
- throw $e;
- }
- $this->_debug();
- }
- /**
- * Get config with the method code and store id and validate
- *
- * @return \Magento\Paypal\Model\Config
- * @throws Exception
- */
- protected function _getConfig()
- {
- $order = $this->_getOrder();
- $methodCode = $order->getPayment()->getMethod();
- $parameters = ['params' => [$methodCode, $order->getStoreId()]];
- $this->_config = $this->_configFactory->create($parameters);
- if (!$this->_config->isMethodActive($methodCode) || !$this->_config->isMethodAvailable()) {
- throw new Exception(sprintf('The "%s" method isn\'t available.', $methodCode));
- }
- /** @link https://cms.paypal.com/cgi-bin/marketingweb?cmd=_render-content&content_ID=
- * developer/e_howto_admin_IPNIntro */
- // verify merchant email intended to receive notification
- $merchantEmail = $this->_config->getValue('businessAccount');
- if (!$merchantEmail) {
- return $this->_config;
- }
- $receiver = $this->getRequestData('business') ?: $this->getRequestData('receiver_email');
- if (strtolower($merchantEmail) != strtolower($receiver)) {
- throw new Exception(
- sprintf(
- 'The requested "%s" and the configured "%s" merchant emails don\'t match.',
- $receiver,
- $merchantEmail
- )
- );
- }
- return $this->_config;
- }
- /**
- * Load order
- *
- * @return \Magento\Sales\Model\Order
- * @throws Exception
- */
- protected function _getOrder()
- {
- $incrementId = $this->getRequestData('invoice');
- $this->_order = $this->_orderFactory->create()->loadByIncrementId($incrementId);
- if (!$this->_order->getId()) {
- throw new Exception(sprintf('The "%s" order ID is incorrect. Verify the ID and try again.', $incrementId));
- }
- return $this->_order;
- }
- /**
- * IPN workflow implementation
- * Everything should be added to order comments. In positive processing cases customer will get email notifications.
- * Admin will be notified on errors.
- *
- * @return void
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- protected function _processOrder()
- {
- $this->_getConfig();
- try {
- // Handle payment_status
- $transactionType = $this->getRequestData('txn_type');
- switch ($transactionType) {
- // handle new case created
- case Info::TXN_TYPE_NEW_CASE:
- $this->_registerDispute();
- break;
- // handle new adjustment is created
- case Info::TXN_TYPE_ADJUSTMENT:
- $this->_registerAdjustment();
- break;
- //handle new transaction created
- default:
- $this->_registerTransaction();
- break;
- }
- } catch (\Magento\Framework\Exception\LocalizedException $e) {
- $comment = $this->_createIpnComment(__('Note: %1', $e->getMessage()), true);
- $comment->save();
- throw $e;
- }
- }
- /**
- * Process dispute notification
- *
- * @return void
- */
- protected function _registerDispute()
- {
- $reasonComment = $this->_paypalInfo->explainReasonCode($this->getRequestData('reason_code'));
- $caseType = $this->getRequestData('case_type');
- $caseTypeLabel = $this->_paypalInfo->getCaseTypeLabel($caseType);
- $caseId = $this->getRequestData('case_id');
- //Add IPN comment about registered dispute
- $message = __(
- 'IPN "%1". Case type "%2". Case ID "%3" %4',
- ucfirst($caseType),
- $caseTypeLabel,
- $caseId,
- $reasonComment
- );
- $this->_order->addStatusHistoryComment($message)->setIsCustomerNotified(false)->save();
- }
- /**
- * Process adjustment notification
- *
- * @return void
- */
- protected function _registerAdjustment()
- {
- $reasonCode = $this->getRequestData('reason_code');
- $reasonComment = $this->_paypalInfo->explainReasonCode($reasonCode);
- $notificationAmount = $this->_order->getBaseCurrency()->formatTxt($this->getRequestData('mc_gross'));
- // Add IPN comment about registered dispute
- $message = __(
- 'IPN "%1". A dispute has been resolved and closed. %2 Transaction amount %3.',
- ucfirst($reasonCode),
- $notificationAmount,
- $reasonComment
- );
- $this->_order->addStatusHistoryComment($message)->setIsCustomerNotified(false)->save();
- }
- /**
- * Process regular IPN notifications
- *
- * @return void
- * @throws \Magento\Framework\Exception\LocalizedException
- * @throws Exception
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- */
- protected function _registerTransaction()
- {
- // Handle payment_status
- $paymentStatus = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
- switch ($paymentStatus) {
- // paid
- case Info::PAYMENTSTATUS_COMPLETED:
- $this->_registerPaymentCapture(true);
- break;
- // the holded payment was denied on paypal side
- case Info::PAYMENTSTATUS_DENIED:
- $this->_registerPaymentDenial();
- break;
- // customer attempted to pay via bank account, but failed
- case Info::PAYMENTSTATUS_FAILED:
- // cancel order
- $this->_registerPaymentFailure();
- break;
- // payment was obtained, but money were not captured yet
- case Info::PAYMENTSTATUS_PENDING:
- $this->_registerPaymentPending();
- break;
- case Info::PAYMENTSTATUS_PROCESSED:
- $this->_registerMasspaymentsSuccess();
- break;
- case Info::PAYMENTSTATUS_REVERSED:
- //break is intentionally omitted
- case Info::PAYMENTSTATUS_UNREVERSED:
- $this->_registerPaymentReversal();
- break;
- case Info::PAYMENTSTATUS_REFUNDED:
- $this->_registerPaymentRefund();
- break;
- // authorization expire/void
- case Info::PAYMENTSTATUS_EXPIRED:
- // break is intentionally omitted
- case Info::PAYMENTSTATUS_VOIDED:
- $this->_registerPaymentVoid();
- break;
- default:
- throw new Exception("The '{$paymentStatus}' payment status couldn't be handled.");
- }
- }
- /**
- * Process completed payment (either full or partial)
- *
- * @param bool $skipFraudDetection
- * @return void
- */
- protected function _registerPaymentCapture($skipFraudDetection = false)
- {
- if ($this->getRequestData('transaction_entity') == 'auth') {
- return;
- }
- $parentTransactionId = $this->getRequestData('parent_txn_id');
- $this->_importPaymentInformation();
- $payment = $this->_order->getPayment();
- $payment->setTransactionId($this->getRequestData('txn_id'));
- $payment->setCurrencyCode($this->getRequestData('mc_currency'));
- $payment->setPreparedMessage($this->_createIpnComment(''));
- $payment->setParentTransactionId($parentTransactionId);
- $payment->setShouldCloseParentTransaction('Completed' === $this->getRequestData('auth_status'));
- $payment->setIsTransactionClosed(0);
- $payment->registerCaptureNotification(
- $this->getRequestData('mc_gross'),
- $skipFraudDetection && $parentTransactionId
- );
- $this->_order->save();
- // notify customer
- $invoice = $payment->getCreatedInvoice();
- if ($invoice && !$this->_order->getEmailSent()) {
- $this->orderSender->send($this->_order);
- $this->_order->addStatusHistoryComment(
- __('You notified customer about invoice #%1.', $invoice->getIncrementId())
- )
- ->setIsCustomerNotified(true)
- ->save();
- }
- }
- /**
- * Process denied payment notification
- *
- * @return void
- * @throws Exception
- */
- protected function _registerPaymentDenial()
- {
- try {
- $this->_importPaymentInformation();
- $this->_order->getPayment()
- ->setTransactionId($this->getRequestData('txn_id'))
- ->setNotificationResult(true)
- ->setIsTransactionClosed(true)
- ->deny(false);
- $this->_order->save();
- } catch (LocalizedException $e) {
- if ($e->getMessage() != __('We cannot cancel this order.')) {
- throw $e;
- }
- }
- }
- /**
- * Treat failed payment as order cancellation
- *
- * @return void
- */
- protected function _registerPaymentFailure()
- {
- $this->_importPaymentInformation();
- $this->_order->registerCancellation($this->_createIpnComment(''))->save();
- }
- /**
- * Process payment pending notification
- *
- * @return void
- * @throws Exception
- */
- public function _registerPaymentPending()
- {
- $reason = $this->getRequestData('pending_reason');
- if ('authorization' === $reason) {
- $this->_registerPaymentAuthorization();
- return;
- }
- if ('order' === $reason) {
- throw new Exception('The "order" authorizations aren\'t implemented.');
- }
- // case when was placed using PayPal standard
- if (\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT == $this->_order->getState()
- && !$this->getRequestData('transaction_entity')
- ) {
- $this->_registerPaymentCapture();
- return;
- }
- $this->_importPaymentInformation();
- $this->_order->getPayment()
- ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainPendingReason($reason)))
- ->setTransactionId($this->getRequestData('txn_id'))
- ->setIsTransactionClosed(0)
- ->update(false);
- $this->_order->save();
- }
- /**
- * Register authorized payment
- *
- * @return void
- */
- protected function _registerPaymentAuthorization()
- {
- /** @var $payment \Magento\Sales\Model\Order\Payment */
- $payment = $this->_order->getPayment();
- if ($this->_order->canFetchPaymentReviewUpdate()) {
- $payment->update(true);
- } else {
- $this->_importPaymentInformation();
- $payment->setPreparedMessage($this->_createIpnComment(''))
- ->setTransactionId($this->getRequestData('txn_id'))
- ->setParentTransactionId($this->getRequestData('parent_txn_id'))
- ->setCurrencyCode($this->getRequestData('mc_currency'))
- ->setIsTransactionClosed(0)
- ->registerAuthorizationNotification($this->getRequestData('mc_gross'));
- }
- if (!$this->_order->getEmailSent()) {
- $this->orderSender->send($this->_order);
- }
- $this->_order->save();
- }
- /**
- * The status "Processed" is used when all Masspayments are successful
- *
- * @return void
- */
- protected function _registerMasspaymentsSuccess()
- {
- $comment = $this->_createIpnComment('', true);
- $comment->save();
- }
- /**
- * Process payment reversal and cancelled reversal notification
- *
- * @return void
- */
- protected function _registerPaymentReversal()
- {
- $reasonCode = $this->getRequestData('reason_code');
- $reasonComment = $this->_paypalInfo->explainReasonCode($reasonCode);
- $notificationAmount = $this->_order->getBaseCurrency()
- ->formatTxt(
- $this->getRequestData('mc_gross') + $this->getRequestData('mc_fee')
- );
- $paymentStatus = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
- $orderStatus = $paymentStatus ==
- Info::PAYMENTSTATUS_REVERSED ? Info::ORDER_STATUS_REVERSED : Info::ORDER_STATUS_CANCELED_REVERSAL;
- //Change order status to PayPal Reversed/PayPal Cancelled Reversal if it is possible.
- $message = __(
- 'IPN "%1". %2 Transaction amount %3. Transaction ID: "%4"',
- $this->getRequestData('payment_status'),
- $reasonComment,
- $notificationAmount,
- $this->getRequestData('txn_id')
- );
- $this->_order->setStatus($orderStatus);
- $this->_order->addStatusHistoryComment($message, $orderStatus)
- ->setIsCustomerNotified(false)
- ->save();
- }
- /**
- * Process a refund
- *
- * @return void
- */
- protected function _registerPaymentRefund()
- {
- $this->_importPaymentInformation();
- $reason = $this->getRequestData('reason_code');
- $isRefundFinal = !$this->_paypalInfo->isReversalDisputable($reason);
- $payment = $this->_order->getPayment()
- ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainReasonCode($reason)))
- ->setTransactionId($this->getRequestData('txn_id'))
- ->setParentTransactionId($this->getRequestData('parent_txn_id'))
- ->setIsTransactionClosed($isRefundFinal)
- ->registerRefundNotification(-1 * $this->getRequestData('mc_gross'));
- $this->_order->save();
- // TODO: there is no way to close a capture right now
- $creditMemo = $payment->getCreatedCreditmemo();
- if ($creditMemo) {
- $this->creditmemoSender->send($creditMemo);
- $this->_order->addStatusHistoryComment(
- __('You notified customer about creditmemo #%1.', $creditMemo->getIncrementId())
- )
- ->setIsCustomerNotified(true)
- ->save();
- }
- }
- /**
- * Process voided authorization
- *
- * @return void
- */
- protected function _registerPaymentVoid()
- {
- $this->_importPaymentInformation();
- $parentTxnId = $this->getRequestData('transaction_entity') == 'auth'
- ? $this->getRequestData('txn_id')
- : $this->getRequestData('parent_txn_id');
- $this->_order->getPayment()
- ->setPreparedMessage($this->_createIpnComment(''))
- ->setParentTransactionId($parentTxnId)
- ->registerVoidNotification();
- $this->_order->save();
- }
- /**
- * Map payment information from IPN to payment object
- * Returns true if there were changes in information
- *
- * @return bool
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- * @SuppressWarnings(PHPMD.NPathComplexity)
- */
- protected function _importPaymentInformation()
- {
- $payment = $this->_order->getPayment();
- $was = $payment->getAdditionalInformation();
- // collect basic information
- $from = [];
- foreach ([
- Info::PAYER_ID,
- 'payer_email' => Info::PAYER_EMAIL,
- Info::PAYER_STATUS,
- Info::ADDRESS_STATUS,
- Info::PROTECTION_EL,
- Info::PAYMENT_STATUS,
- Info::PENDING_REASON,
- ] as $privateKey => $publicKey) {
- if (is_int($privateKey)) {
- $privateKey = $publicKey;
- }
- $value = $this->getRequestData($privateKey);
- if ($value) {
- $from[$publicKey] = $value;
- }
- }
- if (isset($from['payment_status'])) {
- $from['payment_status'] = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
- }
- // collect fraud filters
- $fraudFilters = [];
- for ($i = 1; $value = $this->getRequestData("fraud_management_pending_filters_{$i}"); $i++) {
- $fraudFilters[] = $value;
- }
- if ($fraudFilters) {
- $from[Info::FRAUD_FILTERS] = $fraudFilters;
- }
- $this->_paypalInfo->importToPayment($from, $payment);
- /**
- * Detect pending payment, frauds
- * TODO: implement logic in one place
- * @see \Magento\Paypal\Model\Pro::importPaymentInfo()
- */
- if (Info::isPaymentReviewRequired($payment)) {
- $payment->setIsTransactionPending(true);
- if ($fraudFilters) {
- $payment->setIsFraudDetected(true);
- }
- }
- if (Info::isPaymentSuccessful($payment)) {
- $payment->setIsTransactionApproved(true);
- } elseif (Info::isPaymentFailed($payment)) {
- $payment->setIsTransactionDenied(true);
- }
- return $was != $payment->getAdditionalInformation();
- }
- /**
- * Generate an "IPN" comment with additional explanation.
- * Returns the generated comment or order status history object
- *
- * @param string $comment
- * @param bool $addToHistory
- * @return string|\Magento\Sales\Model\Order\Status\History
- */
- protected function _createIpnComment($comment = '', $addToHistory = false)
- {
- $message = __('IPN "%1"', $this->getRequestData('payment_status'));
- if ($comment) {
- $message .= ' ' . $comment;
- }
- if ($addToHistory) {
- $message = $this->_order->addStatusHistoryComment($message);
- $message->setIsCustomerNotified(null);
- }
- return $message;
- }
- }
|