Skip to content

PRE-3677: PPRO display eligibility by amount and country - #337

Merged
hdelaforce-payplug merged 3 commits into
developfrom
feature/PRE-3677_apm-ppro-display-eligibility
Oct 27, 2026
Merged

hdelaforce-payplug merged 3 commits into
developfrom
feature/PRE-3677_apm-ppro-display-eligibility

Conversation

@hdelaforce-payplug

@hdelaforce-payplug hdelaforce-payplug commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

Bancontact, Wero and Scalapay (the PPRO payment methods present in the plugin) are now shown at checkout only for the cart amounts and billing countries PayPlug accepts for them, and the merchant can narrow both from the payment method page.

Scope

  • PPRO methods covered: Bancontact, Wero, Scalapay. The list is a single registry, Gateway\PproPaymentMethods::API_KEY_BY_FACTORY, so adding Satispay, MyBank, iDEAL or Bizum later is one entry, with no new eligibility code. These four are not in the plugin today and are out of scope.
  • Oney, Apple Pay, American Express and card are not PPRO and have no merchant min/max/country settings. They keep the PayPlug API country filter in SupportedMethodsProvider, which is now guarded and fails open: an account that cannot be read (expired credentials, network error) no longer breaks the checkout payment method list. The error is logged.

Stored configuration (per payment method)

  • Each PPRO gateway config stores its own payment_method block: name, enabled, allowed_countries and default_amount (PayPlug limits, in cents), configured_amount and configured_countries (merchant settings), live, refreshed_at.
  • The PayPlug limits are written:
    • after the OAuth connection (UnifiedAuthenticationController). A warning is shown if they cannot be fetched.
    • when the admin payment method edit page is displayed (PproConfigRefreshOnAdminDisplayListener)
    • after a save that switches the LIVE mode (PostSavePaymentMethodEventListener)
    • reactively, when PayPlug refuses a payment for its billing country or amount (CapturePaymentRequestHandler + PproRefusalClassifier). The customer is sent back to the order page with a message, and the payment request response data carries redirect_url and error for headless frontends. The flash message is only added for the Twig checkout, not for a JSON or XHR request.
    • by the one-shot bin/console payplug:ppro:refresh command, to run once after the upgrade
  • Throttling: the admin page and a refused payment use refreshIfStale(). Limits refreshed less than 5 minutes ago in the same environment are reused with no PayPlug call and no write, so a reloaded page, a browser prefetch or repeated refusals do not call PayPlug each time.
  • Concurrent saves: the refresher reads the gateway config again from the database after the PayPlug call (EntityManager::refresh(), which also runs Sylius's gateway config decryption) and replaces only the payment_method block.
  • A refresh never overwrites the merchant settings. It only clamps them to the new PayPlug limits.
  • Checkout: once limits are stored for the current environment, the checkout reads them only (StoredPproConfigReader → PproEligibilityChecker) and makes no API call for these methods. Otherwise (right after the upgrade, after a logout, when the stored block cannot be read, or when it was fetched in the other LIVE/TEST environment because the refresh after a switch failed), the method is filtered live from the PayPlug account, then by the merchant settings already saved (PproEligibilityChecker::isAllowedByMerchantSettings()), including the former Scalapay range. If the account cannot be read either, the method is shown and PayPlug decides at payment time.
  • An enabled: false reported by PayPlug hides the method for 24 hours after the last refresh. After that it is shown again and PayPlug decides, so a method PayPlug enables later is not hidden for good. The admin alert says so ("at the last refresh…").
  • When PayPlug no longer allows any of the countries the merchant selected, configured_countries becomes [] and the method is hidden everywhere until the merchant selects new countries (flash settings_adjusted, a permanent alert ui.ppro.no_country_left on the page, and the form requires a selection). It is never shown more widely than the merchant chose.
  • An empty amount range is logged once, when it is stored, not on every checkout render.
  • An empty merchant min/max follows the PayPlug limit. configured_countries null means every country PayPlug allows. At least one country must stay selected.
  • A PayPlug logout (GatewayConnectionRevoker) clears the stored PayPlug limits and keeps the merchant settings, so a reconnection to another account never filters with the previous account's limits.

Admin

  • Minimum/maximum are entered in euros and stored in cents. PayPlug limits are shown as placeholders.
  • Live validation by an admin Stimulus controller (ppro-eligibility: amount outside the PayPlug limits, min > max, no country). Server-side validation (PproConfigurationValidator) also refuses the save with an explicit message in en/fr/it, including negative amounts when no PayPlug limit is known.
  • Country picker: multi-select using Symfony UX Autocomplete, which ships with the Sylius admin (no new Composer dependency).
  • Illustrations: public/assets/admin/ppro/illustration-min-max.svg and illustration-pays.svg are the final assets.

BC / behaviour changes (detailed in UPGRADE.md and CHANGELOG.md)

  • Removed: ScalapayGatewayConfigurationTypeExtension, IsScalapayAmountRangeValid and its validator, the amount_range Twig hook and scalapay_amount_range.html.twig, and the ui.scalapay_gateway_config.* / payplug_scalapay.* translation keys. Replacements: PproEligibilityTypeExtension, Ppro\PproConfigurationValidator, hook ppro_eligibility, ui.ppro.* / ppro.*.
  • The Scalapay min/max override is carried over (PproMerchantSettings): until the PayPlug limits are first stored, the old min_amount/max_amount keys keep narrowing the checkout; on the first refresh they become the merchant range, clamped to the PayPlug limits if needed (the admin page then shows a notice), unless the merchant entered a range in the meantime. They are not read again afterwards.
  • ScalapayGatewayFactory::MIN_AMOUNT / MAX_AMOUNT are deprecated (read once for the carry-over).
  • New constructor arguments on SupportedMethodsProvider, CapturePaymentRequestHandler, UnifiedAuthenticationController and PostSavePaymentMethodEventListener. PproConfigurationRefresherInterface gains refreshIfStale().
  • An out-of-limits PPRO amount, a negative amount or an empty country selection is now refused at save, instead of saving the payment method disabled.
  • After the upgrade, run bin/console payplug:ppro:refresh once, and rebuild the admin assets. An application that lists the plugin's controllers in its own assets/admin/controllers.json must add ppro-eligibility there (snippet in UPGRADE.md).

UPC impact: none, no bump. PHP floor unchanged (8.2).

Motivation: Stop showing customers PPRO payment methods they cannot use for their cart amount or billing country, and let merchants control each method's exposure themselves.

Related issue(s): Closes PRE-3677 (epic PRE-3674)


Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) [ ]
  • ✨ New feature (non-breaking change that adds functionality) [x]
  • 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs) [x]: removed Scalapay form extension, constraint and template; new constructor arguments; new interface method
  • 🔧 Refactor (no functional changes, code improvement only) [ ]
  • 📦 Dependency update [ ]
  • 🔒 Security fix [ ]
  • 📝 Documentation update [x]: UPGRADE.md, CHANGELOG.md, README.md, doc/ppro_display_eligibility.md

Checklist

Code Quality

  • Code is linted and formatted: ECS 0 errors, PHPStan level max 0 errors, no new baseline entry. PHPMD: no new violation (only violations that already exist on develop).
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config): PayPlug limits come from the API and are stored in the gateway config. The PPRO method list, the 5-minute refresh interval and the 24-hour enabled TTL are class constants by design.

Testing

  • Unit tests added / updated: PHPUnit 1020 tests / 2285 assertions pass locally (PHP 8.3), also with LANG=fr_FR.UTF-8; includes a render test of the admin PPRO template. node --test 11/11 for the admin validation logic, now also run in CI (npm test step in the coverage job). lint:container OK.
  • New/changed code is covered by tests — SonarCloud Quality Gate (coverage on new code) passes on the sonarcloud CI job: green on 2fc5264, with the full CI (PHPUnit matrix PHP 8.2/8.4 × Sylius 2.0/2.1, Coverage, PHP Quality)

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate: account-read and refresh failures are logged on the payplug channel and fail open. A failed refresh after OAuth or on the admin page shows a warning. The refresh command exits non-zero if a connected method could not be refreshed.

Review

Review feedback from @adumont-payplug and @ilajili addressed in 6b81a97: stale limits after logout, stale enabled flag, throttled admin and refusal refreshes, concurrent saves, removed rollback, negative amounts, log flooding, flash wording, headless flash, CI JS tests, controllers.json snippet, and the upgrade window without stored limits (live fallback + Scalapay carry-over).

Review feedback from @jhoaraupp addressed in 2fc5264: limits from the other LIVE/TEST environment no longer used at checkout, merchant settings and the former Scalapay range applied in the live fallback, Scalapay range kept when the block is created before the first refresh, empty country intersection hides the method (product decision), locale-independent form tests, reworded 24 h alert. Also fixed: the disabled_by_payplug admin alert never showed (|default(true) turns false into true in Twig).

Not run / to verify before release

  • Behat not run (no test DB available locally).
  • Admin Stimulus controller and UX Autocomplete country picker not yet checked in a browser.
  • Refusal classification (details.amount / details.billing.country in the PayPlug error body) not checked against real sandbox responses: needs a Wero payment with a billing country outside BE/DE, and a Scalapay payment below its minimum, to add as test fixtures.
  • Merchant release notes: announce the payplug:ppro:refresh command and the admin asset rebuild.

Possible later point (no ticket)

  • A scheduled periodic refresh of the stored PayPlug limits (for example, making payplug:ppro:refresh schedulable).

🤖 Generated with Claude Code

@hdelaforce-payplug
hdelaforce-payplug force-pushed the feature/PRE-3677_apm-ppro-display-eligibility branch from 3261a37 to c409ca6 Compare October 22, 2026 12:40

@adumont-payplug adumont-payplug left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review of PRE-3677. Run on head c409ca6 in a separate worktree: full PHPUnit (968 tests) OK on PHP 8.2, PHPStan on src OK, ECS OK, node tests 11/11. Not run: PHPMD, Behat, admin UI in a browser, real PayPlug calls.

No blocking finding. Verdict: approve after the Medium findings on logout (stale limits), stale enabled, refusal classification and placeholder art. The admin GET refresh and the controllers.json note are strongly recommended before release.

Not anchored inline (file not in the diff or not line-specific): GatewayConnectionRevoker needs the payment_method block cleared (see the OAuth callback comment). The PR text says 950 tests; the head has 968.

Positives: fail-open design with malformed data logged and ignored, createForPaymentMethod() used everywhere, credentials flushed before the refresh, merchant settings only clamped never overwritten, single PPRO registry, strong failure-path tests, translations consistent across en/fr/it.

Comment thread src/Ppro/PproConfig.php Resolved
Comment thread src/Ppro/PproRefusalClassifier.php Resolved
Comment thread UPGRADE.md Resolved
Comment thread src/Ppro/PproConfigurationValidator.php Resolved
Comment thread src/Ppro/PproEligibilityChecker.php Resolved
Comment thread translations/flashes.en.yml Resolved
Comment thread src/Ppro/PproConfigurationRefresher.php Resolved
Comment thread src/Ppro/PproConfigurationRefresher.php Resolved
Comment thread src/Ppro/PproConfigurationRefresher.php Resolved
Comment thread UPGRADE.md Resolved
Comment thread assets/package.json Resolved

@jhoaraupp jhoaraupp 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.

Review sur 6b81a97, dans un worktree séparé avec un composer install du lock de la PR.

  • PHPUnit : 1001 tests, 2 échecs liés à la locale (voir le commentaire sur PproEligibilityTypeExtensionTest), verts avec LC_ALL=C
  • PHPStan : 0 erreur
  • ECS (ruleset/ecs.php) : 0 erreur
  • node --test : 11/11
  • Clés de traduction ppro.* identiques en en/fr/it

Non lancés : Behat, PHPMD, navigateur.

Rien de bloquant. Les correctifs de 6b81a97 répondent bien aux threads précédents ; je ne reprends pas la classification des refus, déjà ouverte. Nouveaux points : un Medium (limites de l'autre environnement LIVE/TEST au checkout) et quelques Low ci-dessous, plus une réponse dans le thread d'ilajili sur UPGRADE.md concernant le fallback live.

Comment thread src/Ppro/PproEligibilityChecker.php Resolved
Comment thread src/Ppro/PproConfigurationRefresher.php Resolved
Comment thread src/Ppro/PproSettingsAdjuster.php Resolved

@jhoaraupp jhoaraupp 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.

Re-review sur 2fc5264 : nos remarques précédentes sont toutes traitées (threads résolus). PHPUnit 1020 tests OK, y compris avec LANG=fr_FR.UTF-8, PHPStan 0 erreur, ECS 0 erreur, node --test 11/11. Une dernière remarque non bloquante ci-dessous. Reste ouvert avant merge, hors de nos remarques : la vérification de PproRefusalClassifier sur des réponses sandbox réelles (thread d'adumont) et le test dans un navigateur du formulaire admin.

Comment thread src/Ppro/PproConfigurationRefresher.php Resolved
@hdelaforce-payplug
hdelaforce-payplug merged commit ee074d2 into develop Oct 27, 2026
12 of 13 checks passed
@hdelaforce-payplug
hdelaforce-payplug deleted the feature/PRE-3677_apm-ppro-display-eligibility branch October 27, 2026 10:31
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.

4 participants