Skip to content

PRE-3603: Overload scalapay min/max amounts from API - #317

Open
adumont-payplug wants to merge 1 commit into
developfrom
feature/PRE-3603_overload_min_max_scalapay
Open

PRE-3603: Overload scalapay min/max amounts from API#317
adumont-payplug wants to merge 1 commit into
developfrom
feature/PRE-3603_overload_min_max_scalapay

Conversation

@adumont-payplug

@adumont-payplug adumont-payplug commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Description

Scalapay currently shows at checkout for any cart amount within the limits reported by the PayPlug API, with no way for the merchant to tighten that range. This adds merchant-configurable min/max amount limits for Scalapay, mirroring the existing amount-filtering mechanism already used for the other PPRO gateways.

  • Two new optional fields (min_amount, max_amount) on the Scalapay gateway admin config, rendered as MoneyType (EUR) inputs. Left blank, they fall back to whatever the PayPlug API reports for the account.
  • SupportedMethodsProvider now applies the merchant's configured bounds (when set) instead of the raw API bounds when filtering Scalapay at checkout.
  • A new save-time validator (IsScalapayAmountRangeValid) rejects configs where min > max, or where either value falls outside the live API-reported range — the merchant can only narrow the range, never widen it beyond what PayPlug authorizes.

Motivation: Merchants want to control which cart sizes see Scalapay as a payment option (UX and fee control), similar to existing carve-outs for other financing/BNPL methods.

Related issue(s): Closes [PRE-3603](https://payplug-prod.atlassian.net/browse/PRE-3603)


Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Checklist

Code Quality

  • Code is linted and formatted (ECS clean)
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (bounds come from the live API, not hardcoded constants)

Testing

  • Unit tests added / updated (SupportedMethodsProviderTest, PaymentMethodValidatorTest, new ScalapayGatewayConfigurationTypeExtensionTest, new IsScalapayAmountRangeValidValidatorTest)
  • New/changed code is covered by tests

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate (API failures during validation fail open — a transient PayPlug API error doesn't block saving the payment method; existing behavior for other save-time checks)

@adumont-payplug adumont-payplug self-assigned this Sep 1, 2026

@hdelaforce-payplug hdelaforce-payplug left a comment

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.

Automated review pass — 7 findings, most severe first. #1 is the one I'd block merge on: it's a config the admin form actively allows the merchant to save, and it silently disables Scalapay at checkout with zero diagnostic.

Comment thread src/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidator.php Outdated
Comment thread src/Provider/SupportedMethodsProvider.php Outdated
Comment thread src/Provider/SupportedMethodsProvider.php Outdated
Comment thread src/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidator.php Outdated
Comment thread src/Gateway/Validator/Constraints/IsScalapayAmountRangeValidValidator.php Outdated
@adumont-payplug
adumont-payplug force-pushed the feature/PRE-3603_overload_min_max_scalapay branch 2 times, most recently from b3e80fc to c1d700f Compare September 3, 2026 09:19

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@adumont-payplug
adumont-payplug force-pushed the feature/PRE-3603_overload_min_max_scalapay branch 4 times, most recently from 85ed560 to d8dcc57 Compare September 3, 2026 12:55
@jhoaraupp

Copy link
Copy Markdown
Contributor

Review

1 medium design gap, 1 medium robustness gap, 2 low/nits — no blockers, solid feature overall.

Summary of the change

  • New optional min_amount/max_amount EUR MoneyType fields on the Scalapay gateway config.
  • SupportedMethodsProvider applies these as an override of the live PayPlug API bounds at checkout (EUR only).
  • New IsScalapayAmountRangeValidValidator rejects configs where min > max or either bound falls outside the live API range, at save time.
  • Good refactor: extracted AccountAmountRangeResolver to deduplicate the account-parsing logic that used to live only in SupportedMethodsProvider (and is now reused by the validator) — the removed phpstan-baseline entries confirm the mixed-offset issue was genuinely fixed, not just suppressed elsewhere.

Findings

[MEDIUM] The min/max override isn't actually scoped to Scalapay

src/Provider/SupportedMethodsProvider.php (resolveAmountBounds) reads min_amount/max_amount off whatever gateway config is being processed, gated only by activeCurrencyCode === 'EUR' — not by $gatewayConfig->getFactoryName() === ScalapayGatewayFactory::FACTORY_NAME. This is confirmed by the PR's own test using PayPlugGatewayFactory::FACTORY_NAME with a min_amount key and getting the override behavior (tests/PHPUnit/Provider/SupportedMethodsProviderTest.php, testProvide_withMerchantConfiguredMinAmount_overridesApiMin).

The problem: the safety net — IsScalapayAmountRangeValidValidator, which checks the configured range against the live API bounds — is wired only for Scalapay in PaymentMethodValidator::process(). If another gateway extension later reuses these same config keys (plausible, since this is presented as a generic mechanism), it would silently inherit the checkout-time override with no save-time validation guardrail, letting a merchant configure a range wider than PayPlug actually authorizes for that method.

Suggested fix — make the coupling explicit:

private function resolveAmountBounds(
    GatewayConfigInterface $gatewayConfig,
    string $activeCurrencyCode,
    array $authorizedRange,
): array {
    if ('EUR' !== $activeCurrencyCode || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) {
        return [$authorizedRange['min_amount'], $authorizedRange['max_amount']];
    }
    ...

[MEDIUM] Malformed config would crash checkout, not just fail validation

Both SupportedMethodsProvider::resolveAmountBounds (checkout path) and IsScalapayAmountRangeValidValidator::resolveConfiguredAmounts (save path) call Assert::nullOrInteger($minAmount) on raw config values with no surrounding try/catch. I checked SyliusMoneyTransformer::reverseTransform — it correctly returns null for a blank field, so this isn't reachable through the admin form today. But:

  • SupportedMethodsProvider::provide() is called directly, unguarded, from ScalapayPaymentMethodsResolverDecorator::getSupportedMethods() (and the other gateway decorators) on every checkout page — an uncaught Webmozart\Assert\InvalidArgumentException here would break payment-method resolution for the whole checkout, not just hide Scalapay.
  • This is inconsistent with the "fail open on API errors" philosophy the PR explicitly documents for the validator's own API call — but there's no equivalent guard for locally-stored, potentially-stale/malformed config data (e.g. a future direct DB edit, import script, or admin API v2 write bypassing the form).

Cheap fix — treat assertion failure the same as "not configured":

try {
    Assert::nullOrInteger($minAmount);
    Assert::nullOrInteger($maxAmount);
} catch (InvalidArgumentException) {
    return [$authorizedRange['min_amount'], $authorizedRange['max_amount']];
}

[LOW] Fail-open on API error skips the "effective range inversion" check silently

In IsScalapayAmountRangeValidValidator, if only one side is configured (e.g. max_amount only) and the PayPlug API call fails during save, the inversion check in applyRangeViolations (which needs the live API value for the other side) never runs — the config saves as valid. Later, once the API is reachable again, SupportedMethodsProvider computes an inverted effective range and Scalapay silently disappears from checkout for every amount, with no signal to the merchant that anything is wrong. This matches existing fail-open conventions in the codebase per the PR checklist, so not a regression — just worth a follow-up ticket (e.g. log a warning when API-bound checks are skipped due to a fetch failure).

[NIT] Unrelated formatting change bundled in

src/PaymentProcessing/PaymentTransitionApplier.php diff is a pure re-indent of the existing \in_array(...) call, unrelated to the Scalapay feature. Worth dropping from this PR or splitting into its own style commit to keep the diff focused.

[NIT] Violation message amounts render without decimals

IsScalapayAmountRangeValidValidator.php(string) ($authorizedRange['min_amount'] / 100) turns 500 cents into "5" rather than "5.00", inconsistent with how amounts are normally shown elsewhere. Minor cosmetic polish.

Positive notes

  • Test coverage is thorough: IsScalapayAmountRangeValidValidatorTest hits min>max, both-sides-out-of-range, disabled method, API exceptions (Unauthorized, Connection), and the subtle "only one side configured, effective range inverts" case with a clear explanatory docblock.
  • SupportedMethodsProviderTest has a dedicated regression test (testProvide_merchantConfiguredAmountsIgnoredForNonEurCurrency) proving the EUR-denominated override doesn't leak into other checkout currencies — exactly the kind of edge case that's easy to miss.
  • The "merchant can only narrow, never widen beyond what PayPlug authorizes" design constraint is a sound business/security rule and is enforced against the live API rather than a stale cached value.

@adumont-payplug
adumont-payplug force-pushed the feature/PRE-3603_overload_min_max_scalapay branch from d8dcc57 to 760553c Compare September 8, 2026 10:03

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants