diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 5f0063151..f0638fa31 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -50,6 +50,13 @@ + + + + + + + diff --git a/src/Resources/config/services/scheduled_tasks.xml b/src/Resources/config/services/scheduled_tasks.xml index 1c072edfc..5ab26627a 100755 --- a/src/Resources/config/services/scheduled_tasks.xml +++ b/src/Resources/config/services/scheduled_tasks.xml @@ -22,5 +22,20 @@ + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/subscriber.xml b/src/Resources/config/services/subscriber.xml index 3efd493f7..fad289545 100644 --- a/src/Resources/config/services/subscriber.xml +++ b/src/Resources/config/services/subscriber.xml @@ -67,9 +67,7 @@ - - - + diff --git a/src/ScheduledTask/OrderStatus/ExpireOrderTask.php b/src/ScheduledTask/OrderStatus/ExpireOrderTask.php new file mode 100644 index 000000000..4b062807a --- /dev/null +++ b/src/ScheduledTask/OrderStatus/ExpireOrderTask.php @@ -0,0 +1,19 @@ +getRepository(), $logger); + $this->logger = $logger; + $this->orderRepository = $orderRepository; + $this->orderExpireService = $orderExpireService; + } + + public function run(): void + { + $this->logger->info('Start resetting in_progress orders'); + + $context = new Context(new SystemSource()); + + $criteria = new Criteria(); + $criteria->addAssociation('transactions.stateMachineState'); + $criteria->getAssociation('transactions.stateMachineState')->addFilter(new EqualsFilter('technicalName', OrderStates::STATE_IN_PROGRESS)); + + $this->logger->debug('Search for orders which are in progress state'); + + $searchResult = $this->orderRepository->search($criteria, $context); + if ($searchResult->count() === 0) { + $this->logger->debug('No in progress orders found'); + return; + } + + $this->logger->info('Found orders which are in progress', ['foundOrders' => $searchResult->count()]); + + /** + * @var OrderCollection $orders + */ + $orders = $searchResult->getEntities(); + $resetted = $this->orderExpireService->cancelExpiredOrders($orders, $context); + + $this->logger->info('Rested expired orders', ['restedOrders' => $resetted]); + } + + /** + * @return iterable + */ + public static function getHandledMessages(): iterable + { + return [ + ExpireOrderTask::class + ]; + } +} diff --git a/src/ScheduledTask/Subscription/RenewalReminder/RenewalReminderTaskHandler.php b/src/ScheduledTask/Subscription/RenewalReminder/RenewalReminderTaskHandler.php index 5f04a93a0..3387c939e 100755 --- a/src/ScheduledTask/Subscription/RenewalReminder/RenewalReminderTaskHandler.php +++ b/src/ScheduledTask/Subscription/RenewalReminder/RenewalReminderTaskHandler.php @@ -8,6 +8,8 @@ use Shopware\Core\Framework\Context; use Shopware\Core\Framework\MessageQueue\ScheduledTask\ScheduledTaskHandler; +#[\Symfony\Component\Messenger\Attribute\AsMessageHandler(handles: RenewalReminderTask::class)] +#[\Symfony\Component\Messenger\Attribute\AsMessageHandler(handles: RenewalReminderTaskDev::class)] class RenewalReminderTaskHandler extends ScheduledTaskHandler { /** @@ -28,7 +30,8 @@ class RenewalReminderTaskHandler extends ScheduledTaskHandler */ public function __construct(ScheduledTaskRepositoryInterface $scheduledTaskRepository, SubscriptionManager $subscriptionManager, LoggerInterface $logger) { - parent::__construct($scheduledTaskRepository->getRepository()); + /** @phpstan-ignore-next-line */ + parent::__construct($scheduledTaskRepository->getRepository(), $logger); $this->subscriptionManager = $subscriptionManager; $this->logger = $logger; diff --git a/src/Service/Order/OrderExpireService.php b/src/Service/Order/OrderExpireService.php new file mode 100644 index 000000000..22a742872 --- /dev/null +++ b/src/Service/Order/OrderExpireService.php @@ -0,0 +1,144 @@ +orderStatusUpdater = $orderStatusUpdater; + $this->orderTimeService = $orderTimeService; + $this->settingsService = $settingsService; + $this->logger = $logger; + } + + /** + * If an order is "in progress" but the payment link is already expired, the order is changed to cannceled + * @param OrderCollection $orders + * @param Context $context + * @return int + */ + public function cancelExpiredOrders(OrderCollection $orders, Context $context): int + { + $resetted = 0; + /** @var OrderEntity $order */ + foreach ($orders as $order) { + if (! $order instanceof OrderEntity) { + continue; + } + + $orderAttributes = new OrderAttributes($order); + + if (strlen($orderAttributes->getMollieOrderId()) === 0) { + continue; + } + + $transactions = $order->getTransactions(); + + if ($transactions === null || $transactions->count() === 0) { + continue; + } + + $transactions->sort(function (OrderTransactionEntity $a, OrderTransactionEntity $b) { + return $a->getCreatedAt() <=> $b->getCreatedAt(); + }); + + /** @var OrderTransactionEntity $lastTransaction */ + $lastTransaction = $transactions->last(); + + $stateMachineState = $lastTransaction->getStateMachineState(); + if ($stateMachineState === null) { + continue; + } + + $lastStatus = $stateMachineState->getTechnicalName(); + + // disregard any orders that are not in progress + if ($lastStatus !== OrderStates::STATE_IN_PROGRESS) { + continue; + } + + $settings = $this->settingsService->getSettings(); + $finalizeTransactionTimeInMinutes = $settings->getPaymentFinalizeTransactionTime(); + + if ($this->orderUsesSepaPayment($lastTransaction)) { + $finalizeTransactionTimeInMinutes = (int)ceil($settings->getPaymentMethodBankTransferDueDateDays() / 24 / 60); + } + + if ($this->orderTimeService->isOrderAgeGreaterThan($order, $finalizeTransactionTimeInMinutes) === false) { + continue; + } + + // orderStatusUpdater needs the order to be set on the transaction + $lastTransaction->setOrder($order); + + // this forces the order to be open again + $context->addState(OrderStatusUpdater::ORDER_STATE_FORCE_OPEN); + + try { + $this->orderStatusUpdater->updatePaymentStatus($lastTransaction, MolliePaymentStatus::MOLLIE_PAYMENT_CANCELED, $context); + $resetted++; + } catch (\Exception $exception) { + $this->logger->error('Failed to update payment status for transaction', [ + 'transaction' => $lastTransaction->getId(), + 'order' => $order->getOrderNumber() + ]); + } + } + + return $resetted; + } + + /** + * @param OrderTransactionEntity $transaction + * @return bool + * @todo refactor once php8.0 is minimum version. Use Null-safe operator + */ + private function orderUsesSepaPayment(OrderTransactionEntity $transaction): bool + { + $paymentMethod = $transaction->getPaymentMethod(); + + if ($paymentMethod === null) { + return false; + } + + return $paymentMethod->getHandlerIdentifier() === BankTransferPayment::class; + } +} diff --git a/src/Subscriber/OrderEditSubscriber.php b/src/Subscriber/OrderEditSubscriber.php index 29e22039a..927cf859f 100644 --- a/src/Subscriber/OrderEditSubscriber.php +++ b/src/Subscriber/OrderEditSubscriber.php @@ -3,43 +3,22 @@ namespace Kiener\MolliePayments\Subscriber; -use Closure; -use Kiener\MolliePayments\Handler\Method\BankTransferPayment; -use Kiener\MolliePayments\Service\Mollie\MolliePaymentStatus; -use Kiener\MolliePayments\Service\Order\OrderStatusUpdater; -use Kiener\MolliePayments\Service\Order\OrderTimeService; -use Kiener\MolliePayments\Service\SettingsService; -use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity; -use Shopware\Core\Checkout\Order\OrderEntity; -use Shopware\Core\Checkout\Order\OrderStates; +use Kiener\MolliePayments\Service\Order\OrderExpireService; +use Shopware\Core\Checkout\Order\OrderCollection; use Shopware\Storefront\Page\Account\Order\AccountOrderPageLoadedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class OrderEditSubscriber implements EventSubscriberInterface { /** - * @var OrderStatusUpdater + * @var OrderExpireService */ - private $orderStatusUpdater; - - /** - * @var OrderTimeService - */ - private $orderTimeService; - - /** - * @var SettingsService - */ - private $settingsService; + private $orderExpireService; public function __construct( - OrderStatusUpdater $orderStatusUpdater, - OrderTimeService $orderTimeService, - SettingsService $settingsService + OrderExpireService $orderExpireService ) { - $this->orderStatusUpdater = $orderStatusUpdater; - $this->orderTimeService = $orderTimeService; - $this->settingsService = $settingsService; + $this->orderExpireService = $orderExpireService; } public static function getSubscribedEvents(): array @@ -51,95 +30,11 @@ public static function getSubscribedEvents(): array public function accountOrderDetailPageLoaded(AccountOrderPageLoadedEvent $event): void { - $orders = $event->getPage()->getOrders(); - - foreach ($orders as $order) { - if (!$order instanceof OrderEntity || $this->isMolliePayment($order) === false) { - continue; - } - - $transactions = $order->getTransactions(); - - if ($transactions === null || $transactions->count() === 0) { - continue; - } + /** @var OrderCollection $orders */ + $orders = $event->getPage()->getOrders()->getEntities(); - $transactions->sort(Closure::fromCallable([$this, 'sortTransactionsByDate'])); + $context = $event->getContext(); - $lastTransaction = $transactions->last(); - - $lastStatus = $lastTransaction->getStateMachineState()->getTechnicalName(); - - // disregard any orders that are not in progress - if ($lastStatus !== OrderStates::STATE_IN_PROGRESS) { - continue; - } - - $settings = $this->settingsService->getSettings(); - $finalizeTransactionTimeInMinutes = $settings->getPaymentFinalizeTransactionTime(); - - if ($this->orderUsesSepaPayment($order)) { - $finalizeTransactionTimeInMinutes = (int) ceil($settings->getPaymentMethodBankTransferDueDateDays() / 24 / 60); - } - - if ($this->orderTimeService->isOrderAgeGreaterThan($order, $finalizeTransactionTimeInMinutes) === false) { - continue; - } - - // orderStatusUpdater needs the order to be set on the transaction - $lastTransaction->setOrder($order); - $context = $event->getContext(); - // this forces the order to be open again - $context->addState(OrderStatusUpdater::ORDER_STATE_FORCE_OPEN); - try { - $this->orderStatusUpdater->updatePaymentStatus($lastTransaction, MolliePaymentStatus::MOLLIE_PAYMENT_CANCELED, $context); - } catch (\Exception $exception) { - } - } - } - - /** - * @param OrderEntity $order - * @return bool - * @todo refactor once php8.0 is minimum version. Use Null-safe operator - */ - private function orderUsesSepaPayment(OrderEntity $order): bool - { - $transactions = $order->getTransactions(); - - if ($transactions === null || count($transactions) === 0) { - return false; - } - - $lastTransaction = $transactions->last(); - - if ($lastTransaction instanceof OrderTransactionEntity === false) { - return false; - } - - $paymentMethod = $lastTransaction->getPaymentMethod(); - - if ($paymentMethod === null) { - return false; - } - - return $paymentMethod->getHandlerIdentifier() === BankTransferPayment::class; - } - - private function isMolliePayment(OrderEntity $order): bool - { - $customFields = $order->getCustomFields(); - - return is_array($customFields) && count($customFields) && isset($customFields['mollie_payments']); - } - - /** - * @param OrderTransactionEntity $a - * @param OrderTransactionEntity $b - * @return int - */ - private function sortTransactionsByDate(OrderTransactionEntity $a, OrderTransactionEntity $b): int - { - return $a->getCreatedAt() <=> $b->getCreatedAt(); + $rested = $this->orderExpireService->cancelExpiredOrders($orders, $context); } }