Skip to content

Commit

Permalink
[ECP-9177] Implement a cronjob to remove processed webhooks (#2837)
Browse files Browse the repository at this point in the history
* [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 <angel.campos@adyen.com>

* Update view/adminhtml/templates/notification/webhook_removal_job_notice.phtml

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* Update etc/adminhtml/system/adyen_testing_performance.xml

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* Update etc/adminhtml/system/adyen_testing_performance.xml

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* Update Helper/Config.php

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* Update Model/Config/Backend/ProcessedWebhookRemoval.php

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* Update Helper/Config.php

Co-authored-by: Ángel Campos <angel.campos@adyen.com>

* [EPC-9597] Update crontab schedule and use constants for DB table alias

* [EPC-9177] Refactor the overview page title

---------

Co-authored-by: Can Demiralp <can.demiralp@adyen.com>
Co-authored-by: Ángel Campos <angel.campos@adyen.com>
  • Loading branch information
3 people authored Feb 17, 2025
1 parent 8221c0b commit edd889f
Show file tree
Hide file tree
Showing 29 changed files with 1,418 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Api/Data/NotificationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface NotificationInterface
/**
* Constants for keys of data array. Identical to the name of the getter in snake case.
*/
const TABLE_NAME = 'adyen_notification';
const TABLE_NAME_ALIAS = 'notification';
const ENTITY_ID = 'entity_id';
const PSPREFRENCE = 'pspreference';
const ORIGINAL_REFERENCE = 'original_reference';
Expand Down
22 changes: 22 additions & 0 deletions Api/Repository/AdyenNotificationRepositoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2025 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Api\Repository;

interface AdyenNotificationRepositoryInterface
{
/**
* @param array $entityIds
* @return void
*/
public function deleteByIds(array $entityIds): void;
}
45 changes: 45 additions & 0 deletions Block/Adminhtml/Notification/WebhookRemovalJobNotice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Block\Adminhtml\Notification;

use Adyen\Payment\Helper\Config;
use Magento\Framework\View\Element\Template\Context;
use Magento\Theme\Block\Html\Notices;

class WebhookRemovalJobNotice extends Notices
{
public function __construct(
Context $context,
private readonly Config $configHelper,
array $data = []
) {
parent::__construct($context, $data);
}

/**
* @return bool
*/
public function isProcessedWebhookRemovalEnabled(): bool
{
return $this->configHelper->getIsProcessedWebhookRemovalEnabled();
}

/**
* Returns the number of days after which the notifications will be cleaned-up.
*
* @return int
*/
public function getNumberOfDays(): int
{
return $this->configHelper->getProcessedWebhookRemovalTime();
}
}
18 changes: 13 additions & 5 deletions Controller/Adminhtml/Notifications/Overview.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,26 @@

namespace Adyen\Payment\Controller\Adminhtml\Notifications;

class Overview extends \Magento\Backend\App\Action
use Magento\Backend\App\Action;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\View\Result\Page;

class Overview extends Action
{
/**
* Load the page defined in corresponding layout XML
*
* @return \Magento\Framework\View\Result\Page
* @return Page
*/
public function execute()
public function execute(): Page
{
$resultPage = $this->resultFactory->create(\Magento\Framework\Controller\ResultFactory::TYPE_PAGE);
/** @var Page $resultPage */
$resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->setActiveMenu('Adyen_Payment::notifications_overview')
->getConfig()->getTitle()->prepend(__('Adyen Notifications Overview'));
->getConfig()
->getTitle()
->prepend(__('Adyen Webhooks Overview'));

return $resultPage;
}
}
59 changes: 59 additions & 0 deletions Cron/Providers/ProcessedWebhooksProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Cron\Providers;

use Adyen\Payment\Api\Data\NotificationInterface;
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Model\ResourceModel\Notification\Collection;
use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory;

class ProcessedWebhooksProvider implements WebhooksProviderInterface
{
/**
* @param CollectionFactory $notificationCollectionFactory
* @param Config $configHelper
*/
public function __construct(
private readonly CollectionFactory $notificationCollectionFactory,
private readonly Config $configHelper
) { }

/**
* Provides the `entity_id`s of the processed webhooks limited by the removal time
*
* @return array
*/
public function provide(): array
{
$numberOfDays = $this->configHelper->getProcessedWebhookRemovalTime();

/** @var Collection $notificationCollection */
$notificationCollection = $this->notificationCollectionFactory->create();
$notificationCollection->getProcessedWebhookIdsByTimeLimit($numberOfDays, self::BATCH_SIZE);

if ($notificationCollection->getSize() > 0) {
return $notificationCollection->getColumnValues(NotificationInterface::ENTITY_ID);
} else {
return [];
}
}

/**
* Returns the provider name
*
* @return string
*/
public function getProviderName(): string
{
return "Adyen processed webhooks provider";
}
}
27 changes: 27 additions & 0 deletions Cron/Providers/WebhooksProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Cron\Providers;

interface WebhooksProviderInterface
{
const BATCH_SIZE = 1000;

/**
* @return array
*/
public function provide(): array;

/**
* @return string
*/
public function getProviderName(): string;
}
85 changes: 85 additions & 0 deletions Cron/RemoveProcessedWebhooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Cron;

use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
use Adyen\Payment\Cron\Providers\WebhooksProviderInterface;
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Logger\AdyenLogger;
use Exception;

class RemoveProcessedWebhooks
{
/**
* @param WebhooksProviderInterface[] $providers
* @param AdyenLogger $adyenLogger
* @param Config $configHelper
* @param AdyenNotificationRepositoryInterface $adyenNotificationRepository
*/
public function __construct(
private readonly array $providers,
private readonly AdyenLogger $adyenLogger,
private readonly Config $configHelper,
private readonly AdyenNotificationRepositoryInterface $adyenNotificationRepository
) { }

/**
* @return void
*/
public function execute(): void
{
$isWebhookCleanupEnabled = $this->configHelper->getIsProcessedWebhookRemovalEnabled();

if ($isWebhookCleanupEnabled === true) {
$numberOfItemsRemoved = 0;

foreach ($this->providers as $provider) {
$webhookIdsToRemove = $provider->provide();
$numberOfWebhooksProvided = count($webhookIdsToRemove);

if ($numberOfWebhooksProvided > 0) {
try {
$this->adyenNotificationRepository->deleteByIds($webhookIdsToRemove);
$numberOfItemsRemoved += $numberOfWebhooksProvided;
} catch (Exception $e) {
$message = __(
'%1: An error occurred while deleting webhooks! %2',
$provider->getProviderName(),
$e->getMessage()
);

$this->adyenLogger->error($message);
}
}
}

if ($numberOfItemsRemoved > 0) {
$successMessage = __(
'%1 processed webhooks have been removed by the RemoveProcessedWebhooks cronjob.',
$numberOfItemsRemoved
);

$this->adyenLogger->addAdyenNotification($successMessage);
} else {
$debugMessage = __(
'There is no webhooks to be removed by RemoveProcessedWebhooks cronjob.',
$numberOfItemsRemoved
);

$this->adyenLogger->addAdyenDebug($debugMessage);
}
} else {
$message = __('Processed webhook removal feature is disabled. The cronjob has been skipped!');
$this->adyenLogger->addAdyenDebug($message);
}
}
}
37 changes: 37 additions & 0 deletions Helper/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class Config
const XML_RECURRING_CONFIGURATION = 'recurring_configuration';
const XML_ALLOW_MULTISTORE_TOKENS = 'allow_multistore_tokens';
const XML_THREEDS_FLOW = 'threeds_flow';
const XML_REMOVE_PROCESSED_WEBHOOKS = 'remove_processed_webhooks';
const XML_PROCESSED_WEBHOOK_REMOVAL_TIME = 'processed_webhook_removal_time';

protected ScopeConfigInterface $scopeConfig;
private EncryptorInterface $encryptor;
Expand Down Expand Up @@ -592,6 +594,41 @@ public function getThreeDSFlow(int $storeId = null): string
);
}

/**
* Indicates whether if the processed webhook removal cronjob is enabled or not.
*
* This field can only be configured on default scope level as
* the notification table doesn't have nay relation with the stores.
*
* @return bool
*/
public function getIsProcessedWebhookRemovalEnabled(): bool
{
return $this->getConfigData(
self::XML_REMOVE_PROCESSED_WEBHOOKS,
self::XML_ADYEN_ABSTRACT_PREFIX,
null,
true
);
}

/**
* Returns the configured amount of days a webhook has to be older than in order to be removed.
*
* This field can only be configured on default scope level as
* the notification table doesn't have any relation with the stores.
*
* @return int
*/
public function getProcessedWebhookRemovalTime(): int
{
return (int) $this->getConfigData(
self::XML_PROCESSED_WEBHOOK_REMOVAL_TIME,
self::XML_ADYEN_ABSTRACT_PREFIX,
null
);
}

public function getIsCvcRequiredForRecurringCardPayments(int $storeId = null): bool
{
return (bool) $this->getConfigData(
Expand Down
44 changes: 44 additions & 0 deletions Model/AdyenNotificationRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2025 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <magento@adyen.com>
*/

namespace Adyen\Payment\Model;

use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory;
use Magento\Framework\ObjectManagerInterface;

class AdyenNotificationRepository implements AdyenNotificationRepositoryInterface
{
/**
* @param ObjectManagerInterface $objectManager
* @param string $resourceModel
*/
public function __construct(
private readonly ObjectManagerInterface $objectManager,
private readonly string $resourceModel
) { }

/**
* Delete multiple entities with the given IDs
*
* @param array $entityIds
* @return void
*/
public function deleteByIds(array $entityIds): void
{
if (empty($entityIds)) {
return;
}

$resource = $this->objectManager->get($this->resourceModel);
$resource->deleteByIds($entityIds);
}
}
Loading

0 comments on commit edd889f

Please sign in to comment.