[B2BTEAM-3748] Forward priceToken on addToCart (Pricing Fallback V2) - #17
[B2BTEAM-3748] Forward priceToken on addToCart (Pricing Fallback V2)#17wender wants to merge 2 commits into
Conversation
Pricing Fallback V2: read the signed price (commertialOffer.PriceToken) from the product context and forward it as priceToken in the addToCart payload, so the Checkout can close the cart while the Pricing is unavailable. The token is read from the seller entry of the very SKU sent on addToCart, since it signs that seller's price for that item, and it is only added to the payload when the search actually returned one, keeping the payload unchanged while search-graphql/product-context do not expose the field yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Beep boop 🤖 I noticed you didn't make any changes at the
In order to keep track, I'll create an issue if you decide now is not a good time
|
|
Hi! I'm VTEX IO CI/CD Bot and I'll be helping you to publish your app! 🤖 Please select which version do you want to release:
And then you just need to merge your PR when you are ready! There is no need to create a release commit/tag.
|
`vtex.search-graphql` 0.72.0 / `search-resolver` 1.106.0 (deployed 2026-07-20) expose the signed price on the `Offer` type as `priceToken`, not `PriceToken` — the PascalCase name is the one returned by the raw Catalog Search API, which `search-resolver` maps from. Since this app reads the offer through `vtex.product-context`, fed by search-graphql, it has to look for the camelCase field, keeping the Catalog spelling as a fallback for other data sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds support for forwarding a signed priceToken from the product-context commercial offer into the addToCart mutation payload, enabling Checkout to honor the searched price during Pricing incidents (Pricing Fallback V2).
Changes:
- Extend typings to include optional
CommercialOffer.priceToken/PriceTokenandOrderFormItemInput.priceToken. - Resolve
priceTokenfrom the selected item’s matching seller inmapCatalogItemToCart. - Conditionally include
priceTokenin theaddToCartmutation input and add unit tests for token resolution behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| react/typings.ts | Adds optional priceToken fields to offer + order form item input typings. |
| react/components/BuyButton/modules/catalogItemToCart.ts | Resolves priceToken from the selected item’s seller commercial offer and returns it on mapped cart items. |
| react/components/BuyButton/AddToCartButton.tsx | Conditionally forwards priceToken into the addToCart mutation variables. |
| react/tests/catalogItemToCart.test.ts | Adds unit tests covering token resolution and seller matching behavior. |
| CHANGELOG.md | Documents the new signed price token forwarding behavior under Unreleased. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // Only sent when the search actually returned a token, keeping the payload | ||
| // unchanged while the field is not exposed by the product context yet | ||
| ...(item.priceToken ? { priceToken: item.priceToken } : {}), | ||
| })) |
What does this PR do? *
Pricing Fallback V2 (B2BTEAM-3748): captures the signed price returned by the search — exposed as
commertialOffer.priceTokenand read here fromvtex.product-context— and forwards it aspriceTokenin theaddToCartpayload, so the Checkout can close the cart with that price even while the Pricing is unavailable.catalogItemToCart.tsreads the token from the seller of the SKU being added and returns it aspriceTokenon the mapped cart item.AddToCartButton.tsx(adjustItemsForMutationInput) sendspriceTokenin thevtex.checkout-resourcesmutation body, alongsideid/index/seller/quantity/options.CommercialOffergains optionalpriceToken/PriceToken,OrderFormItemInputan optionalpriceToken.mapCatalogItemToCart.Three deliberate choices:
priceToken(camelCase), withPriceTokenas fallback.vtex.search-graphql@0.72.0/search-resolver@1.106.0(both deployed 2026-07-20) expose it on theOffertype aspriceToken; the PascalCase spelling is the one the raw Catalog Search API returns, whichsearch-resolvermaps from. As this app reads the offer throughvtex.product-context, the camelCase name is the one that actually arrives, and the fallback covers Catalog-shaped data.BuyButton/Wrapper.tsxresolvesselectedSellerfromproductContext.selectedItem.sellers[0], while the item sent to the cart is built from theselectedItemprop — which, when the button is rendered per row bySkuBuyButton, is that row's SKU, not the product context one. So instead of reading the token straight offselectedSeller, it is looked up insideselectedItem.sellersby the already resolvedsellerId. This way the token can never sign the price of a different SKU/seller than the one added to the cart (if no match is found, no token is sent). The pre-existingseller/price/listPriceresolution is left untouched.priceTokenis only added to the payload when the search returns one (conditional spread), per the product decision that the token is optional and only used during a Pricing incident.How to test it? *
Requires an account with price signing enabled on Intelligent Search (still behind a feature flag — only test stores such as
storeframework; the search team enables it on request), and a theme whose product query requestscommertialOffer { priceToken }, since this app only consumesvtex.product-contextand does not own the query.vtex linkthe app and open a page withsku-listand its buy button.items[].sellers[].commertialOffer.priceTokenshould be present.addToCartmutation payload — the item should carrypriceTokenalongsideid,index,seller,quantityandoptions.priceTokenin the payload.cd react && yarn test— themapCatalogItemToCartspecs cover both field spellings, token absent, and seller mismatch.End-to-end validation of the fallback itself can only be done by intentionally opening the circuit with the Pricing, as agreed in the reference thread.
Describe alternatives you've considered, if any. *
selectedSeller. Simpler, but on the per-rowSkuBuyButtonusageselectedSellercomes from the product context item while the SKU sent comes from the row, so it could sign another SKU's price.selectedSellerresolution inWrapper.tsxto always derive from theselectedItemprop. That would also correctprice/listPrice/sellerfor the per-row case, but it is a behaviour change well beyond this ticket and deserves its own investigation.Related to / Depends on *
Blocked — do not merge yet.
ItemInputonvtex.checkout-graphql(master, 0.68.0) still has nopriceTokenfield, andvtex.checkout-resourcesdeclares the mutation as$items: [ItemInput]. Sending an unknown input field makes GraphQL reject the variable, so on an account where the token is present this would break add to cart. This can only be merged after the Checkout Experience team adds the field — their spike was scheduled for the sprint starting 2026-08-03. We opened checkout-graphql#219 proposing exactly that field, as a starting point for them.Once the field exists, the token reaches checkout through the verb that honours it: this app's mutation goes
addToCart→checkout.addItem→PATCH /orderForm/{id}/items. Guilherme Schirmer confirmed in the thread that onlyPATCHhonourspriceToken—POST /itemsignores it (and is no longer meant to be used), so any integration adding items overPOSTwould drop the token silently. Not a concern here, but worth knowing for anyone reusing this pattern outside the GraphQL path.The
index: 0this app sends is also not a risk of overwriting cart lines: thecheckout-graphqlresolver stripsindex(along withoptionsanduniqueId) before building the REST payload, and an item withoutindexis what makesPATCHadd instead of update.Confirmed at runtime on
b2bstoreqa(price signing enabled,search-graphql@0.72.0, product query patched to request the field):The token itself was correct — claims
{"price":390,"seller":"1","id":"1","accountName":"b2bstoreqa","salesChannel":"1"}, matching the SKU, seller and price of the row that was added, with the expected 30-minute window. So the forwarding works end to end and the only thing missing is the Checkout input field.Status of the chain (per the reference thread):
intsch) price signingsearch-resolver/search-graphql1.106.0/0.72.0, deployed 2026-07-20 — unblocks the prerequisite noted in the ticketcheckout-graphql(ItemInput.priceToken)commertialOffer { priceToken }vtex.product-contextand this PR stays inertMerge order: checkout-graphql#219 → store-resources#199 → this PR. Inverting the last two would make consumers forward a token that the Checkout still rejects.
Other notes:
PriceTokenfromvtex.search-graphql, where the field ispriceToken.yarn lintalready fails onmaster(prettier/prettierparsing errors on the optional chaining inSkuHighlights.tsxandSkuSpecifications.tsx, with the pinned Prettier 1.x). Unrelated to this PR —tsc --noEmit, the tests and ESLint on the touched files all pass.