Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
10 changes: 10 additions & 0 deletions src/BusinessLogic/BootstrapComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use SeQura\Core\BusinessLogic\CheckoutAPI\Checkout\Controller\CheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\ExpressCheckout\Controller\ExpressCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\PaymentMethodsCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController;
use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController;
Expand Down Expand Up @@ -902,6 +903,15 @@ static function () {
}
);

ServiceRegister::registerService(
PaymentMethodsCheckoutController::class,
static function () {
return new PaymentMethodsCheckoutController(
ServiceRegister::getService(OrderService::class)
);
}
);

ServiceRegister::registerService(
PromotionalWidgetsCheckoutController::class,
static function () {
Expand Down
14 changes: 14 additions & 0 deletions src/BusinessLogic/CheckoutAPI/CheckoutAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use SeQura\Core\BusinessLogic\CheckoutAPI\ExpressCheckout\Controller\ExpressCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\PaymentMethodsCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController;
use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController;

Expand Down Expand Up @@ -60,6 +61,19 @@ public function cachedPaymentMethods(string $storeId): object
->beforeEachMethodOfService(CachedPaymentMethodsController::class);
}

/**
* @param string $storeId
*
* @return object
*/
public function paymentMethods(string $storeId): object

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The new store-scoped endpoint provides no actual store isolation.

StoreContextAspect($storeId) only constrains store-scoped repositories, and SeQuraOrderRepository is not one: it has no StoreContext and getByOrderReference() filters on reference alone (src/BusinessLogic/DataAccess/Order/Repositories/SeQuraOrderRepository.php).

So in a multistore install, CheckoutAPI::get()->paymentMethods('storeA')->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest($refFromStoreB)) resolves store B's order and builds the authorized proxy from store B's merchant id.

This contradicts .claude/docs/codingStandard.md §8 ("Repositories are store-scoped: inject StoreContext, filter every query by storeId") and CLAUDE.md ("Anything reading/writing per-store config must respect the active store"). The gap is pre-existing, but this PR is what turns it into a storefront-facing, $storeId-parameterised endpoint.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Naming: the generic name went to the narrower feature.

CheckoutAPI::paymentMethods() and CheckoutAPI::cachedPaymentMethods() now sit side by side, resolving PaymentMethodsCheckoutController and CachedPaymentMethodsController from the same CheckoutAPI\PaymentMethods namespace.

An integrator picking paymentMethods() by name has no way to know it requires an already-solicited order reference while the other takes a merchant id. Consider naming it for what it does (e.g. solicitedOrderPaymentMethods()), or adding it as a second method on the existing controller.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The new store-scoped endpoint provides no actual store isolation.

StoreContextAspect($storeId) only constrains store-scoped repositories, and SeQuraOrderRepository is not one: it has no StoreContext and getByOrderReference() filters on reference alone (src/BusinessLogic/DataAccess/Order/Repositories/SeQuraOrderRepository.php).

So in a multistore install, CheckoutAPI::get()->paymentMethods('storeA')->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest($refFromStoreB)) resolves store B's order and builds the authorized proxy from store B's merchant id.

This contradicts .claude/docs/codingStandard.md §8 ("Repositories are store-scoped: inject StoreContext, filter every query by storeId") and CLAUDE.md ("Anything reading/writing per-store config must respect the active store"). The gap is pre-existing, but this PR is what turns it into a storefront-facing, $storeId-parameterised endpoint.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Naming: the generic name went to the narrower feature.

CheckoutAPI::paymentMethods() and CheckoutAPI::cachedPaymentMethods() now sit side by side, resolving PaymentMethodsCheckoutController and CachedPaymentMethodsController from the same CheckoutAPI\PaymentMethods namespace.

An integrator picking paymentMethods() by name has no way to know it requires an already-solicited order reference while the other takes a merchant id. Consider naming it for what it does (e.g. solicitedOrderPaymentMethods()), or adding it as a second method on the existing controller.

{
return Aspects
::run(new ErrorHandlingAspect())
->andRun(new StoreContextAspect($storeId))
->beforeEachMethodOfService(PaymentMethodsCheckoutController::class);
}

/**
* @param string $storeId
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods;

use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Requests\PaymentMethodsInCategoriesRequest;
use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Responses\PaymentMethodsInCategoriesResponse;
use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\OrderNotFoundException;
use SeQura\Core\BusinessLogic\Domain\Order\Service\OrderService;
use SeQura\Core\Infrastructure\Http\Exceptions\HttpRequestException;

/**
* Class PaymentMethodsCheckoutController.
*
* Storefront endpoint returning the payment methods of an already solicited order. It depends on OrderService
* alone, so integrations that solicit orders without configuring the checkout library can use it.
*
* @package SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods
*/
class PaymentMethodsCheckoutController
{
/**
* @var OrderService
*/
protected $orderService;

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

/**
* Returns the payment methods available for a solicited order, grouped in the categories SeQura returns them in.
*
* @param PaymentMethodsInCategoriesRequest $request
*
* @return PaymentMethodsInCategoriesResponse
*
* @throws HttpRequestException

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Incomplete @throws, on both this method and the rewritten OrderService one.

AuthorizedProxyFactory::build() declares @throws ConnectionDataNotFoundException, CredentialsNotFoundException, DeploymentNotFoundException, and OrderProxy::getAvailablePaymentMethodsInCategories() calls it. None of the three is listed here or on OrderService::getAvailablePaymentMethodsInCategories().

.claude/docs/codingStandard.md §6: "@throws must list every exception a method can propagate (PHPStan level 6 + callers rely on it)" and "Keep @throws honest when you refactor". Both docblocks were rewritten in this PR (adding OrderNotFoundException) without completing them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Incomplete @throws, on both this method and the rewritten OrderService one.

AuthorizedProxyFactory::build() declares @throws ConnectionDataNotFoundException, CredentialsNotFoundException, DeploymentNotFoundException, and OrderProxy::getAvailablePaymentMethodsInCategories() calls it. None of the three is listed here or on OrderService::getAvailablePaymentMethodsInCategories().

.claude/docs/codingStandard.md §6: "@throws must list every exception a method can propagate (PHPStan level 6 + callers rely on it)" and "Keep @throws honest when you refactor". Both docblocks were rewritten in this PR (adding OrderNotFoundException) without completing them.

* @throws OrderNotFoundException
*/
public function getPaymentMethodsInCategories(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The endpoint's primary failure mode degrades to a generic unhandled error.

OrderNotFoundException extends Infrastructure\Exceptions\BaseException, so ErrorHandlingAspect misses its BaseTranslatableException catch and falls through to the generic Throwable catch. Verified output:

['statusCode' => 0, 'errorCode' => 'general.errors.unknown',
 'errorMessage' => 'Unhandled error occurred: SeQura order with reference X is not found.']

The 404 that OrderService::getSeQuraOrder() deliberately sets is discarded, the storefront cannot distinguish "unknown order" from "seQura is down", and every stale checkout page hit is logged at ERROR as an unhandled error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The endpoint's primary failure mode degrades to a generic unhandled error.

OrderNotFoundException extends Infrastructure\Exceptions\BaseException, so ErrorHandlingAspect misses its BaseTranslatableException catch and falls through to the generic Throwable catch. Verified output:

['statusCode' => 0, 'errorCode' => 'general.errors.unknown',
 'errorMessage' => 'Unhandled error occurred: SeQura order with reference X is not found.']

The 404 that OrderService::getSeQuraOrder() deliberately sets is discarded, the storefront cannot distinguish "unknown order" from "seQura is down", and every stale checkout page hit is logged at ERROR as an unhandled error.

PaymentMethodsInCategoriesRequest $request
): PaymentMethodsInCategoriesResponse {
return new PaymentMethodsInCategoriesResponse(
$this->orderService->getAvailablePaymentMethodsInCategories($request->getOrderRef())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Requests;

/**
* Class PaymentMethodsInCategoriesRequest.
*
* @package SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Requests
*/
class PaymentMethodsInCategoriesRequest
{
/**
* @var string
*/
protected $orderRef;

/**
* @param string $orderRef Reference of the solicited order.
*/
public function __construct(string $orderRef)
{
$this->orderRef = $orderRef;
}

/**
* @return string
*/
public function getOrderRef(): string
{
return $this->orderRef;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Responses;

use SeQura\Core\BusinessLogic\AdminAPI\Response\Response;
use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Models\SeQuraPaymentMethod;
use SeQura\Core\BusinessLogic\Domain\PaymentMethod\Models\SeQuraPaymentMethodCategory;

/**
* Class PaymentMethodsInCategoriesResponse.
*
* @package SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Responses
*/
class PaymentMethodsInCategoriesResponse extends Response
{
/**
* @var SeQuraPaymentMethodCategory[]
*/
protected $paymentMethodCategories;

/**
* @param SeQuraPaymentMethodCategory[] $paymentMethodCategories
*/
public function __construct(array $paymentMethodCategories)
{
$this->paymentMethodCategories = $paymentMethodCategories;
}

/**
* @return SeQuraPaymentMethodCategory[]
*/
public function getPaymentMethodCategories(): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

getPaymentMethodCategories() has no caller.

Grep across src/ and tests/ finds only the declaration. CLAUDE.md working principle 2: "Write the minimum code that solves the problem — nothing speculative... no abstractions for single-use code." Drop it, or add the test that justifies it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

getPaymentMethodCategories() has no caller.

Grep across src/ and tests/ finds only the declaration. CLAUDE.md working principle 2: "Write the minimum code that solves the problem — nothing speculative... no abstractions for single-use code." Drop it, or add the test that justifies it.

{
return $this->paymentMethodCategories;
}

/**
* Determines whether at least one category offers a payment method. Categories with no method are returned
* as well, so the presence of categories does not mean the buyer has anything to choose from.
*
* @return bool
*/
public function hasAvailablePaymentMethods(): bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hasAvailablePaymentMethods() is truthy on the error path.

On a failed call the facade returns TranslatableErrorResponse (via ErrorHandlingAspect), and ErrorResponse::__call() swallows unknown methods and returns $this. Verified by executing it: get_class() is TranslatableErrorResponse and (bool) $response->hasAvailablePaymentMethods() is true.

Any storefront doing if ($response->hasAvailablePaymentMethods()) { render seQura } renders seQura payment methods on every failed call (unknown order ref, seQura down). The helper is only safe behind an isSuccessful() guard, which nothing enforces and no test covers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hasAvailablePaymentMethods() is truthy on the error path.

On a failed call the facade returns TranslatableErrorResponse (via ErrorHandlingAspect), and ErrorResponse::__call() swallows unknown methods and returns $this. Verified by executing it: get_class() is TranslatableErrorResponse and (bool) $response->hasAvailablePaymentMethods() is true.

Any storefront doing if ($response->hasAvailablePaymentMethods()) { render seQura } renders seQura payment methods on every failed call (unknown order ref, seQura down). The helper is only safe behind an isSuccessful() guard, which nothing enforces and no test covers.

{
foreach ($this->paymentMethodCategories as $category) {
if (!empty($category->getMethods())) {
return true;
}
}

return false;
}

/**
* @inheritDoc
*/
public function toArray(): array
{
$categories = [];
foreach ($this->paymentMethodCategories as $category) {
$categories[] = [
'title' => $category->getTitle(),
'description' => $category->getDescription(),
'icon' => $category->getIcon(),
'methods' => array_map(static function (SeQuraPaymentMethod $paymentMethod) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The public checkout payload delegates to the ORM persistence serializer.

SeQuraPaymentMethod::toArray() is what DataAccess/PaymentMethod/Entities/PaymentMethod::toArray() persists: it emits long_title, starts_at, ends_at, cost_description, min_amount.

The sibling CachedPaymentMethodsResponse::toArray() in this same Responses/ folder emits longTitle, startsAt, endsAt, costDescription, minAmount for the same model. Storefront JS consuming CheckoutAPI now gets two different shapes for one entity, and any future change to the entity storage format silently changes the public checkout API.

Map the fields explicitly here, matching CachedPaymentMethodsResponse.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The public checkout payload delegates to the ORM persistence serializer.

SeQuraPaymentMethod::toArray() is what DataAccess/PaymentMethod/Entities/PaymentMethod::toArray() persists: it emits long_title, starts_at, ends_at, cost_description, min_amount.

The sibling CachedPaymentMethodsResponse::toArray() in this same Responses/ folder emits longTitle, startsAt, endsAt, costDescription, minAmount for the same model. Storefront JS consuming CheckoutAPI now gets two different shapes for one entity, and any future change to the entity storage format silently changes the public checkout API.

Map the fields explicitly here, matching CachedPaymentMethodsResponse.

return $paymentMethod->toArray();
}, $category->getMethods()),
];
}

return $categories;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,16 @@ private function removeAllDeploymentData(string $deploymentId): void

// Removes country configurations connected to the deployment
$countryConfigurations = $this->countryConfigurationRepository->getCountryConfiguration();
$newCountyConfigurations = [];
foreach ($countryConfigurations as $countryConfiguration) {
if (!\in_array($countryConfiguration->getMerchantId(), $merchantIds, true)) {
$newCountyConfigurations[] = $countryConfiguration;
if ($countryConfigurations) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Untested, unrelated to LIS-116, and only half-solves the stated problem.

The guard does fix the null case — getCountryConfiguration() returns ?array, so this previously did foreach (null) and created a spurious empty entity row. But when the entity exists and every config belongs to the disconnected deployment, setCountryConfiguration([]) still writes an empty array, which is the "unnecessary update" that actually matters.

DisconnectServiceTest covers neither the null-config branch nor the all-configs-removed branch. And the change is outside the scope of a LIS-116 PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Untested, unrelated to LIS-116, and only half-solves the stated problem.

The guard does fix the null case — getCountryConfiguration() returns ?array, so this previously did foreach (null) and created a spurious empty entity row. But when the entity exists and every config belongs to the disconnected deployment, setCountryConfiguration([]) still writes an empty array, which is the "unnecessary update" that actually matters.

DisconnectServiceTest covers neither the null-config branch nor the all-configs-removed branch. And the change is outside the scope of a LIS-116 PR.

$newCountyConfigurations = [];
foreach ($countryConfigurations as $countryConfiguration) {
if (!\in_array($countryConfiguration->getMerchantId(), $merchantIds, true)) {
$newCountyConfigurations[] = $countryConfiguration;
}
}
}

$this->countryConfigurationRepository->setCountryConfiguration($newCountyConfigurations);
$this->countryConfigurationRepository->setCountryConfiguration($newCountyConfigurations);
}

// Removes all payment methods connected to the deployment
foreach ($merchantIds as $merchantId) {
Expand Down
24 changes: 11 additions & 13 deletions src/BusinessLogic/Domain/Order/Service/OrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,22 @@ public function getAvailablePaymentMethods(SeQuraOrder $order): array
}

/**
* Gets available payment methods for solicited order in categories.
* Gets available payment methods for solicited order in categories. The merchant the order was solicited for
* is taken from the stored order, so callers only need its reference.
*
* @param string $orderRef
* @param string $merchantId
*
* @return SeQuraPaymentMethodCategory[]
*
* @throws HttpRequestException
* @throws OrderNotFoundException
*/
public function getAvailablePaymentMethodsInCategories(string $orderRef, string $merchantId): array
public function getAvailablePaymentMethodsInCategories(string $orderRef): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Silent BC break for host integrations.

This is a consumed library, and dropping $merchantId from a public method changes its contract. PHP does not error on extra args to userland functions, so an existing getAvailablePaymentMethodsInCategories($ref, $merchantId) call in WooCommerce/PrestaShop keeps compiling while the merchant id is silently ignored and resolved from local storage instead.

It also now calls getSeQuraOrder() and throws OrderNotFoundException for any order not persisted in the host DB — a case that previously worked precisely because the caller supplied the merchant. Consider keeping the parameter as an optional deprecation shim.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Silent BC break for host integrations.

This is a consumed library, and dropping $merchantId from a public method changes its contract. PHP does not error on extra args to userland functions, so an existing getAvailablePaymentMethodsInCategories($ref, $merchantId) call in WooCommerce/PrestaShop keeps compiling while the merchant id is silently ignored and resolved from local storage instead.

It also now calls getSeQuraOrder() and throws OrderNotFoundException for any order not persisted in the host DB — a case that previously worked precisely because the caller supplied the merchant. Consider keeping the parameter as an optional deprecation shim.

{
$order = $this->getSeQuraOrder($orderRef);

return $this->proxy->getAvailablePaymentMethodsInCategories(
new GetAvailablePaymentMethodsRequest($orderRef, $merchantId)
new GetAvailablePaymentMethodsRequest($orderRef, (string)$order->getMerchant()->getId())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The (string) cast turns a missing merchant id into ''.

Merchant::getId() is untyped (@return int|string over an untyped property). For a stored order whose merchant record lost its id, (string) null yields '', which OrderProxy hands to authorizedProxyFactory->build(''); that fails deep in credentials lookup with a confusing CredentialsNotFoundException rather than a clear "order has no merchant".

Note getAvailablePaymentMethods() about 20 lines above does not cast at all — the same expression handled two different ways in adjacent methods. An explicit check/exception would be clearer than either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The (string) cast turns a missing merchant id into ''.

Merchant::getId() is untyped (@return int|string over an untyped property). For a stored order whose merchant record lost its id, (string) null yields '', which OrderProxy hands to authorizedProxyFactory->build(''); that fails deep in credentials lookup with a confusing CredentialsNotFoundException rather than a clear "order has no merchant".

Note getAvailablePaymentMethods() about 20 lines above does not cast at all — the same expression handled two different ways in adjacent methods. An explicit check/exception would be clearer than either.

);
}

Expand Down Expand Up @@ -319,8 +322,7 @@ public function createOrder(Webhook $webhook): string
$updatedSeQuraOrder->setPaymentMethod(
$this->getOrderPaymentMethodInfo(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Redundant repository read in the webhook hot path.

createOrder(Webhook) already loaded $seQuraOrder above and built $updatedSeQuraOrder from its merchant. getOrderPaymentMethodInfo()getAvailablePaymentMethodsInCategories()getSeQuraOrder($orderReference) then re-queries the DB for the same record just to read back the same merchant id.

That is one extra SELECT per approved-order webhook, plus a new OrderNotFoundException path in a hot path that could not previously fail that way. Pass the already-loaded $seQuraOrder/merchant through, or keep a private helper that accepts it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Redundant repository read in the webhook hot path.

createOrder(Webhook) already loaded $seQuraOrder above and built $updatedSeQuraOrder from its merchant. getOrderPaymentMethodInfo()getAvailablePaymentMethodsInCategories()getSeQuraOrder($orderReference) then re-queries the DB for the same record just to read back the same merchant id.

That is one extra SELECT per approved-order webhook, plus a new OrderNotFoundException path in a hot path that could not previously fail that way. Pass the already-loaded $seQuraOrder/merchant through, or keep a private helper that accepts it.

$updatedSeQuraOrder->getReference(),
$webhook->getProductCode(),
(string)$updatedSeQuraOrder->getMerchant()->getId()
$webhook->getProductCode()
)
);

Expand Down Expand Up @@ -477,21 +479,17 @@ public function getSeQuraOrder(string $orderReference): SeQuraOrder
*
* @param string $orderReference
* @param string $paymentMethodId
* @param string $merchantId
*
* @return PaymentMethod|null
*
* @throws HttpRequestException
* @throws OrderNotFoundException
*/
private function getOrderPaymentMethodInfo(
string $orderReference,
string $paymentMethodId,
string $merchantId
string $paymentMethodId
): ?PaymentMethod {
$methodCategories = $this->getAvailablePaymentMethodsInCategories(
$orderReference,
$merchantId
);
$methodCategories = $this->getAvailablePaymentMethodsInCategories($orderReference);

foreach ($methodCategories as $category) {
foreach ($category->getMethods() as $method) {
Expand Down
Loading