From 0914b3fe901df3ff2918e39a133931ddfb2290d7 Mon Sep 17 00:00:00 2001 From: Uli Ehler Date: Thu, 17 Jul 2025 14:17:23 +0200 Subject: [PATCH 1/2] PISHPW-273: fixed stock handling, cancel order positions if order is cancelled --- Components/Order/OrderUpdater.php | 21 ++ Components/Services/StockService.php | 26 ++- Resources/services/components/mixed.xml | 3 +- Resources/services/services.xml | 6 +- Services/IsMolliePaymentValidator.php | 36 +++ Subscriber/StockSubscriber.php | 17 +- .../Services/IsMolliePaymentValidatorTest.php | 54 +++++ .../Subscriber/StockSubscriberTest.php | 209 ++++++++++++++++++ 8 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 Services/IsMolliePaymentValidator.php create mode 100644 Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php create mode 100644 Tests/PHPUnit/Subscriber/StockSubscriberTest.php diff --git a/Components/Order/OrderUpdater.php b/Components/Order/OrderUpdater.php index 74f14a99..77accfb8 100644 --- a/Components/Order/OrderUpdater.php +++ b/Components/Order/OrderUpdater.php @@ -6,6 +6,7 @@ use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\ORMException; use MollieShopware\Components\Config; +use MollieShopware\Components\Constants\PaymentStatus; use MollieShopware\Components\StatusConverter\OrderStatusConverter; use MollieShopware\Components\StatusConverter\PaymentStatusConverter; use MollieShopware\Components\Validator\PaymentStatusMailValidator; @@ -15,12 +16,15 @@ use Psr\Log\LoggerInterface; use Shopware\Components\ContainerAwareEventManager; use Shopware\Components\Model\ModelManager; +use Shopware\Models\Article\Detail; +use Shopware\Models\Order\DetailStatus; use Shopware\Models\Order\History; use Shopware\Models\Order\Order; use sOrder; class OrderUpdater { + const ORDER_DETAIL_STATUS_CANCELLED = 2; /** * @var LoggerInterface @@ -408,6 +412,23 @@ private function updateOrderStatus(Order $order, $mollieStatus, $sendMail) ); } + if ($mollieStatus === PaymentStatus::MOLLIE_PAYMENT_CANCELED) { + $this->cancelOrderPositions($order); + } + return true; } + + private function cancelOrderPositions(Order $order) + { + /** @var DetailStatus $cancelledStatus */ + $cancelledStatus = $this->modelManager->find(DetailStatus::class, self::ORDER_DETAIL_STATUS_CANCELLED); + + /** @var Detail $detail */ + foreach ($order->getDetails() as $detail) { + $detail->setStatus($cancelledStatus); + } + + $this->modelManager->flush(); + } } diff --git a/Components/Services/StockService.php b/Components/Services/StockService.php index 68348379..bb1ea022 100644 --- a/Components/Services/StockService.php +++ b/Components/Services/StockService.php @@ -4,7 +4,10 @@ use Enlight_Components_Db_Adapter_Pdo_Mysql; use MollieShopware\Components\Config; +use MollieShopware\Services\IsMolliePaymentValidator; use Psr\Log\LoggerInterface; +use Shopware\Models\Payment\Payment; +use function sprintf; class StockService { @@ -31,18 +34,29 @@ class StockService */ private $config; + /** + * @var IsMolliePaymentValidator + */ + private $isMolliePaymentValidator; + /** * @param OrderService $orderService * @param Enlight_Components_Db_Adapter_Pdo_Mysql $db * @param LoggerInterface $logger * @param mixed $config */ - public function __construct($orderService, $config, $db, $logger) - { + public function __construct( + OrderService $orderService, + Config $config, + Enlight_Components_Db_Adapter_Pdo_Mysql $db, + LoggerInterface $logger, + IsMolliePaymentValidator $isMolliePaymentValidator, + ) { $this->orderService = $orderService; $this->db = $db; $this->logger = $logger; $this->config = $config; + $this->isMolliePaymentValidator = $isMolliePaymentValidator; } @@ -59,6 +73,14 @@ public function updateOrderStocks($orderId, $reset = true) } $order = $this->orderService->getOrderById($orderId); + /** @var Payment $payment */ + $payment = $order->getPayment(); + if (!$this->isMolliePaymentValidator->validate($payment)) { + $this->logger->debug(sprintf('payment "%s" is not a mollie payment', $payment->getName())); + + return; + } + $this->logger->debug('Start to reset the stocks for order', ['orderId' => $orderId]); foreach ($order->getDetails() as $orderDetail) { diff --git a/Resources/services/components/mixed.xml b/Resources/services/components/mixed.xml index 3313066a..d99c16bf 100644 --- a/Resources/services/components/mixed.xml +++ b/Resources/services/components/mixed.xml @@ -206,7 +206,8 @@ + - \ No newline at end of file + diff --git a/Resources/services/services.xml b/Resources/services/services.xml index 935182a9..25edaf62 100644 --- a/Resources/services/services.xml +++ b/Resources/services/services.xml @@ -16,6 +16,10 @@ + + %mollie_shopware.plugin_name% + - \ No newline at end of file + diff --git a/Services/IsMolliePaymentValidator.php b/Services/IsMolliePaymentValidator.php new file mode 100644 index 00000000..6b46430b --- /dev/null +++ b/Services/IsMolliePaymentValidator.php @@ -0,0 +1,36 @@ +pluginName = $pluginName; + } + + /** + * @param Payment $payment + * @return bool + */ + public function validate(Payment $payment) + { + /** @var null|Plugin $plugin */ + $plugin = $payment->getPlugin(); + + if (!($plugin instanceof Plugin)) { + return false; + } + + return $plugin->getName() === $this->pluginName; + } +} diff --git a/Subscriber/StockSubscriber.php b/Subscriber/StockSubscriber.php index b7eb8811..bd56a485 100644 --- a/Subscriber/StockSubscriber.php +++ b/Subscriber/StockSubscriber.php @@ -4,7 +4,7 @@ use Enlight\Event\SubscriberInterface; use Enlight_Event_EventArgs; -use Enlight_Hook_HookArgs; +use function in_array; use MollieShopware\Components\Services\StockService; use Shopware\Components\Model\ModelManager; use Shopware\Models\Order\Order; @@ -12,6 +12,11 @@ class StockSubscriber implements SubscriberInterface { + const PAYMENT_STATE_FOR_STOCK_INCREASE = [ + Status::PAYMENT_STATE_COMPLETELY_PAID, + Status::PAYMENT_STATE_THE_PAYMENT_HAS_BEEN_ORDERED, + ]; + /** * @var StockService */ @@ -42,7 +47,7 @@ public function increaseStocks(Enlight_Event_EventArgs $eventArgs) $orderId = (int)$eventArgs->get('orderId'); $paymentStatusId = (int)$eventArgs->get('paymentStatusId'); - if ($paymentStatusId !== Status::PAYMENT_STATE_COMPLETELY_PAID) { + if (!in_array($paymentStatusId, self::PAYMENT_STATE_FOR_STOCK_INCREASE, true)) { return; } @@ -74,7 +79,13 @@ public function entityEvent(Enlight_Event_EventArgs $eventArgs) if ($oldId === $newId) { return; } - if ($newId !== Status::PAYMENT_STATE_COMPLETELY_PAID) { + + if ($oldId === Status::PAYMENT_STATE_THE_PAYMENT_HAS_BEEN_ORDERED) { + // stock was already updated so we may not update it again + return; + } + + if (!in_array($newId, self::PAYMENT_STATE_FOR_STOCK_INCREASE, true)) { return; } diff --git a/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php b/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php new file mode 100644 index 00000000..ec4a57c8 --- /dev/null +++ b/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php @@ -0,0 +1,54 @@ +isMolliePaymentValidator = new IsMolliePaymentValidator('MollieShopware'); + } + + /** + * @dataProvider validateDataProvider + */ + public function testValidate(Payment $payment, bool $expected) + { + $this->assertSame($expected, $this->isMolliePaymentValidator->validate($payment)); + } + + public static function validateDataProvider() + { + $molliePlugin = new Plugin(); + $molliePlugin->setName('MollieShopware'); + + $prepayment = new Payment(); + $prepayment->setName('prepayment'); + + $invoice = new Payment(); + $invoice->setName('invoice'); + + $creditCard = new Payment(); + $creditCard->setName(MollieShopware::PAYMENT_PREFIX . 'credit_card'); + $creditCard->setPlugin($molliePlugin); + + $klarnaPayNow = new Payment(); + $klarnaPayNow->setName(MollieShopware::PAYMENT_PREFIX . 'klarna_pay_now'); + $klarnaPayNow->setPlugin($molliePlugin); + + return [ + [$prepayment, false], + [$invoice, false], + [$creditCard, true], + [$klarnaPayNow, true], + ]; + } +} diff --git a/Tests/PHPUnit/Subscriber/StockSubscriberTest.php b/Tests/PHPUnit/Subscriber/StockSubscriberTest.php new file mode 100644 index 00000000..231d6ce2 --- /dev/null +++ b/Tests/PHPUnit/Subscriber/StockSubscriberTest.php @@ -0,0 +1,209 @@ +assertSame($expected, StockSubscriber::PAYMENT_STATE_FOR_STOCK_INCREASE); + } + + public function testGetSubscribedEvents() + { + $expected = [ + 'Shopware_Modules_Order_SaveOrder_OrderCreated' => 'resetStocks', + 'sOrder::setPaymentStatus::after' => 'increaseStocks', + 'Shopware\Models\Order\Order::postUpdate' => 'entityEvent', + ]; + + $this->assertSame($expected, StockSubscriber::getSubscribedEvents()); + } + + public function testResetStocks() + { + $orderId = 123; + + $stockServiceMock = $this->createMock(StockService::class); + $stockServiceMock->expects(self::once())->method('updateOrderStocks')->with($orderId, true); + + $event = new Enlight_Event_EventArgs([ + 'orderId' => $orderId, + ]); + + $stockSubscriber = new StockSubscriber($stockServiceMock); + $stockSubscriber->resetStocks($event); + } + + /** + * @dataProvider increaseStocksDataProvider + * @param mixed $paymentStatusId + * @param mixed $methodIsExpectedToBeCalled + */ + public function testIncreaseStocks($paymentStatusId, $methodIsExpectedToBeCalled) + { + $orderId = 123; + + $expectedExecutionCount = $methodIsExpectedToBeCalled ? self::once() : self::never(); + + $stockServiceMock = $this->createMock(StockService::class); + $stockServiceMock->expects($expectedExecutionCount)->method('updateOrderStocks')->with($orderId, false); + + $event = new Enlight_Event_EventArgs([ + 'orderId' => $orderId, + 'paymentStatusId' => $paymentStatusId, + ]); + + $stockSubscriber = new StockSubscriber($stockServiceMock); + $stockSubscriber->increaseStocks($event); + } + + public static function increaseStocksDataProvider() + { + return [ + [Status::PAYMENT_STATE_PARTIALLY_INVOICED, false], + [Status::PAYMENT_STATE_COMPLETELY_INVOICED, false], + [Status::PAYMENT_STATE_PARTIALLY_PAID, false], + [Status::PAYMENT_STATE_COMPLETELY_PAID, true], + [Status::PAYMENT_STATE_1ST_REMINDER, false], + [Status::PAYMENT_STATE_2ND_REMINDER, false], + [Status::PAYMENT_STATE_3RD_REMINDER, false], + [Status::PAYMENT_STATE_ENCASHMENT, false], + [Status::PAYMENT_STATE_OPEN, false], + [Status::PAYMENT_STATE_RESERVED, false], + [Status::PAYMENT_STATE_DELAYED, false], + [Status::PAYMENT_STATE_RE_CREDITING, false], + [Status::PAYMENT_STATE_REVIEW_NECESSARY, false], + [Status::PAYMENT_STATE_NO_CREDIT_APPROVED, false], + [Status::PAYMENT_STATE_THE_CREDIT_HAS_BEEN_PRELIMINARILY_ACCEPTED, false], + [Status::PAYMENT_STATE_THE_CREDIT_HAS_BEEN_ACCEPTED, false], + [Status::PAYMENT_STATE_THE_PAYMENT_HAS_BEEN_ORDERED, true], + [Status::PAYMENT_STATE_A_TIME_EXTENSION_HAS_BEEN_REGISTERED, false], + [Status::PAYMENT_STATE_THE_PROCESS_HAS_BEEN_CANCELLED, false], + ]; + } + + public function testEntityEventPaymentStatusHasntChanged() + { + $stockServiceMock = $this->createMock(StockService::class); + $stockServiceMock->expects(self::never())->method('updateOrderStocks'); + + $unitOfWorkMock = $this->createMock(UnitOfWork::class); + $unitOfWorkMock->method('getEntityChangeSet')->willReturn([ + 'foo' => 'bar', + ]); + + $entityManagerMock = $this->createMock(EntityManagerInterface::class); + $entityManagerMock->method('getUnitOfWork')->willReturn($unitOfWorkMock); + + $event = new Enlight_Event_EventArgs([ + 'entityManager' => $entityManagerMock, + 'entity' => new Order(), + ]); + + $stockSubscriber = new StockSubscriber($stockServiceMock); + $stockSubscriber->entityEvent($event); + } + + /** + * @dataProvider entityEventDataProvider + * @param mixed $methodIsExpectedToBeCalled + */ + public function testEntityEventOldPaymentStatusIsNewPaymentStatus(array $changeSet, $methodIsExpectedToBeCalled) + { + $orderId = 123; + $expectedExecutionCount = $methodIsExpectedToBeCalled ? self::once() : self::never(); + + $stockServiceMock = $this->createMock(StockService::class); + $stockServiceMock->expects($expectedExecutionCount)->method('updateOrderStocks')->with($orderId, false); + + $unitOfWorkMock = $this->createMock(UnitOfWork::class); + $unitOfWorkMock->method('getEntityChangeSet')->willReturn($changeSet); + + $entityManagerMock = $this->createMock(EntityManagerInterface::class); + $entityManagerMock->method('getUnitOfWork')->willReturn($unitOfWorkMock); + + $order = new Order(); + $reflection = new ReflectionObject($order); + $reflectionProperty = $reflection->getProperty('id'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($order, $orderId); + + $event = new Enlight_Event_EventArgs([ + 'entityManager' => $entityManagerMock, + 'entity' => $order, + ]); + + $stockSubscriber = new StockSubscriber($stockServiceMock); + $stockSubscriber->entityEvent($event); + } + + public static function entityEventDataProvider() + { + return [ + 'emptyChangeSet' => [ + [], + false, + ], + 'old id is new id' => [ + [ + 'paymentStatus' => [ + self::getPaymentStatus(Status::PAYMENT_STATE_COMPLETELY_PAID), + self::getPaymentStatus(Status::PAYMENT_STATE_COMPLETELY_PAID), + ], + ], + false, + ], + 'old status is payment has been ordered' => [ + [ + 'paymentStatus' => [ + self::getPaymentStatus(Status::PAYMENT_STATE_THE_PAYMENT_HAS_BEEN_ORDERED), + self::getPaymentStatus(Status::PAYMENT_STATE_COMPLETELY_PAID), + ], + ], + false, + ], + 'new status is not completely paid' => [ + [ + 'paymentStatus' => [ + self::getPaymentStatus(Status::PAYMENT_STATE_OPEN), + self::getPaymentStatus(Status::PAYMENT_STATE_PARTIALLY_PAID), + ], + ], + false, + ], + 'completely paid, update stocks' => [ + [ + 'paymentStatus' => [ + self::getPaymentStatus(Status::PAYMENT_STATE_OPEN), + self::getPaymentStatus(Status::PAYMENT_STATE_COMPLETELY_PAID), + ], + ], + true, + ], + ]; + } + + private static function getPaymentStatus($paymentStatusId) + { + $paymentStatus = new Status(); + $paymentStatus->setId($paymentStatusId); + + return $paymentStatus; + } +} From cc2a54a83cffbc0b3c7b8b78889717024a89ef3e Mon Sep 17 00:00:00 2001 From: Uli Ehler Date: Thu, 17 Jul 2025 14:35:15 +0200 Subject: [PATCH 2/2] PISHPW-273: fixed typehints --- Components/Services/StockService.php | 12 ++++-------- .../Services/IsMolliePaymentValidatorTest.php | 5 ++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Components/Services/StockService.php b/Components/Services/StockService.php index bb1ea022..f3fae078 100644 --- a/Components/Services/StockService.php +++ b/Components/Services/StockService.php @@ -43,15 +43,11 @@ class StockService * @param OrderService $orderService * @param Enlight_Components_Db_Adapter_Pdo_Mysql $db * @param LoggerInterface $logger - * @param mixed $config + * @param Config $config + * @param IsMolliePaymentValidator $isMolliePaymentValidator */ - public function __construct( - OrderService $orderService, - Config $config, - Enlight_Components_Db_Adapter_Pdo_Mysql $db, - LoggerInterface $logger, - IsMolliePaymentValidator $isMolliePaymentValidator, - ) { + public function __construct($orderService, $config, $db, $logger, $isMolliePaymentValidator) + { $this->orderService = $orderService; $this->db = $db; $this->logger = $logger; diff --git a/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php b/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php index ec4a57c8..972113cc 100644 --- a/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php +++ b/Tests/PHPUnit/Services/IsMolliePaymentValidatorTest.php @@ -10,7 +10,10 @@ class IsMolliePaymentValidatorTest extends TestCase { - private IsMolliePaymentValidator $isMolliePaymentValidator; + /** + * @var IsMolliePaymentValidator + */ + private $isMolliePaymentValidator; protected function setUp(): void {