Ipn.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Paypal\Model;
  7. use Exception;
  8. use Magento\Framework\Exception\LocalizedException;
  9. use Magento\Sales\Model\Order\Email\Sender\CreditmemoSender;
  10. use Magento\Sales\Model\Order\Email\Sender\OrderSender;
  11. /**
  12. * PayPal Instant Payment Notification processor model
  13. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  14. */
  15. class Ipn extends \Magento\Paypal\Model\AbstractIpn implements IpnInterface
  16. {
  17. /**
  18. * @var \Magento\Sales\Model\Order
  19. */
  20. protected $_order;
  21. /**
  22. * @var \Magento\Sales\Model\OrderFactory
  23. */
  24. protected $_orderFactory;
  25. /**
  26. * PayPal info instance
  27. *
  28. * @var Info
  29. */
  30. protected $_paypalInfo;
  31. /**
  32. * @var OrderSender
  33. */
  34. protected $orderSender;
  35. /**
  36. * @var CreditmemoSender
  37. */
  38. protected $creditmemoSender;
  39. /**
  40. * @param \Magento\Paypal\Model\ConfigFactory $configFactory
  41. * @param \Psr\Log\LoggerInterface $logger
  42. * @param \Magento\Framework\HTTP\Adapter\CurlFactory $curlFactory
  43. * @param \Magento\Sales\Model\OrderFactory $orderFactory
  44. * @param Info $paypalInfo
  45. * @param OrderSender $orderSender
  46. * @param CreditmemoSender $creditmemoSender
  47. * @param array $data
  48. */
  49. public function __construct(
  50. \Magento\Paypal\Model\ConfigFactory $configFactory,
  51. \Psr\Log\LoggerInterface $logger,
  52. \Magento\Framework\HTTP\Adapter\CurlFactory $curlFactory,
  53. \Magento\Sales\Model\OrderFactory $orderFactory,
  54. Info $paypalInfo,
  55. OrderSender $orderSender,
  56. CreditmemoSender $creditmemoSender,
  57. array $data = []
  58. ) {
  59. parent::__construct($configFactory, $logger, $curlFactory, $data);
  60. $this->_orderFactory = $orderFactory;
  61. $this->_paypalInfo = $paypalInfo;
  62. $this->orderSender = $orderSender;
  63. $this->creditmemoSender = $creditmemoSender;
  64. }
  65. /**
  66. * Get ipn data, send verification to PayPal, run corresponding handler
  67. *
  68. * @return void
  69. * @throws Exception
  70. */
  71. public function processIpnRequest()
  72. {
  73. $this->_addDebugData('ipn', $this->getRequestData());
  74. try {
  75. $this->_getConfig();
  76. $this->_postBack();
  77. $this->_processOrder();
  78. } catch (Exception $e) {
  79. $this->_addDebugData('exception', $e->getMessage());
  80. $this->_debug();
  81. throw $e;
  82. }
  83. $this->_debug();
  84. }
  85. /**
  86. * Get config with the method code and store id and validate
  87. *
  88. * @return \Magento\Paypal\Model\Config
  89. * @throws Exception
  90. */
  91. protected function _getConfig()
  92. {
  93. $order = $this->_getOrder();
  94. $methodCode = $order->getPayment()->getMethod();
  95. $parameters = ['params' => [$methodCode, $order->getStoreId()]];
  96. $this->_config = $this->_configFactory->create($parameters);
  97. if (!$this->_config->isMethodActive($methodCode) || !$this->_config->isMethodAvailable()) {
  98. throw new Exception(sprintf('The "%s" method isn\'t available.', $methodCode));
  99. }
  100. /** @link https://cms.paypal.com/cgi-bin/marketingweb?cmd=_render-content&content_ID=
  101. * developer/e_howto_admin_IPNIntro */
  102. // verify merchant email intended to receive notification
  103. $merchantEmail = $this->_config->getValue('businessAccount');
  104. if (!$merchantEmail) {
  105. return $this->_config;
  106. }
  107. $receiver = $this->getRequestData('business') ?: $this->getRequestData('receiver_email');
  108. if (strtolower($merchantEmail) != strtolower($receiver)) {
  109. throw new Exception(
  110. sprintf(
  111. 'The requested "%s" and the configured "%s" merchant emails don\'t match.',
  112. $receiver,
  113. $merchantEmail
  114. )
  115. );
  116. }
  117. return $this->_config;
  118. }
  119. /**
  120. * Load order
  121. *
  122. * @return \Magento\Sales\Model\Order
  123. * @throws Exception
  124. */
  125. protected function _getOrder()
  126. {
  127. $incrementId = $this->getRequestData('invoice');
  128. $this->_order = $this->_orderFactory->create()->loadByIncrementId($incrementId);
  129. if (!$this->_order->getId()) {
  130. throw new Exception(sprintf('The "%s" order ID is incorrect. Verify the ID and try again.', $incrementId));
  131. }
  132. return $this->_order;
  133. }
  134. /**
  135. * IPN workflow implementation
  136. * Everything should be added to order comments. In positive processing cases customer will get email notifications.
  137. * Admin will be notified on errors.
  138. *
  139. * @return void
  140. * @throws \Magento\Framework\Exception\LocalizedException
  141. */
  142. protected function _processOrder()
  143. {
  144. $this->_getConfig();
  145. try {
  146. // Handle payment_status
  147. $transactionType = $this->getRequestData('txn_type');
  148. switch ($transactionType) {
  149. // handle new case created
  150. case Info::TXN_TYPE_NEW_CASE:
  151. $this->_registerDispute();
  152. break;
  153. // handle new adjustment is created
  154. case Info::TXN_TYPE_ADJUSTMENT:
  155. $this->_registerAdjustment();
  156. break;
  157. //handle new transaction created
  158. default:
  159. $this->_registerTransaction();
  160. break;
  161. }
  162. } catch (\Magento\Framework\Exception\LocalizedException $e) {
  163. $comment = $this->_createIpnComment(__('Note: %1', $e->getMessage()), true);
  164. $comment->save();
  165. throw $e;
  166. }
  167. }
  168. /**
  169. * Process dispute notification
  170. *
  171. * @return void
  172. */
  173. protected function _registerDispute()
  174. {
  175. $reasonComment = $this->_paypalInfo->explainReasonCode($this->getRequestData('reason_code'));
  176. $caseType = $this->getRequestData('case_type');
  177. $caseTypeLabel = $this->_paypalInfo->getCaseTypeLabel($caseType);
  178. $caseId = $this->getRequestData('case_id');
  179. //Add IPN comment about registered dispute
  180. $message = __(
  181. 'IPN "%1". Case type "%2". Case ID "%3" %4',
  182. ucfirst($caseType),
  183. $caseTypeLabel,
  184. $caseId,
  185. $reasonComment
  186. );
  187. $this->_order->addStatusHistoryComment($message)->setIsCustomerNotified(false)->save();
  188. }
  189. /**
  190. * Process adjustment notification
  191. *
  192. * @return void
  193. */
  194. protected function _registerAdjustment()
  195. {
  196. $reasonCode = $this->getRequestData('reason_code');
  197. $reasonComment = $this->_paypalInfo->explainReasonCode($reasonCode);
  198. $notificationAmount = $this->_order->getBaseCurrency()->formatTxt($this->getRequestData('mc_gross'));
  199. // Add IPN comment about registered dispute
  200. $message = __(
  201. 'IPN "%1". A dispute has been resolved and closed. %2 Transaction amount %3.',
  202. ucfirst($reasonCode),
  203. $notificationAmount,
  204. $reasonComment
  205. );
  206. $this->_order->addStatusHistoryComment($message)->setIsCustomerNotified(false)->save();
  207. }
  208. /**
  209. * Process regular IPN notifications
  210. *
  211. * @return void
  212. * @throws \Magento\Framework\Exception\LocalizedException
  213. * @throws Exception
  214. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  215. */
  216. protected function _registerTransaction()
  217. {
  218. // Handle payment_status
  219. $paymentStatus = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
  220. switch ($paymentStatus) {
  221. // paid
  222. case Info::PAYMENTSTATUS_COMPLETED:
  223. $this->_registerPaymentCapture(true);
  224. break;
  225. // the holded payment was denied on paypal side
  226. case Info::PAYMENTSTATUS_DENIED:
  227. $this->_registerPaymentDenial();
  228. break;
  229. // customer attempted to pay via bank account, but failed
  230. case Info::PAYMENTSTATUS_FAILED:
  231. // cancel order
  232. $this->_registerPaymentFailure();
  233. break;
  234. // payment was obtained, but money were not captured yet
  235. case Info::PAYMENTSTATUS_PENDING:
  236. $this->_registerPaymentPending();
  237. break;
  238. case Info::PAYMENTSTATUS_PROCESSED:
  239. $this->_registerMasspaymentsSuccess();
  240. break;
  241. case Info::PAYMENTSTATUS_REVERSED:
  242. //break is intentionally omitted
  243. case Info::PAYMENTSTATUS_UNREVERSED:
  244. $this->_registerPaymentReversal();
  245. break;
  246. case Info::PAYMENTSTATUS_REFUNDED:
  247. $this->_registerPaymentRefund();
  248. break;
  249. // authorization expire/void
  250. case Info::PAYMENTSTATUS_EXPIRED:
  251. // break is intentionally omitted
  252. case Info::PAYMENTSTATUS_VOIDED:
  253. $this->_registerPaymentVoid();
  254. break;
  255. default:
  256. throw new Exception("The '{$paymentStatus}' payment status couldn't be handled.");
  257. }
  258. }
  259. /**
  260. * Process completed payment (either full or partial)
  261. *
  262. * @param bool $skipFraudDetection
  263. * @return void
  264. */
  265. protected function _registerPaymentCapture($skipFraudDetection = false)
  266. {
  267. if ($this->getRequestData('transaction_entity') == 'auth') {
  268. return;
  269. }
  270. $parentTransactionId = $this->getRequestData('parent_txn_id');
  271. $this->_importPaymentInformation();
  272. $payment = $this->_order->getPayment();
  273. $payment->setTransactionId($this->getRequestData('txn_id'));
  274. $payment->setCurrencyCode($this->getRequestData('mc_currency'));
  275. $payment->setPreparedMessage($this->_createIpnComment(''));
  276. $payment->setParentTransactionId($parentTransactionId);
  277. $payment->setShouldCloseParentTransaction('Completed' === $this->getRequestData('auth_status'));
  278. $payment->setIsTransactionClosed(0);
  279. $payment->registerCaptureNotification(
  280. $this->getRequestData('mc_gross'),
  281. $skipFraudDetection && $parentTransactionId
  282. );
  283. $this->_order->save();
  284. // notify customer
  285. $invoice = $payment->getCreatedInvoice();
  286. if ($invoice && !$this->_order->getEmailSent()) {
  287. $this->orderSender->send($this->_order);
  288. $this->_order->addStatusHistoryComment(
  289. __('You notified customer about invoice #%1.', $invoice->getIncrementId())
  290. )
  291. ->setIsCustomerNotified(true)
  292. ->save();
  293. }
  294. }
  295. /**
  296. * Process denied payment notification
  297. *
  298. * @return void
  299. * @throws Exception
  300. */
  301. protected function _registerPaymentDenial()
  302. {
  303. try {
  304. $this->_importPaymentInformation();
  305. $this->_order->getPayment()
  306. ->setTransactionId($this->getRequestData('txn_id'))
  307. ->setNotificationResult(true)
  308. ->setIsTransactionClosed(true)
  309. ->deny(false);
  310. $this->_order->save();
  311. } catch (LocalizedException $e) {
  312. if ($e->getMessage() != __('We cannot cancel this order.')) {
  313. throw $e;
  314. }
  315. }
  316. }
  317. /**
  318. * Treat failed payment as order cancellation
  319. *
  320. * @return void
  321. */
  322. protected function _registerPaymentFailure()
  323. {
  324. $this->_importPaymentInformation();
  325. $this->_order->registerCancellation($this->_createIpnComment(''))->save();
  326. }
  327. /**
  328. * Process payment pending notification
  329. *
  330. * @return void
  331. * @throws Exception
  332. */
  333. public function _registerPaymentPending()
  334. {
  335. $reason = $this->getRequestData('pending_reason');
  336. if ('authorization' === $reason) {
  337. $this->_registerPaymentAuthorization();
  338. return;
  339. }
  340. if ('order' === $reason) {
  341. throw new Exception('The "order" authorizations aren\'t implemented.');
  342. }
  343. // case when was placed using PayPal standard
  344. if (\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT == $this->_order->getState()
  345. && !$this->getRequestData('transaction_entity')
  346. ) {
  347. $this->_registerPaymentCapture();
  348. return;
  349. }
  350. $this->_importPaymentInformation();
  351. $this->_order->getPayment()
  352. ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainPendingReason($reason)))
  353. ->setTransactionId($this->getRequestData('txn_id'))
  354. ->setIsTransactionClosed(0)
  355. ->update(false);
  356. $this->_order->save();
  357. }
  358. /**
  359. * Register authorized payment
  360. *
  361. * @return void
  362. */
  363. protected function _registerPaymentAuthorization()
  364. {
  365. /** @var $payment \Magento\Sales\Model\Order\Payment */
  366. $payment = $this->_order->getPayment();
  367. if ($this->_order->canFetchPaymentReviewUpdate()) {
  368. $payment->update(true);
  369. } else {
  370. $this->_importPaymentInformation();
  371. $payment->setPreparedMessage($this->_createIpnComment(''))
  372. ->setTransactionId($this->getRequestData('txn_id'))
  373. ->setParentTransactionId($this->getRequestData('parent_txn_id'))
  374. ->setCurrencyCode($this->getRequestData('mc_currency'))
  375. ->setIsTransactionClosed(0)
  376. ->registerAuthorizationNotification($this->getRequestData('mc_gross'));
  377. }
  378. if (!$this->_order->getEmailSent()) {
  379. $this->orderSender->send($this->_order);
  380. }
  381. $this->_order->save();
  382. }
  383. /**
  384. * The status "Processed" is used when all Masspayments are successful
  385. *
  386. * @return void
  387. */
  388. protected function _registerMasspaymentsSuccess()
  389. {
  390. $comment = $this->_createIpnComment('', true);
  391. $comment->save();
  392. }
  393. /**
  394. * Process payment reversal and cancelled reversal notification
  395. *
  396. * @return void
  397. */
  398. protected function _registerPaymentReversal()
  399. {
  400. $reasonCode = $this->getRequestData('reason_code');
  401. $reasonComment = $this->_paypalInfo->explainReasonCode($reasonCode);
  402. $notificationAmount = $this->_order->getBaseCurrency()
  403. ->formatTxt(
  404. $this->getRequestData('mc_gross') + $this->getRequestData('mc_fee')
  405. );
  406. $paymentStatus = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
  407. $orderStatus = $paymentStatus ==
  408. Info::PAYMENTSTATUS_REVERSED ? Info::ORDER_STATUS_REVERSED : Info::ORDER_STATUS_CANCELED_REVERSAL;
  409. //Change order status to PayPal Reversed/PayPal Cancelled Reversal if it is possible.
  410. $message = __(
  411. 'IPN "%1". %2 Transaction amount %3. Transaction ID: "%4"',
  412. $this->getRequestData('payment_status'),
  413. $reasonComment,
  414. $notificationAmount,
  415. $this->getRequestData('txn_id')
  416. );
  417. $this->_order->setStatus($orderStatus);
  418. $this->_order->addStatusHistoryComment($message, $orderStatus)
  419. ->setIsCustomerNotified(false)
  420. ->save();
  421. }
  422. /**
  423. * Process a refund
  424. *
  425. * @return void
  426. */
  427. protected function _registerPaymentRefund()
  428. {
  429. $this->_importPaymentInformation();
  430. $reason = $this->getRequestData('reason_code');
  431. $isRefundFinal = !$this->_paypalInfo->isReversalDisputable($reason);
  432. $payment = $this->_order->getPayment()
  433. ->setPreparedMessage($this->_createIpnComment($this->_paypalInfo->explainReasonCode($reason)))
  434. ->setTransactionId($this->getRequestData('txn_id'))
  435. ->setParentTransactionId($this->getRequestData('parent_txn_id'))
  436. ->setIsTransactionClosed($isRefundFinal)
  437. ->registerRefundNotification(-1 * $this->getRequestData('mc_gross'));
  438. $this->_order->save();
  439. // TODO: there is no way to close a capture right now
  440. $creditMemo = $payment->getCreatedCreditmemo();
  441. if ($creditMemo) {
  442. $this->creditmemoSender->send($creditMemo);
  443. $this->_order->addStatusHistoryComment(
  444. __('You notified customer about creditmemo #%1.', $creditMemo->getIncrementId())
  445. )
  446. ->setIsCustomerNotified(true)
  447. ->save();
  448. }
  449. }
  450. /**
  451. * Process voided authorization
  452. *
  453. * @return void
  454. */
  455. protected function _registerPaymentVoid()
  456. {
  457. $this->_importPaymentInformation();
  458. $parentTxnId = $this->getRequestData('transaction_entity') == 'auth'
  459. ? $this->getRequestData('txn_id')
  460. : $this->getRequestData('parent_txn_id');
  461. $this->_order->getPayment()
  462. ->setPreparedMessage($this->_createIpnComment(''))
  463. ->setParentTransactionId($parentTxnId)
  464. ->registerVoidNotification();
  465. $this->_order->save();
  466. }
  467. /**
  468. * Map payment information from IPN to payment object
  469. * Returns true if there were changes in information
  470. *
  471. * @return bool
  472. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  473. * @SuppressWarnings(PHPMD.NPathComplexity)
  474. */
  475. protected function _importPaymentInformation()
  476. {
  477. $payment = $this->_order->getPayment();
  478. $was = $payment->getAdditionalInformation();
  479. // collect basic information
  480. $from = [];
  481. foreach ([
  482. Info::PAYER_ID,
  483. 'payer_email' => Info::PAYER_EMAIL,
  484. Info::PAYER_STATUS,
  485. Info::ADDRESS_STATUS,
  486. Info::PROTECTION_EL,
  487. Info::PAYMENT_STATUS,
  488. Info::PENDING_REASON,
  489. ] as $privateKey => $publicKey) {
  490. if (is_int($privateKey)) {
  491. $privateKey = $publicKey;
  492. }
  493. $value = $this->getRequestData($privateKey);
  494. if ($value) {
  495. $from[$publicKey] = $value;
  496. }
  497. }
  498. if (isset($from['payment_status'])) {
  499. $from['payment_status'] = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
  500. }
  501. // collect fraud filters
  502. $fraudFilters = [];
  503. for ($i = 1; $value = $this->getRequestData("fraud_management_pending_filters_{$i}"); $i++) {
  504. $fraudFilters[] = $value;
  505. }
  506. if ($fraudFilters) {
  507. $from[Info::FRAUD_FILTERS] = $fraudFilters;
  508. }
  509. $this->_paypalInfo->importToPayment($from, $payment);
  510. /**
  511. * Detect pending payment, frauds
  512. * TODO: implement logic in one place
  513. * @see \Magento\Paypal\Model\Pro::importPaymentInfo()
  514. */
  515. if (Info::isPaymentReviewRequired($payment)) {
  516. $payment->setIsTransactionPending(true);
  517. if ($fraudFilters) {
  518. $payment->setIsFraudDetected(true);
  519. }
  520. }
  521. if (Info::isPaymentSuccessful($payment)) {
  522. $payment->setIsTransactionApproved(true);
  523. } elseif (Info::isPaymentFailed($payment)) {
  524. $payment->setIsTransactionDenied(true);
  525. }
  526. return $was != $payment->getAdditionalInformation();
  527. }
  528. /**
  529. * Generate an "IPN" comment with additional explanation.
  530. * Returns the generated comment or order status history object
  531. *
  532. * @param string $comment
  533. * @param bool $addToHistory
  534. * @return string|\Magento\Sales\Model\Order\Status\History
  535. */
  536. protected function _createIpnComment($comment = '', $addToHistory = false)
  537. {
  538. $message = __('IPN "%1"', $this->getRequestData('payment_status'));
  539. if ($comment) {
  540. $message .= ' ' . $comment;
  541. }
  542. if ($addToHistory) {
  543. $message = $this->_order->addStatusHistoryComment($message);
  544. $message->setIsCustomerNotified(null);
  545. }
  546. return $message;
  547. }
  548. }