diff --git a/Api/Repository/AdyenNotificationRepositoryInterface.php b/Api/Repository/AdyenNotificationRepositoryInterface.php new file mode 100644 index 0000000000..67453ec80b --- /dev/null +++ b/Api/Repository/AdyenNotificationRepositoryInterface.php @@ -0,0 +1,39 @@ + + */ + +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; +} diff --git a/Cron/CleanupNotifications.php b/Cron/CleanupNotifications.php new file mode 100644 index 0000000000..fc3bf26dfc --- /dev/null +++ b/Cron/CleanupNotifications.php @@ -0,0 +1,66 @@ + + */ + +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 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) { + foreach ($provider->provide() as $notificationToCleanup) { + $isSuccessfullyDeleted = $this->adyenNotificationRepository->delete($notificationToCleanup); + + if ($isSuccessfullyDeleted) { + $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); + } + } +} diff --git a/Cron/Providers/NotificationsProviderInterface.php b/Cron/Providers/NotificationsProviderInterface.php new file mode 100644 index 0000000000..34f9b06256 --- /dev/null +++ b/Cron/Providers/NotificationsProviderInterface.php @@ -0,0 +1,25 @@ + + */ + +namespace Adyen\Payment\Cron\Providers; + +interface NotificationsProviderInterface +{ + /** + * @return array + */ + public function provide(): array; + + /** + * @return string + */ + public function getProviderName(): string; +} diff --git a/Cron/Providers/ProcessedOldNotificationsProvider.php b/Cron/Providers/ProcessedOldNotificationsProvider.php new file mode 100644 index 0000000000..60196b7c12 --- /dev/null +++ b/Cron/Providers/ProcessedOldNotificationsProvider.php @@ -0,0 +1,63 @@ + + */ + +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!'), + $numberOfDays + ); + + $this->adyenLogger->error($errorMessage); + + return []; + } + } + + public function getProviderName(): string + { + return "Adyen processed old webhook notifications"; + } +} diff --git a/Helper/Config.php b/Helper/Config.php index 8657bfe26a..483960e026 100644 --- a/Helper/Config.php +++ b/Helper/Config.php @@ -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; @@ -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 getConfigData(string $field, string $xmlPrefix, ?int $storeId, bool $flag = false): mixed { $path = implode("/", [self::XML_PAYMENT_PREFIX, $xmlPrefix, $field]); diff --git a/Model/AdyenNotificationRepository.php b/Model/AdyenNotificationRepository.php new file mode 100644 index 0000000000..277d12be60 --- /dev/null +++ b/Model/AdyenNotificationRepository.php @@ -0,0 +1,62 @@ + + */ + +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; + } +} diff --git a/etc/adminhtml/system/adyen_testing_performance.xml b/etc/adminhtml/system/adyen_testing_performance.xml index c384428841..358be8ffcf 100644 --- a/etc/adminhtml/system/adyen_testing_performance.xml +++ b/etc/adminhtml/system/adyen_testing_performance.xml @@ -55,5 +55,14 @@ ]]> + + + Magento\Config\Model\Config\Source\Yesno + payment/adyen_abstract/cleanup_old_webhooks + + Webhooks older than certain days will be removed from the database by a cronjob if this feature is enabled. + The default value is 90 days and this can be configured by overriding `payment/adyen_abstract/required_days_old_webhooks` configuration path. + + diff --git a/etc/config.xml b/etc/config.xml index b432bd83ff..76e66ba2b4 100755 --- a/etc/config.xml +++ b/etc/config.xml @@ -34,6 +34,8 @@ canceled manual 1 + 0 + 90 0 diff --git a/etc/crontab.xml b/etc/crontab.xml index d638f45ad1..cf093da854 100755 --- a/etc/crontab.xml +++ b/etc/crontab.xml @@ -24,5 +24,8 @@ 0 0 * * * + + 0 0 * * * + diff --git a/etc/di.xml b/etc/di.xml index aefb9b552a..7a3438e34c 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1693,6 +1693,8 @@ + @@ -1717,6 +1719,20 @@ + + + Adyen\Payment\Model\ResourceModel\Notification + + + + + + + Adyen\Payment\Cron\Providers\ProcessedOldNotificationsProvider + + + +