File: /var/www/vhost/disk-apps/magento.bikenow.co/vendor/magento/module-paypal/Model/Ipn.php
<?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;
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()) {
// phpcs:ignore Magento2.Exceptions.DirectThrow
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)) {
// phpcs:ignore Magento2.Exceptions.DirectThrow
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()) {
// phpcs:ignore Magento2.Exceptions.DirectThrow
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:
if ($this->_order->getState() === \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW) {
$this->_registerPaymentDenial();
} else {
$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:
// phpcs:ignore Magento2.Exceptions.DirectThrow
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);
if ($this->_order->getState() === Order::STATE_PENDING_PAYMENT) {
$this->_order->setState(Order::STATE_PROCESSING);
}
$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();
$payment = $this->_order->getPayment();
$payment->setTransactionId($this->getRequestData('txn_id'));
$payment->setPreparedMessage($this->_createIpnComment(''));
$payment->setNotificationResult(true);
$payment->setIsTransactionClosed(true);
$payment->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) {
// phpcs:ignore Magento2.Exceptions.DirectThrow
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 = [];
$index = 1;
while ($value = $this->getRequestData("fraud_management_pending_filters_{$index}")) {
$fraudFilters[] = $value;
$index++;
}
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;
}
}