Repository navigation
PRE-3722: bump UPC for the new Unified API contract - #335
Merged
adumont-payplug merged 3 commits intoOct 9, 2026
Merged
Conversation
There was a problem hiding this comment.
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.
Require UPC 1.2.1 and read the alias from storedId first. Backfill the payment id from the webhook, refuse refunds cleanly when it is missing, and make the SDK URL a parameter that defaults to production.
The webhook top-level id is the operation id, not the payment id, so the backfill could never apply. Remove it with the fallback lookup; keep the refund refusal.
UPC 1.2.1 requires a currency in AmountHelper. Deferred capture and cancel now pass the payment currency, and a typed amount in a currency that is not 2-decimal is refused before any API call.
adumont-payplug
force-pushed
the
feature/PRE-3722_bump-upc-unified-api-contract
branch
from
October 9, 2026 08:10
a8139b6 to
a422ec1
Compare
jhoaraupp
approved these changes
Oct 9, 2026
adumont-payplug
deleted the
feature/PRE-3722_bump-upc-unified-api-contract
branch
October 9, 2026 08:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
The Unified API changed its payment creation contract: the Hosted Fields token is now read from
paymentMethod.hfToken, the saved-card alias frompaymentMethod.storedId, and the created alias comes back inpaymentMethod.storedIdinstead ofpaymentMethod.id. Withpayplug/unified-plugin-core(UPC) before 1.2.1 every Hosted Fields payment (new card or saved card) is refused with400 INVALID_REQUEST, and no card is ever saved. This PR moves to UPC 1.2.1 (PRE-3717), which sends both the old and the new fields, and adapts the plugin around it, includingdevelop's new deferred capture and cancel (PRE-3673), which UPC 1.2.1 broke.1. UPC bump. The constraint goes from
^1.1.3to^1.2.1.develop's lock pinned UPC under the label1.3.0, which is not a published version (neither a git tag nor on Packagist): it is commit050f14e, older than 1.1.3 and without the new contract. The lock now pins the published 1.2.1 (05423ee). See Impact for the other packages the lock moves. The plugin builds nohfToken,idorstoredIditself, so the bump alone fixes payment creation. A contract test (UnifiedApiContractTest) fails if the constraint ever allows a UPC that predates the new contract, and characterization tests lock in that both capture handlers never put those keys inpaymentMethod.2. Alias read from
storedIdfirst.CardDataFromPaymentMethodExtractorreadspaymentMethod.storedIdand falls back topaymentMethod.id, as UPC does. The real webhook carries the alias only aspaymentMethod.storedId, whilegetOperationreturns it aspaymentMethod.id; reading both lets the card the shopper asked to keep be saved on either path.3. Creation response without a top-level
id. On staging the creation response sometimes lacks its top-levelid, which is the id refunds are keyed on (the refund route refuses the operation id).PaymentCaptureOutcomeApplier::warnWhenPaymentIdMissing(), called by both capture handlers).RefundPaymentProcessor, before the lock, the API call and anyRefundHistoryordetails['refunds']write. The refusal logs an error, adds a translated back-office flash (payment_refund_missing_payment_id, en/fr/it) in addition to Sylius's own generic error, and throwsUpdateHandlingException, instead of the rawInvalidArgumentExceptionraised so far.4. Configurable Hosted Fields SDK URL. The checkout no longer hardcodes the staging SDK host:
templates/shop/hosted_fields/index.html.twiguses the newpayplug_hosted_fields_sdk_url()Twig function, backed by thepayplug.hosted_fields_sdk_urlparameter and overridable withPAYPLUG_HOSTED_FIELDS_SDK_URL. The default is the production SDKhttps://js.dalenys.com/hosted-fields/v2.2.0/hosted-fields.min.js.5. Deferred capture and cancel on UPC 1.2.1. UPC 1.2.1 also contains PRE-3724 (BREAKING):
AmountHelper::toCents()andfromCents()now require a currency and throwInvalidCurrencyExceptionfor an empty, malformed or 3-decimal one.develop's PRE-3673 admin capture/cancel called them without one in three places, which failed withArgumentCountErroronce on 1.2.1. They now pass the payment's currency:AuthorizedPaymentController::parseAmount()(the typed amount),AuthorizedPaymentOperationProcessor(the remaining amount in the "amount exceeds what remains" message) andAuthorizationExtension(the amount fields' placeholder). See Impact for the currency restriction this implies.Motivation: every Hosted Fields payment is refused on staging with UPC before 1.2.1 and saved cards are never persisted; the plugin must follow the Unified API contract change confirmed by the API team on 2026-10-06, must not fail with a raw error when a payment created without an id is refunded, and must keep
develop's deferred capture and cancel working on the UPC that carries the new contract.Related issue(s): PRE-3722 (builds on the UPC change PRE-3717)
Type of Change
Breaking for setups that need UPC below 1.2.1, for code constructing
PayPlugExtensionby hand, and for merchants typing a partial capture or cancel amount in a currency that is not 2-decimal: see Impact below.Checklist
Code Quality
Testing
sonarcloudCI jobSecurity & Ops
Notes for Reviewer
Impact / BC
payplug/unified-plugin-core ^1.2.1. UPC's PHP floor is unchanged (>=7.4, plugin^8.2).AmountHelperconverts with a factor of 100 (EUR, USD...), detected by asking the helper itself (AuthorizedPaymentOperationProcessor::supportsTypedAmounts()), not with a currency table. Zero-decimal (JPY, XPF), 3-decimal (KWD), empty and malformed currencies are refused with a flash (new keyadmin.authorization.error.unsupported_currency, en/fr/it) before any API call or state change; the refusal is logged with ids and the currency only. A blank amount (the whole remaining amount) involves no conversion and works in every currency, as before. Reason: the plugin stores every amount in Sylius's two-decimal scale whatever the currency, while UPC's helper converts to the currency's real minor unit, so a typed JPY amount would have moved a hundredth of what the merchant meant. EUR/USD behaviour is unchanged.PayPlugExtension::__construct()gainsstring $hostedFieldsSdkUrl(autowired, bound to%payplug.hosted_fields_sdk_url%); this is the only extender-facing constructor change and is listed in the CHANGELOG BC table.PAYPLUG_HOSTED_FIELDS_SDK_URLto the staging SDK URL, since the default is the production SDK. Merchants have nothing to configure.composer.lockmoves more than UPC. Besides UPC1.3.0(050f14e) → 1.2.1, the author ran a broadcomposer updateafter the rebase to check nothing breaks: 58 packages differ fromdevelop's lock (55 runtime, 3 dev, UPC included). Notablysylius/refund-pluginv2.1.0 → v2.2.1 (a minor version),symfony/framework-bundlev6.4.46 → v6.4.47 and most of Symfony 6.4 to v6.4.47,api-platform/*v4.3.20 → v4.3.21, andphpunit/phpunit9.6.37 → 9.6.38.sylius/syliusstays v2.2.10 anddoctrine/orm3.6.9; the package count is unchanged. The content-hash differs fromdevelop's only because of the UPC constraint incomposer.json.## [2.0.0] - Unreleased.History of this PR
UnifiedApiIpnAction. That rested on the webhook's top-levelidbeing the payment id; it is the operation id, so the recovery could never work. The second commit removes it entirely:UnifiedApiIpnAction,PaymentRepositoryandPaymentRepositoryInterfaceare back to theirdevelopstate.develop(PRE-3673 deferred capture/cancel), and the third commit adapts PRE-3673'sAmountHelpercalls to UPC 1.2.1. The earlier statement that PRE-3724 did not affect the plugin was true on the old base and is no longer.Testing
phpunitexit 0 (744 tests, 1477 assertions),phpstanexit 0,ecsexit 0,composer validateexit 0.phpmdexits 2 with 45 violations, the same list asdevelop, except that the existingExcessiveClassComplexityofAuthorizedPaymentOperationProcessorgrows from 59 to 64.develop's refreshed lock with Sylius v2.2.10 fixed theknp_gaufretteinstall failure).AmountHelperchange): saved-card payment with 3DS paid (the request carried bothidandstoredId, 200); new-card payment with 3DS and card saving paid (hfTokensent top-level and insidepaymentMethod), card persisted via the status poll; partial refund succeeded using the creation payment id (200).storedIdread is covered by a unit test with the real payload shape only;id: not reproducible on staging, unit-tested only;AmountHelperchange: unit-tested only, not run on staging;Risks / follow-ups
CommonFieldsDtowithoutAmountHelper. For JPY that may be the 100x issue UPC 1.2.1 now handles on its side. This needs the API team's answer and a separate ticket; meanwhile JPY and KWD merchants cannot type a partial capture or cancel amount (a blank amount still works).idremains unrefundable from the back office: the refusal is the intended behaviour. Open question for the API team: when can the creation response omitid, and how should such a payment be refunded?LIKEmatch ondetails. A follow-up security ticket is being prepared.SyliusUnifiedApiHttpClient) and of the hosted payment request payload (CaptureHostedPaymentRequestHandler) contain the single-usehfToken, now in both places UPC sends it.AmountHelpersays deferred capture and cancel keep working, without saying that typed amounts are limited to 2-decimal currencies and that a blank amount works everywhere;doc/authorized_payment.md(Capture and Cancel sections) does not mention that currency restriction;PaymentCaptureOutcomeApplier::warnWhenPaymentIdMissing(), text pinned in its test), which implies a recovery that does not exist.🤖 Generated with Claude Code