421 lines
16 KiB
PHP
421 lines
16 KiB
PHP
<?php
|
|
|
|
use Paynow\Model\Payment\Status;
|
|
|
|
/**
|
|
* NOTICE OF LICENSE
|
|
*
|
|
* This source file is subject to the MIT License (MIT)
|
|
* that is bundled with this package in the file LICENSE.md.
|
|
*
|
|
* @author mElements S.A.
|
|
* @copyright mElements S.A.
|
|
* @license MIT License
|
|
*/
|
|
|
|
class PaynowOrderStateProcessor
|
|
{
|
|
/** @var Paynow */
|
|
public $module;
|
|
|
|
/** @var \PaynowLockingHelper */
|
|
private $lockingHelper;
|
|
|
|
public function __construct($module)
|
|
{
|
|
$this->module = $module;
|
|
$this->lockingHelper = new PaynowLockingHelper();
|
|
}
|
|
|
|
/**
|
|
* @throws \PrestaShopException
|
|
* @throws \PaynowNotificationRetryProcessing
|
|
* @throws \PaynowNotificationStopProcessing
|
|
*/
|
|
public function processNotification($data)
|
|
{
|
|
PaynowLogger::info('Lock checking...', $data);
|
|
|
|
$externalIdForLockingSystem = $data['externalId'] ?? $data['paymentId'] ?? 'unknown';
|
|
if ($this->lockingHelper->checkAndCreate($externalIdForLockingSystem)) {
|
|
for ($i = 1; $i<=3; $i++) {
|
|
sleep(1);
|
|
$isNotificationLocked = $this->lockingHelper->checkAndCreate($externalIdForLockingSystem);
|
|
if (!$isNotificationLocked) {
|
|
break;
|
|
} elseif ($i == 3) {
|
|
throw new PaynowNotificationRetryProcessing(
|
|
'Skipped processing. Previous notification is still processing.',
|
|
$data
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
PaynowLogger::info('Lock passed successfully, notification validation starting.', $data);
|
|
|
|
if (empty($data['modifiedAt'])) {
|
|
$data['modifiedAt'] = (new DateTime('now', new DateTimeZone(Configuration::get('PS_TIMEZONE'))))->format('Y-m-d H:i:s');
|
|
} else {
|
|
$data['modifiedAt'] = str_replace('T', ' ', $data['modifiedAt']);
|
|
}
|
|
|
|
$isNew = $data['status'] == Status::STATUS_NEW;
|
|
$isConfirmed = $data['status'] == Status::STATUS_CONFIRMED;
|
|
|
|
// Delay NEW status, in case when API sends notifications in bundle,
|
|
// status NEW should finish processing at the very end
|
|
if ($isNew) {
|
|
sleep(1);
|
|
}
|
|
|
|
/** @var \PaynowPaymentData $payment */
|
|
$payment = PaynowPaymentData::getActiveByExternalId($data['externalId'], true, $data['paymentId'] ?? 'unknown');
|
|
|
|
if (empty($payment)) {
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Skipped processing. Payment, Order or Cart not found.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
$data['activePaymentId'] = $payment->id_payment;
|
|
$data['activePaymentStatus'] = $payment->status;
|
|
$data['activePaymentDate'] = $payment->sent_at;
|
|
|
|
if ($payment->status === Status::STATUS_CONFIRMED) {
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Skipped processing. An order already has a paid status.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
if ($data['status'] == $payment->status
|
|
&& $data['paymentId'] == $payment->id_payment) {
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
sprintf(
|
|
'Skipped processing. Transition status (%s) already consumed.',
|
|
$payment->status
|
|
),
|
|
$data
|
|
);
|
|
}
|
|
|
|
// proceed with creating order strategy
|
|
if ($payment->id_order == '0') {
|
|
$cart = new Cart((int)$payment->id_cart);
|
|
$canProcessCreateOrder = PaynowHelper::canProcessCreateOrder(
|
|
(int)$payment->id_order,
|
|
$data['status'],
|
|
(int)$payment->locked,
|
|
$cart->orderExists()
|
|
);
|
|
|
|
if ($canProcessCreateOrder) {
|
|
if ($payment->id_payment != $data['paymentId']) {
|
|
PaynowPaymentData::updatePaymentIdByExternalId($data['externalId'], $data['paymentId']);
|
|
}
|
|
|
|
$data['cartId'] = $payment->id_cart;
|
|
$data['paymentLocked'] = $payment->locked;
|
|
PaynowLogger::info(
|
|
'Processing notification to create new order from cart',
|
|
$data
|
|
);
|
|
if (method_exists($cart, 'getCartTotalPrice')) {
|
|
$cartTotalPrice = $cart->getCartTotalPrice();
|
|
} else {
|
|
$cartTotalPrice = $this->getCartTotalPrice($cart);
|
|
}
|
|
if ((float)$payment->total !== $cartTotalPrice) {
|
|
$data['cartTotalPrice'] = $cartTotalPrice;
|
|
$data['paymentTotal'] = (float)$payment->total;
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Inconsistent payment and cart amount.',
|
|
$data
|
|
);
|
|
}
|
|
PaynowHelper::createOrder($cart, $data['externalId'], $data['paymentId']);
|
|
$payment = PaynowPaymentData::getActiveByExternalId($data['externalId']);
|
|
}
|
|
}
|
|
|
|
if ($data['paymentId'] != $payment->id_payment && !$isNew && !$isConfirmed) {
|
|
$this->retryProcessingNTimes(
|
|
$payment,
|
|
'Skipped processing. Order has another active payment.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
if (!empty($payment->sent_at) && $payment->sent_at > $data['modifiedAt'] && !$isConfirmed) {
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Skipped processing. Order has newer status. Time travels are prohibited.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
$order = new Order($payment->id_order);
|
|
if (!Validate::isLoadedObject($order)) {
|
|
$data['payment->id_order'] = var_export($payment->id_order, true);
|
|
$this->retryProcessingNTimes(
|
|
$payment,
|
|
'Skipped processing. Order not found.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
if ($order->module !== $this->module->name) {
|
|
$data['orderModule'] = $order->module;
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Skipped processing. Order has other payment (other payment driver).',
|
|
$data
|
|
);
|
|
}
|
|
|
|
if ((int)$order->current_state === (int)Configuration::get('PAYNOW_ORDER_CONFIRMED_STATE')) {
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
throw new PaynowNotificationStopProcessing(
|
|
'Skipped processing. The order has already paid status.',
|
|
$data
|
|
);
|
|
}
|
|
|
|
if (!$this->isCorrectStatus($payment->status, $data['status']) && !$isConfirmed && !$isNew) {
|
|
$this->retryProcessingNTimes(
|
|
$payment,
|
|
sprintf(
|
|
'Skipped processing. Status transition is incorrect (%s => %s).',
|
|
$payment->status,
|
|
$data['status']
|
|
),
|
|
$data
|
|
);
|
|
}
|
|
|
|
switch ($data['status']) {
|
|
case Paynow\Model\Payment\Status::STATUS_NEW:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_INITIAL_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
break;
|
|
case Paynow\Model\Payment\Status::STATUS_REJECTED:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_REJECTED_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
break;
|
|
case Paynow\Model\Payment\Status::STATUS_CONFIRMED:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_CONFIRMED_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
$this->addOrderPayment($order, $data['paymentId']);
|
|
break;
|
|
case Paynow\Model\Payment\Status::STATUS_ERROR:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_ERROR_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
break;
|
|
case Paynow\Model\Payment\Status::STATUS_ABANDONED:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_ABANDONED_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
break;
|
|
case Paynow\Model\Payment\Status::STATUS_EXPIRED:
|
|
$this->changeState(
|
|
$order,
|
|
(int)Configuration::get('PAYNOW_ORDER_EXPIRED_STATE'),
|
|
$data['status'],
|
|
$data['paymentId'],
|
|
$data['externalId']
|
|
);
|
|
break;
|
|
}
|
|
|
|
|
|
if ($isNew) {
|
|
PaynowPaymentData::create(
|
|
$data['paymentId'],
|
|
$data['status'],
|
|
$order->id,
|
|
$payment->id_cart,
|
|
$payment->order_reference,
|
|
$data['externalId'],
|
|
$payment->total,
|
|
$data['modifiedAt']
|
|
);
|
|
} else {
|
|
$payment->status = $data['status'];
|
|
$payment->sent_at = $data['modifiedAt'];
|
|
$payment->save();
|
|
}
|
|
|
|
$this->lockingHelper->delete($externalIdForLockingSystem);
|
|
|
|
}
|
|
|
|
private function changeState($order, $new_order_state_id, $payment_status, $id_payment, $external_id, $withEmail = true)
|
|
{
|
|
$history = new OrderHistory();
|
|
$history->id_order = $order->id;
|
|
if ($order->current_state != $new_order_state_id) {
|
|
PaynowLogger::info(
|
|
'Adding new state to order\'s history {externalId={}, orderId={}, orderReference={}, paymentId={}, paymentStatus={}, state={}}',
|
|
[
|
|
$external_id,
|
|
$order->id,
|
|
$order->reference,
|
|
$id_payment,
|
|
$payment_status,
|
|
$new_order_state_id
|
|
]
|
|
);
|
|
$history->changeIdOrderState(
|
|
$new_order_state_id,
|
|
$order->id
|
|
);
|
|
$history->addWithemail($withEmail);
|
|
PaynowLogger::info(
|
|
'Added new state to order\'s history {externalId={}, orderId={}, orderReference={}, paymentId={}, paymentStatus={}, state={}, historyId={}}',
|
|
[
|
|
$external_id,
|
|
$order->id,
|
|
$order->reference,
|
|
$id_payment,
|
|
$payment_status,
|
|
$new_order_state_id,
|
|
$history->id
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
private function isCorrectStatus($previous_status, $next_status): bool
|
|
{
|
|
$payment_status_flow = [
|
|
Paynow\Model\Payment\Status::STATUS_NEW => [
|
|
Paynow\Model\Payment\Status::STATUS_NEW,
|
|
Paynow\Model\Payment\Status::STATUS_PENDING,
|
|
Paynow\Model\Payment\Status::STATUS_ERROR,
|
|
Paynow\Model\Payment\Status::STATUS_CONFIRMED,
|
|
Paynow\Model\Payment\Status::STATUS_REJECTED,
|
|
Paynow\Model\Payment\Status::STATUS_EXPIRED
|
|
|
|
],
|
|
Paynow\Model\Payment\Status::STATUS_PENDING => [
|
|
Paynow\Model\Payment\Status::STATUS_NEW,
|
|
Paynow\Model\Payment\Status::STATUS_CONFIRMED,
|
|
Paynow\Model\Payment\Status::STATUS_REJECTED,
|
|
Paynow\Model\Payment\Status::STATUS_EXPIRED,
|
|
Paynow\Model\Payment\Status::STATUS_ABANDONED
|
|
],
|
|
Paynow\Model\Payment\Status::STATUS_REJECTED => [
|
|
Paynow\Model\Payment\Status::STATUS_CONFIRMED,
|
|
Paynow\Model\Payment\Status::STATUS_ABANDONED,
|
|
Paynow\Model\Payment\Status::STATUS_NEW
|
|
],
|
|
Paynow\Model\Payment\Status::STATUS_CONFIRMED => [],
|
|
Paynow\Model\Payment\Status::STATUS_ERROR => [
|
|
Paynow\Model\Payment\Status::STATUS_CONFIRMED,
|
|
Paynow\Model\Payment\Status::STATUS_REJECTED,
|
|
Paynow\Model\Payment\Status::STATUS_ABANDONED,
|
|
Paynow\Model\Payment\Status::STATUS_NEW
|
|
],
|
|
Paynow\Model\Payment\Status::STATUS_EXPIRED => [
|
|
Paynow\Model\Payment\Status::STATUS_NEW
|
|
],
|
|
Paynow\Model\Payment\Status::STATUS_ABANDONED => [
|
|
Paynow\Model\Payment\Status::STATUS_NEW
|
|
]
|
|
];
|
|
$previous_status_exists = isset($payment_status_flow[$previous_status]);
|
|
$is_change_possible = in_array($next_status, $payment_status_flow[$previous_status]);
|
|
return $previous_status_exists && $is_change_possible;
|
|
}
|
|
|
|
/**
|
|
* @param $order
|
|
* @param $id_payment
|
|
*/
|
|
private function addOrderPayment($order, $id_payment)
|
|
{
|
|
$payments = $order->getOrderPaymentCollection()->getResults();
|
|
if (count($payments) > 0) {
|
|
$payments[0]->transaction_id = $id_payment;
|
|
$payments[0]->update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param PaynowPaymentData $payment
|
|
* @param $message
|
|
* @param $data
|
|
* @param int $counter
|
|
* @throws \PaynowNotificationStopProcessing
|
|
* @throws \PaynowNotificationRetryProcessing
|
|
* @throws \PrestaShopException
|
|
*/
|
|
private function retryProcessingNTimes(PaynowPaymentData $payment, $message, $data, $counter = 5)
|
|
{
|
|
$payment->counter = (int)$payment->counter + 1;
|
|
$payment->save();
|
|
|
|
$data['counter'] = $payment->counter;
|
|
$this->lockingHelper->delete($data['externalId'] ?? $data['paymentId'] ?? 'unknown');
|
|
if ($payment->counter >= $counter) {
|
|
throw new PaynowNotificationStopProcessing($message, $data);
|
|
} else {
|
|
throw new PaynowNotificationRetryProcessing($message, $data);
|
|
}
|
|
}
|
|
|
|
private function getCartTotalPrice($cart): float
|
|
{
|
|
$summary = $cart->getSummaryDetails();
|
|
|
|
if (version_compare(_PS_VERSION_, '1.7.1.0', 'ge')) {
|
|
$id_order = (int)Order::getIdByCartId($cart->id);
|
|
} else {
|
|
$id_order = (int)Order::getOrderByCartId($cart->id);
|
|
}
|
|
|
|
$order = new Order($id_order);
|
|
|
|
if (Validate::isLoadedObject($order)) {
|
|
$taxCalculationMethod = $order->getTaxCalculationMethod();
|
|
} else {
|
|
$taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id);
|
|
}
|
|
|
|
return $taxCalculationMethod == PS_TAX_EXC ?
|
|
(float)$summary['total_price_without_tax'] :
|
|
(float)$summary['total_price'];
|
|
}
|
|
|
|
}
|