Skip to content

Carry shopper product customization through add-to-cart - #1534

Open
ajaynes-godaddy wants to merge 4 commits into
mainfrom
airobuild-9325/widen-cart-contract
Open

ajaynes-godaddy wants to merge 4 commits into
mainfrom
airobuild-9325/widen-cart-contract

Conversation

@ajaynes-godaddy

@ajaynes-godaddy ajaynes-godaddy commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add-to-cart calls can now carry an optional details object (a product asset URL and selected add-ons) and metafields alongside the existing skuId/name/quantity fields, so a shopper's product customization travels with the line item all the way through checkout instead of being limited to the client. Existing add-to-cart calls that don't use these fields are unaffected.

Changeset

  • Changeset added (docs)

Test Plan

Setup

No special setup required — standard local dev environment.

Steps

  1. Run the following:

    pnpm --filter @godaddy/gd-commerce-server test
    • All tests pass, including new cases asserting details/metafields reach the addLineItemBySkuId mutation and that an invalid productAssetUrl or an oversized add-on list is rejected
  2. Run the following:

    pnpm --filter @godaddy/gd-commerce-storefront build && pnpm --filter @godaddy/gd-commerce-storefront test
    • All tests pass, including a new case asserting a custom addItem() call forwards details/metafields unchanged in the cart POST body

    Note: colors.test.ts and artifacts.test.ts currently fail locally with localStorage.clear is not a function — this is a pre-existing environment issue unrelated to this change (reproduces identically on main) and does not affect the 179 other tests, including the new one above.

  3. Confirm CI checks pass

    • All GitHub Actions checks green

AIROBUILD-9325

AddToCartItemInput was narrowed to { skuId, name, quantity }, so shopper
customization (uploaded designs, add-ons, metafields) never reached
order-api or checkout. Widen the cart contract end-to-end:

- order-subgraph.ts: add optional details (productAssetUrl,
  selectedAddons) and metafields to AddToCartItemInput, forward them in
  buildAddLineItemBySkuIdInput with length/HTTPS/count validation, and
  select metafields in getCartOrderQuery so they round-trip on read.
- cart/[id]/items/POST.ts: forward the full validated body instead of a
  hand-picked three-field literal.
- cart-model.ts: widen the storefront-side AddToCartItemInput identically.
- commerce-provider.tsx: addItem() was rebuilding a narrowed
  { skuId, name, quantity } object before calling addToCart(), dropping
  details/metafields even after the type widened; pass the full item
  through instead.
@ajaynes-godaddy
ajaynes-godaddy requested a review from a team as a code owner November 13, 2026 17:37
@ajaynes-godaddy ajaynes-godaddy self-assigned this Nov 13, 2026
@changeset-bot

changeset-bot Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b014094

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@godaddy/gd-commerce-server Patch
@godaddy/gd-commerce-storefront Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@sbolinger-godaddy

Copy link
Copy Markdown
Collaborator

Thanks Drew, this is a nice, small change and the contract matches order-api's AddLineItemInput. A few things I noticed:

Worth fixing

  • Invalid customization on the first add leaves an empty cart and returns a 500. assertValidCartItemInput runs inside buildAddLineItemBySkuIdInput, and on POST /cart that's called after addDraftOrder has already created the cart. That's the orphaned cart the pre-create validation loop in cart/POST.ts is there to prevent. It also throws a plain Error, so a bad URL or too many add-ons comes back as a 500 internal_error instead of a 400. Suggest throwing InvalidRequestError and calling the validator from that pre-create loop too, then updating the tests to expect 400.

Smaller items

  • The cart drawer shows the image, but not the add-ons: cart-state.ts maps only selectedOptions. Mapping selectedAddons there would cover the ticket's "drawer matches checkout" item.
  • The add-on value type only allows inputValue, so LIST options (optionValueId) aren't supported, and sending one would hit a TypeError on .inputValue.length. Either support it or note that only TEXT/NUMBER are supported.
  • The "order-api's schema is the authoritative enforcement point" comment isn't quite right. order-api uses plain z.string() for these fields, so this guard is the only place the HTTPS and length limits are enforced.

What I checked

  • Compared the new input types with order-api's storefront.graphql and createFromSkuId.ts. Lines with add-ons always become separate lines (findMatchingLineItem), so two customizations of one SKU make two lines.
  • Read the code path: plain add-to-cart calls build the same mutation input as before.
  • Locally, on Node 24: commerce-server typecheck, lint and tests pass (262), and commerce-storefront build, typecheck and tests pass (186).

Could you also add screenshot proof from the MCP playground showing a customized item in hosted checkout (the add-on text and the preview image)? Thanks!

@ajaynes-godaddy

ajaynes-godaddy commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Could you also add screenshot proof from the MCP playground showing a customized item in hosted checkout (the add-on text and the preview image)?

@sbolinger-godaddy Here's a short Loom showing the line item data before and after: https://www.loom.com/share/0e97ef9539b4401bb1290b648785bd0c

I'll get you a catalog add-to-cart view version of this and report back.

Edit: Here's another short loom showing the product detail and image (the mug): https://www.loom.com/share/2a72b573652f457b9f5442df1051cfa9

Let me know if you had something else in mind.

The cart drawer mapped only catalog selectedOptions (Color, Size) into
CartItemOptions, so a line's selectedAddons (shopper customization) showed
the preview image but not the add-on text, leaving the drawer out of sync
with checkout. Fold selectedAddons into the same options list useCart()
already exposes, reusing the existing "Label: value" rendering.
… cart

assertValidCartItemInput ran inside buildAddLineItemBySkuIdInput, which on
POST /cart is only reached after addDraftOrder already created the cart —
an invalid productAssetUrl or an oversized add-on list orphaned that cart
and, since it threw a plain Error, came back as a 500 instead of a 400.
Run the same validation in the pre-create loop, and switch every check to
InvalidRequestError so it maps to 400.

Also reject a LIST-option addon value ({ optionValueId }) explicitly: only
TEXT/NUMBER options ({ inputValue }) are supported by this change, and an
optionValueId-only value previously crashed with a TypeError on
.inputValue.length instead of a clean validation error.

Correct a comment that called this guard UX-only with order-api's schema
as the authoritative enforcement point — order-api's schema has no such
length/HTTPS/count limits, so this is the only place they're enforced.
@ajaynes-godaddy

Copy link
Copy Markdown
Collaborator Author

@sbolinger-godaddy I believe I've addressed all of your feedback in 454ab95 and b014094.

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.

2 participants