From 65decdef1261d2643f07e0b6ea2404c9db66b0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Guti=C3=A9rrez?= Date: Tue, 16 Jun 2026 11:41:04 +0200 Subject: [PATCH 1/4] [QRD-7899] feat(configuration-webhook): add affiliate config topic Add a reusable "affiliate" configuration topic to ConfigurationWebhookAPI so payment plugins receive affiliate (Prime) config (enabled, offerId, securityToken) through the existing signed config-push mechanism, with no manual UI (Option A, autoconfig). Mirrors the AdvancedSettings vertical slice: domain model/service, per-store persistence (entity + repository), get/save topic handlers, and wiring in the Topics enum and BootstrapComponent. Signature validation and topic dispatch are reused as-is. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/BusinessLogic/BootstrapComponent.php | 53 +++++ .../Affiliate/GetAffiliateSettingsHandler.php | 46 ++++ .../SaveAffiliateSettingsHandler.php | 41 ++++ .../Handlers/Enums/Topics.php | 12 +- .../SaveAffiliateSettingsRequest.php | 63 ++++++ .../Affiliate/AffiliateSettingsResponse.php | 35 ++++ .../Affiliate/Entities/AffiliateSettings.php | 108 ++++++++++ .../AffiliateSettingsRepository.php | 107 ++++++++++ .../Affiliate/Models/AffiliateSettings.php | 72 +++++++ .../AffiliateSettingsRepositoryInterface.php | 30 +++ .../Services/AffiliateSettingsService.php | 45 ++++ tests/BusinessLogic/Common/BaseTestCase.php | 33 +++ .../MockAffiliateSettingsRepository.php | 43 ++++ .../MockAffiliateSettingsService.php | 37 ++++ .../ConfigurationWebhookAPITest.php | 102 +++++++++ .../Entities/AffiliateSettingsEntityTest.php | 22 ++ .../AffiliateSettingsRepositoryTest.php | 197 ++++++++++++++++++ .../Services/AffiliateSettingsServiceTest.php | 113 ++++++++++ 18 files changed, 1158 insertions(+), 1 deletion(-) create mode 100644 src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php create mode 100644 src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/SaveAffiliateSettingsHandler.php create mode 100644 src/BusinessLogic/ConfigurationWebhookAPI/Requests/Affiliate/SaveAffiliateSettingsRequest.php create mode 100644 src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php create mode 100644 src/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettings.php create mode 100644 src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php create mode 100644 src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php create mode 100644 src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php create mode 100644 src/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsService.php create mode 100644 tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php create mode 100644 tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php create mode 100644 tests/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettingsEntityTest.php create mode 100644 tests/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepositoryTest.php create mode 100644 tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php diff --git a/src/BusinessLogic/BootstrapComponent.php b/src/BusinessLogic/BootstrapComponent.php index f9de9782..1f42c7a3 100644 --- a/src/BusinessLogic/BootstrapComponent.php +++ b/src/BusinessLogic/BootstrapComponent.php @@ -21,6 +21,8 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\GetAdvancedSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\SaveAdvancedSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\GetAffiliateSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\SaveAffiliateSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\GetBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\SaveBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Enums\Topics; @@ -40,6 +42,8 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\WidgetSettings\SaveWidgetSettingsHandler; use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Entities\AdvancedSettings; use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Repositories\AdvancedSettingsRepository; +use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Entities\AffiliateSettings; +use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Repositories\AffiliateSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Entities\BannerSettings; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData; @@ -67,6 +71,8 @@ use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\RepositoryContracts\AdvancedSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedLoggerSettingsProvider; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedSettingsService; +use SeQura\Core\BusinessLogic\Domain\Affiliate\RepositoryContracts\AffiliateSettingsRepositoryInterface; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Integration\Banner\BannerServiceInterface; @@ -332,6 +338,16 @@ static function () { ); } ); + + ServiceRegister::registerService( + AffiliateSettingsRepositoryInterface::class, + static function () { + return new AffiliateSettingsRepository( + RepositoryRegistry::getRepository(AffiliateSettings::getClassName()), + ServiceRegister::getService(StoreContext::class) + ); + } + ); } /** @@ -667,6 +683,15 @@ static function () { } ); + ServiceRegister::registerService( + AffiliateSettingsService::class, + static function () { + return new AffiliateSettingsService( + ServiceRegister::getService(AffiliateSettingsRepositoryInterface::class) + ); + } + ); + ServiceRegister::registerService( LoggerSettingsProviderInterface::CLASS_NAME, static function () { @@ -1056,6 +1081,16 @@ protected static function initTopicHandlers(): void SaveAdvancedSettingsHandler::class ); + TopicHandlerRegistry::register( + Topics::GET_AFFILIATE_SETTINGS, + GetAffiliateSettingsHandler::class + ); + + TopicHandlerRegistry::register( + Topics::SAVE_AFFILIATE_SETTINGS, + SaveAffiliateSettingsHandler::class + ); + TopicHandlerRegistry::register( Topics::GET_BANNER_SETTINGS, GetBannerSettingsHandler::class @@ -1189,6 +1224,24 @@ static function () { } ); + ServiceRegister::registerService( + GetAffiliateSettingsHandler::class, + static function () { + return new GetAffiliateSettingsHandler( + ServiceRegister::getService(AffiliateSettingsService::class) + ); + } + ); + + ServiceRegister::registerService( + SaveAffiliateSettingsHandler::class, + static function () { + return new SaveAffiliateSettingsHandler( + ServiceRegister::getService(AffiliateSettingsService::class) + ); + } + ); + ServiceRegister::registerService( GetBannerSettingsHandler::class, static function () { diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php new file mode 100644 index 00000000..63acf978 --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php @@ -0,0 +1,46 @@ +affiliateSettingsService = $affiliateSettingsService; + } + + /** + * @param mixed[] $payload + * + * @return Response + */ + public function handle(array $payload): Response + { + $affiliateSettings = $this->affiliateSettingsService->getAffiliateSettings(); + + if (!$affiliateSettings) { + return new SuccessResponse(); + } + + return new AffiliateSettingsResponse($affiliateSettings); + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/SaveAffiliateSettingsHandler.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/SaveAffiliateSettingsHandler.php new file mode 100644 index 00000000..1d948f9a --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/SaveAffiliateSettingsHandler.php @@ -0,0 +1,41 @@ +affiliateSettingsService = $affiliateSettingsService; + } + + /** + * @inheritDoc + */ + public function handle(array $payload): Response + { + $request = SaveAffiliateSettingsRequest::fromPayload($payload); + $this->affiliateSettingsService->setAffiliateSettings($request->transformToDomainModel()); + + return new SuccessResponse(); + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php index 91debd32..2d7b70d7 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php @@ -77,6 +77,14 @@ interface Topics * @var string */ public const GET_STORE_INFO = 'get-store-info'; + /** + * @var string + */ + public const GET_AFFILIATE_SETTINGS = 'get-affiliate-settings'; + /** + * @var string + */ + public const SAVE_AFFILIATE_SETTINGS = 'save-affiliate-settings'; /** * @var string[] */ @@ -97,6 +105,8 @@ interface Topics self::GET_SHOP_CATEGORIES, self::GET_SHOP_PRODUCTS, self::GET_SELLING_COUNTRIES, - self::GET_STORE_INFO + self::GET_STORE_INFO, + self::GET_AFFILIATE_SETTINGS, + self::SAVE_AFFILIATE_SETTINGS ]; } diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Requests/Affiliate/SaveAffiliateSettingsRequest.php b/src/BusinessLogic/ConfigurationWebhookAPI/Requests/Affiliate/SaveAffiliateSettingsRequest.php new file mode 100644 index 00000000..7afe6a7a --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Requests/Affiliate/SaveAffiliateSettingsRequest.php @@ -0,0 +1,63 @@ +isEnabled = $isEnabled; + $this->offerId = $offerId; + $this->securityToken = $securityToken; + } + + /** + * @param mixed[] $payload + * + * @return self + */ + public static function fromPayload(array $payload): object + { + return new self( + $payload['isEnabled'] ?? false, + (string)($payload['offerId'] ?? ''), + (string)($payload['securityToken'] ?? '') + ); + } + + /** + * @return AffiliateSettings + */ + public function transformToDomainModel(): AffiliateSettings + { + return new AffiliateSettings($this->isEnabled, $this->offerId, $this->securityToken); + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php new file mode 100644 index 00000000..b0ee06fa --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php @@ -0,0 +1,35 @@ +settings = $settings; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return !$this->settings ? [] : $this->settings->toArray(); + } +} diff --git a/src/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettings.php b/src/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettings.php new file mode 100644 index 00000000..1121a518 --- /dev/null +++ b/src/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettings.php @@ -0,0 +1,108 @@ +storeId = $data['storeId'] ?? ''; + + $this->affiliateSettings = new DomainAffiliateSettings( + (bool)self::getDataValue($affiliateSettings, 'isEnabled', false), + (string)self::getDataValue($affiliateSettings, 'offerId', ''), + (string)self::getDataValue($affiliateSettings, 'securityToken', '') + ); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $data = parent::toArray(); + $data['storeId'] = $this->storeId; + $data['affiliateSettings'] = [ + 'isEnabled' => $this->affiliateSettings->isEnabled(), + 'offerId' => $this->affiliateSettings->getOfferId(), + 'securityToken' => $this->affiliateSettings->getSecurityToken() + ]; + + return $data; + } + + /** + * @inheritDoc + */ + public function getConfig(): EntityConfiguration + { + $indexMap = new IndexMap(); + + $indexMap->addStringIndex('storeId'); + + return new EntityConfiguration($indexMap, 'AffiliateSettings'); + } + + /** + * @return string + */ + public function getStoreId(): string + { + return $this->storeId; + } + + /** + * @param string $storeId + */ + public function setStoreId(string $storeId): void + { + $this->storeId = $storeId; + } + + /** + * @return DomainAffiliateSettings + */ + public function getAffiliateSettings(): DomainAffiliateSettings + { + return $this->affiliateSettings; + } + + /** + * @param DomainAffiliateSettings $affiliateSettings + */ + public function setAffiliateSettings(DomainAffiliateSettings $affiliateSettings): void + { + $this->affiliateSettings = $affiliateSettings; + } +} diff --git a/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php b/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php new file mode 100644 index 00000000..9a046574 --- /dev/null +++ b/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php @@ -0,0 +1,107 @@ +repository = $repository; + $this->storeContext = $storeContext; + } + + /** + * @inheritDoc + * + * @throws QueryFilterInvalidParamException + */ + public function getAffiliateSettings(): ?AffiliateSettings + { + $entity = $this->getAffiliateSettingsEntity(); + + return $entity ? $entity->getAffiliateSettings() : null; + } + + /** + * @inheritDoc + * + * @throws QueryFilterInvalidParamException + */ + public function setAffiliateSettings(AffiliateSettings $settings): void + { + $existingAffiliateSettings = $this->getAffiliateSettingsEntity(); + + if ($existingAffiliateSettings) { + $existingAffiliateSettings->setAffiliateSettings($settings); + $existingAffiliateSettings->setStoreId($this->storeContext->getStoreId()); + $this->repository->update($existingAffiliateSettings); + + return; + } + + $entity = new AffiliateSettingsEntity(); + $entity->setStoreId($this->storeContext->getStoreId()); + $entity->setAffiliateSettings($settings); + $this->repository->save($entity); + } + + /** + * @return void + * + * @throws QueryFilterInvalidParamException + */ + public function deleteAffiliateSettings(): void + { + $entity = $this->getAffiliateSettingsEntity(); + + $entity && $this->repository->delete($entity); + } + + /** + * Gets the affiliate settings entity from the database. + * + * @return ?AffiliateSettingsEntity + * + * @throws QueryFilterInvalidParamException + */ + protected function getAffiliateSettingsEntity(): ?AffiliateSettingsEntity + { + $queryFilter = new QueryFilter(); + $queryFilter->where('storeId', Operators::EQUALS, $this->storeContext->getStoreId()); + + /** + * @var AffiliateSettingsEntity $affiliateSettings + */ + $affiliateSettings = $this->repository->selectOne($queryFilter); + + return $affiliateSettings; + } +} diff --git a/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php b/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php new file mode 100644 index 00000000..a0d3713c --- /dev/null +++ b/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php @@ -0,0 +1,72 @@ +isEnabled = $isEnabled; + $this->offerId = $offerId; + $this->securityToken = $securityToken; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->isEnabled; + } + + /** + * @return string + */ + public function getOfferId(): string + { + return $this->offerId; + } + + /** + * @return string + */ + public function getSecurityToken(): string + { + return $this->securityToken; + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'isEnabled' => $this->isEnabled, + 'offerId' => $this->offerId, + 'securityToken' => $this->securityToken, + ]; + } +} diff --git a/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php b/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php new file mode 100644 index 00000000..3ca683c2 --- /dev/null +++ b/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php @@ -0,0 +1,30 @@ +affiliateSettingsRepository = $affiliateSettingsRepository; + } + + /** + * @return ?AffiliateSettings + */ + public function getAffiliateSettings(): ?AffiliateSettings + { + return $this->affiliateSettingsRepository->getAffiliateSettings(); + } + + /** + * @param AffiliateSettings $affiliateSettings + * + * @return void + */ + public function setAffiliateSettings(AffiliateSettings $affiliateSettings): void + { + $this->affiliateSettingsRepository->setAffiliateSettings($affiliateSettings); + } +} diff --git a/tests/BusinessLogic/Common/BaseTestCase.php b/tests/BusinessLogic/Common/BaseTestCase.php index b2244fa3..84edf24d 100644 --- a/tests/BusinessLogic/Common/BaseTestCase.php +++ b/tests/BusinessLogic/Common/BaseTestCase.php @@ -21,6 +21,8 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\GetAdvancedSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\SaveAdvancedSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\GetAffiliateSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\SaveAffiliateSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\GetBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\SaveBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Enums\Topics; @@ -39,6 +41,7 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\WidgetSettings\GetWidgetSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\WidgetSettings\SaveWidgetSettingsHandler; use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Entities\AdvancedSettings; +use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Entities\AffiliateSettings; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Entities\BannerSettings; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData; @@ -63,6 +66,7 @@ use SeQura\Core\BusinessLogic\DataAccess\TransactionLog\Entities\TransactionLog; use SeQura\Core\BusinessLogic\DataAccess\TransactionLog\Repositories\TransactionLogRepository; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedSettingsService; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\ProxyContracts\ConnectionProxyInterface; @@ -856,6 +860,24 @@ static function () { } ); + TestServiceRegister::registerService( + GetAffiliateSettingsHandler::class, + static function () { + return new GetAffiliateSettingsHandler( + TestServiceRegister::getService(AffiliateSettingsService::class) + ); + } + ); + + TestServiceRegister::registerService( + SaveAffiliateSettingsHandler::class, + static function () { + return new SaveAffiliateSettingsHandler( + TestServiceRegister::getService(AffiliateSettingsService::class) + ); + } + ); + TestServiceRegister::registerService( GetBannerSettingsHandler::class, static function () { @@ -977,6 +999,16 @@ static function () { SaveAdvancedSettingsHandler::class ); + TopicHandlerRegistry::register( + Topics::GET_AFFILIATE_SETTINGS, + GetAffiliateSettingsHandler::class + ); + + TopicHandlerRegistry::register( + Topics::SAVE_AFFILIATE_SETTINGS, + SaveAffiliateSettingsHandler::class + ); + TopicHandlerRegistry::register( Topics::GET_BANNER_SETTINGS, GetBannerSettingsHandler::class @@ -1045,6 +1077,7 @@ static function () { TestRepositoryRegistry::registerRepository(Credentials::getClassName(), MemoryRepository::getClassName()); TestRepositoryRegistry::registerRepository(Deployment::getClassName(), MemoryRepository::getClassName()); TestRepositoryRegistry::registerRepository(AdvancedSettings::getClassName(), MemoryRepository::getClassName()); + TestRepositoryRegistry::registerRepository(AffiliateSettings::getClassName(), MemoryRepository::getClassName()); } /** diff --git a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php new file mode 100644 index 00000000..76e7cd48 --- /dev/null +++ b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php @@ -0,0 +1,43 @@ +settings; + } + + /** + * @inheritDoc + */ + public function setAffiliateSettings(AffiliateSettings $settings): void + { + $this->settings = $settings; + } + + /** + * @return void + */ + public function deleteAffiliateSettings(): void + { + $this->settings = null; + } +} diff --git a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php new file mode 100644 index 00000000..66a9fd67 --- /dev/null +++ b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php @@ -0,0 +1,37 @@ +affiliateSettings; + } + + /** + * @param ?AffiliateSettings $affiliateSettings + * + * @return void + */ + public function setAffiliateSettings(?AffiliateSettings $affiliateSettings): void + { + $this->affiliateSettings = $affiliateSettings; + } +} diff --git a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php index 11e59241..17b34491 100644 --- a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php +++ b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php @@ -6,6 +6,8 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\BannerSettings\BannerSettingsResponse; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Models\AdvancedSettings; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedSettingsService; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Models\AffiliateSettings; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Exceptions\InvalidBannerUrlException; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\Banner; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\BannerSettings; @@ -61,6 +63,8 @@ use SeQura\Core\Tests\BusinessLogic\Common\BaseTestCase; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAdvancedSettingsRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAdvancedSettingsService; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsRepository; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerSettingsRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerSettingsService; @@ -186,6 +190,11 @@ class ConfigurationWebhookAPITest extends BaseTestCase */ private $advancedSettingsService; + /** + * @var AffiliateSettingsService $affiliateSettingsService + */ + private $affiliateSettingsService; + /** * @var MockBannerService $bannerService */ @@ -365,6 +374,14 @@ function () { return $this->advancedSettingsService; }); + $this->affiliateSettingsService = new MockAffiliateSettingsService( + new MockAffiliateSettingsRepository() + ); + + TestServiceRegister::registerService(AffiliateSettingsService::class, function () { + return $this->affiliateSettingsService; + }); + $this->bannerService = new MockBannerService(); TestServiceRegister::registerService(BannerServiceInterface::class, function () { @@ -1829,6 +1846,91 @@ public function testGetAdvancedSettingsResponseNoAdvancedSettings(): void self::assertEmpty($response->toArray()); } + /** + * @return void + * + * @throws InvalidEnvironmentException + * @throws EmptyCategoryParameterException + */ + public function testSaveAffiliateSettingsResponse(): void + { + //Arrange + $this->affiliateSettingsService->setAffiliateSettings(null); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "save-affiliate-settings", + "isEnabled" => true, + "offerId" => "1234", + "securityToken" => "abc123token" + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertEmpty($response->toArray()); + $saved = $this->affiliateSettingsService->getAffiliateSettings(); + self::assertNotNull($saved); + self::assertTrue($saved->isEnabled()); + self::assertEquals("1234", $saved->getOfferId()); + self::assertEquals("abc123token", $saved->getSecurityToken()); + } + + /** + * @return void + * + * @throws InvalidEnvironmentException + * @throws EmptyCategoryParameterException + */ + public function testGetAffiliateSettingsResponse(): void + { + //Arrange + $affiliateSettings = new AffiliateSettings(true, "1234", "abc123token"); + $this->affiliateSettingsService->setAffiliateSettings($affiliateSettings); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "get-affiliate-settings" + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertEquals([ + 'isEnabled' => true, + 'offerId' => '1234', + 'securityToken' => 'abc123token' + ], $response->toArray()); + } + + /** + * @return void + * + * @throws InvalidEnvironmentException + * @throws EmptyCategoryParameterException + */ + public function testGetAffiliateSettingsResponseNoAffiliateSettings(): void + { + //Arrange + $this->affiliateSettingsService->setAffiliateSettings(null); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "get-affiliate-settings" + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertEmpty($response->toArray()); + } + /** * @return void * diff --git a/tests/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettingsEntityTest.php b/tests/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettingsEntityTest.php new file mode 100644 index 00000000..1d998331 --- /dev/null +++ b/tests/BusinessLogic/DataAccess/Affiliate/Entities/AffiliateSettingsEntityTest.php @@ -0,0 +1,22 @@ +repository = TestRepositoryRegistry::getRepository(AffiliateSettingsEntity::getClassName()); + $this->affiliateSettingsRepository = new AffiliateSettingsRepository( + TestRepositoryRegistry::getRepository(AffiliateSettingsEntity::getClassName()), + StoreContext::getInstance() + ); + + TestServiceRegister::registerService(AffiliateSettingsRepositoryInterface::class, function () { + return $this->affiliateSettingsRepository; + }); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetSettingsNoSettings(): void + { + // act + $result = StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'getAffiliateSettings'] + ); + + // assert + self::assertEmpty($result); + } + + /** + * @throws Exception + */ + public function testGetAffiliateSettings(): void + { + // arrange + $affiliateSettings = new AffiliateSettings(true, '1234', 'abc123token'); + $entity = new AffiliateSettingsEntity(); + + $entity->setAffiliateSettings($affiliateSettings); + $entity->setStoreId('1'); + $this->repository->save($entity); + + // act + $result = StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'getAffiliateSettings'] + ); + + // assert + self::assertEquals($affiliateSettings, $result); + } + + /** + * @throws Exception + */ + public function testGetSettingsDifferentStores(): void + { + // arrange + $affiliateSettings1 = new AffiliateSettings(true, '1234', 'tokenone'); + $entity = new AffiliateSettingsEntity(); + $entity->setAffiliateSettings($affiliateSettings1); + $entity->setStoreId('1'); + $this->repository->save($entity); + + $affiliateSettings2 = new AffiliateSettings(false, '5678', 'tokentwo'); + $entity = new AffiliateSettingsEntity(); + $entity->setAffiliateSettings($affiliateSettings2); + $entity->setStoreId('2'); + $this->repository->save($entity); + + // act + $result1 = StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'getAffiliateSettings'] + ); + $result2 = StoreContext::doWithStore( + '2', + [$this->affiliateSettingsRepository, 'getAffiliateSettings'] + ); + + // assert + self::assertEquals($affiliateSettings1, $result1); + self::assertEquals($affiliateSettings2, $result2); + } + + /** + * @throws Exception + */ + public function testSetAffiliateSettings(): void + { + // arrange + $affiliateSettings = new AffiliateSettings(true, '1234', 'abc123token'); + + // act + StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'setAffiliateSettings'], + [$affiliateSettings] + ); + + // assert + $savedEntity = $this->repository->select(); + self::assertEquals($affiliateSettings, $savedEntity[0]->getAffiliateSettings()); + } + + /** + * @throws Exception + */ + public function testUpdateAffiliateSettings(): void + { + // arrange + $affiliateSettings1 = new AffiliateSettings(true, '1234', 'tokenone'); + $affiliateSettings2 = new AffiliateSettings(false, '5678', 'tokentwo'); + + // act + StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'setAffiliateSettings'], + [$affiliateSettings1] + ); + StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'setAffiliateSettings'], + [$affiliateSettings2] + ); + + // assert + $savedEntity = $this->repository->select(); + self::assertCount(1, $savedEntity); + self::assertEquals($affiliateSettings2, $savedEntity[0]->getAffiliateSettings()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testDeleteAffiliateSettings(): void + { + // arrange + $affiliateSettings1 = new AffiliateSettings(true, '1234', 'abc123token'); + $entity = new AffiliateSettingsEntity(); + + $entity->setAffiliateSettings($affiliateSettings1); + $entity->setStoreId('1'); + $this->repository->save($entity); + + // act + StoreContext::doWithStore( + '1', + [$this->affiliateSettingsRepository, 'deleteAffiliateSettings'] + ); + + // assert + $entities = $this->repository->select(); + self::assertCount(0, $entities); + } +} diff --git a/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php b/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php new file mode 100644 index 00000000..567c2fda --- /dev/null +++ b/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php @@ -0,0 +1,113 @@ +repository = new MockAffiliateSettingsRepository(); + $this->service = new AffiliateSettingsService($this->repository); + } + + /** + * @return void + */ + public function testGetAffiliateSettingsNoSettings(): void + { + //Arrange + + //Act + $result = $this->service->getAffiliateSettings(); + + //Assert + self::assertNull($result); + } + + /** + * @return void + */ + public function testGetAffiliateSettings(): void + { + //Arrange + $affiliateSettings = new AffiliateSettings(true, '1234', 'abc123token'); + $this->repository->setAffiliateSettings($affiliateSettings); + + //Act + $result = $this->service->getAffiliateSettings(); + + //Assert + self::assertNotNull($result); + self::assertTrue($result->isEnabled()); + self::assertEquals('1234', $result->getOfferId()); + self::assertEquals('abc123token', $result->getSecurityToken()); + } + + /** + * @return void + */ + public function testSetAffiliateSettingsNoSettingsInDB(): void + { + //Arrange + $affiliateSettings = new AffiliateSettings(true, '1234', 'abc123token'); + + //Act + $this->service->setAffiliateSettings($affiliateSettings); + + //Assert + $result = $this->repository->getAffiliateSettings(); + self::assertNotNull($result); + self::assertTrue($result->isEnabled()); + self::assertEquals('1234', $result->getOfferId()); + self::assertEquals('abc123token', $result->getSecurityToken()); + } + + /** + * @return void + */ + public function testSetAffiliateSettingsSettingsChanged(): void + { + //Arrange + $affiliateSettings = new AffiliateSettings(true, '1234', 'abc123token'); + $this->repository->setAffiliateSettings(new AffiliateSettings(false, '9999', 'oldtoken')); + + //Act + $this->service->setAffiliateSettings($affiliateSettings); + + //Assert + $result = $this->repository->getAffiliateSettings(); + + self::assertNotNull($result); + self::assertTrue($result->isEnabled()); + self::assertEquals('1234', $result->getOfferId()); + self::assertEquals('abc123token', $result->getSecurityToken()); + } +} From ed1716a4dd705e719555b620e03c422ade51ecae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Guti=C3=A9rrez?= Date: Fri, 19 Jun 2026 13:17:04 +0200 Subject: [PATCH 2/4] [QRD-7899] fix(affiliate): GET enabled-only, safe default, enabled-requires-creds, drop dead delete - get-affiliate-settings returns only { isEnabled } and never echoes the offer id or security token - AffiliateSettingsService::getAffiliateSettings() returns a safe disabled default instead of null, so consumers never null-check and absent means disabled - AffiliateSettings coerces enabled to disabled when offer id or security token is empty, keeping isEnabled() trustworthy for every consumer - drop unreachable deleteAffiliateSettings() from the repository and its interface Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Affiliate/GetAffiliateSettingsHandler.php | 12 ++---- .../Affiliate/AffiliateSettingsResponse.php | 13 +++--- .../AffiliateSettingsRepository.php | 12 ------ .../Affiliate/Models/AffiliateSettings.php | 5 ++- .../AffiliateSettingsRepositoryInterface.php | 5 --- .../Services/AffiliateSettingsService.php | 9 +++-- .../MockAffiliateSettingsRepository.php | 8 ---- .../MockAffiliateSettingsService.php | 6 +-- .../ConfigurationWebhookAPITest.php | 40 +++++++++++++++---- .../AffiliateSettingsRepositoryTest.php | 26 ------------ .../Services/AffiliateSettingsServiceTest.php | 7 +++- 11 files changed, 60 insertions(+), 83 deletions(-) diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php index 63acf978..4b9ee783 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Affiliate/GetAffiliateSettingsHandler.php @@ -5,7 +5,6 @@ use SeQura\Core\BusinessLogic\AdminAPI\Response\Response; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\TopicHandlerInterface; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\Affiliate\AffiliateSettingsResponse; -use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\SuccessResponse; use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; /** @@ -35,12 +34,9 @@ public function __construct(AffiliateSettingsService $affiliateSettingsService) */ public function handle(array $payload): Response { - $affiliateSettings = $this->affiliateSettingsService->getAffiliateSettings(); - - if (!$affiliateSettings) { - return new SuccessResponse(); - } - - return new AffiliateSettingsResponse($affiliateSettings); + // GET is a boolean-state read: return only whether the feature is enabled, never echo the + // offer id or security token. The service always yields settings (disabled by default when + // none are stored), so the response is enabled=false when nothing is configured. + return new AffiliateSettingsResponse($this->affiliateSettingsService->getAffiliateSettings()->isEnabled()); } } diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php index b0ee06fa..68dd1d92 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/Affiliate/AffiliateSettingsResponse.php @@ -3,7 +3,6 @@ namespace SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\Affiliate; use SeQura\Core\BusinessLogic\AdminAPI\Response\Response; -use SeQura\Core\BusinessLogic\Domain\Affiliate\Models\AffiliateSettings; /** * Class AffiliateSettingsResponse @@ -13,16 +12,16 @@ class AffiliateSettingsResponse extends Response { /** - * @var AffiliateSettings $settings + * @var bool $isEnabled */ - protected $settings; + protected $isEnabled; /** - * @param ?AffiliateSettings $settings + * @param bool $isEnabled */ - public function __construct(?AffiliateSettings $settings) + public function __construct(bool $isEnabled) { - $this->settings = $settings; + $this->isEnabled = $isEnabled; } /** @@ -30,6 +29,6 @@ public function __construct(?AffiliateSettings $settings) */ public function toArray(): array { - return !$this->settings ? [] : $this->settings->toArray(); + return ['isEnabled' => $this->isEnabled]; } } diff --git a/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php b/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php index 9a046574..b6eab3ea 100644 --- a/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php +++ b/src/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepository.php @@ -73,18 +73,6 @@ public function setAffiliateSettings(AffiliateSettings $settings): void $this->repository->save($entity); } - /** - * @return void - * - * @throws QueryFilterInvalidParamException - */ - public function deleteAffiliateSettings(): void - { - $entity = $this->getAffiliateSettingsEntity(); - - $entity && $this->repository->delete($entity); - } - /** * Gets the affiliate settings entity from the database. * diff --git a/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php b/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php index a0d3713c..0fe7533b 100644 --- a/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php +++ b/src/BusinessLogic/Domain/Affiliate/Models/AffiliateSettings.php @@ -29,7 +29,10 @@ class AffiliateSettings */ public function __construct(bool $isEnabled, string $offerId, string $securityToken) { - $this->isEnabled = $isEnabled; + // "Enabled" requires credentials: an enabled flag with no offer id or security token is not + // usable, so it is coerced to disabled. This keeps isEnabled() trustworthy for every consumer + // (postback gating, config provider) without each one having to re-check the credentials. + $this->isEnabled = $isEnabled && '' !== $offerId && '' !== $securityToken; $this->offerId = $offerId; $this->securityToken = $securityToken; } diff --git a/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php b/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php index 3ca683c2..a4712006 100644 --- a/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php +++ b/src/BusinessLogic/Domain/Affiliate/RepositoryContracts/AffiliateSettingsRepositoryInterface.php @@ -22,9 +22,4 @@ public function getAffiliateSettings(): ?AffiliateSettings; * @return void */ public function setAffiliateSettings(AffiliateSettings $settings): void; - - /** - * @return void - */ - public function deleteAffiliateSettings(): void; } diff --git a/src/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsService.php b/src/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsService.php index f000e517..8235a57e 100644 --- a/src/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsService.php +++ b/src/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsService.php @@ -26,11 +26,14 @@ public function __construct(AffiliateSettingsRepositoryInterface $affiliateSetti } /** - * @return ?AffiliateSettings + * Returns the stored affiliate settings, or a safe disabled default when none are stored, so + * consumers never have to null-check and "absent" deterministically means disabled. + * + * @return AffiliateSettings */ - public function getAffiliateSettings(): ?AffiliateSettings + public function getAffiliateSettings(): AffiliateSettings { - return $this->affiliateSettingsRepository->getAffiliateSettings(); + return $this->affiliateSettingsRepository->getAffiliateSettings() ?? new AffiliateSettings(false, '', ''); } /** diff --git a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php index 76e7cd48..7a322869 100644 --- a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php +++ b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsRepository.php @@ -32,12 +32,4 @@ public function setAffiliateSettings(AffiliateSettings $settings): void { $this->settings = $settings; } - - /** - * @return void - */ - public function deleteAffiliateSettings(): void - { - $this->settings = null; - } } diff --git a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php index 66a9fd67..dc144beb 100644 --- a/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php +++ b/tests/BusinessLogic/Common/MockComponents/MockAffiliateSettingsService.php @@ -18,11 +18,11 @@ class MockAffiliateSettingsService extends AffiliateSettingsService private $affiliateSettings; /** - * @return ?AffiliateSettings + * @return AffiliateSettings */ - public function getAffiliateSettings(): ?AffiliateSettings + public function getAffiliateSettings(): AffiliateSettings { - return $this->affiliateSettings; + return $this->affiliateSettings ?? new AffiliateSettings(false, '', ''); } /** diff --git a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php index 17b34491..f3e4b3b9 100644 --- a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php +++ b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php @@ -1878,6 +1878,34 @@ public function testSaveAffiliateSettingsResponse(): void self::assertEquals("abc123token", $saved->getSecurityToken()); } + /** + * @return void + * + * @throws InvalidEnvironmentException + * @throws EmptyCategoryParameterException + */ + public function testSaveAffiliateSettingsEnabledWithoutCredentialsIsCoercedToDisabled(): void + { + //Arrange + $this->affiliateSettingsService->setAffiliateSettings(null); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "save-affiliate-settings", + "isEnabled" => true, + "offerId" => "", + "securityToken" => "" + ] + ); + + //Assert: enabled with no credentials is unusable, so it persists as disabled. + self::assertTrue($response->isSuccessful()); + $saved = $this->affiliateSettingsService->getAffiliateSettings(); + self::assertFalse($saved->isEnabled()); + } + /** * @return void * @@ -1898,13 +1926,9 @@ public function testGetAffiliateSettingsResponse(): void ] ); - //Assert + //Assert: GET returns only the enabled state, never the offer id or security token. self::assertTrue($response->isSuccessful()); - self::assertEquals([ - 'isEnabled' => true, - 'offerId' => '1234', - 'securityToken' => 'abc123token' - ], $response->toArray()); + self::assertEquals(['isEnabled' => true], $response->toArray()); } /** @@ -1926,9 +1950,9 @@ public function testGetAffiliateSettingsResponseNoAffiliateSettings(): void ] ); - //Assert + //Assert: no stored settings reads as a deterministic enabled=false. self::assertTrue($response->isSuccessful()); - self::assertEmpty($response->toArray()); + self::assertEquals(['isEnabled' => false], $response->toArray()); } /** diff --git a/tests/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepositoryTest.php b/tests/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepositoryTest.php index 99e55ddd..028cc8b7 100644 --- a/tests/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepositoryTest.php +++ b/tests/BusinessLogic/DataAccess/Affiliate/Repositories/AffiliateSettingsRepositoryTest.php @@ -168,30 +168,4 @@ public function testUpdateAffiliateSettings(): void self::assertCount(1, $savedEntity); self::assertEquals($affiliateSettings2, $savedEntity[0]->getAffiliateSettings()); } - - /** - * @return void - * - * @throws Exception - */ - public function testDeleteAffiliateSettings(): void - { - // arrange - $affiliateSettings1 = new AffiliateSettings(true, '1234', 'abc123token'); - $entity = new AffiliateSettingsEntity(); - - $entity->setAffiliateSettings($affiliateSettings1); - $entity->setStoreId('1'); - $this->repository->save($entity); - - // act - StoreContext::doWithStore( - '1', - [$this->affiliateSettingsRepository, 'deleteAffiliateSettings'] - ); - - // assert - $entities = $this->repository->select(); - self::assertCount(0, $entities); - } } diff --git a/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php b/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php index 567c2fda..c2188f75 100644 --- a/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php +++ b/tests/BusinessLogic/Domain/Affiliate/Services/AffiliateSettingsServiceTest.php @@ -48,8 +48,11 @@ public function testGetAffiliateSettingsNoSettings(): void //Act $result = $this->service->getAffiliateSettings(); - //Assert - self::assertNull($result); + //Assert: absent settings yield a safe disabled default, never null. + self::assertNotNull($result); + self::assertFalse($result->isEnabled()); + self::assertSame('', $result->getOfferId()); + self::assertSame('', $result->getSecurityToken()); } /** From e4f14cfd5b54ba9b4607f024a665d0a6a4df82e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Guti=C3=A9rrez?= Date: Fri, 19 Jun 2026 13:31:26 +0200 Subject: [PATCH 3/4] [QRD-7899] feat(affiliate): consume connect-time configuration_data.affiliate Read the affiliate block (enabled/offer_id/security_token) from the connect-time configuration_data and persist it via AffiliateSettingsService, so (re)connecting a store provisions its affiliate settings. This closes the headline gap: the block was delivered by timon and then dropped. When no credential carries an affiliate block (e.g. the merchant API is not emitting it yet) the stored settings are left untouched. - inject AffiliateSettingsService into CredentialsService; persist in validateAndUpdateCredentials - correct Credentials::getPayload() docblock to array - wire AffiliateSettingsService in BootstrapComponent and the test service register - cover with ConnectionServiceTest::testConnectAffiliateSettingsSaved Co-Authored-By: Claude Opus 4.8 (1M context) --- src/BusinessLogic/BootstrapComponent.php | 3 +- .../Domain/Connection/Models/Credentials.php | 2 +- .../Services/CredentialsService.php | 42 ++++++++++++++++++- tests/BusinessLogic/Common/BaseTestCase.php | 16 ++++++- .../ConfigurationWebhookAPITest.php | 3 +- .../Services/ConnectionServiceTest.php | 40 ++++++++++++++++++ .../Tasks/StoreIntegrationMigrateTaskTest.php | 5 ++- .../MerchantOrderRequestBuilderTest.php | 5 ++- 8 files changed, 109 insertions(+), 7 deletions(-) diff --git a/src/BusinessLogic/BootstrapComponent.php b/src/BusinessLogic/BootstrapComponent.php index 1f42c7a3..fd620ac8 100644 --- a/src/BusinessLogic/BootstrapComponent.php +++ b/src/BusinessLogic/BootstrapComponent.php @@ -400,7 +400,8 @@ static function () { ServiceRegister::getService(ConnectionProxyInterface::class), ServiceRegister::getService(CredentialsRepositoryInterface::class), ServiceRegister::getService(CountryConfigurationRepositoryInterface::class), - ServiceRegister::getService(PaymentMethodRepositoryInterface::class) + ServiceRegister::getService(PaymentMethodRepositoryInterface::class), + ServiceRegister::getService(AffiliateSettingsService::class) ); } ); diff --git a/src/BusinessLogic/Domain/Connection/Models/Credentials.php b/src/BusinessLogic/Domain/Connection/Models/Credentials.php index da89e4dd..8e539429 100644 --- a/src/BusinessLogic/Domain/Connection/Models/Credentials.php +++ b/src/BusinessLogic/Domain/Connection/Models/Credentials.php @@ -98,7 +98,7 @@ public function getAssetsKey(): string } /** - * @return array + * @return array */ public function getPayload(): array { diff --git a/src/BusinessLogic/Domain/Connection/Services/CredentialsService.php b/src/BusinessLogic/Domain/Connection/Services/CredentialsService.php index ded13ec6..6b9f98b6 100644 --- a/src/BusinessLogic/Domain/Connection/Services/CredentialsService.php +++ b/src/BusinessLogic/Domain/Connection/Services/CredentialsService.php @@ -2,6 +2,8 @@ namespace SeQura\Core\BusinessLogic\Domain\Connection\Services; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Models\AffiliateSettings; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\BadMerchantIdException; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\CredentialsNotFoundException; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\WrongCredentialsException; @@ -43,22 +45,30 @@ class CredentialsService */ protected $paymentMethodRepository; + /** + * @var AffiliateSettingsService + */ + protected $affiliateSettingsService; + /** * @param ConnectionProxyInterface $connectionProxy * @param CredentialsRepositoryInterface $credentialsRepository * @param CountryConfigurationRepositoryInterface $countryConfigurationRepository * @param PaymentMethodRepositoryInterface $paymentMethodRepository + * @param AffiliateSettingsService $affiliateSettingsService */ public function __construct( ConnectionProxyInterface $connectionProxy, CredentialsRepositoryInterface $credentialsRepository, CountryConfigurationRepositoryInterface $countryConfigurationRepository, - PaymentMethodRepositoryInterface $paymentMethodRepository + PaymentMethodRepositoryInterface $paymentMethodRepository, + AffiliateSettingsService $affiliateSettingsService ) { $this->connectionProxy = $connectionProxy; $this->credentialsRepository = $credentialsRepository; $this->countryConfigurationRepository = $countryConfigurationRepository; $this->paymentMethodRepository = $paymentMethodRepository; + $this->affiliateSettingsService = $affiliateSettingsService; } /** @@ -85,10 +95,40 @@ public function validateAndUpdateCredentials(ConnectionData $connectionData): ar $this->credentialsRepository->deleteCredentialsByDeploymentId($connectionData->getDeployment()); $this->credentialsRepository->setCredentials($credentials); + $this->updateAffiliateSettingsFromCredentials($credentials); return $credentials; } + /** + * Persists the affiliate settings carried in the connect-time configuration_data, when present. + * + * The block is merchant-level (identical across the per-country credentials), so the first one + * that carries it wins. When no credential carries an affiliate block (e.g. the merchant API has + * not started emitting it yet) the stored settings are left untouched. + * + * @param Credentials[] $credentials + * + * @return void + */ + private function updateAffiliateSettingsFromCredentials(array $credentials): void + { + foreach ($credentials as $credential) { + $affiliate = $credential->getPayload()['affiliate'] ?? null; + if (!\is_array($affiliate)) { + continue; + } + + $this->affiliateSettingsService->setAffiliateSettings(new AffiliateSettings( + (bool)($affiliate['enabled'] ?? false), + (string)($affiliate['offer_id'] ?? ''), + (string)($affiliate['security_token'] ?? '') + )); + + return; + } + } + /** * Updates country configuration with new merchant ids and remove payment methods with old merchant ids * diff --git a/tests/BusinessLogic/Common/BaseTestCase.php b/tests/BusinessLogic/Common/BaseTestCase.php index 84edf24d..d67a0472 100644 --- a/tests/BusinessLogic/Common/BaseTestCase.php +++ b/tests/BusinessLogic/Common/BaseTestCase.php @@ -42,6 +42,7 @@ use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\WidgetSettings\SaveWidgetSettingsHandler; use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Entities\AdvancedSettings; use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Entities\AffiliateSettings; +use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Repositories\AffiliateSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Entities\BannerSettings; use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData; @@ -66,6 +67,7 @@ use SeQura\Core\BusinessLogic\DataAccess\TransactionLog\Entities\TransactionLog; use SeQura\Core\BusinessLogic\DataAccess\TransactionLog\Repositories\TransactionLogRepository; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedSettingsService; +use SeQura\Core\BusinessLogic\Domain\Affiliate\RepositoryContracts\AffiliateSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; @@ -304,12 +306,24 @@ protected function setUp(): void TestServiceRegister::getService(StoreIntegrationService::class) ); }, + AffiliateSettingsRepositoryInterface::class => function () { + return new AffiliateSettingsRepository( + TestRepositoryRegistry::getRepository(AffiliateSettings::getClassName()), + StoreContext::getInstance() + ); + }, + AffiliateSettingsService::class => static function () { + return new AffiliateSettingsService( + TestServiceRegister::getService(AffiliateSettingsRepositoryInterface::class) + ); + }, CredentialsService::class => static function () { return new CredentialsService( TestServiceRegister::getService(ConnectionProxyInterface::class), TestServiceRegister::getService(CredentialsRepositoryInterface::class), TestServiceRegister::getService(CountryConfigurationRepositoryInterface::class), - TestServiceRegister::getService(PaymentMethodRepositoryInterface::class) + TestServiceRegister::getService(PaymentMethodRepositoryInterface::class), + TestServiceRegister::getService(AffiliateSettingsService::class) ); }, PaymentMethodsService::class => static function () { diff --git a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php index f3e4b3b9..4cb006a1 100644 --- a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php +++ b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php @@ -398,7 +398,8 @@ function () { new MockConnectionProxy(), new MockCredentialsRepository(), new MockCountryConfigurationRepository(), - new MockPaymentMethodRepository() + new MockPaymentMethodRepository(), + $this->affiliateSettingsService ); TestServiceRegister::registerService(CredentialsService::class, function () { diff --git a/tests/BusinessLogic/Domain/Connection/Services/ConnectionServiceTest.php b/tests/BusinessLogic/Domain/Connection/Services/ConnectionServiceTest.php index 31d10d08..640779a6 100644 --- a/tests/BusinessLogic/Domain/Connection/Services/ConnectionServiceTest.php +++ b/tests/BusinessLogic/Domain/Connection/Services/ConnectionServiceTest.php @@ -10,6 +10,7 @@ use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\InvalidEnvironmentException; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\WrongCredentialsException; use SeQura\Core\BusinessLogic\Domain\Connection\Models\AuthorizationCredentials; +use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\Models\Credentials; use SeQura\Core\BusinessLogic\Domain\Connection\ProxyContracts\ConnectionProxyInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\ConnectionDataRepositoryInterface; @@ -295,6 +296,45 @@ public function testConnectCredentialsSaved(): void self::assertEquals($credentials, $savedCredentials); } + /** + * @return void + * + * @throws BadMerchantIdException + * @throws CapabilitiesEmptyException + * @throws HttpRequestException + * @throws InvalidEnvironmentException + * @throws PaymentMethodNotFoundException + * @throws WrongCredentialsException + */ + public function testConnectAffiliateSettingsSaved(): void + { + //Arrange + $connectionData = new DomainConnectionData( + BaseProxy::TEST_MODE, + 'test_merchant', + 'sequra', + new AuthorizationCredentials('test_username', 'test_password') + ); + $this->mockConnectionProxy->setMockCredentials([ + new Credentials('logeecom1', 'PT', 'EUR', 'assetsKey1', [ + 'affiliate' => [ + 'enabled' => true, + 'offer_id' => 'mock-affiliate-offer', + 'security_token' => 'mock-affiliate-security-token', + ], + ], 'sequra'), + ]); + + //Act + $this->connectionService->connect([$connectionData]); + + //Assert: the connect-time affiliate block is persisted via the affiliate settings service. + $affiliateSettings = TestServiceRegister::getService(AffiliateSettingsService::class)->getAffiliateSettings(); + self::assertTrue($affiliateSettings->isEnabled()); + self::assertEquals('mock-affiliate-offer', $affiliateSettings->getOfferId()); + self::assertEquals('mock-affiliate-security-token', $affiliateSettings->getSecurityToken()); + } + /** * @return void * diff --git a/tests/BusinessLogic/Domain/Migration/Tasks/StoreIntegrationMigrateTaskTest.php b/tests/BusinessLogic/Domain/Migration/Tasks/StoreIntegrationMigrateTaskTest.php index 67597f9a..55680dd2 100644 --- a/tests/BusinessLogic/Domain/Migration/Tasks/StoreIntegrationMigrateTaskTest.php +++ b/tests/BusinessLogic/Domain/Migration/Tasks/StoreIntegrationMigrateTaskTest.php @@ -13,6 +13,8 @@ use SeQura\Core\Infrastructure\ORM\Exceptions\RepositoryClassException; use SeQura\Core\Infrastructure\ORM\Exceptions\RepositoryNotRegisteredException; use SeQura\Core\Tests\BusinessLogic\Common\BaseTestCase; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsRepository; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionDataRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionProxy; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionService; @@ -88,7 +90,8 @@ protected function setUp(): void new MockConnectionProxy(), new MockCredentialsRepository(), new MockCountryConfigurationRepository(), - new MockPaymentMethodRepository() + new MockPaymentMethodRepository(), + new MockAffiliateSettingsService(new MockAffiliateSettingsRepository()) ), $this->storeIntegrationService ); diff --git a/tests/BusinessLogic/Domain/Order/Builders/MerchantOrderRequestBuilderTest.php b/tests/BusinessLogic/Domain/Order/Builders/MerchantOrderRequestBuilderTest.php index 260a91eb..731295f4 100644 --- a/tests/BusinessLogic/Domain/Order/Builders/MerchantOrderRequestBuilderTest.php +++ b/tests/BusinessLogic/Domain/Order/Builders/MerchantOrderRequestBuilderTest.php @@ -20,6 +20,8 @@ use SeQura\Core\BusinessLogic\Domain\StoreIntegration\Services\StoreIntegrationService; use SeQura\Core\Infrastructure\ORM\Exceptions\RepositoryClassException; use SeQura\Core\Tests\BusinessLogic\Common\BaseTestCase; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsRepository; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAffiliateSettingsService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionDataRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionProxy; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionService; @@ -70,7 +72,8 @@ public function setUp(): void new MockConnectionProxy(), new MockCredentialsRepository(), new MockCountryConfigurationRepository(), - new MockPaymentMethodRepository() + new MockPaymentMethodRepository(), + new MockAffiliateSettingsService(new MockAffiliateSettingsRepository()) ); $this->connectionService = new MockConnectionService( new MockConnectionDataRepository(), From 2c4f05cb88787d1c5dd2bd1ad5d1813c0a385ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Guti=C3=A9rrez?= Date: Fri, 19 Jun 2026 13:35:01 +0200 Subject: [PATCH 4/4] [QRD-7899] docs(affiliate): document config webhook topics in README + CHANGELOG - README: list GetAffiliateSettingsHandler and SaveAffiliateSettingsHandler under TopicHandlerRegistry - CHANGELOG: add a v5.5.0 entry for the affiliate config topics, entity/service and connect-time provisioning (note: the changelog had drifted at v1.0.13 while tags reached v5.4.0) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ README.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c99be372..64985449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +# [v5.5.0](https://github.com/sequra/integration-core/tree/v5.5.0) +## Added +- Affiliate configuration support: the `AffiliateSettings` entity and `AffiliateSettingsService`, the `get-affiliate-settings` and `save-affiliate-settings` configuration webhook topics, and connect time provisioning that reads the `affiliate` block from the merchant `configuration_data` and persists it. + # [v1.0.13](https://github.com/sequra/integration-core/tree/v1.0.13) ## Changed - Added compatibility with PHP8.2. diff --git a/README.md b/README.md index 6562abb4..045b9915 100644 --- a/README.md +++ b/README.md @@ -3171,6 +3171,8 @@ The Configuration WebhookAPI follows a **simplified Controller pattern** with ** - **GetShopProductsHandler**: Handles `get-shop-products` webhook topic - **GetStoreInfoHandler**: Handles `get-store-info` webhook topic - **SaveWidgetSettingsHandler**: Handles `save-widget-settings` webhook topic + - **GetAffiliateSettingsHandler**: Handles `get-affiliate-settings` webhook topic + - **SaveAffiliateSettingsHandler**: Handles `save-affiliate-settings` webhook topic #### Webhook Processing Flow