Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Forward the signed price (`commertialOffer.priceToken`) from the product context as `priceToken` on the `addToCart` payload, so the Checkout can close the cart with the signed price while the Pricing is unavailable (Pricing Fallback V2)

## [1.3.2] - 2026-03-06

### Changed
Expand Down
89 changes: 89 additions & 0 deletions react/__tests__/catalogItemToCart.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { mapCatalogItemToCart } from '../components/BuyButton/modules/catalogItemToCart'
import { Item, Product, Seller } from '../typings'

const makeSeller = (
sellerId: string,
token?: { priceToken?: string; PriceToken?: string }
): Seller => ({
sellerId,
sellerName: `seller ${sellerId}`,
commertialOffer: {
Price: 10,
ListPrice: 12,
PriceWithoutDiscount: 12,
RewardValue: 0,
AvailableQuantity: 5,
Installments: [],
...token,
},
})

const makeItem = (sellers: Seller[]): Item => ({
itemId: '1',
name: 'SKU name',
measurementUnit: 'un',
unitMultiplier: 1,
images: [],
variations: [],
sellers,
})

const product: Product = {
productId: '1',
productName: 'Product name',
productReference: 'ref-1',
linkText: 'product-name',
brand: 'Brand',
brandId: 1,
items: [],
categories: ['/Category/'],
}

const mapWith = (sellers: Seller[], selectedSeller: Seller) =>
mapCatalogItemToCart({
product,
selectedItem: makeItem(sellers),
quantity: 1,
selectedSeller,
})[0]

describe('mapCatalogItemToCart priceToken', () => {
it('takes the token exposed by search-graphql as `priceToken`', () => {
const seller = makeSeller('1', { priceToken: 'signed-price-token' })

expect(mapWith([seller], seller).priceToken).toBe('signed-price-token')
})

it('falls back to the Catalog Search API `PriceToken`', () => {
const seller = makeSeller('1', { PriceToken: 'catalog-price-token' })

expect(mapWith([seller], seller).priceToken).toBe('catalog-price-token')
})

it('leaves the price token undefined when the search does not return one', () => {
const seller = makeSeller('1')

expect(mapWith([seller], seller).priceToken).toBeUndefined()
})

it('does not take the price token of another seller', () => {
const selectedSeller = makeSeller('1')
const otherSeller = makeSeller('2', { priceToken: 'other-seller-token' })

expect(mapWith([otherSeller], selectedSeller).priceToken).toBeUndefined()
})

it('takes the price token of the selected item, not of the product context item', () => {
const sellerOnSelectedItem = makeSeller('1', {
priceToken: 'selected-item-token',
})

const sellerOnContextItem = makeSeller('1', {
priceToken: 'context-item-token',
})

expect(
mapWith([sellerOnSelectedItem], sellerOnContextItem).priceToken
).toBe('selected-item-token')
})
})
3 changes: 3 additions & 0 deletions react/components/BuyButton/AddToCartButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const adjustItemsForMutationInput = (
seller: item.seller,
quantity: item.quantity,
options: item.options,
// 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 } : {}),
}))
Comment on lines +58 to 61
}

Expand Down
33 changes: 31 additions & 2 deletions react/components/BuyButton/modules/catalogItemToCart.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { path } from 'ramda'
import { find, path, pathOr, propEq } from 'ramda'

import {
transformAssemblyOptions,
sumAssembliesPrice,
ParsedAssemblyOptionsMeta,
Option,
} from './assemblyOptions'
import { AssemblyOptionItem, Item, Maybe, Product } from '../../../typings'
import {
AssemblyOptionItem,
Item,
Maybe,
Product,
Seller,
} from '../../../typings'

interface MapCatalogItemToCartArgs {
product: Maybe<Product>
Expand Down Expand Up @@ -34,6 +40,7 @@ export interface MapCatalogItemToCartReturn {
variant: string
skuId: string
imageUrl: string | undefined
priceToken: string | undefined
sellingPriceWithAssemblies: number
options: Option[]
assemblyOptions: ParsedAssemblyOptionsMeta
Expand All @@ -46,6 +53,27 @@ export function mapCatalogItemToCart({
selectedSeller,
assemblyOptions,
}: MapCatalogItemToCartArgs): MapCatalogItemToCartReturn[] {
// Signed price from the search response, forwarded on addToCart so the
// Checkout can close the cart even while the Pricing is unavailable. It has
// to be read from the very SKU and seller sent on addToCart, since it signs
// that seller's price for that item
const sellersFromSelectedItem: Seller[] = pathOr(
[],
['sellers'],
selectedItem
)

const signedSeller = find(
propEq('sellerId', selectedSeller && selectedSeller.sellerId),
sellersFromSelectedItem
)

// `vtex.search-graphql` exposes it as `priceToken` (>= 0.72.0), while the raw
// Catalog Search API returns it as `PriceToken`
const priceToken: string | undefined =
path(['commertialOffer', 'priceToken'], signedSeller) ||
path(['commertialOffer', 'PriceToken'], signedSeller)

return (
product &&
selectedItem &&
Expand All @@ -68,6 +96,7 @@ export function mapCatalogItemToCart({
variant: selectedItem.name,
skuId: selectedItem.itemId,
imageUrl: path(['images', '0', 'imageUrl'], selectedItem),
priceToken,
...transformAssemblyOptions(
path(['items'], assemblyOptions),
path(['inputValues'], assemblyOptions),
Expand Down
7 changes: 7 additions & 0 deletions react/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export interface CommercialOffer {
RewardValue: number
AvailableQuantity: number
Installments: Installment[]
// Signed price generated by the search request that returned this offer.
// Optional because it is only present when price signing is enabled for the
// account. `vtex.search-graphql` exposes it as `priceToken` (>= 0.72.0),
// while the raw Catalog Search API returns it as `PriceToken`
priceToken?: string
PriceToken?: string
}

export interface Installment {
Expand Down Expand Up @@ -416,4 +422,5 @@ export interface OrderFormItemInput {
seller?: string
uniqueId?: string
options?: AssemblyOptionInput[]
priceToken?: string
}