feat: send priceToken on addToCart in useQuote [B2BTEAM-3733] - #89
Open
wender wants to merge 8 commits into
Open
feat: send priceToken on addToCart in useQuote [B2BTEAM-3733]#89wender wants to merge 8 commits into
wender wants to merge 8 commits into
Conversation
The `useQuote` mutation applies quote items stored in Master Data, so there
is no live search response to reuse a `PriceToken` from. Fetch a fresh signed
price from the catalog search at the moment the quote is applied and forward
it to `POST /orderForm/{id}/items`, so Checkout can build the cart even while
Pricing is unavailable.
The negotiated price keeps being applied afterwards through
`PUT /orderForm/{id}/items/update`, so the token never changes the price
charged. The token is optional by design (the field is behind a feature flag
on the search API): any failure is logged and the items are added exactly as
before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
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.
|
|
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
|
…TEAM-3733] - Accept both casings: the raw search API returns `PriceToken` (PascalCase), while `search-graphql@0.72.0`/`search-resolver@1.106.0` expose the same value as `priceToken`. - Log price token coverage on `useQuote`, mirroring the add-to-cart with/without token instrumentation the other storefronts are adding so the feature flag rollout can be followed from this app too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…3733]
Validated on b2bstoreqa (price signing flag enabled): the Catalog Search REST
API returns `PriceToken` (PascalCase) and the token is a JWT signed by
`session/data-signer`, valid for 30 minutes, whose claims bind the price to
`{ id, seller, accountName, salesChannel }`.
- Drop the camelCase `priceToken` alias added earlier: that name only exists in
`vtex.search-graphql`, which maps the REST field, so it is unreachable here
and the typing was asserting a field the REST response does not have.
- Document why the sales channel must be forwarded to the search (it is part of
the claims) and that it resolves to the account default when the quote has
none - the same default `addToCart` falls back to.
- Log the sales channel alongside the coverage counters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A local install bumped @vtex/api from 6.50.1 to 6.51.0 and it rode along in the previous commit. Unrelated to this PR, reverted to match master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirmed by Schirmer in the Pricing Fallback thread: `POST /orderForm/{id}/items`
does not honor `priceToken` - only `PATCH` does - and POST is no longer meant to
be used at all. Sending the token on POST would have been silently ignored,
since nothing in the response reports whether the token was consumed.
Items keep being added as new items: `PATCH` only updates existing ones when an
`index` is sent, which this call never does. The cart is cleared right before,
so there is nothing to update anyway.
`RequestHub` gains a `patch` method. Note there is no `patchRaw` in @vtex/api,
so it resolves to the response body and the call site no longer unwraps `.data`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Schirmer checked `POST /orderForm/{id}/items/update` and it does not honor
`priceToken` either, and confirmed PATCH /items is meant to be the only route
used. That matters here: with the price override still on the old route, a
Pricing outage would break `useQuote` at the second call even though the items
had already made it into the cart with a signed price.
Both cart operations now go through `PATCH /orderForm/{id}/items`: adding the
items (no `index`) and overwriting the price with the negotiated one (`index`).
The price payload gains `id` and `seller`, which PATCH requires and the old
route did not, and it carries `priceToken` as well.
Since both calls now hit the same URL, `routes.addToCart` and
`routes.addPriceToItems` collapse into a single `routes.cartItems`, normalized
to the documented path without the trailing slash.
Also fixes comments that described the price override as `PUT /items/update`.
It was a POST - the wrong verb came from the ticket description.
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.
What problem is this solving?
B2BTEAM-3733 — Pricing Fallback V2.
The
useQuotemutation applies a quote to the cart by adding the items with{ id, seller, quantity }and only then overwrites the price with the negotiated value in a second call. Since the final price is already overwritten manually, apriceTokendoes not affect the amount charged — but the initialaddToCartcall can still fail if Pricing is unavailable, blocking the application of already-approved quotes during an incident.Evaluation (the "avaliar" part of the ticket): unlike the standard flow (Portal/SF/FS/quickorder),
useQuotehas no live search to reuse aPriceTokenfrom — items come from a Master Data record. Persisting the token with the quote is not viable either: it is valid for 30 minutes while a quote lives for days. The remaining option is to fetch a fresh signed price at the moment the quote is applied, treating the token as optional.What this PR does:
Catalogclient (JanusClient) that queriesGET /api/catalog_system/pub/products/search?fq=skuId:…&sc=…. This is the exact endpoint where price signing was delivered in part 1 of the initiative (vtex/catalog-search#445), returningitems[].sellers[].commertialOffer.PriceToken. Configured withretries: 1andtimeout: 3000so it never weighs on the critical path.getPriceTokens, which batches 50 SKUs per request, builds a${skuId}-${seller} → tokenmap, and swallows any failure (logger.warn+ empty map). It readsPriceToken(PascalCase), which is the name used by the Catalog Search REST API — the camelCasepriceTokenonly exists invtex.search-graphql, which maps the REST field.PATCH /orderForm/{id}/items. Confirmed in the thread that neitherPOST /itemsnorPOST /items/updatehonorspriceToken, and thatPATCHis meant to be the only route used. Adding the items sends noindex; overwriting the price sendsindex. The price payload gainsidandseller, whichPATCHrequires and the old/items/updatedid not, and it carriespriceTokentoo — otherwise a Pricing outage would still breakuseQuoteon the second call, with the items already in the cart.routes.addToCartandroutes.addPriceToItemscollapse into oneroutes.cartItems.RequestHubgains apatchmethod (there is nopatchRawin@vtex/api, so it resolves to the body and the call site no longer unwraps.data).useQuoteattachespriceTokento eachorderItemwhen one is available. With no token, the payload is what it is today.useQuote-priceTokenCoverage(items with/without token, plus the sales channel). Neither theaddToCartresponse nor theorderFormreports whether the token was received or used, and the fallback only activates during a Pricing outage — so this log is the only practical evidence that tokens are getting through, short of simulating an outage.outbound-accesspolicy for/api/catalog_system/pub/*, catalog typings, and a CHANGELOG entry.Validation on
b2bstoreqa(price signing flag enabled)Read-only
GETs against the account confirm the two assumptions this PR rests on:PriceTokenpresentsalesChannelclaimexp - iatintelligent-search/v1/product-search?an=b2bstoreqa&sc=1catalog_system/pub/products/search?fq=skuId:…&sc=1"1"catalog_system/pub/products/search?fq=skuId:…(nosc)"1"catalog_system/pub/products/search?fq=skuId:…&sc=2intschwork.session/data-signer, 30 minutes of validity, with claims{ price, priceWithoutDiscount, seller, id, accountName, salesChannel }. The signed price is the catalog price, not the negotiated one — which is consistent with this flow: the token only has to get the item into the cart, and the second call then applies the negotiated price as a manual price change.scthe search resolves it to the account default ("1"), the same defaultaddToCartfalls back to when the quote carries no sales channel.End-to-end evidence (workspace
b2bstoreqa/pricetoken)Quote applied through the storefront's "Use quote" button with the app linked. From OpenSearch:
{"level":"info","app":"vtex.b2b-quotes-graphql@4.0.6","account":"b2bstoreqa","workspace":"pricetoken", "data":{"message":"useQuote-priceTokenCoverage","itemsWithPriceToken":1,"totalItems":1,"salesChannel":"1"}}Confirms, from inside the IO service: the catalog search succeeds (no
getPriceTokens-catalogSearchError), the${skuId}-${seller}mapping matches, the sales channel is forwarded, and every item leaves with a token. NoteuseQuoteresolves tonullon success by design (the resolver has no return value), so the mutation response carries no signal — this log is the signal.That run predates the move to
PATCH, so it validates the token plumbing but not the current routes. Still to exercise: a multi-SKU, multi-seller quote (stronger test of the composite key and of the index mapping), the 50-SKU batching, and the degradation path.How to test it?
[Workspace](Link goes here!)
b2bstoreqawas requested for B2B validation;storeframeworkis the FastStore test store).useQuote.useQuote-priceTokenCoveragelog showsitemsWithPriceToken == totalItems.useQuotebehaves exactly as before (no token in the payload, no error).POST /itemsandPOST /items/update→PATCH /items) — the main thing to exercise now, and none of it needs Pricing to be down:itemsAdded.forEach+ theisGiftskip + the parallelquoteItemIndexcounter) assumes the cart returns items in the same order as the quote; ifPATCHreorders or consolidates them, the wrong item gets the wrong price. This is a charged-value bug, not a resilience one, and it is the pre-existing fragility most exposed by this change.${skuId}-${seller}key).salesChannelis not the account default lands on the right channel —?sc=is undocumented onPATCH, so confirm it is still honored.Screenshots or example usage:
N/A — backend only.
Describe alternatives you've considered, if any.
PriceTokenalongside the quote in Master Data: discarded, the token expires in 30 minutes while quotes live for days.search-graphql: it is exposed there sincesearch-graphql@0.72.0, but that would add an app dependency and a GraphQL round trip to a backend-only flow. The raw Catalog Search API is where the field originates and needs no app dependency.Points that need a decision before release
WhetherAnswered: neitherPOST /itemshonors the token.POST /itemsnorPOST /items/updatedoes — onlyPATCH /items, which is meant to be the only route used. Both calls were migrated, so the review focus moves to that change; the remaining risk is behavioral rather than about the token, and item B2BQUOTES-28 #6 under "How to test it?" covers it.PATCH /itemsacceptspriceon insert, so the two calls could in principle collapse into one and the index mapping would disappear entirely. That is a redesign rather than an adaptation, and it needs confirmation that Checkout accepts a manual price at insert time — worth its own ticket if the index mapping keeps causing trouble.outbound-accesspolicy requires merchants to re-accept app permissions on update. If that is undesirable right now, we can hold the merge until the field leaves the feature flag.useQuotepath: one catalog search (cached, ~10 min API Cache) beforeaddToCart. Bounded bytimeout: 3000/retries: 1and fully non-blocking on failure.cy-runner), so no automated tests were added.Not blocking this PR
vtex.checkout-graphql'sItemInputhas nopriceTokenfield (vtex-apps/checkout-graphql#219), which blocks the apps that add to cart through the GraphQL mutation —sku-list(B2BTEAM-3748) andquickorder. This app talks to the Checkout REST API directly, so it does not depend on that fix and can move on its own.Worth flagging to those apps, though: since
POST /itemsdoes not honor the token, addingpriceTokentoItemInputis necessary but may not be sufficient — whatevercheckout-graphqlcalls underneath has to bePATCHtoo.Related to / Depends on
sku-list(B2BTEAM-3748) andquickorder, both blocked oncheckout-graphql.How does this PR make you feel? 🔗