Skip to content

[B2BTEAM-3732] Forward priceToken on addToCart (Pricing Fallback V2) - #185

Draft
wender wants to merge 3 commits into
masterfrom
feature/B2BTEAM-3732_forward-price-token-on-add-to-cart
Draft

[B2BTEAM-3732] Forward priceToken on addToCart (Pricing Fallback V2)#185
wender wants to merge 3 commits into
masterfrom
feature/B2BTEAM-3732_forward-price-token-on-add-to-cart

Conversation

@wender

@wender wender commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do? *

Pricing Fallback V2 (B2BTEAM-3732): captures the signed price returned by the search and forwards it as priceToken in the addToCart payload, so the Checkout can close the cart with that price even while the Pricing is unavailable.

  • react/queries/product.gql and react/queries/productsByCategory.gql now request commertialOffer { priceToken } under sellers.
  • AutocompleteBlock captures the token of the default seller both on onSelect (single-SKU product) and on selectSku (SKU switch), and sends it on callAddUnitToCart.
  • CategoryBlock keeps a priceTokens map by SKU, filled when the quantity is set, and sends it on callAddToCart.

Note on the field name: the raw Catalog Search API exposes it as PriceToken (PascalCase), but search-graphql/search-resolver expose it as priceToken, which is what this code reads.

Two deliberate choices:

  • The token is read from the seller that is actually sent. In CategoryBlock the seller sent to the cart may be the sellerDefault or the first one in the list (pre-existing fallback), so the token is looked up by the already resolved sellerId instead of assuming the default. The token signs accountName + skuId + price + seller + salesChannel, so a token taken from a different seller would not validate against the item we send.
  • priceToken is only added to the payload when the search returns one (conditional spread), keeping it strictly optional, as agreed in the thread: an add to cart must never be blocked by a missing token, since the token only matters during a Pricing incident.

Also carries an unrelated one-liner: .claude/ added to .gitignore, since the folder holds per-developer Claude Code settings that should not be versioned.

How to test it? *

Requires an account where the price signing feature flag is enabled on the Intelligent Search — b2bstoreqa already has it. Confirm with:

GET api.vtexcommercestable.com.br/api/intelligent-search/v1/product-search?an=b2bstoreqa

Depends on the Checkout apps — see Related to / Depends on.

  1. vtex link the app and open a page with quickorder.autocomplete.
  2. Select a product and inspect the product query response — sellers[].commertialOffer.priceToken should be present.
  3. Add it to the cart and inspect the addToCart mutation payload — the item should carry priceToken alongside id, quantity and seller. To tell our request apart from the PDP button in the Network tab: this app sends only { items }, while vtex.add-to-cart-button also sends marketingData and allowedOutdatedData.
  4. For a multi-SKU product, switch the SKU tag before adding and confirm the token belongs to the newly selected SKU.
  5. Repeat on a page with quickorder.category, including a product whose seller comes from the fallback (no sellerDefault), and confirm the token matches the seller that was sent.
  6. Regression: on an account without the flag enabled, both blocks must keep working with no priceToken in the payload.

End-to-end validation of the fallback itself can only be done by intentionally opening the circuit with the Pricing and checking that orders still close, as pointed out in the thread.

Validation status on b2bstoreqa (price signing flag enabled):

  • AutocompleteBlockworks. The Product query returns sellers[].commertialOffer.priceToken, and the token is a JWT whose payload can be decoded to assert it belongs to the seller being sent (data.seller, data.id, data.price in cents, exp - iat = 1800s). SKU 960137919 is a good case: three sellers, two of them with distinct valid tokens, so picking the wrong seller's token would be visible.
  • CategoryBlockdormant, never exercised end to end. productSearch returns priceToken: null for every seller, so the token map is always empty and the field is never added to the payload. The token is not missing at the origin: both the Intelligent Search API (full-text and category navigation) and the Catalog Search API return PriceToken for the same SKUs, and the GraphQL product query preserves it — only productSearch drops it. The same SKU also reports unitMultiplier 0 through product and 1 through productSearch, which suggests the offer is rebuilt by a Checkout simulation in that path, discarding the token. Reported to the Search team; this code starts working with no further change once search-resolver preserves the field, but it should be validated then.

Describe alternatives you've considered, if any. *

  • Fetching the token in a separate, error-tolerant query, so a schema mismatch could not break the product lookup. Dropped in favour of the simpler final shape, since the field is now exposed on search-graphql.
  • Gating the field behind a public app setting. Dropped to avoid shipping a setting that would have to be removed later.

Related to / Depends on *

Search side is done: search-graphql@0.72.0 and search-resolver@1.106.0 were deployed on 2026-07-20, exposing priceToken. On the Intelligent Search the price signing is still behind a feature flag, enabled only for test accounts.

Blocked on vtex-apps/checkout-graphql#219 — do not merge before it ships. ItemInput on vtex.checkout-graphql has no priceToken, confirmed at runtime: sending it fails the mutation with Field "priceToken" is not defined by type ItemInput. Since the search already returns a token on flagged accounts, merging this first would break add to cart. checkout-graphql#219 proposes the field as optional, with the resolver untouched — the rest-spread already forwards it to PATCH /orderForm/{id}/items. The same dependency blocks the equivalent work on Store Framework (store-resources, add-to-cart-button, minicart, store-components) and on vtex-apps/sku-list (B2BTEAM-3748).

The mutation lands on the verb that honors the token. Only PATCH /orderForm/{id}/items honors priceTokenPOST /items ignores it silently, with nothing in the response or in the orderForm to tell you it was dropped. This app is on the honored path: addToCart (vtex.checkout-resources) → the addToCart resolver in checkout-graphqlcheckout.addItem, which issues a PATCH on /api/checkout/pub/orderForm/{id}/items with orderItems (node/clients/checkout.ts) — the same shape FastStore confirmed working. That resolver also strips index and uniqueId from the items before the call, so nothing here depends on the current cart layout; the checkout engine matches the line by SKU + seller, which is a subset of what the token signs.

Out of scope, to be handled in a follow-up: TextAreaBlock and UploadBlock resolve SKUs through this app's own skuFromRefIds resolver (node/resolvers/search/index.ts), which relies on stockkeepingunitidsbyrefids + Checkout simulation — neither returns the token. Covering them needs a Catalog Search API call in node, a new field on ItemsSeller (graphql/types/Refids.graphql), a new outbound-access policy and propagation through ReviewBlock.

Worth confirming with the Checkout team: the token is valid for 30 minutes and cannot be renewed. In CategoryBlock it is captured when the category is opened, so a user browsing for a long time may send an expired token.

Pricing Fallback V2: fetch the signed price (commertialOffer.PriceToken)
in the search-graphql product queries and forward it as priceToken in the
addToCart payload, so the Checkout can close the cart while the Pricing is
unavailable.

Covers AutocompleteBlock and CategoryBlock. TextAreaBlock and UploadBlock
resolve SKUs through the app's own skuFromRefIds resolver, which has no
access to the token, and are left for a follow-up.

The token is only added to the payload when the search actually returns
one, keeping the payload unchanged while the field is not exposed yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vtex-io-ci-cd

vtex-io-ci-cd Bot commented Jul 30, 2026

Copy link
Copy Markdown

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:

  • Patch (backwards-compatible bug fixes)

  • Minor (backwards-compatible functionality)

  • Major (incompatible API changes)

And then you just need to merge your PR when you are ready! There is no need to create a release commit/tag.

  • No thanks, I would rather do it manually 😞

@vtex-io-docs-bot

vtex-io-docs-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

Beep boop 🤖

I noticed you didn't make any changes at the docs/ folder

  • There's nothing new to document 🤔
  • I'll do it later 😞

In order to keep track, I'll create an issue if you decide now is not a good time

  • I just updated 🎉🎉

The raw Catalog Search API exposes the field as PriceToken, but
search-graphql/search-resolver expose it as priceToken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.claude/ holds per-developer Claude Code settings (settings.local.json),
which should not be versioned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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