diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 5d6cd27d..64c5ff81 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 id: cache-composer with: path: | diff --git a/composer.lock b/composer.lock index 1555a6d1..0a46c1aa 100644 --- a/composer.lock +++ b/composer.lock @@ -671,16 +671,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -725,7 +720,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1178,16 +1173,16 @@ }, { "name": "sebastian/comparator", - "version": "3.0.5", + "version": "3.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" + "reference": "bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52", + "reference": "bc7d8ac2fe1cce229bff9b5fd4efe65918a1ff52", "shasum": "" }, "require": { @@ -1240,15 +1235,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.7" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:31:48+00:00" + "time": "2026-01-24T09:20:25+00:00" }, { "name": "sebastian/diff", @@ -1381,16 +1388,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.6", + "version": "3.1.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" + "reference": "64cfeaa341951ceb2019d7b98232399d57bb2296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64cfeaa341951ceb2019d7b98232399d57bb2296", + "reference": "64cfeaa341951ceb2019d7b98232399d57bb2296", "shasum": "" }, "require": { @@ -1446,28 +1453,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:21:38+00:00" + "time": "2025-09-24T05:55:14+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.5", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9" + "reference": "800689427e3e8cf57a8fe38fcd1d4344c9b2f046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/91c7c47047a971f02de57ed6f040087ef110c5d9", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/800689427e3e8cf57a8fe38fcd1d4344c9b2f046", + "reference": "800689427e3e8cf57a8fe38fcd1d4344c9b2f046", "shasum": "" }, "require": { @@ -1510,15 +1529,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:13:16+00:00" + "time": "2025-08-10T05:40:12+00:00" }, { "name": "sebastian/object-enumerator", @@ -1634,16 +1665,16 @@ }, { "name": "sebastian/recursion-context", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" + "reference": "8fe7e75986a9d24b4cceae847314035df7703a5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/8fe7e75986a9d24b4cceae847314035df7703a5a", + "reference": "8fe7e75986a9d24b4cceae847314035df7703a5a", "shasum": "" }, "require": { @@ -1685,15 +1716,27 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2024-03-01T14:07:30+00:00" + "time": "2025-08-10T05:25:53+00:00" }, { "name": "sebastian/resource-operations", @@ -1912,16 +1955,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -1938,11 +1981,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -1992,20 +2030,20 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -2034,7 +2072,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -2042,32 +2080,32 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -2098,14 +2136,14 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -2114,9 +2152,9 @@ "ext-ctype": "*", "ext-mbstring": "*" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/run-tests.sh b/run-tests.sh index 5865ad08..4924ff19 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -20,6 +20,9 @@ echo -e "\e[32mUnit tests: PHP 8.3\e[39m" echo -e "\e[32mUnit tests: PHP 8.4\e[39m" /usr/bin/php8.4 ./vendor/bin/phpunit --configuration ./phpunit.xml +echo -e "\e[32mUnit tests: PHP 8.5\e[39m" +/usr/bin/php8.5 ./vendor/bin/phpunit --configuration ./phpunit.xml + echo -e "\e[32mPHP_CodeSniffer\e[39m" vendor/bin/phpcs --standard=.phpcs.xml.dist --warning-severity=0 . diff --git a/src/BusinessLogic/AdminAPI/AdminAPI.php b/src/BusinessLogic/AdminAPI/AdminAPI.php index 4e412736..18d33018 100644 --- a/src/BusinessLogic/AdminAPI/AdminAPI.php +++ b/src/BusinessLogic/AdminAPI/AdminAPI.php @@ -4,6 +4,7 @@ use SeQura\Core\BusinessLogic\AdminAPI\Aspects\ErrorHandlingAspect; use SeQura\Core\BusinessLogic\AdminAPI\Aspects\StoreContextAspect; +use SeQura\Core\BusinessLogic\AdminAPI\BannerSettings\BannerSettingsController; use SeQura\Core\BusinessLogic\AdminAPI\Connection\ConnectionController; use SeQura\Core\BusinessLogic\AdminAPI\CountryConfiguration\CountryConfigurationController; use SeQura\Core\BusinessLogic\AdminAPI\Deployments\DeploymentsController; @@ -98,6 +99,21 @@ public function widgetConfiguration(string $storeId): Aspects ->beforeEachMethodOfService(PromotionalWidgetsController::class); } + /** + * Returns a BannerSettingsController instance. + * + * @param string $storeId + * + * @return Aspects + */ + public function bannerSettings(string $storeId): Aspects + { + return Aspects + ::run(new ErrorHandlingAspect()) + ->andRun(new StoreContextAspect($storeId)) + ->beforeEachMethodOfService(BannerSettingsController::class); + } + /** * Returns a PaymentMethodsController instance. * diff --git a/src/BusinessLogic/AdminAPI/BannerSettings/BannerSettingsController.php b/src/BusinessLogic/AdminAPI/BannerSettings/BannerSettingsController.php new file mode 100644 index 00000000..273b9898 --- /dev/null +++ b/src/BusinessLogic/AdminAPI/BannerSettings/BannerSettingsController.php @@ -0,0 +1,72 @@ +bannerSettingsService = $bannerSettingsService; + $this->bannerService = $bannerService; + } + + /** + * Gets active banner settings. + * + * @throws Exception + */ + public function getBannerSettings(): BannerSettingsResponse + { + return new BannerSettingsResponse( + $this->bannerSettingsService->getBannerSettings(), + $this->bannerService->getBannerDisplayLocations() + ); + } + + /** + * Sets banner settings. + * + * @param BannerSettingsRequest $settingsRequest + * + * @return BannerSettingsResponse + * + * @throws BannerImageRequiredException + * @throws InvalidURLException + */ + public function setBannerSettings(BannerSettingsRequest $settingsRequest): BannerSettingsResponse + { + return new BannerSettingsResponse( + $this->bannerSettingsService->setBannerSettings($settingsRequest->transformToDomainModel()), + $this->bannerService->getBannerDisplayLocations() + ); + } +} diff --git a/src/BusinessLogic/AdminAPI/BannerSettings/Requests/BannerSettingsRequest.php b/src/BusinessLogic/AdminAPI/BannerSettings/Requests/BannerSettingsRequest.php new file mode 100644 index 00000000..b3ba9dd2 --- /dev/null +++ b/src/BusinessLogic/AdminAPI/BannerSettings/Requests/BannerSettingsRequest.php @@ -0,0 +1,15 @@ +> + */ + protected $bannerConfigs; + + /** + * @param array> $bannerConfigs + */ + public function __construct( + array $bannerConfigs = [] + ) { + $this->bannerConfigs = $bannerConfigs; + } + + /** + * Transforms the request to a BannerSettings object. + * + * @return BannerSettings + */ + public function transformToDomainModel(): object + { + $arrayOfBannerConfigs = []; + foreach ($this->bannerConfigs as $bannerConfig) { + $arrayOfBannerConfigs[] = new Banner( + $bannerConfig['country'] ?? '', + $bannerConfig['linkUrl'] ?? '', + '', + $bannerConfig['displayLocation'] ?? '', + isset($bannerConfig['imageBase64']) && $bannerConfig['imageBase64'] !== '' + ? $bannerConfig['imageBase64'] + : null + ); + } + + return new BannerSettings( + $arrayOfBannerConfigs + ); + } +} diff --git a/src/BusinessLogic/AdminAPI/BannerSettings/Responses/BannerSettingsResponse.php b/src/BusinessLogic/AdminAPI/BannerSettings/Responses/BannerSettingsResponse.php new file mode 100644 index 00000000..dbba2ba5 --- /dev/null +++ b/src/BusinessLogic/AdminAPI/BannerSettings/Responses/BannerSettingsResponse.php @@ -0,0 +1,47 @@ +bannerSettings = $bannerSettings; + $this->displayLocations = $displayLocations; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $settingsArray = $this->bannerSettings ? $this->bannerSettings->toArray() : []; + + return [ + 'displayLocations' => $this->displayLocations, + 'bannerConfigs' => $settingsArray['bannerConfigs'] ?? [], + ]; + } +} diff --git a/src/BusinessLogic/BootstrapComponent.php b/src/BusinessLogic/BootstrapComponent.php index 742f6d75..384670dc 100644 --- a/src/BusinessLogic/BootstrapComponent.php +++ b/src/BusinessLogic/BootstrapComponent.php @@ -2,6 +2,7 @@ namespace SeQura\Core\BusinessLogic; +use SeQura\Core\BusinessLogic\AdminAPI\BannerSettings\BannerSettingsController; use SeQura\Core\BusinessLogic\AdminAPI\Connection\ConnectionController; use SeQura\Core\BusinessLogic\AdminAPI\CountryConfiguration\CountryConfigurationController; use SeQura\Core\BusinessLogic\AdminAPI\Deployments\DeploymentsController; @@ -13,12 +14,15 @@ use SeQura\Core\BusinessLogic\AdminAPI\PromotionalWidgets\PromotionalWidgetsController; use SeQura\Core\BusinessLogic\AdminAPI\Store\StoreController; use SeQura\Core\BusinessLogic\AdminAPI\TransactionLogs\TransactionLogsController; +use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController; 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\BannerSettings\GetBannerSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\SaveBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Enums\Topics; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\GeneralSettings\GetGeneralSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\GeneralSettings\SaveGeneralSettingsHandler; @@ -36,6 +40,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\BannerSettings\Entities\BannerSettings; +use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Repositories\ConnectionDataRepository; use SeQura\Core\BusinessLogic\DataAccess\CountryConfiguration\Entities\CountryConfiguration; @@ -61,6 +67,9 @@ 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\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; +use SeQura\Core\BusinessLogic\Domain\Integration\Banner\BannerServiceInterface; use SeQura\Core\Infrastructure\Logger\Interfaces\LoggerSettingsProviderInterface; use SeQura\Core\BusinessLogic\Domain\Connection\ProxyContracts\ConnectionProxyInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\ConnectionDataRepositoryInterface; @@ -254,6 +263,16 @@ static function () { } ); + ServiceRegister::registerService( + BannerSettingsRepositoryInterface::class, + static function () { + return new BannerSettingsRepository( + RepositoryRegistry::getRepository(BannerSettings::getClassName()), + ServiceRegister::getService(StoreContext::class) + ); + } + ); + ServiceRegister::registerService( TransactionLogRepositoryInterface::class, static function () { @@ -490,7 +509,8 @@ static function () { ServiceRegister::getService(StatisticalDataRepositoryInterface::class), ServiceRegister::getService(TransactionLogRepositoryInterface::class), ServiceRegister::getService(StoreIntegrationService::class), - ServiceRegister::getService(AdvancedSettingsRepositoryInterface::class) + ServiceRegister::getService(AdvancedSettingsRepositoryInterface::class), + ServiceRegister::getService(BannerSettingsService::class) ); } ); @@ -567,6 +587,16 @@ static function () { } ); + ServiceRegister::registerService( + BannerSettingsService::class, + static function () { + return new BannerSettingsService( + ServiceRegister::getService(BannerSettingsRepositoryInterface::class), + ServiceRegister::getService(BannerServiceInterface::class) + ); + } + ); + ServiceRegister::registerService( TransactionLogService::class, static function () { @@ -732,6 +762,16 @@ static function () { } ); + ServiceRegister::registerService( + BannerSettingsController::class, + static function () { + return new BannerSettingsController( + ServiceRegister::getService(BannerSettingsService::class), + ServiceRegister::getService(BannerServiceInterface::class) + ); + } + ); + ServiceRegister::registerService( TransactionLogsController::class, static function () { @@ -787,6 +827,15 @@ static function () { } ); + ServiceRegister::registerService( + BannerCheckoutController::class, + static function () { + return new BannerCheckoutController( + ServiceRegister::getService(BannerSettingsService::class) + ); + } + ); + ServiceRegister::registerService( DeploymentsController::class, static function () { @@ -1006,6 +1055,16 @@ protected static function initTopicHandlers(): void SaveAdvancedSettingsHandler::class ); + TopicHandlerRegistry::register( + Topics::GET_BANNER_SETTINGS, + GetBannerSettingsHandler::class + ); + + TopicHandlerRegistry::register( + Topics::SAVE_BANNER_SETTINGS, + SaveBannerSettingsHandler::class + ); + TopicHandlerRegistry::register( Topics::GET_LOG_CONTENT, GetLogContentHandler::class @@ -1129,6 +1188,28 @@ static function () { } ); + ServiceRegister::registerService( + GetBannerSettingsHandler::class, + static function () { + return new GetBannerSettingsHandler( + ServiceRegister::getService(BannerSettingsService::class), + ServiceRegister::getService(BannerServiceInterface::class), + ServiceRegister::getService(CountryConfigurationService::class) + ); + } + ); + + ServiceRegister::registerService( + SaveBannerSettingsHandler::class, + static function () { + return new SaveBannerSettingsHandler( + ServiceRegister::getService(BannerSettingsService::class), + ServiceRegister::getService(BannerServiceInterface::class), + ServiceRegister::getService(CountryConfigurationService::class) + ); + } + ); + ServiceRegister::registerService( GetLogContentHandler::class, static function () { diff --git a/src/BusinessLogic/CheckoutAPI/Banners/BannerCheckoutController.php b/src/BusinessLogic/CheckoutAPI/Banners/BannerCheckoutController.php new file mode 100644 index 00000000..ecb8f02b --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/Banners/BannerCheckoutController.php @@ -0,0 +1,42 @@ +bannerSettingsService = $bannerSettingsService; + } + + /** + * Returns banner data + * + * @param GetBannerForLocationRequest $request + * + * @return GetBannerForLocationResponse + */ + public function getBannerForLocation(GetBannerForLocationRequest $request): GetBannerForLocationResponse + { + return new GetBannerForLocationResponse( + $this->bannerSettingsService->getBannerData($request->getCountry(), $request->getDisplayLocation()) + ); + } +} diff --git a/src/BusinessLogic/CheckoutAPI/Banners/Requests/GetBannerForLocationRequest.php b/src/BusinessLogic/CheckoutAPI/Banners/Requests/GetBannerForLocationRequest.php new file mode 100644 index 00000000..b34ddfb9 --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/Banners/Requests/GetBannerForLocationRequest.php @@ -0,0 +1,73 @@ +country = $country; + $this->displayLocation = $displayLocation; + } + + /** + * @return string + */ + public function getCountry(): string + { + return $this->country; + } + + /** + * @return string + */ + public function getDisplayLocation(): string + { + return $this->displayLocation; + } + + /** + * @param array $data + * + * @return self + */ + public static function fromArray(array $data): self + { + return new self( + self::getDataValue($data, 'country'), + self::getDataValue($data, 'displayLocation') + ); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'country' => $this->country, + 'displayLocation' => $this->displayLocation, + ]; + } +} diff --git a/src/BusinessLogic/CheckoutAPI/Banners/Responses/GetBannerForLocationResponse.php b/src/BusinessLogic/CheckoutAPI/Banners/Responses/GetBannerForLocationResponse.php new file mode 100644 index 00000000..ba111c90 --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/Banners/Responses/GetBannerForLocationResponse.php @@ -0,0 +1,44 @@ +banner = $banner; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + if ($this->banner === null) { + return []; + } + + return [ + 'country' => $this->banner->getCountry(), + 'displayLocation' => $this->banner->getDisplayLocation(), + 'linkUrl' => $this->banner->getLinkUrl(), + 'imageUrl' => $this->banner->getImageUrl(), + ]; + } +} diff --git a/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php b/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php index f1357751..b36f2d66 100644 --- a/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php +++ b/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php @@ -5,6 +5,7 @@ use SeQura\Core\BusinessLogic\AdminAPI\Aspects\ErrorHandlingAspect; use SeQura\Core\BusinessLogic\AdminAPI\Aspects\StoreContextAspect; use SeQura\Core\BusinessLogic\Bootstrap\Aspect\Aspects; +use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController; @@ -68,4 +69,17 @@ public function promotionalWidgets(string $storeId): object ->andRun(new StoreContextAspect($storeId)) ->beforeEachMethodOfService(PromotionalWidgetsCheckoutController::class); } + + /** + * @param string $storeId + * + * @return object + */ + public function banners(string $storeId): object + { + return Aspects + ::run(new ErrorHandlingAspect()) + ->andRun(new StoreContextAspect($storeId)) + ->beforeEachMethodOfService(BannerCheckoutController::class); + } } diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/GetBannerSettingsHandler.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/GetBannerSettingsHandler.php new file mode 100644 index 00000000..60bdc951 --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/GetBannerSettingsHandler.php @@ -0,0 +1,74 @@ +bannerSettingsService = $bannerSettingsService; + $this->bannerService = $bannerService; + $this->countryConfigurationService = $countryConfigurationService; + } + + /** + * @param mixed[] $payload + * + * @return Response + * + * @throws FailedToRetrieveSellingCountriesException + */ + public function handle(array $payload): Response + { + $bannerSettings = $this->bannerSettingsService->getBannerSettings() ?? new BannerSettings([]); + + $countryConfigurations = $this->countryConfigurationService->getCountryConfiguration() ?? []; + $sellingCountries = array_map(function (CountryConfiguration $countyConfiguration) { + return $countyConfiguration->getCountryCode(); + }, $countryConfigurations); + + return new BannerSettingsResponse( + $bannerSettings, + $this->bannerService->getBannerDisplayLocations(), + $sellingCountries + ); + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/SaveBannerSettingsHandler.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/SaveBannerSettingsHandler.php new file mode 100644 index 00000000..7e29e8be --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/BannerSettings/SaveBannerSettingsHandler.php @@ -0,0 +1,77 @@ +bannerSettingsService = $bannerSettingsService; + $this->bannerService = $bannerService; + $this->countryConfigurationService = $countryConfigurationService; + } + + /** + * @inheritDoc + * + * @throws BannerImageRequiredException + * @throws InvalidURLException + * @throws FailedToRetrieveSellingCountriesException + */ + public function handle(array $payload): Response + { + $request = SaveBannerSettingsRequest::fromPayload($payload); + $saved = $this->bannerSettingsService->setBannerSettings($request->transformToDomainModel()); + + $countryConfigurations = $this->countryConfigurationService->getCountryConfiguration() ?? []; + $sellingCountries = array_map(function (CountryConfiguration $countryConfiguration) { + return $countryConfiguration->getCountryCode(); + }, $countryConfigurations); + + return new BannerSettingsResponse( + $saved, + $this->bannerService->getBannerDisplayLocations(), + $sellingCountries + ); + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php index 771f9002..91debd32 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Handlers/Enums/Topics.php @@ -45,6 +45,14 @@ interface Topics * @var string */ public const SAVE_ADVANCED_SETTINGS = 'save-advanced-settings'; + /** + * @var string + */ + public const GET_BANNER_SETTINGS = 'get-banner-settings'; + /** + * @var string + */ + public const SAVE_BANNER_SETTINGS = 'save-banner-settings'; /** * @var string */ @@ -82,6 +90,8 @@ interface Topics self::SAVE_ORDER_STATUS_SETTINGS, self::GET_ADVANCED_SETTINGS, self::SAVE_ADVANCED_SETTINGS, + self::GET_BANNER_SETTINGS, + self::SAVE_BANNER_SETTINGS, self::GET_LOG_CONTENT, self::REMOVE_LOG_CONTENT, self::GET_SHOP_CATEGORIES, diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Requests/BannerSettings/SaveBannerSettingsRequest.php b/src/BusinessLogic/ConfigurationWebhookAPI/Requests/BannerSettings/SaveBannerSettingsRequest.php new file mode 100644 index 00000000..c71888a7 --- /dev/null +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Requests/BannerSettings/SaveBannerSettingsRequest.php @@ -0,0 +1,24 @@ +settings = $settings; + $this->displayLocations = $displayLocations; + $this->sellingCountries = $sellingCountries; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $settingsArray = $this->settings->toArray(); + + return [ + 'displayLocations' => $this->displayLocations, + 'sellingCountries' => $this->sellingCountries, + 'bannerConfigs' => $settingsArray['bannerConfigs'] ?? [], + ]; + } +} diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/TopicMissingErrorResponse.php b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/TopicMissingErrorResponse.php index 3ad12ec8..5ce23be8 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/TopicMissingErrorResponse.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/TopicMissingErrorResponse.php @@ -22,8 +22,7 @@ class TopicMissingErrorResponse extends Response public function toArray(): array { return [ - 'success' => false, - 'error' => 'Topic field is required in the webhook payload.', + 'errorMessage' => 'Topic field is required in the webhook payload.', 'errorCode' => 'TOPIC_MISSING' ]; } diff --git a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/UnknownTopicErrorResponse.php b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/UnknownTopicErrorResponse.php index 104204d3..acddc950 100644 --- a/src/BusinessLogic/ConfigurationWebhookAPI/Responses/UnknownTopicErrorResponse.php +++ b/src/BusinessLogic/ConfigurationWebhookAPI/Responses/UnknownTopicErrorResponse.php @@ -35,8 +35,7 @@ public function __construct(string $topic) public function toArray(): array { return [ - 'success' => false, - 'error' => "Unknown or unsupported topic: {$this->topic}", + 'errorMessage' => "Unknown or unsupported topic: {$this->topic}", 'errorCode' => 'UNKNOWN_TOPIC' ]; } diff --git a/src/BusinessLogic/DataAccess/BannerSettings/Entities/BannerSettings.php b/src/BusinessLogic/DataAccess/BannerSettings/Entities/BannerSettings.php new file mode 100644 index 00000000..568b2da8 --- /dev/null +++ b/src/BusinessLogic/DataAccess/BannerSettings/Entities/BannerSettings.php @@ -0,0 +1,121 @@ +storeId = $data['storeId'] ?? ''; + $this->bannerSettings = new DomainBannerSettings( + $this->inflateBannerConfigs(static::getDataValue($bannerSettings, 'bannerConfigs', [])) + ); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $data = parent::toArray(); + + $data['bannerSettings'] = $this->bannerSettings->toArray(); + + return $data; + } + + /** + * @inheritDoc + */ + public function getConfig(): EntityConfiguration + { + $indexMap = new IndexMap(); + + $indexMap->addStringIndex('storeId'); + + return new EntityConfiguration($indexMap, 'BannerSettings'); + } + + /** + * @return string + */ + public function getStoreId(): string + { + return $this->storeId; + } + + /** + * @param string $storeId + */ + public function setStoreId(string $storeId): void + { + $this->storeId = $storeId; + } + + /** + * @return DomainBannerSettings + */ + public function getBannerSettings(): DomainBannerSettings + { + return $this->bannerSettings; + } + + /** + * @param DomainBannerSettings $bannerSettings + */ + public function setBannerSettings(DomainBannerSettings $bannerSettings): void + { + $this->bannerSettings = $bannerSettings; + } + + /** + * @param array> $bannerConfigs + * + * @return Banner[] + */ + protected function inflateBannerConfigs(array $bannerConfigs): array + { + $arrayOffBannerConfigs = []; + foreach ($bannerConfigs as $bannerConfig) { + $arrayOffBannerConfigs [] = new Banner( + $bannerConfig['country'], + $bannerConfig['linkUrl'], + $bannerConfig['imageUrl'], + $bannerConfig['displayLocation'] + ); + } + + return $arrayOffBannerConfigs; + } +} diff --git a/src/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepository.php b/src/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepository.php new file mode 100644 index 00000000..fb9820bb --- /dev/null +++ b/src/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepository.php @@ -0,0 +1,107 @@ +repository = $repository; + $this->storeContext = $storeContext; + } + + /** + * @inheritDoc + * + * @throws QueryFilterInvalidParamException + */ + public function setBannerSettings(BannerSettings $settings): void + { + $bannerSettingsEntity = $this->getBannerSettingsEntity(); + + if ($bannerSettingsEntity) { + $bannerSettingsEntity->setBannerSettings($settings); + $bannerSettingsEntity->setStoreId($this->storeContext->getStoreId()); + $this->repository->update($bannerSettingsEntity); + + return; + } + + $entity = new BannerSettingsEntity(); + $entity->setStoreId($this->storeContext->getStoreId()); + $entity->setBannerSettings($settings); + $this->repository->save($entity); + } + + /** + * @inheritDoc + * + * @throws QueryFilterInvalidParamException + */ + public function getBannerSettings(): ?BannerSettings + { + $entity = $this->getBannerSettingsEntity(); + + return $entity ? $entity->getBannerSettings() : null; + } + + /** + * @inheritDoc + * + * @throws QueryFilterInvalidParamException + */ + public function deleteBannerSettings(): void + { + $entity = $this->getBannerSettingsEntity(); + + $entity && $this->repository->delete($entity); + } + + /** + * Gets the banner settings entity from the database. + * + * @return BannerSettingsEntity|null + * + * @throws QueryFilterInvalidParamException + */ + protected function getBannerSettingsEntity(): ?BannerSettingsEntity + { + $queryFilter = new QueryFilter(); + $queryFilter->where('storeId', Operators::EQUALS, $this->storeContext->getStoreId()); + + /** + * @var BannerSettingsEntity $bannerSettings + */ + $bannerSettings = $this->repository->selectOne($queryFilter); + + return $bannerSettings; + } +} diff --git a/src/BusinessLogic/Domain/BannerSettings/Exceptions/BannerImageRequiredException.php b/src/BusinessLogic/Domain/BannerSettings/Exceptions/BannerImageRequiredException.php new file mode 100644 index 00000000..91ee1ba5 --- /dev/null +++ b/src/BusinessLogic/Domain/BannerSettings/Exceptions/BannerImageRequiredException.php @@ -0,0 +1,14 @@ +country = $country; + $this->linkUrl = $linkUrl; + $this->imageUrl = $imageUrl; + $this->displayLocation = $displayLocation; + $this->imageBase64 = $imageBase64; + } + + /** + * @return string + */ + public function getCountry(): string + { + return $this->country; + } + + /** + * @param string $country + */ + public function setCountry(string $country): void + { + $this->country = $country; + } + + /** + * @return string + */ + public function getLinkUrl(): string + { + return $this->linkUrl; + } + + /** + * @param string $linkUrl + */ + public function setLinkUrl(string $linkUrl): void + { + $this->linkUrl = $linkUrl; + } + + /** + * @return string + */ + public function getImageUrl(): string + { + return $this->imageUrl; + } + + /** + * @param string $imageUrl + */ + public function setImageUrl(string $imageUrl): void + { + $this->imageUrl = $imageUrl; + } + + /** + * @return string + */ + public function getDisplayLocation(): string + { + return $this->displayLocation; + } + + /** + * @param string $displayLocation + */ + public function setDisplayLocation(string $displayLocation): void + { + $this->displayLocation = $displayLocation; + } + + /** + * @return string|null + */ + public function getImageBase64(): ?string + { + return $this->imageBase64; + } + + /** + * @param string|null $imageBase64 + */ + public function setImageBase64(?string $imageBase64): void + { + $this->imageBase64 = $imageBase64; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'country' => $this->country, + 'linkUrl' => $this->linkUrl, + 'imageUrl' => $this->imageUrl, + 'displayLocation' => $this->displayLocation, + ]; + } +} diff --git a/src/BusinessLogic/Domain/BannerSettings/Models/BannerSettings.php b/src/BusinessLogic/Domain/BannerSettings/Models/BannerSettings.php new file mode 100644 index 00000000..284df399 --- /dev/null +++ b/src/BusinessLogic/Domain/BannerSettings/Models/BannerSettings.php @@ -0,0 +1,56 @@ +bannerConfigs = $bannerConfigs; + } + + /** + * @return Banner[] + */ + public function getBannerConfigs(): array + { + return $this->bannerConfigs; + } + + /** + * @param Banner[] $bannerConfigs + */ + public function setBannerConfigs(array $bannerConfigs): void + { + $this->bannerConfigs = $bannerConfigs; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + if (empty($this->bannerConfigs)) { + return []; + } + + return [ + 'bannerConfigs' => Banner::toBatchArray($this->bannerConfigs), + ]; + } +} diff --git a/src/BusinessLogic/Domain/BannerSettings/RepositoryContracts/BannerSettingsRepositoryInterface.php b/src/BusinessLogic/Domain/BannerSettings/RepositoryContracts/BannerSettingsRepositoryInterface.php new file mode 100644 index 00000000..bce3c0e3 --- /dev/null +++ b/src/BusinessLogic/Domain/BannerSettings/RepositoryContracts/BannerSettingsRepositoryInterface.php @@ -0,0 +1,36 @@ +bannerSettingsRepository = $bannerSettingsRepository; + $this->bannerService = $bannerService; + } + + /** + * Retrieves banner settings. + * + * @return BannerSettings|null + */ + public function getBannerSettings(): ?BannerSettings + { + return $this->bannerSettingsRepository->getBannerSettings(); + } + + /** + * Sets banner settings. + * + * @param BannerSettings $bannerSettings + * + * @return BannerSettings + * + * @throws BannerImageRequiredException + * @throws InvalidURLException + */ + public function setBannerSettings(BannerSettings $bannerSettings): BannerSettings + { + $existingBanners = $this->getExistingBanners(); + $existingByCountry = $this->indexByCountry($existingBanners); + $incomingBanners = $bannerSettings->getBannerConfigs(); + + $resolvedBanners = $this->resolveIncomingBanners($incomingBanners, $existingByCountry); + $this->deleteRemovedBanners($existingBanners, $this->indexByCountry($incomingBanners)); + + $persisted = new BannerSettings($resolvedBanners); + $this->bannerSettingsRepository->setBannerSettings($persisted); + + return $persisted; + } + + /** + * Removes banner images currently uploaded to the integration server. + * + * @return void + */ + public function deleteUploadedBannerImages(): void + { + $bannerSettings = $this->getBannerSettings(); + if ($bannerSettings === null) { + return; + } + + foreach ($bannerSettings->getBannerConfigs() as $banner) { + try { + $this->bannerService->deleteBannerImage( + $banner->getCountry(), + $banner->getDisplayLocation() + ); + } catch (Throwable $e) { + Logger::logError( + 'Failed to delete uploaded banner image.', + 'Core', + [ + new LogContextData('country', $banner->getCountry()), + new LogContextData('displayLocation', $banner->getDisplayLocation()), + new LogContextData('message', $e->getMessage()), + new LogContextData('type', \get_class($e)), + ] + ); + } + } + } + + /** + * Deletes the banner settings + * + * @return void + */ + public function deleteBannerSettings(): void + { + $this->bannerSettingsRepository->deleteBannerSettings(); + } + + /** + * Removes both the uploaded banner images and banner settings. + * + * @return void + */ + public function clearBannerSettings(): void + { + $this->deleteUploadedBannerImages(); + $this->deleteBannerSettings(); + } + + /** + * Returns banner data + * + * @param string $country + * @param string $displayLocation + * + * @return Banner|null + */ + public function getBannerData(string $country, string $displayLocation): ?Banner + { + $bannerSettings = $this->getBannerSettings(); + + if ($bannerSettings === null) { + return null; + } + + foreach ($bannerSettings->getBannerConfigs() as $bannerConfig) { + if ($bannerConfig->getCountry() === $country && $bannerConfig->getDisplayLocation() === $displayLocation) { + return $bannerConfig; + } + } + + return null; + } + + /** + * @return Banner[] + */ + protected function getExistingBanners(): array + { + $existingBannerSettings = $this->getBannerSettings(); + + return $existingBannerSettings ? $existingBannerSettings->getBannerConfigs() : []; + } + + /** + * @param Banner[] $incomingBanners + * @param array $existingByCountry + * + * @return Banner[] + * + * @throws BannerImageRequiredException + * @throws InvalidURLException + */ + protected function resolveIncomingBanners(array $incomingBanners, array $existingByCountry): array + { + $resolved = []; + foreach ($incomingBanners as $banner) { + $this->assertValidUrl($banner->getLinkUrl()); + $this->assertBannerHasImageSource($banner, $existingByCountry); + + $resolved[] = $this->resolveBannerImage($banner, $existingByCountry); + } + + return $resolved; + } + + /** + * Deletes images for countries that are no longer present in the incoming set. + * + * @param Banner[] $existingBanners + * @param array $incomingByCountry + */ + protected function deleteRemovedBanners(array $existingBanners, array $incomingByCountry): void + { + foreach ($existingBanners as $banner) { + if (!isset($incomingByCountry[$banner->getCountry()])) { + $this->bannerService->deleteBannerImage( + $banner->getCountry(), + $banner->getDisplayLocation() + ); + } + } + } + + /** + * Verifies whether the banner contains an imageBase64 payload + * or already has a persisted image in storage for the country. + * + * @param Banner $banner + * @param array $existingByCountry + * + * @throws BannerImageRequiredException + */ + protected function assertBannerHasImageSource(Banner $banner, array $existingByCountry): void + { + if ($this->hasImageBase64($banner) || isset($existingByCountry[$banner->getCountry()])) { + return; + } + + throw new BannerImageRequiredException( + new TranslatableLabel( + 'A new banner must include an imageBase64.', + 'general.errors.bannerSettings.imageRequired' + ) + ); + } + + /** + * Resolves the image URL for an incoming banner: + * - If a new imageBase64 is supplied, uploads it (deleting the previous + * image first when the display location has changed). + * - Otherwise reuses the previous URL if the display location is + * unchanged, or asks the integration to relocate the image when the + * display location has changed. + * + * @param Banner $banner + * @param array $existingByCountry + * + * @return Banner + * + * @throws InvalidURLException + */ + protected function resolveBannerImage(Banner $banner, array $existingByCountry): Banner + { + $existing = $existingByCountry[$banner->getCountry()] ?? null; + + if ($this->hasImageBase64($banner)) { + $this->uploadBannerImage($banner, $existing); + } elseif ($existing !== null) { + $banner->setImageUrl($this->reuseOrRelocateImageUrl($banner, $existing)); + } + + $banner->setImageBase64(null); + $this->assertValidUrl($banner->getImageUrl()); + + return $banner; + } + + /** + * Uploads the banner image, deleting the previously stored one when the + * display location has changed. + * + * @param Banner $banner + * @param Banner|null $existing + */ + protected function uploadBannerImage(Banner $banner, ?Banner $existing): void + { + $this->deleteImageIfLocationChanged($banner, $existing); + + $banner->setImageUrl( + $this->bannerService->saveBannerImage( + $banner->getCountry(), + $banner->getDisplayLocation(), + $banner->getImageBase64() + ) + ); + } + + /** + * Returns the existing image URL when the display location is unchanged, + * otherwise asks the integration to relocate the image and returns the + * new URL. + * + * @param Banner $banner + * @param Banner $existing + * + * @return string + */ + protected function reuseOrRelocateImageUrl(Banner $banner, Banner $existing): string + { + if ($existing->getDisplayLocation() === $banner->getDisplayLocation()) { + return $existing->getImageUrl(); + } + + return $this->bannerService->changeBannerImageDisplayLocation( + $banner->getCountry(), + $existing->getDisplayLocation(), + $banner->getDisplayLocation() + ); + } + + /** + * @param Banner $banner + * @param Banner|null $existing + */ + protected function deleteImageIfLocationChanged(Banner $banner, ?Banner $existing): void + { + if ($existing === null || $existing->getDisplayLocation() === $banner->getDisplayLocation()) { + return; + } + + $this->bannerService->deleteBannerImage( + $existing->getCountry(), + $existing->getDisplayLocation() + ); + } + + /** + * @param Banner $banner + * + * @return bool + */ + protected function hasImageBase64(Banner $banner): bool + { + $base64 = $banner->getImageBase64(); + + return $base64 !== null && $base64 !== ''; + } + + /** + * @param Banner[] $banners + * + * @return array + */ + protected function indexByCountry(array $banners): array + { + $indexed = []; + foreach ($banners as $banner) { + $indexed[$banner->getCountry()] = $banner; + } + + return $indexed; + } + + /** + * Validates the URL + * + * @throws InvalidURLException + */ + protected function assertValidUrl(string $url): void + { + if (mb_strlen($url) > 2048) { + throw new InvalidURLException( + new TranslatableLabel( + 'URL is too long (max 2048 characters)', + 'general.errors.bannerSettings.urlTooLong' + ) + ); + } + + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new InvalidURLException( + new TranslatableLabel( + 'URL format is invalid', + 'general.errors.bannerSettings.invalidUrlFormat' + ) + ); + } + + $scheme = parse_url($url, PHP_URL_SCHEME); + if (!\in_array($scheme, ['http', 'https'], true)) { + throw new InvalidURLException( + new TranslatableLabel( + 'URL must use http or https', + 'general.errors.bannerSettings.invalidUrlScheme' + ) + ); + } + } +} diff --git a/src/BusinessLogic/Domain/Connection/Services/ConnectionService.php b/src/BusinessLogic/Domain/Connection/Services/ConnectionService.php index 77c97833..786b85e6 100644 --- a/src/BusinessLogic/Domain/Connection/Services/ConnectionService.php +++ b/src/BusinessLogic/Domain/Connection/Services/ConnectionService.php @@ -12,6 +12,7 @@ use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Exceptions\PaymentMethodNotFoundException; use SeQura\Core\BusinessLogic\Domain\StoreIntegration\Exceptions\CapabilitiesEmptyException; use SeQura\Core\BusinessLogic\Domain\StoreIntegration\Services\StoreIntegrationService; +use SeQura\Core\BusinessLogic\Domain\URL\Exceptions\InvalidUrlException; use SeQura\Core\Infrastructure\Http\Exceptions\HttpRequestException; /** @@ -192,6 +193,7 @@ public function getConnectionDataByMerchantId(string $merchantId): ConnectionDat * @return void * * @throws CapabilitiesEmptyException + * @throws InvalidUrlException */ public function reRegisterWebhooks(ConnectionData $connectionData): void { @@ -205,6 +207,7 @@ public function reRegisterWebhooks(ConnectionData $connectionData): void * @return void * * @throws CapabilitiesEmptyException + * @throws InvalidUrlException */ protected function registerWebhooks(ConnectionData $connectionData): void { diff --git a/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php b/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php index e55f1b37..225d6841 100644 --- a/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php +++ b/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php @@ -3,6 +3,7 @@ namespace SeQura\Core\BusinessLogic\Domain\Disconnect\Services; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\RepositoryContracts\AdvancedSettingsRepositoryInterface; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\ConnectionDataRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\CredentialsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\CountryConfiguration\RepositoryContracts\CountryConfigurationRepositoryInterface; @@ -105,6 +106,11 @@ class DisconnectService */ protected $advancedSettingsRepository; + /** + * @var BannerSettingsService $bannerSettingsService + */ + protected $bannerSettingsService; + /** * @param DisconnectServiceInterface $integrationDisconnectService * @param SendReportRepositoryInterface $sendReportRepository @@ -121,6 +127,7 @@ class DisconnectService * @param TransactionLogRepositoryInterface $transactionLogRepository * @param StoreIntegrationService $storeIntegrationService * @param AdvancedSettingsRepositoryInterface $advancedSettingsRepository + * @param BannerSettingsService $bannerSettingsService */ public function __construct( DisconnectServiceInterface $integrationDisconnectService, @@ -137,7 +144,8 @@ public function __construct( StatisticalDataRepositoryInterface $statisticalDataRepository, TransactionLogRepositoryInterface $transactionLogRepository, StoreIntegrationService $storeIntegrationService, - AdvancedSettingsRepositoryInterface $advancedSettingsRepository + AdvancedSettingsRepositoryInterface $advancedSettingsRepository, + BannerSettingsService $bannerSettingsService ) { $this->integrationDisconnectService = $integrationDisconnectService; $this->sendReportRepository = $sendReportRepository; @@ -154,6 +162,7 @@ public function __construct( $this->transactionLogRepository = $transactionLogRepository; $this->storeIntegrationService = $storeIntegrationService; $this->advancedSettingsRepository = $advancedSettingsRepository; + $this->bannerSettingsService = $bannerSettingsService; } /** @@ -206,6 +215,7 @@ public function disconnect(string $deploymentId, bool $isFullDisconnect): void $this->orderStatusSettingsRepository->deleteOrderStatusMapping(); $this->paymentMethodRepository->deleteAllPaymentMethods(); $this->widgetSettingsRepository->deleteWidgetSettings(); + $this->bannerSettingsService->clearBannerSettings(); $this->sendReportRepository->deleteSendReportForContext(StoreContext::getInstance()->getStoreId()); $this->statisticalDataRepository->deleteStatisticalData(); $this->transactionLogRepository->deleteAllTransactionLogs(); diff --git a/src/BusinessLogic/Domain/Integration/Banner/BannerServiceInterface.php b/src/BusinessLogic/Domain/Integration/Banner/BannerServiceInterface.php new file mode 100644 index 00000000..e961786a --- /dev/null +++ b/src/BusinessLogic/Domain/Integration/Banner/BannerServiceInterface.php @@ -0,0 +1,57 @@ +bannerSettingsRepository = TestServiceRegister::getService( + BannerSettingsRepositoryInterface::class + ); + + $this->bannerService = new MockBannerService(); + $this->bannerService->setBannerDisplayLocations([ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ]); + TestServiceRegister::registerService(BannerServiceInterface::class, function () { + return $this->bannerService; + }); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetSettings(): void + { + // arrange + $settings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ) + ] + ); + StoreContext::doWithStore('store1', [$this->bannerSettingsRepository, 'setBannerSettings'], [$settings]); + + // act + $result = AdminAPI::get()->bannerSettings('store1')->getBannerSettings(); + + // assert + self::assertEquals( + [ + 'displayLocations' => [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + 'bannerConfigs' => [ + [ + 'country' => $settings->getBannerConfigs()[0]->getCountry(), + 'linkUrl' => $settings->getBannerConfigs()[0]->getLinkUrl(), + 'imageUrl' => $settings->getBannerConfigs()[0]->getImageUrl(), + 'displayLocation' => $settings->getBannerConfigs()[0]->getDisplayLocation() + ], + [ + 'country' => $settings->getBannerConfigs()[1]->getCountry(), + 'linkUrl' => $settings->getBannerConfigs()[1]->getLinkUrl(), + 'imageUrl' => $settings->getBannerConfigs()[1]->getImageUrl(), + 'displayLocation' => $settings->getBannerConfigs()[1]->getDisplayLocation() + ] + ] + ], + $result->toArray() + ); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetSettingsEmpty(): void + { + // act + $result = AdminAPI::get()->bannerSettings('store1')->getBannerSettings(); + + // assert + self::assertEquals( + [ + 'displayLocations' => [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + 'bannerConfigs' => [], + ], + $result->toArray() + ); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetSettings(): void + { + // arrange + $settings = new BannerSettingsRequest( + [ + [ + 'country' => 'ES', + 'displayLocation' => 'displayOnHomePage', + 'linkUrl' => 'https://www.sequra.com/es/faq#shoppers', + 'imageBase64' => 'ES-base64' + ], + [ + 'country' => 'PT', + 'displayLocation' => 'displayOnCartPage', + 'linkUrl' => 'https://www.sequra.com/it/faq#shoppers', + 'imageBase64' => 'PT-base64' + ], + ] + ); + + // act + $result = AdminAPI::get()->bannerSettings('store1')->setBannerSettings($settings); + + // assert + self::assertTrue($result->isSuccessful()); + $payload = $result->toArray(); + self::assertEquals( + [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + $payload['displayLocations'] + ); + self::assertCount(2, $payload['bannerConfigs']); + self::assertNotSame('', $payload['bannerConfigs'][0]['imageUrl']); + self::assertNotSame('', $payload['bannerConfigs'][1]['imageUrl']); + + $savedSettings = StoreContext::doWithStore( + 'store1', + [ + $this->bannerSettingsRepository, + 'getBannerSettings' + ] + ); + self::assertNotNull($savedSettings); + self::assertCount(2, $savedSettings->getBannerConfigs()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetSettingsInvalidURL(): void + { + // arrange + $settings = new BannerSettingsRequest( + [ + [ + 'country' => 'ES', + 'displayLocation' => 'displayOnHomePage', + 'linkUrl' => 'https://www.sequra.com/es/faq#shoppers', + 'imageBase64' => 'ES-base64' + ], + [ + 'country' => 'PT', + 'displayLocation' => 'displayOnCartPage', + 'linkUrl' => 'string', + 'imageBase64' => 'PT-base64' + ], + ] + ); + + // act + $result = AdminAPI::get()->bannerSettings('store1')->setBannerSettings($settings); + + // assert + self::assertFalse($result->isSuccessful()); + self::assertEquals([ + 'statusCode' => 0, + 'errorCode' => 'general.errors.bannerSettings.invalidUrlFormat', + 'errorMessage' => 'URL format is invalid', + 'errorParameters' => [], + ], $result->toArray()); + + $savedSettings = StoreContext::doWithStore( + 'store1', + [ + $this->bannerSettingsRepository, + 'getBannerSettings' + ] + ); + self::assertNull($savedSettings); + } +} diff --git a/tests/BusinessLogic/AdminAPI/Disconnection/DisconnectionControllerApiTest.php b/tests/BusinessLogic/AdminAPI/Disconnection/DisconnectionControllerApiTest.php index 62a44100..6b3628b0 100644 --- a/tests/BusinessLogic/AdminAPI/Disconnection/DisconnectionControllerApiTest.php +++ b/tests/BusinessLogic/AdminAPI/Disconnection/DisconnectionControllerApiTest.php @@ -5,6 +5,7 @@ use SeQura\Core\BusinessLogic\AdminAPI\AdminAPI; use SeQura\Core\BusinessLogic\AdminAPI\Disconnect\Requests\DisconnectRequest; use SeQura\Core\BusinessLogic\AdminAPI\Disconnect\Responses\DisconnectResponse; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\ConnectionDataRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\CredentialsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\CountryConfiguration\RepositoryContracts\CountryConfigurationRepositoryInterface; @@ -57,7 +58,8 @@ protected function setUp(): void ServiceRegister::getService(StatisticalDataRepositoryInterface::class), ServiceRegister::getService(TransactionLogRepositoryInterface::class), ServiceRegister::getService(StoreIntegrationService::class), - new MockAdvancedSettingsRepository() + new MockAdvancedSettingsRepository(), + ServiceRegister::getService(BannerSettingsService::class) ); TestServiceRegister::registerService(DisconnectService::class, function () { diff --git a/tests/BusinessLogic/CheckoutAPI/Banners/BannersApiTest.php b/tests/BusinessLogic/CheckoutAPI/Banners/BannersApiTest.php new file mode 100644 index 00000000..6dd1250e --- /dev/null +++ b/tests/BusinessLogic/CheckoutAPI/Banners/BannersApiTest.php @@ -0,0 +1,109 @@ +mockBannerSettingsService = new MockBannerSettingsService( + TestServiceRegister::getService(BannerSettingsRepositoryInterface::class) + ); + + TestServiceRegister::registerService( + BannerSettingsService::class, + function () { + return $this->mockBannerSettingsService; + } + ); + } + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testGetBannersDataSuccess(): void + { + //Arrange + $this->mockBannerSettingsService->setBannerSettings( + new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop/img/sequra/es/banner/Flag_of_Spain.svg.png', + 'displayOnHomePage' + ) + ] + ) + ); + + //Act + $response = CheckoutAPI::get()->banners('1') + ->getBannerForLocation(new GetBannerForLocationRequest('ES', 'displayOnHomePage')); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertNotEmpty($response->toArray()); + } + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testGetBannersNoDataSuccess(): void + { + //Arrange + $this->mockBannerSettingsService->setBannerSettings( + new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop/img/sequra/es/banner/Flag_of_Spain.svg.png', + 'displayOnHomePage' + ) + ] + ) + ); + + //Act + $response = CheckoutAPI::get()->banners('1') + ->getBannerForLocation(new GetBannerForLocationRequest('ES', 'displayOnCartPage')); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertEmpty($response->toArray()); + } +} diff --git a/tests/BusinessLogic/Common/BaseTestCase.php b/tests/BusinessLogic/Common/BaseTestCase.php index 415bf311..dd6baa0a 100644 --- a/tests/BusinessLogic/Common/BaseTestCase.php +++ b/tests/BusinessLogic/Common/BaseTestCase.php @@ -3,6 +3,7 @@ namespace SeQura\Core\Tests\BusinessLogic\Common; use PHPUnit\Framework\TestCase; +use SeQura\Core\BusinessLogic\AdminAPI\BannerSettings\BannerSettingsController; use SeQura\Core\BusinessLogic\AdminAPI\Connection\ConnectionController; use SeQura\Core\BusinessLogic\AdminAPI\CountryConfiguration\CountryConfigurationController; use SeQura\Core\BusinessLogic\AdminAPI\Deployments\DeploymentsController; @@ -14,11 +15,14 @@ use SeQura\Core\BusinessLogic\AdminAPI\PromotionalWidgets\PromotionalWidgetsController; use SeQura\Core\BusinessLogic\AdminAPI\Store\StoreController; use SeQura\Core\BusinessLogic\AdminAPI\TransactionLogs\TransactionLogsController; +use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; 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\BannerSettings\GetBannerSettingsHandler; +use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\SaveBannerSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Enums\Topics; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\GeneralSettings\GetGeneralSettingsHandler; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\GeneralSettings\SaveGeneralSettingsHandler; @@ -35,6 +39,8 @@ 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\BannerSettings\Entities\BannerSettings; +use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData; use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Repositories\ConnectionDataRepository; use SeQura\Core\BusinessLogic\DataAccess\CountryConfiguration\Entities\CountryConfiguration; @@ -57,6 +63,8 @@ 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\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\ProxyContracts\ConnectionProxyInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\ConnectionDataRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\Connection\RepositoryContracts\CredentialsRepositoryInterface; @@ -72,6 +80,7 @@ use SeQura\Core\BusinessLogic\Domain\GeneralSettings\RepositoryContracts\GeneralSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\GeneralSettings\Services\CategoryService; use SeQura\Core\BusinessLogic\Domain\GeneralSettings\Services\GeneralSettingsService; +use SeQura\Core\BusinessLogic\Domain\Integration\Banner\BannerServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Category\CategoryServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Log\LogServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Order\MerchantDataProviderInterface; @@ -139,8 +148,10 @@ use SeQura\Core\Infrastructure\Logger\Logger; use SeQura\Core\Infrastructure\Logger\LoggerConfiguration; use SeQura\Core\Infrastructure\ORM\Exceptions\RepositoryClassException; +use SeQura\Core\Infrastructure\ORM\RepositoryRegistry; use SeQura\Core\Infrastructure\Serializer\Concrete\JsonSerializer; use SeQura\Core\Infrastructure\Serializer\Serializer; +use SeQura\Core\Infrastructure\ServiceRegister; use SeQura\Core\Infrastructure\TaskExecution\Events\QueueItemStateTransitionEventBus; use SeQura\Core\Infrastructure\TaskExecution\Interfaces\TaskRunnerWakeup; use SeQura\Core\Infrastructure\TaskExecution\QueueItem; @@ -155,6 +166,7 @@ use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockDeploymentsRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockDeploymentsService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockIntegrationStoreIntegrationService; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockMerchantDataProvider; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockMiniWidgetMessagesProvider; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockOrderCreation; @@ -439,12 +451,25 @@ protected function setUp(): void TestServiceRegister::getService(WidgetValidationService::class) ); }, + BannerCheckoutController::class => function () { + return new BannerCheckoutController( + TestServiceRegister::getService(BannerSettingsService::class) + ); + }, WidgetSettingsRepositoryInterface::class => function () { return new WidgetSettingsRepository( TestRepositoryRegistry::getRepository(WidgetSettings::getClassName()), TestServiceRegister::getService(StoreContext::class) ); }, + + BannerSettingsRepositoryInterface::class => function () { + return new BannerSettingsRepository( + TestRepositoryRegistry::getRepository(BannerSettings::getClassName()), + TestServiceRegister::getService(StoreContext::class) + ); + }, + PaymentMethodRepositoryInterface::class => function () { return new PaymentMethodRepository( TestRepositoryRegistry::getRepository(PaymentMethod::getClassName()), @@ -481,6 +506,15 @@ protected function setUp(): void TestServiceRegister::getService(DeploymentsService::class) ); }, + BannerSettingsService::class => function () { + return new BannerSettingsService( + TestServiceRegister::getService(BannerSettingsRepositoryInterface::class), + TestServiceRegister::getService(BannerServiceInterface::class) + ); + }, + BannerServiceInterface::class => function () { + return new MockBannerService(); + }, ProductServiceInterface::class => function () { return new MockProductService(); }, @@ -495,6 +529,12 @@ protected function setUp(): void TestServiceRegister::getService(WidgetSettingsService::class) ); }, + BannerSettingsController::class => function () { + return new BannerSettingsController( + TestServiceRegister::getService(BannerSettingsService::class), + TestServiceRegister::getService(BannerServiceInterface::class) + ); + }, AbstractItemFactory::class => function () { return new ItemFactory(); }, @@ -815,6 +855,28 @@ static function () { } ); + TestServiceRegister::registerService( + GetBannerSettingsHandler::class, + static function () { + return new GetBannerSettingsHandler( + TestServiceRegister::getService(BannerSettingsService::class), + TestServiceRegister::getService(BannerServiceInterface::class), + TestServiceRegister::getService(CountryConfigurationService::class) + ); + } + ); + + TestServiceRegister::registerService( + SaveBannerSettingsHandler::class, + static function () { + return new SaveBannerSettingsHandler( + TestServiceRegister::getService(BannerSettingsService::class), + TestServiceRegister::getService(BannerServiceInterface::class), + TestServiceRegister::getService(CountryConfigurationService::class) + ); + } + ); + TestServiceRegister::registerService( GetLogContentHandler::class, static function () { @@ -914,6 +976,16 @@ static function () { SaveAdvancedSettingsHandler::class ); + TopicHandlerRegistry::register( + Topics::GET_BANNER_SETTINGS, + GetBannerSettingsHandler::class + ); + + TopicHandlerRegistry::register( + Topics::SAVE_BANNER_SETTINGS, + SaveBannerSettingsHandler::class + ); + TopicHandlerRegistry::register( Topics::GET_LOG_CONTENT, GetLogContentHandler::class @@ -963,6 +1035,7 @@ static function () { MemoryRepository::getClassName() ); TestRepositoryRegistry::registerRepository(WidgetSettings::getClassName(), MemoryRepository::getClassName()); + TestRepositoryRegistry::registerRepository(BannerSettings::getClassName(), MemoryRepository::getClassName()); TestRepositoryRegistry::registerRepository( TransactionLog::getClassName(), MemoryRepositoryWithConditionalDelete::getClassName() diff --git a/tests/BusinessLogic/Common/MockComponents/MockBannerService.php b/tests/BusinessLogic/Common/MockComponents/MockBannerService.php new file mode 100644 index 00000000..996327b5 --- /dev/null +++ b/tests/BusinessLogic/Common/MockComponents/MockBannerService.php @@ -0,0 +1,125 @@ + + */ + protected $storedImages = []; + + /** + * @var array + */ + protected $deletedImages = []; + + /** + * @var array + */ + protected $movedImages = []; + + /** + * @inheritDoc + */ + public function getBannerDisplayLocations(): array + { + return $this->bannerDisplayLocations; + } + + /** + * @param string[] $bannerDisplayLocations + */ + public function setBannerDisplayLocations(array $bannerDisplayLocations): void + { + $this->bannerDisplayLocations = $bannerDisplayLocations; + } + + /** + * @inheritDoc + */ + public function saveBannerImage(string $country, string $displayLocation, string $imageBase64): string + { + $key = $this->key($country, $displayLocation); + $this->storedImages[$key] = $imageBase64; + unset($this->deletedImages[$key]); + + return 'https://shop.test/banners/' . $country . '_' . $displayLocation . '.png'; + } + + /** + * @inheritDoc + */ + public function deleteBannerImage(string $country, string $displayLocation): void + { + $key = $this->key($country, $displayLocation); + unset($this->storedImages[$key]); + $this->deletedImages[$key] = true; + } + + /** + * @inheritDoc + */ + public function changeBannerImageDisplayLocation( + string $country, + string $oldDisplayLocation, + string $newDisplayLocation + ): string { + $oldKey = $this->key($country, $oldDisplayLocation); + $newKey = $this->key($country, $newDisplayLocation); + + if (isset($this->storedImages[$oldKey])) { + $this->storedImages[$newKey] = $this->storedImages[$oldKey]; + unset($this->storedImages[$oldKey]); + } + + $this->movedImages[$newKey] = [ + 'country' => $country, + 'from' => $oldDisplayLocation, + 'to' => $newDisplayLocation, + ]; + + return 'https://shop.test/banners/' . $country . '_' . $newDisplayLocation . '.png'; + } + + /** + * @return array + */ + public function getMovedImages(): array + { + return $this->movedImages; + } + + /** + * @return array + */ + public function getStoredImages(): array + { + return $this->storedImages; + } + + /** + * @return string[] + */ + public function getDeletedImageKeys(): array + { + return array_keys($this->deletedImages); + } + + private function key(string $country, string $displayLocation): string + { + return $country . '|' . $displayLocation; + } +} diff --git a/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsRepository.php b/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsRepository.php new file mode 100644 index 00000000..ff34d22a --- /dev/null +++ b/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsRepository.php @@ -0,0 +1,34 @@ +settings = $settings; + } + + public function getBannerSettings(): ?BannerSettings + { + return $this->settings; + } + + public function deleteBannerSettings(): void + { + $this->settings = null; + } +} diff --git a/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsService.php b/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsService.php new file mode 100644 index 00000000..d068f948 --- /dev/null +++ b/tests/BusinessLogic/Common/MockComponents/MockBannerSettingsService.php @@ -0,0 +1,67 @@ +bannerSettings; + } + + /** + * @inheritDoc + */ + public function getBannerData(string $country, string $displayLocation): ?Banner + { + if ($this->bannerSettings === null) { + return null; + } + + foreach ($this->bannerSettings->getBannerConfigs() as $bannerConfig) { + if ($bannerConfig->getCountry() === $country && $bannerConfig->getDisplayLocation() === $displayLocation) { + return $bannerConfig; + } + } + + return null; + } + + /** + * @inheritDoc + */ + public function setBannerSettings(BannerSettings $bannerSettings): BannerSettings + { + foreach ($bannerSettings->getBannerConfigs() as $bannerConfig) { + $this->assertValidUrl($bannerConfig->getLinkUrl()); + } + + $this->bannerSettings = $bannerSettings; + + return $bannerSettings; + } +} diff --git a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php index 037c58e4..4024db6c 100644 --- a/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php +++ b/tests/BusinessLogic/ConfigurationWebhookAPI/ConfigurationWebhookAPITest.php @@ -3,8 +3,13 @@ namespace SeQura\Core\Tests\BusinessLogic\ConfigurationWebhookAPI; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\ConfigurationWebhookAPI; +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\BannerSettings\Exceptions\InvalidURLException; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\Banner; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\BannerSettings; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\InvalidEnvironmentException; use SeQura\Core\BusinessLogic\Domain\Connection\Models\AuthorizationCredentials; use SeQura\Core\BusinessLogic\Domain\Connection\Models\ConnectionData; @@ -24,6 +29,7 @@ use SeQura\Core\BusinessLogic\Domain\GeneralSettings\RepositoryContracts\GeneralSettingsRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\GeneralSettings\Services\CategoryService; use SeQura\Core\BusinessLogic\Domain\GeneralSettings\Services\GeneralSettingsService; +use SeQura\Core\BusinessLogic\Domain\Integration\Banner\BannerServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Category\CategoryServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Log\LogServiceInterface; use SeQura\Core\BusinessLogic\Domain\Integration\Product\ProductServiceInterface; @@ -55,6 +61,9 @@ 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\MockBannerService; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerSettingsRepository; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerSettingsService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockCategoryService; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionProxy; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionService; @@ -177,6 +186,16 @@ class ConfigurationWebhookAPITest extends BaseTestCase */ private $advancedSettingsService; + /** + * @var MockBannerService $bannerService + */ + private $bannerService; + + /** + * @var MockBannerSettingsService $bannerSettingsService + */ + private $bannerSettingsService; + /** * @var MockCredentialsService */ @@ -346,6 +365,18 @@ function () { return $this->advancedSettingsService; }); + $this->bannerService = new MockBannerService(); + + TestServiceRegister::registerService(BannerServiceInterface::class, function () { + return $this->bannerService; + }); + + $this->bannerSettingsService = new MockBannerSettingsService(new MockBannerSettingsRepository()); + + TestServiceRegister::registerService(BannerSettingsService::class, function () { + return $this->bannerSettingsService; + }); + $this->credentialsService = new MockCredentialsService( new MockConnectionProxy(), new MockCredentialsRepository(), @@ -389,8 +420,7 @@ public function testTopicMissingResponse(): void //Assert self::assertFalse($response->isSuccessful()); self::assertEquals([ - 'success' => false, - 'error' => 'Topic field is required in the webhook payload.', + 'errorMessage' => 'Topic field is required in the webhook payload.', 'errorCode' => 'TOPIC_MISSING' ], $response->toArray()); } @@ -465,8 +495,7 @@ public function testUnknownTopicError(): void //Assert self::assertFalse($response->isSuccessful()); self::assertEquals([ - 'success' => false, - 'error' => 'Unknown or unsupported topic: get-payment-data', + 'errorMessage' => 'Unknown or unsupported topic: get-payment-data', 'errorCode' => 'UNKNOWN_TOPIC' ], $response->toArray()); } @@ -1746,4 +1775,232 @@ public function testGetAdvancedSettingsResponseNoAdvancedSettings(): void self::assertTrue($response->isSuccessful()); self::assertEmpty($response->toArray()); } + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testSaveBannerSettingsResponse(): void + { + //Arrange + $this->bannerService->setBannerDisplayLocations([ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ]); + $this->bannerSettingsService->setBannerSettings( + new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop/img/sequra/es/banner/Flag_of_Spain.svg.png', + 'displayOnHomePage' + ) + ] + ) + ); + $this->countryConfigurationService->saveCountryConfiguration([ + new CountryConfiguration('ES', 'merchant1'), + new CountryConfiguration('FR', 'merchant2'), + new CountryConfiguration('IT', 'merchant3'), + new CountryConfiguration('PT', 'merchant4'), + ]); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "save-banner-settings", + "bannerConfigs" => [ + [ + 'country' => 'ES', + 'imageBase64' => 'ES-base64', + 'linkUrl' => 'https://www.sequra.es/es/faq#shoppers', + 'displayLocation' => 'displayOnHomePage' + ], + [ + 'country' => 'PT', + 'imageBase64' => 'PT-base64', + 'linkUrl' => 'https://www.sequra.pt/pt/faq#shoppers', + 'displayLocation' => 'displayOnCartPage' + ], + ] + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + $payload = $response->toArray(); + self::assertEquals( + [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + $payload['displayLocations'] + ); + self::assertEquals(['ES', 'FR', 'IT', 'PT'], $payload['sellingCountries']); + self::assertCount(2, $payload['bannerConfigs']); + self::assertEquals('ES', $payload['bannerConfigs'][0]['country']); + self::assertEquals('PT', $payload['bannerConfigs'][1]['country']); + self::assertCount(2, $this->bannerSettingsService->getBannerSettings()->getBannerConfigs()); + } + + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testSaveBannerSettingsInvalidURLResponse(): void + { + //Arrange + $this->bannerSettingsService->setBannerSettings( + new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop/img/sequra/es/banner/Flag_of_Spain.svg.png', + 'displayOnHomePage' + ) + ] + ) + ); + + //Act + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "save-banner-settings", + "bannerConfigs" => [ + [ + 'country' => 'ES', + 'imageBase64' => 'ES-base64', + 'linkUrl' => 'string', + 'displayLocation' => 'displayOnHomePage' + ], + [ + 'country' => 'PT', + 'imageBase64' => 'PT-base64', + 'linkUrl' => 'https://www.sequra.pt/pt/faq#shoppers', + 'displayLocation' => 'displayOnCartPage' + ], + ] + ] + ); + + //Assert + self::assertFalse($response->isSuccessful()); + self::assertEquals([ + 'statusCode' => 0, + 'errorCode' => 'general.errors.bannerSettings.invalidUrlFormat', + 'errorMessage' => 'URL format is invalid', + 'errorParameters' => [], + ], $response->toArray()); + self::assertCount(1, $this->bannerSettingsService->getBannerSettings()->getBannerConfigs()); + } + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testGetBannerSettingsResponse(): void + { + //Arrange + $this->bannerService->setBannerDisplayLocations([ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ]); + $this->bannerSettingsService->setBannerSettings( + new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop/img/sequra/es/banner/Flag_of_Spain.svg.png', + 'displayOnHomePage' + ) + ] + ) + ); + $this->countryConfigurationService->saveCountryConfiguration([ + new CountryConfiguration('ES', 'merchant1'), + new CountryConfiguration('FR', 'merchant2'), + new CountryConfiguration('IT', 'merchant3'), + new CountryConfiguration('PT', 'merchant4'), + ]); + + //Act + /** @var BannerSettingsResponse $response */ + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "get-banner-settings" + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + $payload = $response->toArray(); + self::assertEquals( + [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + $payload['displayLocations'] + ); + self::assertEquals(['ES', 'FR', 'IT', 'PT'], $payload['sellingCountries']); + self::assertCount(1, $payload['bannerConfigs']); + } + + /** + * @return void + * + * @throws InvalidURLException + */ + public function testGetBannerSettingsEmptyResponse(): void + { + //Arrange + $this->bannerService->setBannerDisplayLocations([ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ]); + + //Act + /** @var BannerSettingsResponse $response */ + $response = ConfigurationWebhookAPI::configurationHandler()->handleRequest( + $this->signature, + [ + "topic" => "get-banner-settings" + ] + ); + + //Assert + self::assertTrue($response->isSuccessful()); + self::assertEquals( + [ + 'displayLocations' => [ + 'displayOnHomePage', + 'displayOnProductPage', + 'displayOnCartPage', + 'displayOnProductListingPage', + ], + 'sellingCountries' => [], + 'bannerConfigs' => [], + ], + $response->toArray() + ); + } } diff --git a/tests/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepositoryTest.php b/tests/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepositoryTest.php new file mode 100644 index 00000000..da4eb8db --- /dev/null +++ b/tests/BusinessLogic/DataAccess/BannerSettings/Repositories/BannerSettingsRepositoryTest.php @@ -0,0 +1,175 @@ +repository = TestRepositoryRegistry::getRepository(BannerSettingEntity::getClassName()); + $this->bannerSettingsRepository = new BannerSettingsRepository( + $this->repository, + StoreContext::getInstance() + ); + + TestServiceRegister::registerService(BannerSettingsRepositoryInterface::class, function () { + return $this->bannerSettingsRepository; + }); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetBannerSettings(): void + { + // arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ) + ] + ); + + $entity = new BannerSettingEntity(); + $entity->setStoreId('1'); + $entity->setBannerSettings($bannerSettings); + $this->repository->save($entity); + + // act + StoreContext::doWithStore( + '1', + [$this->bannerSettingsRepository, 'getBannerSettings'] + ); + + // assert + /** @var BannerSettingEntity[] $result */ + $result = $this->repository->select(); + + self::assertCount(1, $result); + self::assertEquals($bannerSettings, $result[0]->getBannerSettings()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettings(): void + { + // arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnHomePage' + ), + new Banner( + 'PT', + '', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ) + ] + ); + + // act + StoreContext::doWithStore( + '1', + [$this->bannerSettingsRepository, 'setBannerSettings'], + [$bannerSettings] + ); + + // assert + /** @var BannerSettingEntity[] $result */ + $result = $this->repository->select(); + + self::assertCount(1, $result); + self::assertEquals($result[0]->getBannerSettings(), $bannerSettings); + } + + /** + * @throws Exception + */ + public function testDeleteBannerSettings(): void + { + // arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ) + ] + ); + + $entity = new BannerSettingEntity(); + $entity->setStoreId('1'); + $entity->setBannerSettings($bannerSettings); + $this->repository->save($entity); + + // act + StoreContext::doWithStore( + '1', + [$this->bannerSettingsRepository, 'deleteBannerSettings'] + ); + + // assert + $result = $this->repository->select(); + self::assertCount(0, $result); + } +} diff --git a/tests/BusinessLogic/Domain/BannerSettings/Service/BannerSettingsServiceTest.php b/tests/BusinessLogic/Domain/BannerSettings/Service/BannerSettingsServiceTest.php new file mode 100644 index 00000000..6abe3a48 --- /dev/null +++ b/tests/BusinessLogic/Domain/BannerSettings/Service/BannerSettingsServiceTest.php @@ -0,0 +1,668 @@ +bannerSettingsRepository = new MockBannerSettingsRepository(); + $this->bannerService = new MockBannerService(); + $this->bannerSettingsService = new BannerSettingsService( + $this->bannerSettingsRepository, + $this->bannerService + ); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetBannerSettingsNoSettings(): void + { + //Arrange + + //Act + $result = $this->bannerSettingsService->getBannerSettings(); + + //Assert + self::assertNull($result); + } + + /** + * @throws Exception + */ + public function testGetBannerSettings(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ) + ] + ); + $this->bannerSettingsRepository->setBannerSettings($bannerSettings); + + //Act + $result = $this->bannerSettingsService->getBannerSettings(); + + //Assert + self::assertNotNull($result); + self::assertCount(2, $result->getBannerConfigs()); + self::assertEquals('PT', $result->getBannerConfigs()[1]->getCountry()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsNoSettingsInDB(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnHomePage', + 'ES-base64' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + '', + 'displayOnCartPage', + 'PT-base64' + ) + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($bannerSettings); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertNotNull($result); + self::assertCount(2, $result->getBannerConfigs()); + self::assertEquals('PT', $result->getBannerConfigs()[1]->getCountry()); + self::assertEquals( + 'https://shop.test/banners/ES_displayOnHomePage.png', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEquals( + 'https://shop.test/banners/PT_displayOnCartPage.png', + $result->getBannerConfigs()[1]->getImageUrl() + ); + self::assertNull($result->getBannerConfigs()[0]->getImageBase64()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsNewBannerWithoutImageBase64Throws(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnHomePage' + ), + ] + ); + + //Assert + $this->expectException(BannerImageRequiredException::class); + + //Act + $this->bannerSettingsService->setBannerSettings($bannerSettings); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsPreservesImageUrlWhenNoBase64Provided(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/existing.jpg', + 'displayOnHomePage' + ) + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'FR', + 'https://www.sequra.com/fr/updated-link', + '', + 'displayOnHomePage' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertNotNull($result); + self::assertCount(1, $result->getBannerConfigs()); + self::assertEquals( + 'https://shop/sequra/fr/existing.jpg', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEquals( + 'https://www.sequra.com/fr/updated-link', + $result->getBannerConfigs()[0]->getLinkUrl() + ); + self::assertEmpty($this->bannerService->getStoredImages()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsReplacesImageWhenBase64Provided(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/old.jpg', + 'displayOnHomePage' + ) + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + '', + 'displayOnHomePage', + 'FR-new-base64' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertEquals( + 'https://shop.test/banners/FR_displayOnHomePage.png', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEquals(['FR|displayOnHomePage' => 'FR-new-base64'], $this->bannerService->getStoredImages()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsRelocatesImageWhenDisplayLocationChangesWithoutBase64(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/existing.jpg', + 'displayOnHomePage' + ) + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnCartPage' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertNotNull($result); + self::assertCount(1, $result->getBannerConfigs()); + self::assertEquals('displayOnCartPage', $result->getBannerConfigs()[0]->getDisplayLocation()); + self::assertEquals( + 'https://shop.test/banners/ES_displayOnCartPage.png', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEquals( + [ + 'ES|displayOnCartPage' => [ + 'country' => 'ES', + 'from' => 'displayOnHomePage', + 'to' => 'displayOnCartPage', + ], + ], + $this->bannerService->getMovedImages() + ); + self::assertEmpty($this->bannerService->getDeletedImageKeys()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsKeepsImageUrlWhenDisplayLocationUnchanged(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/existing.jpg', + 'displayOnHomePage' + ) + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/updated-link', + '', + 'displayOnHomePage' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertEquals( + 'https://shop/sequra/es/existing.jpg', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEmpty($this->bannerService->getMovedImages()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsDisplayLocationChangeWithBase64DeletesOldImage(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/old.jpg', + 'displayOnHomePage' + ) + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnCartPage', + 'ES-new-base64' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertEquals('displayOnCartPage', $result->getBannerConfigs()[0]->getDisplayLocation()); + self::assertEquals( + 'https://shop.test/banners/ES_displayOnCartPage.png', + $result->getBannerConfigs()[0]->getImageUrl() + ); + self::assertEquals(['ES|displayOnCartPage' => 'ES-new-base64'], $this->bannerService->getStoredImages()); + self::assertEquals(['ES|displayOnHomePage'], $this->bannerService->getDeletedImageKeys()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsDeletesOmittedBanners(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/pt/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ), + ] + )); + + $update = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnHomePage' + ), + ] + ); + + //Act + $this->bannerSettingsService->setBannerSettings($update); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + self::assertCount(1, $result->getBannerConfigs()); + self::assertEquals('ES', $result->getBannerConfigs()[0]->getCountry()); + self::assertEquals(['PT|displayOnCartPage'], $this->bannerService->getDeletedImageKeys()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsSettingsChanged(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + '', + 'displayOnHomePage', + 'ES-base64' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + '', + 'displayOnCartPage', + 'PT-base64' + ), + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + '', + 'displayOnHomePage' + ), + + ] + ); + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings( + [ + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/image.jpg', + 'displayOnHomePage' + ) + ] + )); + + //Act + $this->bannerSettingsService->setBannerSettings($bannerSettings); + + //Assert + $result = $this->bannerSettingsRepository->getBannerSettings(); + + self::assertNotNull($result); + self::assertCount(3, $result->getBannerConfigs()); + self::assertEquals('ES', $result->getBannerConfigs()[0]->getCountry()); + self::assertEquals('PT', $result->getBannerConfigs()[1]->getCountry()); + self::assertEquals('FR', $result->getBannerConfigs()[2]->getCountry()); + self::assertEquals( + 'https://shop/sequra/fr/image.jpg', + $result->getBannerConfigs()[2]->getImageUrl() + ); + } + + /** + * @return void + * + * @throws Exception + */ + public function testSetBannerSettingsInvalidURL(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'link', + '', + 'displayOnHomePage', + 'ES-base64' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + '', + 'displayOnCartPage', + 'PT-base64' + ) + ] + ); + + //Assert + $this->expectException(InvalidURLException::class); + + //Act + $this->bannerSettingsService->setBannerSettings($bannerSettings); + } + + /** + * @throws Exception + */ + public function testGetBannerDataByCountryAndDisplayLocation(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ), + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/image.jpg', + 'displayOnHomePage' + ), + + ] + ); + $this->bannerSettingsRepository->setBannerSettings($bannerSettings); + + $country = 'FR'; + $displayLocation = 'displayOnHomePage'; + + //Act + $result = $this->bannerSettingsService->getBannerData($country, $displayLocation); + + //Assert + self::assertNotNull($result); + self::assertEquals($country, $result->getCountry()); + self::assertEquals($displayLocation, $result->getDisplayLocation()); + } + + /** + * @throws Exception + */ + public function testGetBannerNoDataForDisplayLocation(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.com/it/faq#shoppers', + 'https://shop/sequra/pt/image.jpg', + 'displayOnCartPage' + ), + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/image.jpg', + 'displayOnHomePage' + ), + + ] + ); + $this->bannerSettingsRepository->setBannerSettings($bannerSettings); + + $country = 'FR'; + $displayLocation = 'displayOnCartPage'; + + //Act + $result = $this->bannerSettingsService->getBannerData($country, $displayLocation); + + //Assert + self::assertNull($result); + } + + /** + * @throws Exception + */ + public function testGetBannerNoDataForCountry(): void + { + //Arrange + $bannerSettings = new BannerSettings( + [ + new Banner( + 'ES', + 'https://www.sequra.com/es/faq#shoppers', + 'https://shop/sequra/es/image.jpg', + 'displayOnHomePage' + ), + new Banner( + 'FR', + 'https://www.sequra.com/fr/faq#shoppers', + 'https://shop/sequra/fr/image.jpg', + 'displayOnHomePage' + ), + + ] + ); + $this->bannerSettingsRepository->setBannerSettings($bannerSettings); + + $country = 'PT'; + $displayLocation = 'displayOnCartPage'; + + //Act + $result = $this->bannerSettingsService->getBannerData($country, $displayLocation); + + //Assert + self::assertNull($result); + } +} diff --git a/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php b/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php index cac79378..2be9adc0 100644 --- a/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php +++ b/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php @@ -6,6 +6,9 @@ use Exception; use SeQura\Core\BusinessLogic\DataAccess\TransactionLog\Entities\TransactionLog; use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Models\AdvancedSettings; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\Banner; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Models\BannerSettings; +use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\InvalidEnvironmentException; use SeQura\Core\BusinessLogic\Domain\Connection\Models\AuthorizationCredentials; use SeQura\Core\BusinessLogic\Domain\Connection\Models\ConnectionData; @@ -25,6 +28,7 @@ use SeQura\Core\BusinessLogic\Domain\Order\RepositoryContracts\SeQuraOrderRepositoryInterface; use SeQura\Core\BusinessLogic\Domain\OrderStatusSettings\Models\OrderStatusMapping; use SeQura\Core\BusinessLogic\Domain\OrderStatusSettings\RepositoryContracts\OrderStatusSettingsRepositoryInterface; +use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Exceptions\PaymentMethodNotFoundException; use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Models\SeQuraCost; use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Models\SeQuraPaymentMethod; use SeQura\Core\BusinessLogic\Domain\PaymentMethod\RepositoryContracts\PaymentMethodRepositoryInterface; @@ -37,6 +41,8 @@ use SeQura\Core\BusinessLogic\TransactionLog\RepositoryContracts\TransactionLogRepositoryInterface; use SeQura\Core\Tests\BusinessLogic\Common\BaseTestCase; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockAdvancedSettingsRepository; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerService; +use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockBannerSettingsRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockConnectionDataRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockCountryConfigurationRepository; use SeQura\Core\Tests\BusinessLogic\Common\MockComponents\MockCredentialsRepository; @@ -137,6 +143,21 @@ class DisconnectServiceTest extends BaseTestCase */ private $advancedSettingsRepository; + /** + * @var MockBannerSettingsRepository $bannerSettingsRepository + */ + private $bannerSettingsRepository; + + /** + * @var MockBannerService $bannerService + */ + private $bannerService; + + /** + * @var BannerSettingsService $bannerSettingsService + */ + private $bannerSettingsService; + /** * @var DisconnectService $service */ @@ -166,6 +187,12 @@ protected function setUp(): void new MockStoreInfoService() ); $this->advancedSettingsRepository = new MockAdvancedSettingsRepository(); + $this->bannerSettingsRepository = new MockBannerSettingsRepository(); + $this->bannerService = new MockBannerService(); + $this->bannerSettingsService = new BannerSettingsService( + $this->bannerSettingsRepository, + $this->bannerService + ); $this->service = new DisconnectService( $this->integrationDisconnectService, @@ -182,7 +209,8 @@ protected function setUp(): void $this->statisticalDataRepository, $this->transactionLogRepository, $this->storeIntegrationService, - $this->advancedSettingsRepository + $this->advancedSettingsRepository, + $this->bannerSettingsService ); } @@ -510,6 +538,66 @@ public function testDisconnectIntegrationDeleted(): void self::assertTrue($this->storeIntegrationService->isDeleted()); } + /** + * @return void + * + * @throws PaymentMethodNotFoundException + */ + public function testDisconnectFullClearsBannerSettingsAndImages(): void + { + //Arrange + $this->bannerSettingsRepository->setBannerSettings(new BannerSettings([ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop.test/banners/ES_displayOnHomePage.png', + 'displayOnHomePage' + ), + new Banner( + 'PT', + 'https://www.sequra.pt/pt/faq#shoppers', + 'https://shop.test/banners/PT_displayOnCartPage.png', + 'displayOnCartPage' + ), + ])); + + //Act + $this->service->disconnect('sequra', true); + + //Assert + self::assertNull($this->bannerSettingsRepository->getBannerSettings()); + self::assertEquals( + ['ES|displayOnHomePage', 'PT|displayOnCartPage'], + $this->bannerService->getDeletedImageKeys() + ); + } + + /** + * @return void + * + * @throws PaymentMethodNotFoundException + */ + public function testDisconnectNotFullPreservesBannerSettings(): void + { + //Arrange + $existing = new BannerSettings([ + new Banner( + 'ES', + 'https://www.sequra.es/es/faq#shoppers', + 'https://shop.test/banners/ES_displayOnHomePage.png', + 'displayOnHomePage' + ), + ]); + $this->bannerSettingsRepository->setBannerSettings($existing); + + //Act + $this->service->disconnect('sequra', false); + + //Assert + self::assertNotNull($this->bannerSettingsRepository->getBannerSettings()); + self::assertEmpty($this->bannerService->getDeletedImageKeys()); + } + /** * When the remote store-integration DELETE fails (e.g. the integration was * never registered on the seQura platform, or was already removed), the diff --git a/tests/BusinessLogic/Domain/StoreIntegration/Models/CapabilityModelTest.php b/tests/BusinessLogic/Domain/StoreIntegration/Models/CapabilityModelTest.php index b7e7e90c..a9179d76 100644 --- a/tests/BusinessLogic/Domain/StoreIntegration/Models/CapabilityModelTest.php +++ b/tests/BusinessLogic/Domain/StoreIntegration/Models/CapabilityModelTest.php @@ -62,6 +62,23 @@ public function testWidgetCapability(): void self::assertEquals(Capability::widget(), $capability); } + /** + * @return void + * + * @throws InvalidCapabilityException + */ + public function testBannerCapability(): void + { + // arrange + + // act + $capability = Capability::parse('banner'); + + // assert + self::assertEquals('banner', $capability->getCapability()); + self::assertEquals(Capability::banner(), $capability); + } + /** * @return void *