feat(ep-commerce): Studio extension API MVP — ep.applyCartAdjustment#373
Open
field123 wants to merge 2 commits into
Open
feat(ep-commerce): Studio extension API MVP — ep.applyCartAdjustment#373field123 wants to merge 2 commits into
field123 wants to merge 2 commits into
Conversation
Adds one typed, server-side injection function that lets a tenant
designer's Plasmic server query write a bounded, labelled money line
into the authoritative EP cart. Checkout charges the new server-computed
total; a shopper cannot forge or remove it.
- custom-cart-item.ts: addCustomCartItem deep-module write primitive —
writes an EP custom_item (name + amount), enforces sanity bounds
(amountMinor >= 0 integer, label required, kind in {fee,handling,
shipping}, quantity >= 1), re-reads/normalizes the cart. Pure over an
injected EP client (mockable, request-scope-free) and parameterised by
client so a future negative-amount member can escalate auth.
- epApplyCartAdjustment thin adapter: resolves cartId + auth from the
request-scoped session, calls the primitive, returns the cart. Shopper
auth — a positive fee is harmless to replay.
- Registered ep.applyCartAdjustment with isMutation: true and a flat
Studio param schema; added isMutation support to the registry.
Trust contract: the EP cart is the sole authority for the charged total
(handlePay reads meta.display_price); injected money lands in the cart
via a server-side credentialed write, never the session or a shopper
route. Bounds guard against a buggy tenant function, not the boundary.
Tests: deep-module unit (bounds, payload, re-read headers, error
propagation), adapter (session resolution, guards), registration
(isMutation + flat schema), and security-regression (adjustment is
charged, forged body amount ignored, post-session cart-line tampering
409, public update-session can't inject money). Full suite: 1905 pass.
Mirror epGetCart's browser path: when invoked client-side (a designer's element interaction / Studio canvas) there is no AsyncLocalStorage session, so route through the consumer EP proxy, which re-establishes withEpSession server-side (credentials + cartId from cookies) and runs the same credentialed cart write. A real ALS session (SSR) still wins and writes directly. This makes applyCartAdjustment invokable as an on-demand mutation from a UI action — not just an SSR server query — keeping the money write server-only. Unit tests cover: proxies on the browser path with the flat input, omits quantity when absent, and never proxies when a session is present.
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.
What
Implements the MVP of the Studio extension API (PRD #371): one typed, server-side injection function,
ep.applyCartAdjustment, that lets a tenant designer's Plasmic server query write a bounded, labelled money line into the authoritative EP cart. Checkout then charges the new server-computed total — a shopper cannot forge or remove it.Three layers, smallest-surface-first:
custom-cart-item.ts—addCustomCartItem(client, …)deep-module write primitive. Writes a single EPcustom_item(name + amount, not a catalog product) and re-reads/normalizes the cart. Pure over an injected EP client (mockable, request-scope-free), enforces sanity bounds (amountMinor≥ 0 integer,labelrequired,kind ∈ {fee,handling,shipping},quantity≥ 1). The existingepAddCartItemonly supportedtype: "cart_item"; this is the newcustom_itemcapability. Parameterised by client so a future negative-amount member (a discount) can hand it a client-credentials client without a redesign.epApplyCartAdjustment— thin adapter (incart-mutations.ts). Resolves cartId + auth from the request-scoped session (getCurrentEpSession), calls the primitive, returns the normalized cart. Uses the shopper-auth path: a positive fee is harmless to replay (it only adds cost to the shopper's own cart).ep.applyCartAdjustmentregistered withisMutation: trueand a flat Studio param schema (label,amountMinor,kind,quantity). AddedisMutationsupport to the registry spec.Trust contract
The EP cart is the sole authority for the charged total;
handlePayreads the cart's server-computedmeta.display_price. Injected money lands in the EP cart via a server-side credentialed write, never trusted from the session and never accepted on a shopper-facing route. The bounds here are a guard against a buggy tenant function, not the trust boundary (the cart's pricing is). This is the page-initiated Studio extension API direction; the inverted hook/SPI direction (every-add invariants, post-order side effects) is an explicit non-goal.Testing
custom-cart-item.test.ts, 12) over a mocked EP client: payload shape (custom_itemwith label→name, amountMinor→price, kind tag, default qty 1), locale/currency re-read headers, bounds enforcement (reject negative/non-integer amount, blank label, out-of-enum kind, missing cartId, non-positive quantity — all before any network call), and SDK error propagation.cart-mutations.test.ts, +4): session resolution, no-session / no-cart guards, bound rejection propagated without a write.register-custom-functions.test.ts, +2): registered asisMutationwith the flat param schema; read functions stay non-mutation.security-regression.test.ts, +4):handlePaycharges the EP-cart total including the adjustment (order built from the fee-bearing cart, paid path not free settlement); a client-forged body amount is ignored (cart total governs free-vs-paid); post-session cart-line tampering is rejected 409 (the line set is pinned at session creation, so an adjustment can't be slipped in or stripped behind the session's back); the public update-session route cannot inject a money line.elastic-pathsuite: 1905 passing.Scope
MVP ships exactly one family member.
setShippingLine/setTax/applyDiscountare designed-for (same pattern, primitive reused) but not built. No genericmutateCart. Multi-tenant sandboxing and the inverted SPI direction are out of scope.Dependencies / risks
custom_itempricing (includes_tax: truekeeps the charged amount exactlyamountMinor; tax interaction) should be confirmed against a live store.Update — browser invocation (mutation path)
applyCartAdjustmentis a write, so it's invoked on demand (an element interaction), not as an auto-running page server query. MirrorsepGetCart's browser fallback: when called client-side with no request-scoped session, it routes through the consumer's EP proxy (createEpProxyRoutes/ the proxy route), which re-establishes the session + credentials server-side and runs the same credentialed cart write. A real SSR session still writes directly. New unit tests cover the browser proxy path, quantity omission, and "never proxy when a session is present". Verified end-to-end against a live store via the proxy route (custom_item added, cart re-priced, line removed for cleanup).