Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OrderEditSubscriber #806

Merged
merged 11 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Resources/config/services/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@

<service id="Kiener\MolliePayments\Service\MollieApi\LineItemDataExtractor"/>

<service id="Kiener\MolliePayments\Service\Order\OrderTimeService"/>

<service id="Kiener\MolliePayments\Service\Order\OrderStatusUpdater">
<argument type="service" id="Kiener\MolliePayments\Service\Order\OrderStateService"/>
Expand Down
7 changes: 7 additions & 0 deletions src/Resources/config/services/subscriber.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,12 @@
<tag name="kernel.event_subscriber"/>
</service>

<service id="Kiener\MolliePayments\Subscriber\OrderEditSubscriber">
<argument type="service" id="Kiener\MolliePayments\Service\Order\OrderStatusUpdater"/>
<argument type="service" id="Kiener\MolliePayments\Service\Order\OrderTimeService"/>
<argument type="service" id="Kiener\MolliePayments\Service\SettingsService"/>
<tag name="kernel.event_subscriber" />
</service>

</services>
</container>
3 changes: 2 additions & 1 deletion src/Service/Order/OrderStatusUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

class OrderStatusUpdater
{
public const ORDER_STATE_FORCE_OPEN = 'order-state-force-open';
/**
* @var OrderStateService
*/
Expand Down Expand Up @@ -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);
}
Expand Down
56 changes: 56 additions & 0 deletions src/Service/Order/OrderTimeService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);

namespace Kiener\MolliePayments\Service\Order;

use DateTime;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Order\OrderEntity;

class OrderTimeService
{
/**
* @var DateTime
*/
private $now;

public function __construct(?DateTime $now = null)
{
$this->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;
}
}
6 changes: 6 additions & 0 deletions src/Service/SettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<mixed> $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);
}

Expand Down
15 changes: 15 additions & 0 deletions src/Setting/MollieSettingStruct.php
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ class MollieSettingStruct extends Struct
*/
protected $applePayDirectDomainAllowList = '';

/**
* @var int
*/
protected $paymentFinalizeTransactionTime;

/**
* @return string
*/
Expand Down Expand Up @@ -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;
}
}
144 changes: 144 additions & 0 deletions src/Subscriber/OrderEditSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);

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 Shopware\Storefront\Page\Account\Order\AccountOrderPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OrderEditSubscriber implements EventSubscriberInterface
{
/**
* @var OrderStatusUpdater
*/
private $orderStatusUpdater;

/**
* @var OrderTimeService
*/
private $orderTimeService;

/**
* @var SettingsService
*/
private $settingsService;

public function __construct(
OrderStatusUpdater $orderStatusUpdater,
OrderTimeService $orderTimeService,
SettingsService $settingsService
) {
$this->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();
}
}
75 changes: 75 additions & 0 deletions tests/PHPUnit/Service/Order/OrderTimeServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);

namespace MolliePayments\Tests\Service\Order;


use Kiener\MolliePayments\Service\Order\OrderTimeService;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionCollection;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\System\StateMachine\Aggregation\StateMachineState\StateMachineStateEntity;

class OrderTimeServiceTest extends TestCase
{
/**
* @param \DateTime $now
* @param \DateTime $orderDate
* @param bool $expected
*
* @dataProvider dateComparisonLogicProvider
*/
public function testDateComparisonLogic(\DateTime $now, \DateTime $orderDate, bool $expected): void
{
$order = $this->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
],
];
}
}
Loading