Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

# [v5.5.0](https://github.com/sequra/integration-core/tree/v5.5.0)
## Added
- Affiliate configuration support: the `AffiliateSettings` entity and `AffiliateSettingsService`, the `get-affiliate-settings` and `save-affiliate-settings` configuration webhook topics, and connect time provisioning that reads the `affiliate` block from the merchant `configuration_data` and persists it.

# [v1.0.13](https://github.com/sequra/integration-core/tree/v1.0.13)
## Changed
- Added compatibility with PHP8.2.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3171,6 +3171,8 @@ The Configuration WebhookAPI follows a **simplified Controller pattern** with **
- **GetShopProductsHandler**: Handles `get-shop-products` webhook topic
- **GetStoreInfoHandler**: Handles `get-store-info` webhook topic
- **SaveWidgetSettingsHandler**: Handles `save-widget-settings` webhook topic
- **GetAffiliateSettingsHandler**: Handles `get-affiliate-settings` webhook topic
- **SaveAffiliateSettingsHandler**: Handles `save-affiliate-settings` webhook topic

#### Webhook Processing Flow

Expand Down
56 changes: 55 additions & 1 deletion src/BusinessLogic/BootstrapComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\GetAdvancedSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\AdvancedSettings\SaveAdvancedSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\GetAffiliateSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate\SaveAffiliateSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\GetBannerSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\BannerSettings\SaveBannerSettingsHandler;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Enums\Topics;
Expand All @@ -40,6 +42,8 @@
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\WidgetSettings\SaveWidgetSettingsHandler;
use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Entities\AdvancedSettings;
use SeQura\Core\BusinessLogic\DataAccess\AdvancedSettings\Repositories\AdvancedSettingsRepository;
use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Entities\AffiliateSettings;
use SeQura\Core\BusinessLogic\DataAccess\Affiliate\Repositories\AffiliateSettingsRepository;
use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Entities\BannerSettings;
use SeQura\Core\BusinessLogic\DataAccess\BannerSettings\Repositories\BannerSettingsRepository;
use SeQura\Core\BusinessLogic\DataAccess\ConnectionData\Entities\ConnectionData;
Expand Down Expand Up @@ -67,6 +71,8 @@
use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\RepositoryContracts\AdvancedSettingsRepositoryInterface;
use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedLoggerSettingsProvider;
use SeQura\Core\BusinessLogic\Domain\AdvancedSettings\Services\AdvancedSettingsService;
use SeQura\Core\BusinessLogic\Domain\Affiliate\RepositoryContracts\AffiliateSettingsRepositoryInterface;
use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService;
use SeQura\Core\BusinessLogic\Domain\BannerSettings\RepositoryContracts\BannerSettingsRepositoryInterface;
use SeQura\Core\BusinessLogic\Domain\BannerSettings\Services\BannerSettingsService;
use SeQura\Core\BusinessLogic\Domain\Integration\Banner\BannerServiceInterface;
Expand Down Expand Up @@ -332,6 +338,16 @@ static function () {
);
}
);

ServiceRegister::registerService(
AffiliateSettingsRepositoryInterface::class,
static function () {
return new AffiliateSettingsRepository(
RepositoryRegistry::getRepository(AffiliateSettings::getClassName()),
Comment on lines +343 to +346

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Versioning note: this is a SemVer MINOR and is safe to release as one — no breaking change for existing consumers.

I checked whether updating to this version without a consumer registering the AffiliateSettings entity would break existing integrations. It doesn't, because resolution is lazy and per-topic:

  • ConfigurationWebhookController::handleRequest() resolves only getHandlerForTopic($topic) for the incoming topic.
  • Topics::ALL_TOPICS isn't iterated anywhere (it's referenced only in Topics.php), so adding the two entries has no eager effect.
  • getHandlerForTopic()ServiceRegister::getService() runs the handler closure only when that topic actually arrives; initTopicHandlers() / registerService() only register lazy closures at boot.

So this getRepository(AffiliateSettings::getClassName()) is reached only when a get/save-affiliate-settings webhook is handled — which is opt-in (only sent to stores enrolled in the feature). A consumer that bumps without registering the entity boots fine and keeps all existing behaviour; the worst case is a single 500 on an affiliate webhook for an unconfigured store, not a regression of existing functionality.

Per SemVer that's a MINOR (additive, backward-compatible) — e.g. 5.2.0 on the current 5.1.x line. Heads-up for consumers: registering the entity is required to use the feature (an integration step), not to survive the upgrade.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, agreed it is a backward compatible MINOR. Targeting v5.5.0 as the release tag since the tags already reached v5.4.0 (so not 5.2.0). The plugin will register the entity and bump its constraint to that tag when it consumes this.

ServiceRegister::getService(StoreContext::class)
);
}
);
}

/**
Expand Down Expand Up @@ -384,7 +400,8 @@ static function () {
ServiceRegister::getService(ConnectionProxyInterface::class),
ServiceRegister::getService(CredentialsRepositoryInterface::class),
ServiceRegister::getService(CountryConfigurationRepositoryInterface::class),
ServiceRegister::getService(PaymentMethodRepositoryInterface::class)
ServiceRegister::getService(PaymentMethodRepositoryInterface::class),
ServiceRegister::getService(AffiliateSettingsService::class)
);
}
);
Expand Down Expand Up @@ -667,6 +684,15 @@ static function () {
}
);

ServiceRegister::registerService(
AffiliateSettingsService::class,
static function () {
return new AffiliateSettingsService(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Outbound side isn't wired in the core.

Only the inbound config services are registered here. The conversion/cancellation postbacks the plugin emits are owned by the core as well (the plugin must not make HTTP calls directly), but there's no affiliate proxy under SeQuraAPI/ that resolves the endpoint from the Deployment and sends them. Until that exists the plugin's outbound client stays a no-op, so even once the config is delivered nothing is reported end-to-end.

Include the outbound affiliate proxy here, or split it into the immediate next core change and link it — but it shouldn't be left implicit, since the consumer plugin is already blocked on it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed this belongs in the core, but I would keep it out of this PR and ship it as the immediate follow up. Reasoning: this PR is the inbound config half, which is what unblocks the plugin, and the outbound proxy also depends on timon phase 2 routing, so coupling them would hold back config delivery. I will open the follow up for the affiliate proxy under SeQuraAPI/ (endpoint resolved from the Deployment, request emitted in the destination shape) and link it here. Say the word if you would rather it lands in this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Follow up created: QRD-7949 (https://sequra.atlassian.net/browse/QRD-7949), the integration-core outbound AffiliateProxy, sibling of QRD-7933 (timon Phase 2). Keeping it out of this PR so the inbound config can merge and tag v5.5.0.

ServiceRegister::getService(AffiliateSettingsRepositoryInterface::class)
);
}
);

ServiceRegister::registerService(
LoggerSettingsProviderInterface::CLASS_NAME,
static function () {
Expand Down Expand Up @@ -1056,6 +1082,16 @@ protected static function initTopicHandlers(): void
SaveAdvancedSettingsHandler::class
);

TopicHandlerRegistry::register(
Topics::GET_AFFILIATE_SETTINGS,
GetAffiliateSettingsHandler::class
);

TopicHandlerRegistry::register(
Topics::SAVE_AFFILIATE_SETTINGS,
SaveAffiliateSettingsHandler::class
);

TopicHandlerRegistry::register(
Topics::GET_BANNER_SETTINGS,
GetBannerSettingsHandler::class
Expand Down Expand Up @@ -1189,6 +1225,24 @@ static function () {
}
);

ServiceRegister::registerService(
GetAffiliateSettingsHandler::class,
static function () {
return new GetAffiliateSettingsHandler(
ServiceRegister::getService(AffiliateSettingsService::class)
);
}
);

ServiceRegister::registerService(
SaveAffiliateSettingsHandler::class,
static function () {
return new SaveAffiliateSettingsHandler(
ServiceRegister::getService(AffiliateSettingsService::class)
);
}
);

ServiceRegister::registerService(
GetBannerSettingsHandler::class,
static function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate;

use SeQura\Core\BusinessLogic\AdminAPI\Response\Response;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\TopicHandlerInterface;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\Affiliate\AffiliateSettingsResponse;
use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService;

/**
* Class GetAffiliateSettingsHandler
*
* @package SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate
*/
class GetAffiliateSettingsHandler implements TopicHandlerInterface
{
/**
* @var AffiliateSettingsService
*/
protected $affiliateSettingsService;

/**
* @param AffiliateSettingsService $affiliateSettingsService
*/
public function __construct(AffiliateSettingsService $affiliateSettingsService)
{
$this->affiliateSettingsService = $affiliateSettingsService;
}

/**
* @param mixed[] $payload
*
* @return Response
*/
public function handle(array $payload): Response
{
// GET is a boolean-state read: return only whether the feature is enabled, never echo the
// offer id or security token. The service always yields settings (disabled by default when
// none are stored), so the response is enabled=false when nothing is configured.
return new AffiliateSettingsResponse($this->affiliateSettingsService->getAffiliateSettings()->isEnabled());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate;

use SeQura\Core\BusinessLogic\AdminAPI\Response\Response;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\TopicHandlerInterface;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Requests\Affiliate\SaveAffiliateSettingsRequest;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\SuccessResponse;
use SeQura\Core\BusinessLogic\Domain\Affiliate\Services\AffiliateSettingsService;

/**
* Class SaveAffiliateSettingsHandler
*
* @package SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Handlers\Affiliate
*/
class SaveAffiliateSettingsHandler implements TopicHandlerInterface
{
/**
* @var AffiliateSettingsService
*/
protected $affiliateSettingsService;

/**
* @param AffiliateSettingsService $affiliateSettingsService
*/
public function __construct(AffiliateSettingsService $affiliateSettingsService)
{
$this->affiliateSettingsService = $affiliateSettingsService;
}

/**
* @inheritDoc
*/
public function handle(array $payload): Response
{
$request = SaveAffiliateSettingsRequest::fromPayload($payload);
$this->affiliateSettingsService->setAffiliateSettings($request->transformToDomainModel());

return new SuccessResponse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ interface Topics
* @var string
*/
public const GET_STORE_INFO = 'get-store-info';
/**
* @var string
*/
public const GET_AFFILIATE_SETTINGS = 'get-affiliate-settings';
/**
* @var string
*/
public const SAVE_AFFILIATE_SETTINGS = 'save-affiliate-settings';
Comment on lines +83 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Docs need updating to reflect the new topics + entity. The PR adds the topics but not the matching documentation:

  • README.md — the Configuration Webhook API Layer → TopicHandlerRegistry section enumerates every handler and its topic (currently around lines 3160–3173) but is missing the two new ones. Add GetAffiliateSettingsHandler (get-affiliate-settings) and SaveAffiliateSettingsHandler (save-affiliate-settings). If that section (or the architecture overview) also lists the settings entities/services, add AffiliateSettings / AffiliateSettingsService there too.
  • CHANGELOG.md — add a new version entry (Keep a Changelog format, like the existing # [vX.Y.Z] blocks) describing the new affiliate config topics + AffiliateSettings entity/service, and tag the release (the version lives in the changelog/tag — composer.json has no version field).

(DEVELOPER_GUIDE.md is dev-env/IDE setup only, so nothing needed there.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added in 2c4f05c. Both handlers are now listed under TopicHandlerRegistry in the README, and there is a CHANGELOG entry for the affiliate topics, entity and service plus the connect time provisioning. Heads up on your note: the CHANGELOG had drifted, its top was v1.0.13 while the tags reached v5.4.0, so I added the entry as v5.5.0 at the top but the 5.x history in between is still missing. Worth a separate cleanup if the team wants the changelog accurate again.

/**
* @var string[]
*/
Expand All @@ -97,6 +105,8 @@ interface Topics
self::GET_SHOP_CATEGORIES,
self::GET_SHOP_PRODUCTS,
self::GET_SELLING_COUNTRIES,
self::GET_STORE_INFO
self::GET_STORE_INFO,
self::GET_AFFILIATE_SETTINGS,
self::SAVE_AFFILIATE_SETTINGS
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Requests\Affiliate;

use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Requests\ConfigurationWebhookRequest;
use SeQura\Core\BusinessLogic\Domain\Affiliate\Models\AffiliateSettings;

/**
* Class SaveAffiliateSettingsRequest.
*
* @package SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Requests\Affiliate
*/
class SaveAffiliateSettingsRequest extends ConfigurationWebhookRequest
{
/**
* @var bool $isEnabled
*/
private $isEnabled;

/**
* @var string $offerId
*/
private $offerId;

/**
* @var string $securityToken
*/
private $securityToken;

/**
* @param bool $isEnabled
* @param string $offerId
* @param string $securityToken
*/
public function __construct(bool $isEnabled, string $offerId, string $securityToken)
{
$this->isEnabled = $isEnabled;
$this->offerId = $offerId;
$this->securityToken = $securityToken;
}

/**
* @param mixed[] $payload
*
* @return self
*/
public static function fromPayload(array $payload): object
{
return new self(
$payload['isEnabled'] ?? false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Decision needed — enabled is accepted without its credentials. fromPayload casts with ?? false / ?? '' and there's no validation, and the domain AffiliateSettings exposes isEnabled() but has no notion of "fully configured". So a payload with isEnabled = true and empty offerId / securityToken persists as enabled-but-unusable, and any consumer that trusts isEnabled() would treat the feature as ON with no credentials to send.

Decide whether the core should require non-empty offerId + securityToken when isEnabled is true (reject, or coerce to disabled), or whether that "enabled ⇒ has creds" guard is explicitly the consumer's responsibility. Either way it shouldn't be left implicit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Decided to guard it in the core, in ed1716a. The AffiliateSettings model now coerces isEnabled to false when offerId or securityToken is empty, so an enabled flag with no credentials can never persist as usable and isEnabled() stays trustworthy everywhere (save webhook, connect time, GET read). I put it in the model rather than only in the request so the connect time path gets the same guarantee. Covered by testSaveAffiliateSettingsEnabledWithoutCredentialsIsCoercedToDisabled.

(string)($payload['offerId'] ?? ''),
(string)($payload['securityToken'] ?? '')
);
}

/**
* @return AffiliateSettings
*/
public function transformToDomainModel(): AffiliateSettings
{
return new AffiliateSettings($this->isEnabled, $this->offerId, $this->securityToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\Affiliate;

use SeQura\Core\BusinessLogic\AdminAPI\Response\Response;

/**
* Class AffiliateSettingsResponse
*
* @package SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Responses\Affiliate
*/
class AffiliateSettingsResponse extends Response
{
/**
* @var bool $isEnabled
*/
protected $isEnabled;

/**
* @param bool $isEnabled
*/
public function __construct(bool $isEnabled)
{
$this->isEnabled = $isEnabled;
}

/**
* @inheritDoc
*/
public function toArray(): array
{
return ['isEnabled' => $this->isEnabled];
}
}
Loading