diff --git a/src/Resources/config/services/services.xml b/src/Resources/config/services/services.xml
index 935d2dfdc..6b460d9c7 100644
--- a/src/Resources/config/services/services.xml
+++ b/src/Resources/config/services/services.xml
@@ -115,6 +115,7 @@
+
diff --git a/src/Resources/config/services/subscriber.xml b/src/Resources/config/services/subscriber.xml
index 5c400c9dc..3efd493f7 100644
--- a/src/Resources/config/services/subscriber.xml
+++ b/src/Resources/config/services/subscriber.xml
@@ -66,5 +66,12 @@
+
+
+
+
+
+
+
diff --git a/src/Service/Order/OrderStatusUpdater.php b/src/Service/Order/OrderStatusUpdater.php
index bebcb6f86..c1ed0d2a1 100644
--- a/src/Service/Order/OrderStatusUpdater.php
+++ b/src/Service/Order/OrderStatusUpdater.php
@@ -16,6 +16,7 @@
class OrderStatusUpdater
{
+ public const ORDER_STATE_FORCE_OPEN = 'order-state-force-open';
/**
* @var OrderStateService
*/
@@ -101,7 +102,7 @@ public function updatePaymentStatus(OrderTransactionEntity $transaction, string
{
# if we are already in_progress...then don't switch to OPEN again
# otherwise SEPA bank transfer would switch back to OPEN
- if ($currentShopwareStatusKey !== OrderTransactionStates::STATE_IN_PROGRESS) {
+ if ($currentShopwareStatusKey !== OrderTransactionStates::STATE_IN_PROGRESS || $context->hasState(self::ORDER_STATE_FORCE_OPEN)) {
$addLog = true;
$this->transactionTransitionService->reOpenTransaction($transaction, $context);
}
diff --git a/src/Service/Order/OrderTimeService.php b/src/Service/Order/OrderTimeService.php
new file mode 100644
index 000000000..0c9666ece
--- /dev/null
+++ b/src/Service/Order/OrderTimeService.php
@@ -0,0 +1,56 @@
+now = $now ?? new DateTime();
+ }
+
+ /**
+ * Checks if the age of the last transaction of the order is greater than the specified number of hours.
+ *
+ * @param OrderEntity $order The order entity to check.
+ * @param int $hours The number of hours to compare against.
+ *
+ * @return bool Returns true if the order is older than the specified number of hours, false otherwise.
+ */
+ public function isOrderAgeGreaterThan(OrderEntity $order, int $hours): bool
+ {
+ $transactions = $order->getTransactions();
+
+ if ($transactions === null || count($transactions) === 0) {
+ return false;
+ }
+
+ /** @var ?OrderTransactionEntity $lastTransaction */
+ $lastTransaction = $transactions->last();
+
+ if ($lastTransaction === null) {
+ return false;
+ }
+
+ $transitionDate = $lastTransaction->getCreatedAt();
+
+ if ($transitionDate === null) {
+ return false;
+ }
+
+ $interval = $this->now->diff($transitionDate);
+ $diffInHours = $interval->h + ($interval->days * 24);
+
+ return $diffInHours > $hours;
+ }
+}
diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php
index 040972781..c1ca33cbb 100644
--- a/src/Service/SettingsService.php
+++ b/src/Service/SettingsService.php
@@ -12,8 +12,10 @@ class SettingsService implements PluginSettingsServiceInterface
{
public const SYSTEM_CONFIG_DOMAIN = 'MolliePayments.config';
private const SYSTEM_CORE_LOGIN_REGISTRATION_CONFIG_DOMAIN = 'core.loginRegistration';
+ private const SYSTEM_CORE_CART_CONFIG_DOMAIN = 'core.cart';
private const PHONE_NUMBER_FIELD_REQUIRED = 'phoneNumberFieldRequired';
+ private const PAYMENT_FINALIZE_TRANSACTION_TIME = 'paymentFinalizeTransactionTime';
const LIVE_API_KEY = 'liveApiKey';
const TEST_API_KEY = 'testApiKey';
const LIVE_PROFILE_ID = 'liveProfileId';
@@ -88,6 +90,10 @@ public function getSettings(?string $salesChannelId = null): MollieSettingStruct
$structData[self::PHONE_NUMBER_FIELD_REQUIRED] = $coreSettings[self::PHONE_NUMBER_FIELD_REQUIRED] ?? false;
+ /** @var array $cartSettings */
+ $cartSettings = $this->systemConfigService->get(self::SYSTEM_CORE_CART_CONFIG_DOMAIN, $salesChannelId);
+ $structData[self::PAYMENT_FINALIZE_TRANSACTION_TIME] = $cartSettings[self::PAYMENT_FINALIZE_TRANSACTION_TIME] ?? 1800;
+
return (new MollieSettingStruct())->assign($structData);
}
diff --git a/src/Setting/MollieSettingStruct.php b/src/Setting/MollieSettingStruct.php
index 97c7078d7..48539b026 100644
--- a/src/Setting/MollieSettingStruct.php
+++ b/src/Setting/MollieSettingStruct.php
@@ -255,6 +255,11 @@ class MollieSettingStruct extends Struct
*/
protected $applePayDirectDomainAllowList = '';
+ /**
+ * @var int
+ */
+ protected $paymentFinalizeTransactionTime;
+
/**
* @return string
*/
@@ -982,4 +987,14 @@ public function setApplePayDirectDomainAllowList(string $applePayDirectDomainAll
{
$this->applePayDirectDomainAllowList = $applePayDirectDomainAllowList;
}
+
+ public function getPaymentFinalizeTransactionTime(): int
+ {
+ return $this->paymentFinalizeTransactionTime;
+ }
+
+ public function setPaymentFinalizeTransactionTime(int $paymentFinalizeTransactionTime): void
+ {
+ $this->paymentFinalizeTransactionTime = $paymentFinalizeTransactionTime;
+ }
}
diff --git a/src/Subscriber/OrderEditSubscriber.php b/src/Subscriber/OrderEditSubscriber.php
new file mode 100644
index 000000000..931fbe20a
--- /dev/null
+++ b/src/Subscriber/OrderEditSubscriber.php
@@ -0,0 +1,144 @@
+orderStatusUpdater = $orderStatusUpdater;
+ $this->orderTimeService = $orderTimeService;
+ $this->settingsService = $settingsService;
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ AccountOrderPageLoadedEvent::class => 'accountOrderDetailPageLoaded'
+ ];
+ }
+
+ 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;
+ }
+
+ $lastTransaction = $transactions->filter(Closure::fromCallable([$this, 'sortTransactionsByDate']))->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();
+ $finalizeTransactionTimeInHours = (int) ceil($finalizeTransactionTimeInMinutes / 60);
+
+ if ($this->orderUsesSepaPayment($order)) {
+ $finalizeTransactionTimeInHours = (int) ceil($settings->getPaymentMethodBankTransferDueDateDays() / 24);
+ }
+
+ if ($this->orderTimeService->isOrderAgeGreaterThan($order, $finalizeTransactionTimeInHours) === 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();
+ }
+}
diff --git a/tests/PHPUnit/Service/Order/OrderTimeServiceTest.php b/tests/PHPUnit/Service/Order/OrderTimeServiceTest.php
new file mode 100644
index 000000000..1221088bd
--- /dev/null
+++ b/tests/PHPUnit/Service/Order/OrderTimeServiceTest.php
@@ -0,0 +1,75 @@
+orderMockWithLastTransactionTimestamp($orderDate);
+
+ $result = (new OrderTimeService($now))->isOrderAgeGreaterThan($order, 1);
+
+ $this->assertSame($expected, $result);
+ }
+
+ private function orderMockWithLastTransactionTimestamp(\DateTime $time): OrderEntity
+ {
+ $entity = $this->createMock(OrderEntity::class);
+ $transaction = $this->createMock(OrderTransactionEntity::class);
+ $transactions = new OrderTransactionCollection([$transaction]);
+
+ $entity->method('getTransactions')->willReturn($transactions);
+
+ $transaction->method('getCreatedAt')->willReturn($time);
+
+ return $entity;
+ }
+
+ public function dateComparisonLogicProvider()
+ {
+ return [
+ 'order is older than 1 hour' => [
+ new \DateTime('2021-01-01 12:00:00'),
+ new \DateTime('2021-01-01 10:00:00'),
+ true
+ ],
+ 'order is not older than 1 hour' => [
+ new \DateTime('2021-01-01 12:00:00'),
+ new \DateTime('2021-01-01 11:00:00'),
+ false
+ ],
+ 'order is not older than 1 hour, but 1 second' => [
+ new \DateTime('2021-01-01 12:00:00'),
+ new \DateTime('2021-01-01 11:59:59'),
+ false
+ ],
+ 'order is older than a year' => [
+ new \DateTime('2021-01-01 12:00:00'),
+ new \DateTime('2020-01-01 12:00:00'),
+ true
+ ],
+ 'order is 2 months old' => [
+ new \DateTime('2021-01-01 12:00:00'),
+ new \DateTime('2020-11-01 12:00:00'),
+ true
+ ],
+ ];
+ }
+}
\ No newline at end of file