Skip to content

feat: validate from the generated interfaces and adopt the shared base - #15

Merged
KristofersOzolinsMagebit merged 48 commits into
developfrom
feat/spec-library-adoption
Sep 9, 2026
Merged

KristofersOzolinsMagebit merged 48 commits into
developfrom
feat/spec-library-adoption

Conversation

@KristofersOzolinsMagebit

Copy link
Copy Markdown
Member

Brings this module onto the pinned specification target and the shared base. develop already
carries an earlier slice of this branch through #14; this is everything since, and the branch is a
content superset of develop, so nothing is lost.

Requests are now checked against the specification, not against hand-written rules

The six request classes carried 509 lines of Symfony constraint trees restating rules the JSON
schema already states. Those are gone, and symfony/validator is gone from the dependencies with
them. A decoded body is now checked against the generated interface: a getter that cannot return
null is a required field, its return type is the field's type, its value constants are the values
allowed, and a new CONSTRAINTS constant carries lengths, patterns, formats, bounds and list
cardinality straight from the schema.

The request classes were also rebuilt on the generated data objects, the way the other module's
already were. That removed FulfillmentDetailsBuilder, PaymentDataBuilder and
AuthenticationResultBuilder outright: the shared hydrator walks the whole tree from the declared
return types, which is what all three did by hand.

Two bugs this exposed

  • Cart and feed writes never stored their response, so a repeated cart create was refused as
    still in flight rather than replayed. Every write now goes through one respond() helper that
    stores before sending, which is what stops it being forgotten again.
  • The same idempotency key was claimed twice per request. It only worked because the second
    answer was discarded unless it was a replay. One decision per request now.

Taken from the shared base

Idempotency arbitration, what a quote still needs before an order can be placed, the stock check
(the two copies were byte-identical), the buyer writer, the total label, the scheduled dispatch and
the JSON endpoint plumbing. The two cart validators collapsed into one that words the shared
findings, and it gained the other module's region handling, which names an unresolvable region
instead of letting it surface at completion.

One deliberate tightening

A cart line item must now carry a quantity. It never was in the specification's own Item, and the
cart side used to default a missing one to a single unit while the checkout side refused it.
Guessing a quantity is worse than saying so, so both refuse it now and name the position that is
wrong.

Notes for review

  • d/check passes: phpcs, phpstan and 156 unit tests. Net −1,879 lines.
  • The Item.quantity defect is documented in the specification library README; it is why the module
    narrows the item type rather than trusting the schema here.
  • Do not merge before the spec library release. This depends on the CONSTRAINTS constant the
    generator now emits; see feat: carry the schema's own validation keywords onto the generated interfaces acp-php-spec#3.
  • Stripe delegated payment still needs verifying against a real account: the endpoint the module
    targets was superseded upstream and cannot be exercised from development.
  • The contract migration that drops the legacy tables is deliberately not here. It must not be
    cut until the new tables are confirmed populated on a real install.

…eplace fulfillment_address and

fulfillment_option_id with fulfillment_details and selected_fulfillment_options, and move the payment
provider into capabilities.payment.handlers where the spec keeps it.
…ts, and serve the well-known

ACP discovery document.
…s[] breakdown, and carry the

quantity the spec requires on LineItem rather than only on Item.
…tial token from where the spec

keeps them, and serve the JSON Schemas the payment handler advertises.
…r while the original request is

in flight, 422 when a key is reused with a different body. Drops the two error types the spec's enum
does not carry.
…pping the code field MessageInfo

forbids and replacing cart_not_active with the conflict code the enum actually allows.
…ule was restating verbatim. Fixes

an annotation that cast a Magento sales order to the ACP order DTO.
…al, tax and total into the typed

totals[] breakdown the spec declares.
…ctly, deleting the module's

response interfaces and DTOs which had become verbatim subsets.
…xtend the generated one so it

adds only the quantity the spec's schema omits.
…ng; it resolved only because the root project installed it.
…bit_AgenticCore. The buyer now gates on email, which the schema requires, instead of demanding both names as well.
… the abandoned-claim takeover it never had. Data patch maps its column names and its zero-instead-of-null in-flight status, and the module finally gets a schema whitelist.
…n map becomes two routes with different methods, and its any-shared-segment match is replaced by an anchored pattern.
…cts go with it: a single-word name no longer discards the address, an absent field no longer erases a populated one, the selected method reaches the shipping assignment, an unknown SKU is a message rather than rejecting the whole cart, and errored carrier rates are no longer offered. Also binds nine spec types the module built through factories without a preference, which made discovery and any session carrying fulfillment details fail at runtime.
…ove ACP onto it. Order placement no longer waits on the receiver, and a failed delivery is retried instead of logged and lost.
…CP stops inferring a placed order from the reserved increment id, so an abandoned payment no longer reports as completed.
…rays. The spec runtime returns null for anything that is not already the right instance, so a submitted delivery address was silently discarded and the payment credential token — two levels down — was never readable, which made completion refuse every request. Its validator also still required the pre-adoption flat token and provider pair rather than handler_id and instrument.
…e sender hardcoded one protocol's Merchant-Signature and Request-Id, which the second consumer cannot use — it signs per RFC 9421 with Webhook-Id and Webhook-Timestamp instead. The queue now asks a DeliveryHeadersProvider per attempt and knows nothing about signing.
…second module delivers with different headers.
@magebit-automation

magebit-automation Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

✅ Magebit Code Review — Review complete

Reviewed up to d333fb270bb34ca23e4dfe9cbac63dafd889505f (incremental pass).

No issues met the confidence threshold to publish.

Findings checklist

  • Fixed — [High] Updating a session without discounts wipes the coupon (Service/CheckoutSessionService.php:364)
  • Open — [High] Migrated idempotency replies are still encrypted and never decrypted (Service/ComplianceService.php:79)
  • Fixed — [Medium] Feed products loads every product id before paging (Service/FeedService.php:112)
  • Fixed — [High] Each configurable on a feed page loads its children in a loop (Service/FeedService.php:211)
  • Fixed — [Low] Temporary checkout-session fixture is unused (Test/Unit/_fixtures/checkout_session.update.tmp.json:38)
  • Open — [Low] Several commit subjects are not Conventional Commits (-:0)
Changed files (174)

.github/

  • .github/workflows/ci.yml +95 −0

Api/

  • Api/CartServiceInterface.php +53 −0
  • Api/ConfigInterface.php +10 −0
  • Api/Data/AddressInterface.php +0 −126
  • Api/Data/BuyerInterface.php +0 −79
  • Api/Data/FulfillmentOptionDigitalInterface.php +0 −22
  • Api/Data/FulfillmentOptionInterface.php +0 −127
  • Api/Data/FulfillmentOptionShippingInterface.php +0 −64
  • Api/Data/IdempotencyInterface.php +0 −118
  • Api/Data/ItemInterface.php +7 −22
  • Api/Data/LineItemInterface.php +0 −124
  • Api/Data/MessageInterface.php +0 −111
  • Api/Data/OrderInterface.php +0 −61
  • Api/Data/PaymentDataInterface.php +0 −66
  • Api/Data/PaymentMethodInterface.php +0 −266
  • Api/Data/PaymentProviderInterface.php +0 −49
  • Api/Data/Request/CartCreateRequestInterface.php +29 −0
  • Api/Data/Request/CartUpdateRequestInterface.php +29 −0
  • Api/Data/Request/CompleteCheckoutSessionRequestInterface.php +9 −14
  • Api/Data/Request/CreateCheckoutSessionRequestInterface.php +11 −17
  • Api/Data/Request/DelegatePaymentRequestInterface.php +0 −102
  • Api/Data/Request/RequestInterface.php +0 −15
  • Api/Data/Request/UpdateCheckoutSessionRequestInterface.php +12 −23
  • Api/Data/Response/CheckoutSessionResponseInterface.php +0 −211
  • Api/Data/Response/CheckoutSessionWithOrderResponseInterface.php +0 −33
  • Api/Data/Response/ErrorResponseInterface.php +2 −2
  • Api/Data/ValidatableDataInterface.php +0 −22
  • Api/FeedServiceInterface.php +43 −0
  • Api/IdempotencyRepositoryInterface.php +0 −71
  • Api/MarketingConsentHandlerInterface.php +33 −0
  • Api/PaymentHandlerInterface.php +8 −2
  • Api/PaymentMethodVaultHandlerInterface.php +1 −1

Controller/

  • Controller/ApiController.php +104 −58
  • Controller/Cart/Cancel.php +130 −0
  • Controller/Cart/Index.php +133 −0
  • Controller/Cart/Retrieve.php +134 −0
  • Controller/Cart/Update.php +144 −0
  • Controller/Checkout/Order.php +17 −23
  • Controller/Checkout/Sessions/Cancel.php +20 −20
  • Controller/Checkout/Sessions/Complete.php +26 −22
  • Controller/Checkout/Sessions/Index.php +24 −21
  • Controller/Checkout/Sessions/Retrieve.php +20 −11
  • Controller/Checkout/Sessions/Update.php +24 −21
  • Controller/ConfiguredRouteProvider.php +83 −0
  • Controller/Delegate/Payment/Index.php +24 −20
  • Controller/Discovery/Index.php +47 −0
  • Controller/Feed/Index.php +133 −0
  • Controller/Feed/Metadata.php +134 −0
  • Controller/Feed/Products.php +146 −0
  • Controller/Router.php +0 −105

+124 more files…

Reviewed by ~x-ai/grok-latest

Comment thread Service/CheckoutSessionService.php Outdated
Comment thread Service/ComplianceService.php
Comment thread Service/FeedService.php
Comment thread Service/FeedService.php
Comment thread Test/Unit/_fixtures/checkout_session.update.tmp.json Outdated
@KristofersOzolinsMagebit

Copy link
Copy Markdown
Member Author

Five of the six findings are addressed. One does not hold up.

Coupon wiped by an update that omits discounts — fixed. applyDiscountCodes is now called only
when getDiscounts() is not null, matching the getLineItems() convention in the same method. Both
behaviours are pinned by tests: an omitted field keeps the coupon, a submitted empty codes array
still clears it.

Migrated idempotency replies never decrypted — this one is wrong, on both premises. The decrypt
does happen; it is in the shared gate, before the decision reaches guard(). And new writes are not
stored as plain JSON, they are encrypted by that same gate. Verified against a running store:

stored row:    0:3:gKsx5VrmVCfpVMOlnAow3DOYn44NEpAPwhPN…
replayed body: {"id":"mdXnczAUxOMSK7l0VR8EcLjygY4fu2tp"…

The replay parses as JSON and is identical to the first response. A migrated row takes the same
path, because it carries the same encryptor envelope, so the data patch needs no change.

Configurable children loaded in a loop — fixed. Child ids for the whole page are collected, then
every child is loaded in one collection. Measured on a page with about seventeen configurables:
thirty-six product-collection loads before, two after. Note the single-query approach was
deliberately rejected: the configurable link collection returns one row per parent-child pair, so a
simple product linked to two configurables on one page would collide on a duplicate item id.

Feed loads every id before paging — fixed, the window is on the select. This finding was stronger
than stated: the feed response carries no total at all, so the full id scan had no correctness
argument behind it. A stable sort was added, since limit and offset without an order can overlap or
skip rows.

Unused fixture — deleted.

Commit subjects — correct, twenty-two of forty-one, all from earlier phases of this work. Not
fixing it: that means force-pushing a branch under review, which discards the review anchoring to
buy nothing functional. The convention holds going forward.

One thing the fix surfaced that was not in the report: the spec interface still carries a deprecated
coupons field meaning the same thing as discounts, and this service ignores it entirely, so an
agent sending the older field has its codes silently dropped. Out of scope here, but it should
either be read or reported rather than dropped in silence.

Verification: d/check green (163 tests). Also added on this branch, a CI workflow running PHPStan
and the unit tests against a real Magento install, matching the one in magento2-mcp-module.

@KristofersOzolinsMagebit
KristofersOzolinsMagebit merged commit 14ba2ef into develop Sep 9, 2026
3 checks passed
@KristofersOzolinsMagebit
KristofersOzolinsMagebit deleted the feat/spec-library-adoption branch September 9, 2026 10:43
@KristofersOzolinsMagebit
KristofersOzolinsMagebit restored the feat/spec-library-adoption branch September 9, 2026 10:43
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