Repository navigation
PRE-3677: PPRO display eligibility by amount and country - #337
hdelaforce-payplug wants to merge 3 commits into
Conversation
0c8f072 to
3261a37
Compare
3261a37 to
c409ca6
Compare
adumont-payplug
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| public function isEligible(int $amount, ?string $billingCountry): bool | ||
| { | ||
| if (!$this->enabled || $this->hasEmptyAmountRange()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') }}"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| | `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` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| return true; | ||
| } | ||
|
|
||
| if ($config->hasEmptyAmountRange()) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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." |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Done in 6b81a97: refresh_failed now says the limits could not be refreshed and points to the payplug log channel, in en/fr/it.
| try { | ||
| $this->entityManager->flush(); | ||
| } catch (\Throwable $exception) { | ||
| $gatewayConfig->setConfig($previousConfig); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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é.
There was a problem hiding this comment.
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).
| try { | ||
| $this->entityManager->flush(); | ||
| } catch (\Throwable $exception) { | ||
| $gatewayConfig->setConfig($previousConfig); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| amount. To capture them right after the upgrade, run once: | ||
|
|
||
| ```bash | ||
| bin/console payplug:ppro:refresh |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| ], | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1" | ||
| "test": "node --test 'admin/controllers/*.test.js'" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 avecLC_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.
| ]); | ||
| } | ||
|
|
||
| return null === $config?->refreshedAt ? null : $config; |
There was a problem hiding this comment.
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']); |
There was a problem hiding this comment.
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().
| private function readPrevious(GatewayConfigInterface $gatewayConfig, array $config): ?PproConfig | ||
| { | ||
| $stored = $config[PproConfig::CONFIG_KEY] ?? null; | ||
| if (null !== $stored || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) { |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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… »).
|
|
||
| $intersection = array_values(array_intersect($configured, $allowed)); | ||
|
|
||
| return [] === $intersection ? null : $intersection; |
There was a problem hiding this comment.
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.
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
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.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)
payment_methodblock:name,enabled,allowed_countriesanddefault_amount(PayPlug limits, in cents),configured_amountandconfigured_countries(merchant settings),live,refreshed_at.UnifiedAuthenticationController). A warning is shown if they cannot be fetched.PproConfigRefreshOnAdminDisplayListener)PostSavePaymentMethodEventListener)CapturePaymentRequestHandler+PproRefusalClassifier). The customer is sent back to the order page with a message, and the payment request response data carriesredirect_urlanderrorfor headless frontends. The flash message is only added for the Twig checkout, not for a JSON or XHR request.bin/console payplug:ppro:refreshcommand, to run once after the upgraderefreshIfStale(). 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.EntityManager::refresh(), which also runs Sylius's gateway config decryption) and replaces only thepayment_methodblock.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.enabled: falsereported 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.configured_countriesnull means every country PayPlug allows. At least one country must stay selected.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
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.public/assets/admin/ppro/illustration-min-max.svgandillustration-pays.svgare the final assets.BC / behaviour changes (detailed in
UPGRADE.mdandCHANGELOG.md)ScalapayGatewayConfigurationTypeExtension,IsScalapayAmountRangeValidand its validator, theamount_rangeTwig hook andscalapay_amount_range.html.twig, and theui.scalapay_gateway_config.*/payplug_scalapay.*translation keys. Replacements:PproEligibilityTypeExtension,Ppro\PproConfigurationValidator, hookppro_eligibility,ui.ppro.*/ppro.*.min_amount/max_amountkeys 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_AMOUNTare deprecated (read once for the carry-over).SupportedMethodsProvider,CapturePaymentRequestHandler,UnifiedAuthenticationControllerandPostSavePaymentMethodEventListener.PproConfigurationRefresherInterfacegainsrefreshIfStale().bin/console payplug:ppro:refreshonce, and rebuild the admin assets. An application that lists the plugin's controllers in its ownassets/admin/controllers.jsonmust addppro-eligibilitythere (snippet inUPGRADE.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
UPGRADE.md,CHANGELOG.md,README.md,doc/ppro_display_eligibility.mdChecklist
Code Quality
develop).enabledTTL are class constants by design.Testing
node --test11/11 for the admin validation logic, now also run in CI (npm teststep in the coverage job).lint:containerOK.sonarcloudCI job: green on6b81a97, with the full CI (PHPUnit matrix PHP 8.2/8.4 × Sylius 2.0/2.1, Coverage, PHP Quality)Security & Ops
payplugchannel 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, staleenabledflag, throttled admin and refusal refreshes, concurrent saves, removed rollback, negative amounts, log flooding, flash wording, headless flash, CI JS tests,controllers.jsonsnippet, and the upgrade window without stored limits (live fallback + Scalapay carry-over).Not run / to verify before release
details.amount/details.billing.countryin 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.payplug:ppro:refreshcommand and the admin asset rebuild.Possible later point (no ticket)
payplug:ppro:refreshschedulable).🤖 Generated with Claude Code