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

[ECP-9177] Implement webhook clean-up cronjob for old webhooks #2837

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
39 changes: 39 additions & 0 deletions Api/Repository/AdyenNotificationRepositoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Api\Repository;

use Adyen\Payment\Api\Data\NotificationInterface;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\Exception\LocalizedException;

interface AdyenNotificationRepositoryInterface
{
/**
* Retrieve Adyen Notification entities which match a specified criteria.
*
* @param SearchCriteriaInterface $searchCriteria
* @return SearchResultsInterface
*
* @throws LocalizedException
*/
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;

/**
* Deletes a specified Adyen notification.
*
* @param NotificationInterface $entity The notification ID.
* @return bool
*/
public function delete(NotificationInterface $entity): bool;
}
72 changes: 72 additions & 0 deletions Cron/CleanupNotifications.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2024 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Cron;

use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
use Adyen\Payment\Cron\Providers\NotificationsProviderInterface;
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Logger\AdyenLogger;
use Adyen\Payment\Model\Notification;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Model\StoreManagerInterface;

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

/**
* @return void
* @throws NoSuchEntityException
*/
public function execute(): void
{
$storeId = $this->storeManager->getStore()->getId();
$isWebhookCleanupEnabled = $this->configHelper->getIsWebhookCleanupEnabled($storeId);

if ($isWebhookCleanupEnabled) {
$numberOfItemsRemoved = 0;

foreach ($this->providers as $provider) {
/** @var Notification $notificationToCleanup */
foreach ($provider->provide() as $notificationToCleanup) {
$isSuccessfullyDeleted = $this->adyenNotificationRepository->delete($notificationToCleanup);

if ($isSuccessfullyDeleted) {
$message = __('%1: Notification with entityId %2 has been deleted.',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add also here one extra info: Notification with entityId %2 has been deleted because it was processed/received X days ago.

Probably means getting that field from the provider

$provider->getProviderName(), $notificationToCleanup->getEntityId());
$this->adyenLogger->addAdyenNotification($message);

$numberOfItemsRemoved++;
}
}
}

$successMessage = sprintf(
__('%s webhook notifications have been cleaned-up by the CleanupNotifications job.'),
$numberOfItemsRemoved
);
$this->adyenLogger->addAdyenDebug($successMessage);
} else {
$message = __('Webhook notification clean-up feature is disabled. The job has been skipped!');
$this->adyenLogger->addAdyenDebug($message);
}
}
}
25 changes: 25 additions & 0 deletions Cron/Providers/NotificationsProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2024 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Cron\Providers;

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

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

namespace Adyen\Payment\Cron\Providers;

use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Logger\AdyenLogger;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Exception\LocalizedException;
use Magento\Store\Model\StoreManagerInterface;

class ProcessedOldNotificationsProvider implements NotificationsProviderInterface
{
public function __construct(
private readonly AdyenNotificationRepositoryInterface $adyenNotificationRepository,
private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
private readonly Config $configHelper,
private readonly StoreManagerInterface $storeManager,
private readonly AdyenLogger $adyenLogger
) { }

public function provide(): array
{
$storeId = $this->storeManager->getStore()->getId();
$numberOfDays = $this->configHelper->getRequiredDaysForOldWebhooks($storeId);

$dateFrom = date('Y-m-d H:i:s', time() - $numberOfDays * 24 * 60 * 60);

$searchCriteria = $this->searchCriteriaBuilder
->addFilter('done', 1)
->addFilter('processing', 0)
->addFilter('created_at', $dateFrom, 'lteq')
->create();

try {
$items = $this->adyenNotificationRepository->getList($searchCriteria);
return $items->getItems();
} catch (LocalizedException $e) {
$errorMessage = sprintf(
__('An error occurred while providing notifications older than %s days!'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the store name or ID here? Maybe that's useful to see in the log line to be able to debug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @acampos1916, thank you for the review.

Notification entity is independent from the stores and we don't have a store value here. Similarly, this catch block targets the issues while fetching the entities. Most probably, there won't be any notification entity for any descriptive features to add to the logs.

$numberOfDays
);

$this->adyenLogger->error($errorMessage);

return [];
}
}

public function getProviderName(): string
candemiralp marked this conversation as resolved.
Show resolved Hide resolved
{
return "Adyen processed old webhook notifications";
}
}
21 changes: 21 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_CLEANUP_OLD_WEBHOOKS = 'cleanup_old_webhooks';
const XML_REQUIRED_DAYS_OLD_WEBHOOKS = 'required_days_old_webhooks';

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

public function getIsWebhookCleanupEnabled(int $storeId = null): bool
{
return $this->getConfigData(
self::XML_CLEANUP_OLD_WEBHOOKS,
self::XML_ADYEN_ABSTRACT_PREFIX,
$storeId,
true
);
}

public function getRequiredDaysForOldWebhooks(int $storeId = null): int
{
return (int) $this->getConfigData(
self::XML_REQUIRED_DAYS_OLD_WEBHOOKS,
self::XML_ADYEN_ABSTRACT_PREFIX,
$storeId
);
}

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

namespace Adyen\Payment\Model;

use Adyen\Payment\Api\Data\NotificationInterface;
use Adyen\Payment\Api\Repository\AdyenNotificationRepositoryInterface;
use Adyen\Payment\Model\ResourceModel\Notification\CollectionFactory;
use Magento\Framework\Api\Search\SearchResultFactory;
use Magento\Framework\Api\SearchCriteria\CollectionProcessor;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Magento\Framework\ObjectManagerInterface;

class AdyenNotificationRepository implements AdyenNotificationRepositoryInterface
{
/**
* @param SearchResultFactory $searchResultsFactory
* @param CollectionFactory $collectionFactory
* @param CollectionProcessor $collectionProcessor
* @param ObjectManagerInterface $objectManager
* @param string $resourceModel
*/
public function __construct(
private readonly SearchResultFactory $searchResultsFactory,
private readonly CollectionFactory $collectionFactory,
private readonly CollectionProcessor $collectionProcessor,
private readonly ObjectManagerInterface $objectManager,
private readonly string $resourceModel
) { }

/**
* @param SearchCriteriaInterface $searchCriteria
* @return SearchResultsInterface
*/
public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
{
$searchResult = $this->searchResultsFactory->create();
$collection = $this->collectionFactory->create();
$this->collectionProcessor->process($searchCriteria, $collection);
$searchResult->setItems($collection->getItems());
$searchResult->setTotalCount($collection->getSize());

return $searchResult;
}

public function delete(NotificationInterface $entity): bool
{
$resource = $this->objectManager->get($this->resourceModel);
$resource->delete($entity);

return true;
}
}
Loading
Loading