fix(ep-commerce): eleven pre-existing defects blocking the 0.4.0 publish - #448
Merged
Conversation
…tions EP rejects A bundle reported itself valid however it was configured, and add-to-cart stayed enabled: the schema's refinements passed `path: [componentKey]` to `refine`, whose path is relative to the schema being refined, so every issue landed at `[componentKey, componentKey]` while `useBundleForm` read only the top level. One `superRefine` per component, no path, carries the count-aware messages that `use-bundle-validation.tsx` has been holding unused since the package landed. Two selection shapes made a bundle with a parent-product option impossible to add. Deselecting held the option at quantity 0 instead of removing it, because the max=1 branch zeroed its siblings and the removal branch read a stale `selectedOptions`; EP fails the whole request on a single zero. And the catalog configuration was merged option-by-option, so the bare parent reappeared next to a chosen `parentId:childId` and EP counted two selections against a max of one. `handleComponentSelection` now computes the component map in one pass, `convertSelectionsForAPI` drops non-positive quantities and superseded parents, and priority 2 fills only components nobody has spoken for. The button no longer offers an add EP will refuse, and shows the reason when one fails rather than swallowing it. Option names fall back to nothing rather than a raw product id, and a component with no maximum stops printing 9007199254740991. The tests that encoded the old setValue call shape are repointed; the new ones run the real resolver over the real schema, which is what the mocked suite could never catch.
…ts, and a working button The provider built its summary only from `state.order`, which does not exist until payment succeeds, and EPOrderTotalsBreakdown prefers that summary over the cart — so the checkout page read "Subtotal $0.00 / Total $0.00" for the whole flow, beside a cart summary showing the real money. The summary falls back to the cart's display_price until an order exists, and the breakdown treats an all-zero summary next to a funded cart as "nothing to report yet". The three field groups defaulted their slot to text labels: a thing that looks like a form, collects nothing, and leaves the flow unable to advance. An empty slot now renders working inputs wired to the group's own setField — a country dropdown included — and anything placed in the slot still replaces them. EP Checkout Button outside a provider was permanently aria-disabled with a dead click handler, which is exactly the cart-page composition the skeletons call for. It now labels itself for the cart and navigates to `checkoutUrl`. EPShippingMethodSelector fetched /api/checkout/calculate-shipping on every valid address — a route this package retired and answers with 410 Gone, because EP has no shopper-facing rates endpoint. The dead path is gone, `loadingContent` follows the session's own loading state, and useCheckout's calculateShipping fails with the reason instead of issuing a request that cannot succeed.
EPVariationPicker resolved the matching child product and wrote its id to the form, but kept the child itself in a context of its own — so nothing outside the picker could read it. A buybox price therefore showed the parent's price while the cart charged the child's: $20.00 on the page, $15.00 at checkout, for the same click. EPProductProvider publishes the chosen child as `currentVariant`, from inside the form scope so it can watch the field and above the content so the buybox can read it. EP Product Field prefers the variant for any field the variant answers (price, SKU) and falls back to the product for the rest.
…d never inherit a price A ChildProduct is flat — `name`, `sku`, `price` — while everything that reads a product looks under `attributes` and `meta.display_price`, so publishing the child as-is left `currentVariant` unreadable and the buybox still quoting the parent. It is projected onto the product shape instead, a drop-in for `currentProduct`. Money is not inherited. A child reports only `without_tax`, so keeping the parent's `with_tax` would answer a tax-inclusive binding with the parent's number, and a child with no price of its own cannot be bought at the parent's price at all — EP rejects the add. Once a variant is chosen, every money leaf resolves against it and an absent price renders empty.
…ust the one they edit react-hook-form fills `formState.errors` only for fields it has seen written, so a bundle that arrived invalid — a shared `bundle_config` link, a catalog default that cannot be satisfied — reported `isValid: false` with no message to go with it. The button was correctly disabled and said nothing about why, which is the same dead end as before from the shopper's side. Validity and messages both come from one `safeParse` against the current selections, so every path into the page is covered and the two can never disagree. The mocked suite could not see this: it stubs the schema and asserts against `formState`. Found in a browser.
An empty slot rendered the literal words "Step" and "Name", once per step — four identical rows telling the shopper nothing. The default now gives the step number and name, and marks the active one with aria-current.
…wrong glyph A sweep of every static slot default found the same shape as the field groups, three more times, each showing a value rather than a label: - EP Order Totals Breakdown defaulted to four rows of the literal text "$0.00" — a page that looks priced and isn't. It renders the real figures, and the discount row only when there is one. - EP Shipping Method Selector and EP Checkout Shipping Rates defaulted to "Shipping Method"/"$0.00", and their live repeaters rendered *nothing* without slot content. Each rate now gets a selectable row — radio semantics, keyboard included — because choosing one is a ref action the designer wires, so a read-only default would leave the rates unpickable. The session rows format their minor units through useMoneyFormat, so they follow a currency switch like every other total. - Both quantity buttons defaulted their slot to "+", so a decrement button dropped in as-is read "+". The glyph follows `action`. Facet placeholders in EP Refinement List, EP Hierarchical Menu and EP Current Refinements are the same class but show a label, not a value, and sit on the search surface this pass never exercised. Left alone deliberately.
Auditing the prepublish fixes turned up four places where behaviour changed and nothing asserted it: EPBillingAddressFields' default inputs, the `currentVariant` projection in EPProductProvider, EPCheckoutShippingRates' default row, and useCheckout's calculateShipping. Each was proven red by restoring the pre-fix behaviour, one revert at a time rather than trusting a green run: - billing: reverting the three fallback sites fails 3 of 4 - projection: removing CurrentVariantScope fails all 6; re-inheriting the parent's display_price fails only the no-price-of-its-own test; merging into the parent's display_price instead of replacing it fails only the tax-inclusive test — so each test earns its place - rate rows: restoring the `children ? … : null` render fails 3 of 4 - calculateShipping: restoring the fetch fails 2 of 3 The projection needed its own harness: the existing EPProductProvider suite mocks DataProvider without its `data`, so nothing published through it was observable.
…non-interactive
The last three placeholder defaults: "Filter Value (0)", "Category (0)", and a
correctly shaped chip reading "Brand: Leather" whatever the shopper had actually
applied.
Both facet lists deliberately pre-render no button or <a>, exposing `toggle` /
`refine` on the item data so the click is wired in Studio. The defaults honour
that: real label, count, refined state — and for the category tree a `data-depth`
attribute, since a flattened hierarchy is unreadable without indentation. Tests
assert the *absence* of any interactive element, so the constraint is now
enforced rather than just documented in a comment.
EP Current Refinements is the exception: its row already carries
`onClick={chip.refine}`, so the chip is interactive by design and its "×" means
something. Only the fiction was replaced; the chip styling stays.
Built test-first, one behaviour at a time — each of the 15 tests written and seen
red before the code that satisfies it. Two mistakes that caught: a marker that
matched inside `export const …` and silently un-exported the component, and tests
written against `attribute` when the prop is `attributes` (a string, not an
array) — the first caught by the suite, the second only by tsc.
handleComponentSelection rebuilt the map from the render-time watch() snapshot, so useVariationSelection's clear-then-set pair both read the same stale object and the set reinstated the child the clear removed. Both children then went to Elastic Path, which refused the lot. Reads getValues(componentKey) instead; the mocked suite's getValues follows.
The slot kept a styled hbox as its defaultValue, so Studio materialised a childless-but-truthy node on insert and the chip branch never ran — one empty pill per active filter. Drops the default and hides the placeholder, matching the other slots whose defaults were removed. The pill styling does not move onto the chip: catalog-search components carry no appearance styles, which the headless styling contract enforces.
Three defects in the summary the checkout provider publishes: - hasDiscount was never set, so the default rows computed a discount and then dropped the row that shows it. - A zero tax on a placed order read "Calculated at next step". Once the order exists, zero tax means zero. - The cart's own meta.display_price.shipping was ignored, so a cart already carrying shipping showed "TBD" while the cart-fed path on the same page showed the figure. That amount is already inside with_tax, so it is now displayed without being added again; a rate the shopper selects here is local state that never reaches the cart, so it still adds to the total. Also flips both hasDiscount predicates to `!== 0`: Elastic Path reports a cart discount as a reduction, so `> 0` would never fire on a promoted cart.
normalizeChildProducts kept only the child's without_tax, so the variant projection had no with_tax to publish and correctly refused to fall back to the parent's — leaving a tax-inclusive PDP showing the parent's price before selection and nothing after it.
…r variants The earlier fix only covered callers that clear the old child first. EPBundleVariationPicker never passes selectedVariationId, so useVariationSelection's clear branch never fires and the set call arrives alone — every switch left the previous child in the map. Browser-verified on the bundle page: switching a size took the Gift component to "2 of 0-3" with one option visibly selected. An option resolves to exactly one variant, so the write itself now drops the option's other variants and its bare parent instead of depending on the caller.
…ate updater React runs state updaters during render, so calling onSelectionChange from inside setVariationSelections made the provider's setValue a render-phase update on another component — "Cannot update a component (EPBundleProviderInner) while rendering a different component (EPBundleVariationPicker)" on every variation click — and StrictMode, which invokes updaters twice, ran the whole selection twice.
…icked Choosing a variation also toggles the option checkbox, which writes the bare parent with no variationId. A variant write already superseded the bare parent; the reverse was missing, so whichever write landed last decided the count and the component read "2 of 0-3" for a single gift.
EP reports without_tax post-discount, so a Discount row beside it read "Subtotal 13.50, Discount -1.50, Total 13.50". Both cart-derived paths now take the subtotal from without_discount, falling back to without_tax when a cart omits it. The order path is unchanged: it reports no discount, so its rows are already consistent.
This was referenced Aug 21, 2026
Closed
Closed
…heckout - Bundle options no longer read as unavailable while the products are still loading: productsLoading was in the context and unused, so every option in the bundle flashed unavailable on first paint. - The variant projection publishes the child's own images. The child fetch already asks for main_image and files; the normalizer discarded them, so selecting a variant changed name, SKU and price but kept the parent's photo. A child without an image of its own still inherits the parent's. - The standalone checkout button handles Enter and Space, and its slot no longer defaults to "Continue" while the button calls itself "Checkout" — an empty slot now renders the step-aware label. - Checkout field errors are tied to their inputs with aria-describedby. - Both rate selectors wrap their default rows in a radiogroup, and only when those rows are the radios being rendered. - An applied promotion comes back as a promotion_item; it is no longer rendered as a purchasable line or counted in itemCount. custom_item stays, being a real adjustment line.
EP references one file from both main_image and files, so both normalizers pushed it twice and a gallery bound to images showed the same photo twice. Seen in the browser on the Sandle PDP, where the parent published two images that were the same file.
The applied state lived only in local component state, so any navigation lost the "TEST1" chip while the discount was still live on the cart. The cart now publishes its promotions separately from its lines — they are still not sold as lines or counted in itemCount — and both promo input variants read the applied promotion from there. CartItem.type was typed as cart_item alone, which is what let promotion_item be mistaken for a purchasable line; it now carries the union Elastic Path actually side-loads.
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.
Verifying 0.4.0's registered components against the EP Commerce Skeletons composition turned up no 0.4.0 regression, but eleven pre-existing defects — most shopper-facing, all reproducing on published 0.3.0. Publishing without them ships known-broken behaviour.
Bundles
refine'spathis relative to the schema being refined, sopath: [componentKey]put every issue at[key, key]where nothing read it. Add-to-cart stayed enabled and EP's 500 was discarded. Validity and messages now come from onesafeParse, which also covers a configuration the page arrived with — react-hook-form only fillsformState.errorsfor fields it has seen written.use-bundle-validation.tsxwas dead code; its wording moved into the schema.quantity: 0(EP rejects the whole request), and the catalog configuration was merged option-by-option so the bare parent reappeared beside a chosenparentId:childId("too many selections").Checkout
$0.00for the entire flow: the provider's summary came only fromstate.order, which exists only after payment, andEPOrderTotalsBreakdownprefers that summary over the cart.EP Checkout Buttonoutside a provider was permanentlyaria-disabledwith a dead click handler — the cart-page composition the skeletons call for. It now navigates tocheckoutUrl.EPShippingMethodSelectorPOSTed/api/checkout/calculate-shippingon every valid address, a route this package retired and answers with 410;useCheckout.calculateShippingdid the same. EP has no shopper-facing rates endpoint (PRD: Studio extension API — tenant server logic feeding plasmic-ep commerce ops (MVP: ep.applyCartAdjustment) #371/fix(ep-commerce): remove phantom getShippingOptions; source shipping rates per #371 #374).Products
EPProductProviderpublishes it ascurrentVariant, projected onto the product shape, and money is never inherited — a child reports onlywithout_tax, and one with no price of its own cannot be bought at the parent's.Slot defaults that showed labels or fake values
Six components shipped defaults that looked like content and weren't: the three checkout field groups (zero
<input>elements), the step indicator ("Step Name" ×4), order totals ("$0.00" ×4), both shipping-rate repeaters (placeholder text and nothing rendered without slot content), both quantity buttons ("+" for decrement too), and the three facet components ("Filter Value (0)", "Category (0)", a chip reading "Brand: Leather"). All now render real, data-driven defaults; slot content still replaces them.The facet lists stay non-interactive by design — they expose
toggle/refineon the item data specifically so they need not pre-render a button, and a test now enforces that rather than leaving it to a comment.Each fix has a test proven to fail against the pre-fix behaviour. 2095 jest tests in 131 suites, 113 vitest.
Not changed:
Playstation 5 Bundle 1has no price in the catalog and itst3component demands two selections while offering one resolvable option — catalog data, not code. Both now surface as legible messages instead of a 500.