Skip to content

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

Merged
wender merged 3 commits into
masterfrom
feature/B2BTEAM-3732_forward-price-token-on-add-to-cart
Sep 22, 2026
Merged

wender merged 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):

  • AutocompleteBlock — works. 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.
  • CategoryBlock — dormant, 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 priceToken — POST /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-graphql → checkout.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>
@wender
wender marked this pull request as ready for review August 13, 2026 14:05
wender added a commit to vtex-apps/checkout-graphql that referenced this pull request Sep 21, 2026
…ack V2) (#219)

#### What problem is this solving?

**Pricing Fallback V2**
([B2BTEAM-3748](https://vtex-dev.atlassian.net/browse/B2BTEAM-3748),
[reference Slack
thread](https://vtex.slack.com/archives/C07N5C1GWCB/p1764774437017629)):
Intelligent Search now signs the price of each offer and returns it as
`priceToken`, so a storefront can forward that signed price on add to
cart and the platform can keep closing carts even while the Pricing
system is down.

Everything upstream of this app is already in production:

| Layer | Status |
|---|---|
| `intsch` price signing (`VTEX.Signer` sidecar) | ✅ production since
2026-07-15, behind a per-account feature flag |
| `vtex.search-resolver@1.106.0` / `vtex.search-graphql@0.72.0` | ✅
deployed 2026-07-20 — exposes `priceToken` on the `Offer` type |
| `PATCH /api/checkout/pub/orderForm/{id}/items` | ✅ already accepts
`priceToken` per order item (confirmed by Guilherme Schirmer in the
thread) |
| **`vtex.checkout-graphql` (`ItemInput`)** | ❌ **this PR** — the field
does not exist, so the storefront cannot send it |

**This app is the blocker of the whole chain.** Because `ItemInput` has
no `priceToken`, any storefront that forwards the token gets its
variable rejected before the request reaches the resolver:

```
GraphQL error: Variable "$items" got invalid value
{ id: 1, index: 0, seller: "1", quantity: 1, options: [], priceToken: "eyJhbGciOiJFUzI1NiIs…" }
at "items[0]"; Field "priceToken" is not defined by type ItemInput.
```

That error was reproduced on `b2bstoreqa` with price signing enabled and
is currently blocking, in parallel:

- `vtex.store-resources` — the product query cannot even be prepared to
expose the field usefully ([store-resources
PR](vtex-apps/store-resources#199))
- Store Framework — `vtex.add-to-cart-button`, `vtex.minicart`,
`vtex.store-components`, per the activity list mapped by Wisney Cardeal
in the thread
- B2B Suite —
[sku-list#17](vtex-apps/sku-list#17),
[quickorder#185](vtex-apps/quickorder#185)

We are not the owners of this app, so this PR is offered as a starting
point for the Checkout Experience team rather than something to merge as
is — Thaynan Nunes scheduled a spike for the sprint starting 2026-08-03
precisely to size this change. If the direction is right, it is already
validated end to end (see below).

**What the change is:**

- `graphql/types/Item.graphql`: optional `priceToken: String` on `input
ItemInput`, with a docstring explaining the semantics.
- `node/typings/global.d.ts`: matching optional `priceToken?: string` on
`OrderFormItemInput`.

**No resolver change was needed**, and that is deliberate:

- `addToCart` builds its REST payload with `items.map(({ options, index,
uniqueId, ...rest }) => ...)`, so `priceToken` already flows through
`rest` into `checkout.addItem` → `PATCH /orderForm/{id}/items`, which
supports the field.
- `updateItems` strips only `id`, so it forwards the token as well.
- `Checkout.addItem` types items as `Omit<OrderFormItemInput, 'uniqueId'
| 'index' | 'options'>`, so the new field is included automatically.

**Backwards compatibility.** The field is optional and input-only: when
a storefront does not send it, the payload reaching the checkout REST
API is byte-for-byte identical to today, and no existing caller has to
change. The builder classified the change by itself during `vtex link`:

```
New GraphQL route types or parameters have been added since the previously published version of the app.
New features: ItemInput.priceToken was added.
```

i.e. an additive change, publishable as a minor. The token is also
deliberately **not** exposed on the `Item` output type — it is signed
data that only needs to travel inbound.

#### How should this be manually tested?

Validated on [b2bstoreqa /
pricetoken](https://pricetoken--b2bstoreqa.myvtex.com/notebook-razer/p)
with three linked apps: this one, `vtex.store-resources` (product query
requesting the field) and `vtex.sku-list` (forwarding it on add to
cart). Requires an account with price signing enabled on Intelligent
Search — it is still behind a feature flag, and the search team enables
it on request.

1. Confirm the search returns the token: `GET
/api/intelligent-search/v1/product-search?an={account}` →
`items[].sellers[].commertialOffer.PriceToken`.
2. Add an item to the cart from a storefront that forwards the token, or
send the mutation directly with `items[0].priceToken`.
3. Before this change: the request fails with the validation error
above. After it: the mutation succeeds and the item is added normally.
4. Decoding the forwarded token shows claims bound to the item that was
added —
`{"price":390,"priceWithoutDiscount":390,"seller":"1","id":"1","accountName":"b2bstoreqa","salesChannel":"1"}`,
valid for 30 minutes.
5. Regression: repeat without `priceToken` in the payload and confirm
the behaviour is unchanged.

Note that the fallback itself is only exercised during a Pricing outage,
so nothing about the signed price is observable in the `addToCart`
response or in the orderForm. Christian Mutti's guidance in the thread
is that the real end-to-end test is to intentionally open the circuit
with the Pricing and watch carts still closing — that part is out of
reach for us here.

#### Checklist/Reminders

- [ ] Updated `README.md` — not applicable, no documented behaviour
changes for existing callers.
- [x] Updated `CHANGELOG.md`.
- [x] Linked this PR to a Jira story —
[B2BTEAM-3748](https://vtex-dev.atlassian.net/browse/B2BTEAM-3748) (B2B
side of the initiative).
- [ ] Updated/created tests — happy to add coverage to
`node/__tests__/items-mutations.test.ts` if the team wants the
pass-through pinned by a test; the current change is schema-only.
- [ ] Deleted the workspace after merging this PR — the `pricetoken`
workspace is ours and will be unlinked regardless of what happens to
this PR.

#### Type of changes

✔️ | Type of Change
---|---
_ | Bug fix
✔️ | New feature
_ | Breaking change
_ | Technical improvements

#### Notes

Two things worth deciding with the initiative owners rather than in this
PR:

- **Whether the token should ever be required.** Both Christian Mutti
and Guilherme Schirmer stated in the thread that it must stay optional —
it is only used during an incident, and an add to cart without a token
must keep working. This PR follows that.
- **Observability.** There is no way to confirm from the API surface
that a token arrived. The plan mentioned in the thread is metrics (a
metrics PR on `intsch`, plus "instrument add-to-cart metrics with and
without token" on the Store Framework list). If this app should emit
anything on its side, that is a natural follow-up and we did not presume
it here.

[B2BTEAM-3748]:
https://vtex-dev.atlassian.net/browse/B2BTEAM-3748?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[B2BTEAM-3748]:
https://vtex-dev.atlassian.net/browse/B2BTEAM-3748?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Lucas Vyskubenko <lucas.vysk@vtex.com.br>
@wender
wender merged commit b3afd0a into master Sep 22, 2026
5 of 6 checks passed
@wender
wender deleted the feature/B2BTEAM-3732_forward-price-token-on-add-to-cart branch September 22, 2026 14:57
@vtex-io-ci-cd

vtex-io-ci-cd Bot commented Sep 22, 2026

Copy link
Copy Markdown

Your PR has been merged! App is being published. 🚀
Version 3.16.8 → 3.16.9

After the publishing process has been completed (check #vtex-io-releases) and doing A/B tests with the new version, you can deploy your release by running:

vtex deploy vtex.quickorder@3.16.9

After that your app will be updated on all accounts.

For more information on the deployment process check the docs. 📖

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