Skip to content

Feature/pre 3550 - #308

Open
jhoaraupp wants to merge 22 commits into
developfrom
feature/PRE-3550
Open

Feature/pre 3550#308
jhoaraupp wants to merge 22 commits into
developfrom
feature/PRE-3550

Conversation

@jhoaraupp

Copy link
Copy Markdown
Contributor

Description

Adds Unified Hosted Fields (UHF) card tokenization at Sylius checkout: a new, distinct payment method (built on PRE-3553's payplug_uhf gateway factory) coexisting with the existing "Integrated Payment" method. The customer enters card details into hosted iframe fields provided by PayPlug/Dalenys' Hosted Fields SDK; the plugin tokenizes client-side (createToken()), then relays the resulting hfToken + selectedBrand (+ save-card opt-in) toward payment processing through a small interface with a temporary no-op stub — swappable later without touching the frontend or the subscriber, once PRE-3551 lands the real UPC-side processing.

Shop side: hosted fields (brand, card, expiry, cvv) rendered as their own checkout choice, styled to match Integrated Payment, with client-side tokenization error handling and an unsupported-card-brand fallback message. Admin side: oneClick (save-card) support added to UhfGatewayConfigurationType, with the same account-permission check payplug itself uses when oneClick is enabled.

Note for reviewers: this branch started as a flag on the payplug gateway, then was migrated onto payplug_uhf mid-way once it became clear PRE-3550 was meant to build on top of PRE-3553 rather than invent a parallel config — see the later commits for that migration and its own review pass. The Hosted Fields SDK is currently loaded from a staging Dalenys CDN URL (no production URL identified yet) — do not enable this payment method on a production channel until that's confirmed.

Motivation:

PRE-3550 — deliver card tokenization at Sylius checkout via PayPlug's Hosted Fields SDK, decoupled from PRE-3551's real backend processing so it can ship and be tested independently.

Related issue(s): PRE-3550 (continuation of PRE-3553)


Type of Change

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

Checklist

Code Quality

  • Code is linted and formatted
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config) — the Hosted Fields SDK URL is hardcoded (see note above); everything else is config-driven

Testing

  • Unit tests added / updated
  • New/changed code is covered by tests — SonarCloud Quality Gate (coverage on new code) passes on the sonarcloud CI job

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

jhoaraupp and others added 22 commits August 7, 2026 14:31
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…same payment method

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… pending PRE-3551

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A merchant must be able to offer Integrated Payment and Hosted Fields side by
side, which requires two PaymentMethod entities sharing factoryName=payplug.
canBeCreated() now bypasses the duplicate check for that factory only; every
other PayPlug-family factory (Oney, Bancontact, Amex, Apple Pay, Scalapay,
Wero) keeps the one-payment-method-per-factory rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
handleHostedFieldsToken() used to process any request carrying a non-empty
hostedfields_token, so a crafted POST could complete checkout through that path
for any payment method. It now verifies the payment method's gateway config
actually has hostedFields=true before delegating to the processor.

Also adds coverage for alterRequestConfigurationForInlineCardCapture().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ected

Three related frontend fixes:
- the wrapping div now carries data-payment-inline-submit="true", so the generic
  checkout "next step" button is disabled while Hosted Fields is selected
  (clicking it submitted an empty hostedfields_token, bypassing tokenization);
- connect() no longer calls dalenys.hostedFields(...).load() unconditionally.
  The cross-origin iframes were mounted into a container that is still hidden at
  connect time (see shop/select_payment/choice.html.twig). The controller now
  mirrors integrated-payment: container target, idempotent openFields()/
  closeFields() and handleShow()/handleHide(), loading on selection only;
- the saved-card radios now pipe handleHide/handleShow to the hosted-fields
  controller alongside integrated-payment, so picking a saved card hides the
  Hosted Fields form when both oneClick and hostedFields are enabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…scenario

The Hosted Fields shop template calls is_payplug_test_mode_enabled(), which
performs a real PayPlug account lookup; the fixture's secretKey 'test' is not a
valid credential. The scenario now uses the existing "This secret Key is valid"
step, whose context had to be registered in the shop suite that runs it.

Also bundles three small fixes:
- HostedFieldsPaymentProcessorInterface is now an alias instead of a second
  definition, so it resolves to the auto-registered service and keeps its
  @monolog.logger.payplug binding;
- fixes the "Paiement Integré" -> "Paiement Intégré" typo in validators.fr.yml;
- refreshes the two stale constraint-count docblocks in PaymentMethodValidatorTest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Hosted Fields payment carries a Dalenys hfToken and no PayPlug payment_id
until PRE-3551 lands, so routing it to sylius_shop_order_pay made StatusAction
markNew(), Payum rebuild the details through Convert and CaptureAction issue a
real createPayment() API call - which the temporary stub must never cause, even
indirectly.

The redirect override cannot simply be dropped: Sylius's CheckoutRedirectListener
listens to the same sylius.order.post_payment event and bails out only when
_sylius['redirect'] is set. Without it, it resolves a route for the `completed`
checkout state, which has no entry in sylius_shop.checkout_resolver.route_map,
and the request dies with a RouteNotFoundException.

Hosted Fields is therefore redirected to sylius_shop_order_show instead (same
token-based, guest-accessible route, no Payum involved). Integrated Payment keeps
sylius_shop_order_pay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The redirect ternary picked sylius_shop_order_pay whenever hasToken() was true,
but handle() checks hasHostedFieldsToken() first. A request carrying both token
fields was therefore processed as Hosted Fields - never writing a payment_id -
while still being redirected to sylius_shop_order_pay, reopening the
StatusAction -> Convert -> CaptureAction::createPayment() chain this redirect
exists to prevent.

The ternary now checks hasHostedFieldsToken() first, mirroring handle(). Tests
pin the invariant on both sides so the two cannot drift apart again.

Also uses self::UPDATE_ORDER_PAYMENT_ROUTE instead of repeating its literal value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t renders

The old hostedFields flag branch in _payplug.html.twig included the card-iframe
markup, but that flag was removed when Hosted Fields moved to its own
payplug_uhf gateway factory, leaving the shop checkout with no include site
for templates/shop/hosted_fields/index.html.twig at all. Add the missing
#payplug_uhf twig-hook entry (following the same pattern as the other
factory-keyed hooks in shop.yaml) with a dedicated
_payplug_uhf.html.twig partial, and drop the now-dead hostedFields branch
from _payplug.html.twig, which is exclusively for the payplug (Integrated
Payment) factory.
… script

Twig's HTML autoescaping does not escape a bare apostrophe, so any of the
translated/dynamic values interpolated into the single-quoted JS string
literals in the hosted_fields inline <script> block (companyId, payment
method code, translated error messages) would silently break the script and
prevent the fields from mounting if the value ever contained one. Apply the
|e('js') filter to those four values.
The exemption let a merchant create two PaymentMethod entities on the
payplug factory, to support the old flag-based Integrated Payment /
Hosted Fields split. That design is gone: Hosted Fields is now its own
payplug_uhf factory, so the exemption just permanently relaxes the
one-payment-method-per-factory rule on the main card-payment gateway,
allowing e.g. two integratedPayment=true payplug methods and the
duplicate-ID breakage that would cause in
templates/shop/integrated/index.html.twig. Remove the exemption and its
test.
…cenario

The scenario's only assertion was the generic "I should be able to select"
step, which just checks the radio input exists and would still pass even
with the Hosted Fields include site missing entirely (see the payplug_uhf
twig-hook fix). Add a reusable "I should see the :selector element on the
page" step to CheckoutContext (extending RawMinkContext for Mink session
access) and use it, after selecting the payment method, to assert
#card-container from hosted_fields/index.html.twig is present.

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

1 participant