SL-386 Fix orders lost when paying with a different card brand than selected - #348
SL-386 Fix orders lost when paying with a different card brand than selected#348TLabutis wants to merge 3 commits into
Conversation
Work in progress, deliberately not ready for review. A shopper who selected one card brand and typed a card of another was left with an authorized payment and no order. On the return leg the module resolved the flow from the brand Saferpay reported instead of how the transaction was initialized, so a brand without saferpay_field.active never reached the Fields completion path and the checkout hung on Awaiting payment. Reloading then sent a second Authorize, which Saferpay refused with TRANSACTION_IN_WRONG_STATE, failing the order. PaymentTypeProvider::getForReturn() now decides from the field token and the saved-card selection, so any card typed into the Fields form completes. SaferPayTransactionProcessedGuard reads the authorized and captured flags off the saferpay_order row before the assert, and return.php takes the same lock key the notification uses, so a reload or a parallel notification cannot authorize twice. CardAliasRegistrationGuard centralises whether an alias is worth requesting, and AssertService::createObjectsFromAssertResponse() now takes that decision as a bool instead of re-deriving it from the posted card option. CardPaymentGroupingService::mergeCurrencies() limits the grouped Cards option to the currencies its own brands support. Deliberately left open, all four need closing before this becomes a PR: - SAFERPAY_GROUP_CARDS is flipped only in SaferPayConfig::getInstallDefaults(), which fresh installs read and nothing else does, so existing shops keep the per-brand list and the single Cards option does not actually ship. Needs an upgrade-script write of the configuration value. - Refusing a brand the merchant has not enabled is not implemented at all. InitializeRequest still strips PaymentMethods when a field token is present and inline-fields.js passes no brand restriction, so a disabled brand is accepted and authorized. - CardAliasRegistrationGuard::shouldRegister() returns !SAFERPAY_GROUP_CARDS, so defaulting grouping on silently disables saved cards. That contradicts keeping single-brand checkouts working as before and needs a product decision, not just code. - No test evidence yet on the reported scenario: no Visa-selected then Mastercard-paid run, no return-page reload test, no Payment Page regression pass.
The Fields SDK accepted any brand the shopper typed because InitializeRequest strips PaymentMethods whenever a field token is set and inline-fields.js passed no restriction, so a Mastercard could be entered into a form opened as Visa. The companion fix already resolves the return flow from the field token; this narrows what the form accepts in the first place so a brand the merchant did not enable is refused at entry. EnabledCardBrandsProvider filters SaferPayConfig::CARD_BRANDS through PaymentRestrictionValidation, PaymentFormAssetLoader turns that into the SDK's lowercase brand names per payment option, and inline-fields.js feeds them to SaferpayFields.init as paymentMethods. InitializeRequest carries the same list to the Initialize call. AMEX joins FIELD_SUPPORTED_PAYMENT_METHODS. The grouped "Cards" option is now the default presentation, enabled for existing shops by the 2.1.0 upgrade. Supporting it meant teaching the rest of the checkout that PAYMENT_CARDS stands for several brands: BasePaymentRestrictionValidation resolves it to whether any enabled brand passes country and currency checks, SaferPayCardAliasRepository looks saved cards up across every enabled brand and the template prefixes each with the brand it was stored under, and CardAliasRegistrationGuard stops suppressing alias registration under grouping. return.php only takes the processing lock when the request is not the Saferpay notification, so the hosted payment page leg is no longer blocked by its own return page. V PAY and myOne have no SDK brand name, so a shop that enables either leaves its Fields form unrestricted rather than declining a card Saferpay would accept. Naming the real brand on an order paid through the grouped option is not included.
VerificationRun on PrestaShop 8.2.3 against a live Saferpay TEST account (JSON API 1.50, business licence on). Five real authorizations, orders 15 to 18. Every result below was confirmed in the database and in How it behaves nowCheckout shows one Cards option instead of a row per brand. Selecting it renders the Saferpay Fields form inline, restricted to the brands the merchant enabled - the SDK receives the brand map built by A card outside the enabled brands is refused while it is being typed. A card inside them completes and creates the order, whichever brand it is. Results
Unit suite: Not covered by this run
One decision needed before mergeWith |
With cards grouped into one option, PaymentTypeProvider read SAFERPAY_GROUP_CARDS before the per-brand saferpay_field toggles, so the "Saferpay Fields" column in the back office did nothing, and a shop without a Fields access token got an unpayable Cards option because only inline-fields.js checked the token. Official Saferpay plugins on other platforms use one integration-mode choice plus an automatic Payment Page fallback, and this adopts that shape. PaymentTypeProvider now takes the Configuration adapter and resolves the flow from business licence, SAFERPAY_USE_FIELDS and a non-empty FIELDS_ACCESS_TOKEN, falling back to BASIC otherwise; getForReturn() keeps keying off the field token, so both legs agree. PaymentFormAssetLoader skips the Fields SDK when the setting is off. The settings page replaces the per-method column with a single switch in Card Display plus an amber warning when the token is missing. The 2.1.0 upgrade derives the setting from the old toggles (off only when rows exist and none is active) and generates missing access tokens from stored credentials, logging failures without failing the upgrade. The saferpay_field table, entity and creator stay untouched; the refresh service still writes brand snapshots there.
Problem
Saferpay Fields let the shopper type any card into a form that was opened for one specific brand. When the brands differed,
return.phpresolved the checkout flow from the brand Saferpay reported back rather than from how the payment was started, so the flow fell through to the redirect branch andcreateAndValidateOrder()was never called. The money was authorized at Saferpay,saferpay_order.authorizedstayed 0 and the shopper sat on the waiting screen forever.Reloading that page made it worse:
postProcessasserted again, and under a business licence the assert postsPayment/v1/Transaction/Authorize, which is state-changing. Saferpay answeredTRANSACTION_IN_WRONG_STATEand the order was marked failed.What changed
The flow is resolved from how the payment was initialized.
PaymentTypeProvider::getForReturn()keys off the field token and the saved-card flag instead of the returned brand. The real brand is still recorded as the order's payment label, it just no longer decides the flow.The return page cannot process the same payment twice.
SaferPayTransactionProcessedGuardshort-circuitspostProcesswhensaferpay_orderis already authorized or captured, and the page redirects straight to the success controller instead of asserting again. The lock is skipped when the request is the Saferpay notification, so the hosted payment page leg is not blocked by its own return page.The Fields form only accepts brands the merchant enabled.
EnabledCardBrandsProviderfilters the card brands through the existing payment restrictions,PaymentFormAssetLoadermaps them to the SDK's lowercase names per payment option, andinline-fields.jspasses them toSaferpayFields.initaspaymentMethods.InitializeRequestsends the same list. A mismatched card is now refused at entry rather than at the end.Checkout shows a single "Cards" option. Enabled by default for existing shops through the 2.1.0 upgrade. Supporting it meant teaching the rest of the checkout that
PAYMENT_CARDSstands for several brands: restrictions resolve to whether any enabled brand passes country and currency checks, saved cards are looked up across every enabled brand and labelled with the brand they were stored under, and alias registration is no longer suppressed under grouping.One "Use Saferpay Fields" setting replaces the per-brand toggles. With cards grouped, the per-brand "Saferpay Fields" column did nothing (grouping was read before the toggles), and a shop without a Fields access token got an unpayable Cards option because only the frontend JS checked the token. Following the official Saferpay plugins on Magento, Shopware and WooCommerce, card form rendering is now a single switch in Payment Processing, and
PaymentTypeProvider::get()falls back to the Payment Page whenever the business licence, the setting or the access token is missing, on both the checkout and return legs. The back office shows a warning when Fields is on but the token is empty. The 2.1.0 upgrade derives the setting from the old toggles (Payment Page only when toggles exist and all are off) and generates missing access tokens from the stored API credentials, logging failures without failing the upgrade.American Express joins
FIELD_SUPPORTED_PAYMENT_METHODS.Known limits
CARDas its payment label, not the brand actually used. Quoted separately.saferpay_fieldtable, entity and creator stay in place; the payment methods refresh service still writes brand snapshots there. Removing the schema is out of scope.Tests
64/64unit tests pass. New coverage forPaymentTypeProvider(full decision tree incl. token fallback and test-mode suffix),EnabledCardBrandsProvider,CardAliasRegistrationGuardand the grouped-cards restriction path.Manual verification on PS 8.2.3 against a live Saferpay TEST account: Fields mode (inline form, order authorized), switch off (checkout option becomes
saferpayPaymentType=basic, redirect to the hosted payment page, order authorized), token-empty fallback (no dead form, redirect works, back office warning shown), and the upgrade path (setting derived from toggles, real token generated and idempotent on rerun). Earlier full verification of the card brand fix is in the PR comment below.Changelog
Release notes for the fix and the Use Saferpay Fields setting added under the
2.1.0release section.