Skip to content

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

Open
hdelaforce-payplug wants to merge 3 commits into
developfrom
feature/PRE-3677_apm-ppro-display-eligibility
Open

hdelaforce-payplug wants to merge 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, the checkout reads them only (StoredPproConfigReader → PproEligibilityChecker) and makes no API call for these methods. Until they are stored (right after the upgrade, or after a logout), or if the stored block cannot be read, the method is filtered live from the PayPlug account as before this PR. 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.
  • 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: on the first refresh, the old min_amount/max_amount keys become the merchant range, clamped to the PayPlug limits if needed (the admin page then shows a notice). 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 1001 tests / 2231 assertions pass locally (PHP 8.3). 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 6b81a97, 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).

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.

$this->entityManager->flush();
// After the credentials are persisted, so a PayPlug outage here cannot lose them. A no-op
// for non-PPRO gateways; never throws. PaymentMethodValidator::process() flushes it.
$this->pproConfigurationRefresher->refresh($paymentMethod, false);

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.

Medium: stale account-bound limits survive logout and a failed refresh.

GatewayConnectionRevoker::CONNECTION_KEYS (src/Auth/GatewayConnectionRevoker.php:43, not touched by this PR) clears only live_client, test_client and account_email. The stored payment_method block is account-bound too, so after a PRE-3632 logout it stays in place. If the merchant then reconnects to a different PayPlug account and this refresh() fails (or the new account has no entry for the method), the old account's limits keep filtering the checkout, and the result is ignored here so nothing tells the merchant.

Fix: clear the PayPlug-derived keys (name, enabled, allowed_countries, default_amount, live, refreshed_at) in the revoker, and flash a warning when the refresh after OAuth returns refreshed === false. A revoker test for the payment_method block would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: GatewayConnectionRevoker now clears the PayPlug limits of the PPRO block (name, enabled, allowed_countries, default_amount, live, refreshed_at) and keeps configured_amount/configured_countries, covered by GatewayConnectionRevokerTest. A failed refresh after the OAuth callback now shows the refresh_failed warning. Without stored limits the checkout also falls back to the live account check, so the previous account's limits can no longer filter it.

Comment thread src/Ppro/PproConfig.php Outdated

public function isEligible(int $amount, ?string $billingCountry): bool
{
if (!$this->enabled || $this->hasEmptyAmountRange()) {

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.

Medium: a stale enabled: false hides the method until an admin opens the edit page.

Limits only move on the listed events (no schedule). If PayPlug later activates Wero/Scalapay on the account, shoppers never see it, so this is fail-closed on stale data while the PR and docs describe the checkout as fail-open.

Fix: only hide on enabled === false when refreshed_at is recent, or ship the scheduled refresh (the "possible later point") before release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: enabled: false only hides the method when refreshed_at is less than 24 hours old (PproEligibilityChecker::DISABLED_FLAG_TTL). Past that, the method is shown and PayPlug decides at payment time; the stored amount and country limits still apply.

return;
}

$result = $this->pproConfigurationRefresher->refresh($paymentMethod, true);

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.

Medium: every admin view of the edit page makes a PayPlug call and a DB write.

refresh() uses getAccount(true) (cache bypass) and flushes each time, so a slow PayPlug API slows the admin page and refreshed_at is rewritten on every view. This is the open question in the PR text; I would not accept it as is.

Fix: skip when refreshed_at is under a few minutes old, and only flush when the stored block actually changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: the listener now calls refreshIfStale(), which skips limits refreshed less than 5 minutes ago in the same environment (no API call, no write). I kept the write when a refresh does run: refreshed_at is both the throttle state and the anchor of the 24 h enabled TTL, so skipping it when nothing else changed would make every view call PayPlug again. That is at most one write per 5 minutes per method.


<div class="row g-4 align-items-start mb-4">
<div class="col-12 col-md-4">
<img class="img-fluid" alt="" src="{{ asset('bundles/payplugsyliuspayplugplugin/assets/admin/ppro/illustration-min-max.svg') }}">

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.

Medium: placeholder art. The PR says these SVGs must be replaced by the real illustration-min-max.jpg / illustration-pays.png, but the template (here and on line 59) references .svg. Replace the assets and update both asset() calls before merge.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The SVGs are the final assets, not placeholders: the template is unchanged, and the PR description no longer lists them as to be replaced.

return null;
}

if (\array_key_exists('amount', $details)) {

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.

Medium: classification not verified against real PayPlug errors. Any error body with a details.amount key is treated as a limits refusal (same for details.billing.country). A false positive triggers a limits refresh and tells the customer the method is unavailable for an unrelated error. Please check against sandbox responses (Wero with a billing country outside BE/DE, Scalapay below its minimum) and add them as test fixtures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, still open: I need real sandbox bodies (Wero with a billing country outside BE/DE, Scalapay below its minimum) to check the details shape and add them as PproRefusalClassifierTest fixtures. Listed under "to verify before release" in the PR description.

Comment thread UPGRADE.md
| `CapturePaymentRequestHandler::__construct()` | now also takes the PPRO refresher, the refusal classifier, `sylius_shop.provider.order_pay.final_url` and the request stack |
| `UnifiedAuthenticationController::__construct()`, `PostSavePaymentMethodEventListener::__construct()` | now also take a `PproConfigurationRefresherInterface` |

The admin form uses a new Stimulus controller, `@payplug/sylius-payplug-plugin/ppro-eligibility`

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.

Medium: host registration step missing. "Rebuild the admin assets" is not enough if the host app enables plugin controllers through its own admin controllers.json: the ppro-eligibility controller would never connect and the live validation silently disappears (server-side validation still protects the save). Please add the exact controllers.json snippet here and confirm it connects in a real admin; the PR says it has not been checked in a browser.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: UPGRADE.md now has the assets/admin/controllers.json snippet for applications that list the plugin's controllers themselves. The browser check is still pending and stays listed in the PR description.

$defaultMin = $stored?->defaultMin;
$defaultMax = $stored?->defaultMax;

if (null !== $min && null !== $defaultMin && $min < $defaultMin) {

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.

Low: negative amounts pass server validation when limits are unknown. With $stored null or without default_amount, violations(null, -500, null, null) returns no errors (read from the code, not executed). A stored negative then makes StoredPproConfigReader::readAmount() throw, so the whole block is treated as malformed and the method fails open. The JS regex blocks it client-side only.

Fix: reject < 0 here (or add a PositiveOrZero constraint on the money fields) and add a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: PproConfigurationValidator refuses a negative min or max with ppro.invalid_amount whatever the stored limits, covered for no stored block, no PayPlug amount, and stored limits.

Comment thread src/Ppro/PproEligibilityChecker.php Outdated
return true;
}

if ($config->hasEmptyAmountRange()) {

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.

Low: this warning is logged on every checkout render while the range is empty. Log it once at refresh time instead, so it does not flood the payplug channel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: the empty-range warning is logged once by PproConfigurationRefresher when it stores the range, and no longer by the checker on each checkout render.

Comment thread translations/flashes.en.yml Outdated
api_error: The operation could not be performed because of a PayPlug error. The payment was not changed; try again in a moment.
unexpected_error: The operation may have been performed by PayPlug, but it could not be recorded here. Check the payment in your PayPlug portal before trying again, to avoid capturing or cancelling twice.
ppro:
refresh_failed: "PayPlug could not be reached: the amount and country limits shown are the last ones saved."

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.

Low: wording. refresh_failed says PayPlug "could not be reached", but the refresher also fails when payment_methods[<key>] is missing from the account payload or the write fails. Consider neutral wording (same for fr/it).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: refresh_failed now says the limits could not be refreshed and points to the payplug log channel, in en/fr/it.

Comment thread src/Ppro/PproConfigurationRefresher.php Outdated
try {
$this->entityManager->flush();
} catch (\Throwable $exception) {
$gatewayConfig->setConfig($previousConfig);

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.

Low: this rollback cannot help after a failed flush. Doctrine closes the EntityManager on a flush exception, so restoring the config in memory does not make later operations in the request safe. Worth a comment, or drop the rollback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6b81a97: rollback removed, with a comment that a failed flush closes the entity manager. The 5-minute throttle also makes that flush rare on the admin page.

$previous = PproConfig::fromArray($config[PproConfig::CONFIG_KEY] ?? null);
$refreshed = PproConfig::fromApiLimits($this->fetchApiLimits($paymentMethod, $apiKey, $config), $previous);

$config[PproConfig::CONFIG_KEY] = $refreshed->toArray();

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.

Medium : un enregistrement concurrent peut être écrasé.

$config est lu ligne 41 avant l'appel réseau getAccount(true), puis le tableau entier est réécrit ici. Pendant cet appel, une sauvegarde admin simultanée (identifiants, canaux, réglages) est perdue au flush.

Fix : relire $gatewayConfig->getConfig() juste avant l'écriture et ne remplacer que la clé payment_method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : après l'appel getAccount(true), le refresher relit la config en base (EntityManager::refresh(), qui repasse aussi par le déchiffrement Sylius de la config) puis ne remplace que la clé payment_method. Les réglages marchand sont lus sur cette version relue. Couvert par testReReadsTheGatewayConfigBeforeWriting.

'refusal' => $refusal,
]);
// Flushed by the doctrine_transaction middleware of the payment request command bus.
$this->pproConfigurationRefresher->refresh($method, 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.

Medium : appel API sans cache déclenché par chaque refus client.

Chaque refus PPRO lance refresh(), donc un getAccount(true) synchrone dans le parcours de paiement. Tant que les limites stockées sont périmées, chaque commande refusée (ou un robot) génère un appel à l'API PayPlug.

Fix : ne rafraîchir que si refreshed_at a plus de quelques minutes (même garde que celle demandée pour l'affichage admin).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : le handler appelle refreshIfStale(), même garde de 5 minutes que l'affichage admin. Le log de refus indique limits_refreshed pour savoir si un appel a eu lieu.

return;
}

$result = $this->pproConfigurationRefresher->refresh($paymentMethod, true);

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.

Medium : écriture en base et appel API sur une requête GET, sans CSRF.

Un lien ou une pré-récupération du navigateur peut forcer le rafraîchissement. L'impact est limité (limites rafraîchies), mais cela s'ajoute à la remarque sur la fréquence d'appel.

Fix : garde par date de refreshed_at et aucun flush si le bloc stocké n'a pas changé.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : garde de 5 minutes via refreshIfStale(), donc un lien ou une pré-récupération ne déclenche ni appel API ni écriture si les limites sont récentes. L'écriture reste nécessaire quand l'actualisation a lieu, car refreshed_at sert de garde (au plus une écriture toutes les 5 minutes par moyen de paiement).

Comment thread src/Ppro/PproConfigurationRefresher.php Outdated
try {
$this->entityManager->flush();
} catch (\Throwable $exception) {
$gatewayConfig->setConfig($previousConfig);

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.

Medium : un flush raté ferme l'EntityManager pour toute la requête admin.

Dans le listener d'affichage admin, refresh() est appelé juste avant le contrôleur : après un flush en échec, la page d'édition peut échouer sans lien visible avec la cause. La restauration en mémoire ne répare rien.

Fix : retirer cette restauration, et ne pas flusher quand la config n'a pas changé (réduit aussi la probabilité de l'échec).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : restauration en mémoire retirée (avec un commentaire). Le flush reste nécessaire quand l'actualisation a lieu (voir le fil sur refreshed_at), mais la garde de 5 minutes le rend rare sur la page admin.

Comment thread UPGRADE.md
amount. To capture them right after the upgrade, run once:

```bash
bin/console payplug:ppro:refresh

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.

Low : plus de filtrage tant que la commande n'est pas lancée.

Avant payplug:ppro:refresh, Bancontact n'est plus filtré par montant ni pays (il l'était en direct par l'API) et les montants Scalapay configurés sont perdus. Le texte le dit, mais à annoncer clairement aux marchands (notes de version / support), ou à lancer la commande automatiquement à la mise à jour.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : tant qu'aucune limite n'est stockée, les méthodes PPRO sont filtrées en direct depuis le compte PayPlug, comme avant la PR, et la plage Scalapay existante est reprise à la première actualisation (ramenée dans les bornes PayPlug si besoin). UPGRADE.md est à jour ; reste à l'annoncer dans les notes de version marchands, listé dans la description de la PR.

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.

Le fallback n'est pas tout à fait « comme avant la PR ». Sur develop, SupportedMethodsProvider::resolveAmountBounds() appliquait au checkout la plage min_amount/max_amount configurée pour Scalapay. Désormais, tant qu'aucune limite n'est stockée, isWithinAccountLimits() (SupportedMethodsProvider.php:72) ne lit que les limites PayPlug. Entre le déploiement et payplug:ppro:refresh, ou indéfiniment si les refreshes échouent, Scalapay est donc proposé pour des paniers que le marchand avait exclus. La plage n'est reprise qu'au premier refresh réussi.

Même chose après un logout : le revoker retire refreshed_at et garde configured_amount/configured_countries, mais le fallback ignore ces réglages jusqu'au refresh suivant.

Fix proposé : dans le fallback, appliquer aussi les réglages marchand stockés, ou à défaut les anciennes clés Scalapay. Par exemple, construire un PproConfig à partir des limites live du compte et des réglages stockés ou anciens, puis appeler isEligible() dessus.

Comment thread assets/package.json Outdated
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test 'admin/controllers/*.test.js'"

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.

Low : les tests JS ne tournent pas en CI.

ci.yml n'est pas modifié et je n'y vois pas d'étape npm test. Les 11 tests Node ne protègent donc pas des régressions. Ajouter une étape dans la CI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : étape npm test (working-directory assets) ajoutée au job coverage. Le script utilisait un glob entre quotes que Node 20 (version de la CI) ne résout pas ; il est maintenant développé par le shell.

$session = null !== $request && $request->hasSession() ? $request->getSession() : null;

if ($session instanceof FlashBagAwareSessionInterface) {
$session->getFlashBag()->add('error', self::PPRO_NOT_AVAILABLE_MESSAGE);

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.

Low : flash écrit aussi pour un front découplé.

redirect_url et error sont déjà renvoyés dans la réponse de la demande de paiement. Le flash restera en session et s'affichera plus tard sur un autre écran. Ne l'ajouter que si la requête vient du parcours Twig.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Corrigé dans 6b81a97 : le flash n'est ajouté que pour le parcours Twig (format de requête html, hors XHR). Une requête JSON ou XHR ne reçoit que redirect_url et error dans les données de réponse. Couvert par testNoFlashForAHeadlessRequest.

@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 Outdated
]);
}

return null === $config?->refreshedAt ? null : $config;

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.

Medium : le checkout peut filtrer avec les limites de l'autre environnement (LIVE/TEST).

Le bloc stocké garde live, et refreshIfStale() le compare au mode courant, mais ici on exige seulement refreshedAt. Scénario : le marchand passe Wero de TEST à LIVE, puis le refresh() de PostSavePaymentMethodEventListener:119 échoue (identifiants LIVE pas encore présents, PayPlug indisponible). L'échec est seulement loggé, et le checkout continue de filtrer avec les pays, les montants et le flag enabled du compte TEST jusqu'à la prochaine ouverture de la page admin. Les comptes TEST et LIVE diffèrent souvent sur enabled et sur les pays autorisés.

Fix : traiter un bloc de l'autre environnement comme « rien de stocké », pour retomber sur le contrôle live :

$live = true === ($gatewayConfig?->getConfig()['live'] ?? false);

return null === $config?->refreshedAt || $config->live !== $live ? null : $config;

À couvrir par un test : bloc stocké avec live: false, gateway en live: true → null.


$view = $form->createView();

self::assertSame('12.50', $view['ppro_min_amount']->vars['value']);

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.

Low : test qui dépend de la locale de la machine.

formatEuros() lit \Locale::getDefault(). Avec LANG=fr_FR.UTF-8, ce test et testAmountFieldsShowTheApiLimitsAsPlaceholders (l. 144) échouent : ils obtiennent 12,50 et 0,30 au lieu de 12.50 et 0.30. Ils passent avec LC_ALL=C, et la CI est verte uniquement parce que ses runners sont en C/en.

Fix : \Locale::setDefault('en') dans setUp(), et restaurer la valeur précédente dans tearDown().

Comment thread src/Ppro/PproConfigurationRefresher.php Outdated
private function readPrevious(GatewayConfigInterface $gatewayConfig, array $config): ?PproConfig
{
$stored = $config[PproConfig::CONFIG_KEY] ?? null;
if (null !== $stored || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) {

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.

Low : la plage Scalapay existante peut être perdue silencieusement.

La reprise ne se fait que si aucun bloc payment_method n'existe. Si le marchand saisit un montant ou des pays PPRO et sauve avant le premier refresh réussi (par exemple quand le refresh de la page admin a échoué), le form crée le bloc (configured_amount/configured_countries, sans refreshed_at). Les anciennes clés min_amount/max_amount ne sont alors plus jamais lues. Le marchand ne peut pas s'en rendre compte, puisque le form ne les affiche pas.

Fix : reprendre les anciennes clés tant que le bloc stocké n'a pas de refreshed_at et pas de configured_amount, ou pré-remplir les champs du form à partir d'elles.

{{ sylius_test_html_attribute('payplug-ppro-eligibility') }}
>
{% if ppro is iterable and ppro['enabled']|default(true) is same as(false) %}
<div class="alert alert-warning" role="alert">{{ 'payplug_sylius_payplug_plugin.ui.ppro.disabled_by_payplug'|trans }}</div>

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.

Low : le message « masqué au checkout » devient faux après 24 h.

Depuis DISABLED_FLAG_TTL, enabled: false ne masque la méthode que si refreshed_at date de moins de 24 h. Au-delà, la méthode est de nouveau affichée au checkout, alors que cette alerte dit toujours qu'elle est masquée.

Fix : n'afficher l'alerte que pendant la fenêtre de 24 h (exposer l'info depuis le checker ou une extension Twig), ou reformuler (« PayPlug indiquait lors de la dernière actualisation que… »).

Comment thread src/Ppro/PproSettingsAdjuster.php Outdated

$intersection = array_values(array_intersect($configured, $allowed));

return [] === $intersection ? null : $intersection;

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.

Low (décision produit) : une intersection vide élargit la sélection à tous les pays PayPlug.

Exemple : le marchand a restreint à FR, puis PayPlug n'autorise plus que BE et DE. L'intersection est vide, configured_countries passe à null, et la méthode est affichée en BE et DE, donc plus largement que le choix du marchand. Le seul signal est le flash settings_adjusted, à sa prochaine visite admin. C'est documenté, mais dans ce cas masquer la méthode (ou laisser une liste vide traitée comme « aucun pays ») semble plus sûr qu'élargir. À trancher côté produit.

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