Skip to content

Commit edd889f

Browse files
[ECP-9177] Implement a cronjob to remove processed webhooks (#2837)
* [ECP-9177] Implement webhook clean-up cronjob * [ECP-9177] Write unit tests * [ECP-9177] Log message with ID after removing the entity * [EPC-9177] Add try-catch for repository operation and write unit tests * [EPC-9177] Update unit test * [EPC-9177] Implement a notice box indication the feature is enabled * [EPC-9177] Write unit tests * [EPC-9177] Fix constructor default value * [EPC-9177] Limit config value to default scope only * [EPC-9177] Limit config value to default scope only * [EPC-9177] Update unit tests * [EPC-9177] Refactor class names and implement comment modal * [EPC-9177] Update label and unit tests * [EPC-9177] Update label and unit tests * [EPC-9177] Add a controller message * [EPC-9177] Write unit tests * [EPC-9177] Update dependency injection * [EPC-9177] Update log type and use notification logs for success messages * [EPC-9177] Add limit to the provider * [EPC-9177] Implement removal in batch via single SQL query * [EPC-9177] Fix CS issue * [EPC-9177] Update unit tests * [EPC-9177] Update unit tests * [EPC-9177] Adjust cron time and update message * [EPC-9177] Get the number of affected webhooks without limiting them with batch size * Update view/adminhtml/templates/notification/webhook_removal_job_notice.phtml Co-authored-by: Ángel Campos <[email protected]> * Update view/adminhtml/templates/notification/webhook_removal_job_notice.phtml Co-authored-by: Ángel Campos <[email protected]> * Update etc/adminhtml/system/adyen_testing_performance.xml Co-authored-by: Ángel Campos <[email protected]> * Update etc/adminhtml/system/adyen_testing_performance.xml Co-authored-by: Ángel Campos <[email protected]> * Update Helper/Config.php Co-authored-by: Ángel Campos <[email protected]> * Update Model/Config/Backend/ProcessedWebhookRemoval.php Co-authored-by: Ángel Campos <[email protected]> * Update Helper/Config.php Co-authored-by: Ángel Campos <[email protected]> * [EPC-9597] Update crontab schedule and use constants for DB table alias * [EPC-9177] Refactor the overview page title --------- Co-authored-by: Can Demiralp <[email protected]> Co-authored-by: Ángel Campos <[email protected]>
1 parent 8221c0b commit edd889f

29 files changed

+1418
-6
lines changed

Api/Data/NotificationInterface.php

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface NotificationInterface
1616
/**
1717
* Constants for keys of data array. Identical to the name of the getter in snake case.
1818
*/
19+
const TABLE_NAME = 'adyen_notification';
20+
const TABLE_NAME_ALIAS = 'notification';
1921
const ENTITY_ID = 'entity_id';
2022
const PSPREFRENCE = 'pspreference';
2123
const ORIGINAL_REFERENCE = 'original_reference';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment Module
5+
*
6+
* Copyright (c) 2025 Adyen N.V.
7+
* This file is open source and available under the MIT license.
8+
* See the LICENSE file for more info.
9+
*
10+
* Author: Adyen <[email protected]>
11+
*/
12+
13+
namespace Adyen\Payment\Api\Repository;
14+
15+
interface AdyenNotificationRepositoryInterface
16+
{
17+
/**
18+
* @param array $entityIds
19+
* @return void
20+
*/
21+
public function deleteByIds(array $entityIds): void;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment module (https://www.adyen.com/)
5+
*
6+
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
7+
* See LICENSE.txt for license details.
8+
*
9+
* Author: Adyen <[email protected]>
10+
*/
11+
12+
namespace Adyen\Payment\Block\Adminhtml\Notification;
13+
14+
use Adyen\Payment\Helper\Config;
15+
use Magento\Framework\View\Element\Template\Context;
16+
use Magento\Theme\Block\Html\Notices;
17+
18+
class WebhookRemovalJobNotice extends Notices
19+
{
20+
public function __construct(
21+
Context $context,
22+
private readonly Config $configHelper,
23+
array $data = []
24+
) {
25+
parent::__construct($context, $data);
26+
}
27+
28+
/**
29+
* @return bool
30+
*/
31+
public function isProcessedWebhookRemovalEnabled(): bool
32+
{
33+
return $this->configHelper->getIsProcessedWebhookRemovalEnabled();
34+
}
35+
36+
/**
37+
* Returns the number of days after which the notifications will be cleaned-up.
38+
*
39+
* @return int
40+
*/
41+
public function getNumberOfDays(): int
42+
{
43+
return $this->configHelper->getProcessedWebhookRemovalTime();
44+
}
45+
}

Controller/Adminhtml/Notifications/Overview.php

+13-5
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,26 @@
1111

1212
namespace Adyen\Payment\Controller\Adminhtml\Notifications;
1313

14-
class Overview extends \Magento\Backend\App\Action
14+
use Magento\Backend\App\Action;
15+
use Magento\Framework\Controller\ResultFactory;
16+
use Magento\Framework\View\Result\Page;
17+
18+
class Overview extends Action
1519
{
1620
/**
1721
* Load the page defined in corresponding layout XML
1822
*
19-
* @return \Magento\Framework\View\Result\Page
23+
* @return Page
2024
*/
21-
public function execute()
25+
public function execute(): Page
2226
{
23-
$resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
27+
/** @var Page $resultPage */
28+
$resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
2429
$resultPage->setActiveMenu('Adyen_Payment::notifications_overview')
25-
->getConfig()->getTitle()->prepend(__('Adyen Notifications Overview'));
30+
->getConfig()
31+
->getTitle()
32+
->prepend(__('Adyen Webhooks Overview'));
33+
2634
return $resultPage;
2735
}
2836
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment module (https://www.adyen.com/)
5+
*
6+
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
7+
* See LICENSE.txt for license details.
8+
*
9+
* Author: Adyen <[email protected]>
10+
*/
11+
12+
namespace Adyen\Payment\Cron\Providers;
13+
14+
use Adyen\Payment\Api\Data\NotificationInterface;
15+
use Adyen\Payment\Helper\Config;
16+
use Adyen\Payment\Model\ResourceModel\Notification\Collection;
17+
use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory;
18+
19+
class ProcessedWebhooksProvider implements WebhooksProviderInterface
20+
{
21+
/**
22+
* @param CollectionFactory $notificationCollectionFactory
23+
* @param Config $configHelper
24+
*/
25+
public function __construct(
26+
private readonly CollectionFactory $notificationCollectionFactory,
27+
private readonly Config $configHelper
28+
) { }
29+
30+
/**
31+
* Provides the `entity_id`s of the processed webhooks limited by the removal time
32+
*
33+
* @return array
34+
*/
35+
public function provide(): array
36+
{
37+
$numberOfDays = $this->configHelper->getProcessedWebhookRemovalTime();
38+
39+
/** @var Collection $notificationCollection */
40+
$notificationCollection = $this->notificationCollectionFactory->create();
41+
$notificationCollection->getProcessedWebhookIdsByTimeLimit($numberOfDays, self::BATCH_SIZE);
42+
43+
if ($notificationCollection->getSize() > 0) {
44+
return $notificationCollection->getColumnValues(NotificationInterface::ENTITY_ID);
45+
} else {
46+
return [];
47+
}
48+
}
49+
50+
/**
51+
* Returns the provider name
52+
*
53+
* @return string
54+
*/
55+
public function getProviderName(): string
56+
{
57+
return "Adyen processed webhooks provider";
58+
}
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment module (https://www.adyen.com/)
5+
*
6+
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
7+
* See LICENSE.txt for license details.
8+
*
9+
* Author: Adyen <[email protected]>
10+
*/
11+
12+
namespace Adyen\Payment\Cron\Providers;
13+
14+
interface WebhooksProviderInterface
15+
{
16+
const BATCH_SIZE = 1000;
17+
18+
/**
19+
* @return array
20+
*/
21+
public function provide(): array;
22+
23+
/**
24+
* @return string
25+
*/
26+
public function getProviderName(): string;
27+
}

Cron/RemoveProcessedWebhooks.php

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment module (https://www.adyen.com/)
5+
*
6+
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
7+
* See LICENSE.txt for license details.
8+
*
9+
* Author: Adyen <[email protected]>
10+
*/
11+
12+
namespace Adyen\Payment\Cron;
13+
14+
use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
15+
use Adyen\Payment\Cron\Providers\WebhooksProviderInterface;
16+
use Adyen\Payment\Helper\Config;
17+
use Adyen\Payment\Logger\AdyenLogger;
18+
use Exception;
19+
20+
class RemoveProcessedWebhooks
21+
{
22+
/**
23+
* @param WebhooksProviderInterface[] $providers
24+
* @param AdyenLogger $adyenLogger
25+
* @param Config $configHelper
26+
* @param AdyenNotificationRepositoryInterface $adyenNotificationRepository
27+
*/
28+
public function __construct(
29+
private readonly array $providers,
30+
private readonly AdyenLogger $adyenLogger,
31+
private readonly Config $configHelper,
32+
private readonly AdyenNotificationRepositoryInterface $adyenNotificationRepository
33+
) { }
34+
35+
/**
36+
* @return void
37+
*/
38+
public function execute(): void
39+
{
40+
$isWebhookCleanupEnabled = $this->configHelper->getIsProcessedWebhookRemovalEnabled();
41+
42+
if ($isWebhookCleanupEnabled === true) {
43+
$numberOfItemsRemoved = 0;
44+
45+
foreach ($this->providers as $provider) {
46+
$webhookIdsToRemove = $provider->provide();
47+
$numberOfWebhooksProvided = count($webhookIdsToRemove);
48+
49+
if ($numberOfWebhooksProvided > 0) {
50+
try {
51+
$this->adyenNotificationRepository->deleteByIds($webhookIdsToRemove);
52+
$numberOfItemsRemoved += $numberOfWebhooksProvided;
53+
} catch (Exception $e) {
54+
$message = __(
55+
'%1: An error occurred while deleting webhooks! %2',
56+
$provider->getProviderName(),
57+
$e->getMessage()
58+
);
59+
60+
$this->adyenLogger->error($message);
61+
}
62+
}
63+
}
64+
65+
if ($numberOfItemsRemoved > 0) {
66+
$successMessage = __(
67+
'%1 processed webhooks have been removed by the RemoveProcessedWebhooks cronjob.',
68+
$numberOfItemsRemoved
69+
);
70+
71+
$this->adyenLogger->addAdyenNotification($successMessage);
72+
} else {
73+
$debugMessage = __(
74+
'There is no webhooks to be removed by RemoveProcessedWebhooks cronjob.',
75+
$numberOfItemsRemoved
76+
);
77+
78+
$this->adyenLogger->addAdyenDebug($debugMessage);
79+
}
80+
} else {
81+
$message = __('Processed webhook removal feature is disabled. The cronjob has been skipped!');
82+
$this->adyenLogger->addAdyenDebug($message);
83+
}
84+
}
85+
}

Helper/Config.php

+37
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class Config
5757
const XML_RECURRING_CONFIGURATION = 'recurring_configuration';
5858
const XML_ALLOW_MULTISTORE_TOKENS = 'allow_multistore_tokens';
5959
const XML_THREEDS_FLOW = 'threeds_flow';
60+
const XML_REMOVE_PROCESSED_WEBHOOKS = 'remove_processed_webhooks';
61+
const XML_PROCESSED_WEBHOOK_REMOVAL_TIME = 'processed_webhook_removal_time';
6062

6163
protected ScopeConfigInterface $scopeConfig;
6264
private EncryptorInterface $encryptor;
@@ -592,6 +594,41 @@ public function getThreeDSFlow(int $storeId = null): string
592594
);
593595
}
594596

597+
/**
598+
* Indicates whether if the processed webhook removal cronjob is enabled or not.
599+
*
600+
* This field can only be configured on default scope level as
601+
* the notification table doesn't have nay relation with the stores.
602+
*
603+
* @return bool
604+
*/
605+
public function getIsProcessedWebhookRemovalEnabled(): bool
606+
{
607+
return $this->getConfigData(
608+
self::XML_REMOVE_PROCESSED_WEBHOOKS,
609+
self::XML_ADYEN_ABSTRACT_PREFIX,
610+
null,
611+
true
612+
);
613+
}
614+
615+
/**
616+
* Returns the configured amount of days a webhook has to be older than in order to be removed.
617+
*
618+
* This field can only be configured on default scope level as
619+
* the notification table doesn't have any relation with the stores.
620+
*
621+
* @return int
622+
*/
623+
public function getProcessedWebhookRemovalTime(): int
624+
{
625+
return (int) $this->getConfigData(
626+
self::XML_PROCESSED_WEBHOOK_REMOVAL_TIME,
627+
self::XML_ADYEN_ABSTRACT_PREFIX,
628+
null
629+
);
630+
}
631+
595632
public function getIsCvcRequiredForRecurringCardPayments(int $storeId = null): bool
596633
{
597634
return (bool) $this->getConfigData(

Model/AdyenNotificationRepository.php

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
/**
3+
*
4+
* Adyen Payment module (https://www.adyen.com/)
5+
*
6+
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
7+
* See LICENSE.txt for license details.
8+
*
9+
* Author: Adyen <[email protected]>
10+
*/
11+
12+
namespace Adyen\Payment\Model;
13+
14+
use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
15+
use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory;
16+
use Magento\Framework\ObjectManagerInterface;
17+
18+
class AdyenNotificationRepository implements AdyenNotificationRepositoryInterface
19+
{
20+
/**
21+
* @param ObjectManagerInterface $objectManager
22+
* @param string $resourceModel
23+
*/
24+
public function __construct(
25+
private readonly ObjectManagerInterface $objectManager,
26+
private readonly string $resourceModel
27+
) { }
28+
29+
/**
30+
* Delete multiple entities with the given IDs
31+
*
32+
* @param array $entityIds
33+
* @return void
34+
*/
35+
public function deleteByIds(array $entityIds): void
36+
{
37+
if (empty($entityIds)) {
38+
return;
39+
}
40+
41+
$resource = $this->objectManager->get($this->resourceModel);
42+
$resource->deleteByIds($entityIds);
43+
}
44+
}

0 commit comments

Comments
 (0)