From 23aa36c240c301acb8642cb1184f91191ac5f0cc Mon Sep 17 00:00:00 2001 From: Emerson Laurentino Date: Mon, 3 Feb 2025 10:58:18 -0300 Subject: [PATCH] feat: implement offer enhancement and fetching logic with aggregation --- packages/api/src/index.ts | 16 +- .../ProductDetails/ProductDetails.tsx | 182 ++++++++++-------- packages/core/src/pages/[slug]/p.tsx | 20 +- packages/core/src/sdk/offer/aggregate.ts | 52 +++++ packages/core/src/sdk/offer/enhance.ts | 20 ++ packages/core/src/sdk/offer/fetcher.ts | 19 ++ packages/core/src/sdk/offer/index.ts | 45 +++++ packages/core/src/sdk/offer/sort.ts | 28 +++ 8 files changed, 293 insertions(+), 89 deletions(-) create mode 100644 packages/core/src/sdk/offer/aggregate.ts create mode 100644 packages/core/src/sdk/offer/enhance.ts create mode 100644 packages/core/src/sdk/offer/fetcher.ts create mode 100644 packages/core/src/sdk/offer/index.ts create mode 100644 packages/core/src/sdk/offer/sort.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 139aeb5155..cbd36882ef 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -24,11 +24,12 @@ const platforms = { }, } -const directives: Directive[] = [ - cacheControlDirective -] +const directives: Directive[] = [cacheControlDirective] -export const getTypeDefs = () => [typeDefs, ...directives.map(d => d.typeDefs)] +export const getTypeDefs = () => [ + typeDefs, + ...directives.map((d) => d.typeDefs), +] export const getResolvers = (options: Options) => platforms[options.platform].getResolvers(options) @@ -47,3 +48,10 @@ export const getSchema = async (options: Options) => { export * from './platforms/vtex/resolvers/root' export type { Resolver } from './platforms/vtex' + +export type { + CommertialOffer, + Item, + ProductSearchResult, + Seller, +} from './platforms/vtex/clients/search/types/ProductSearchResult' diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index 89ef215ab4..1477e901c3 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -144,96 +144,118 @@ function ProductDetails({ return (
-
-
-
- {name}} - {...ProductTitle.props} - label={ - showDiscountBadge && ( - - ) - } - refNumber={showRefNumber && productId} - /> -
- -
-
+
+

Loading...

+
+
+ ) : ( +
+
+
- {name}} + {...ProductTitle.props} + label={ + showDiscountBadge && ( + + ) } + refNumber={showRefNumber && productId} /> +
+ +
+
+ +
+ + {!outOfStock && ( + + )}
- {!outOfStock && ( - )}
- - {shouldDisplayProductDescription && ( - - )}
-
+ )}
) } diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index 89692f1389..10887cd5d8 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -30,6 +30,8 @@ import GlobalSections, { import storeConfig from '../../../faststore.config' import { useProductQuery } from 'src/sdk/product/useProductQuery' import PageProvider, { PDPContext } from 'src/sdk/overrides/PageProvider' +import { getOfferUrl, useOffer } from 'src/sdk/offer' +import Head from 'next/head' /** * Sections: Components imported from each store's custom components and '../components/sections' only. @@ -62,20 +64,27 @@ function Page({ data: server, sections, globalSections, offers, meta }: Props) { const { product } = server const { currency } = useSession() - // Stale while revalidate the product for fetching the new price etc - const { data: client, isValidating } = useProductQuery(product.id, { - product: product, - }) + const offer = useOffer({ skuId: product.sku }) + const client = { product: { offers: offer.offers } } const context = { data: { ...deepmerge(server, client, { arrayMerge: overwriteMerge }), - isValidating, + isValidating: offer.isValidating, }, } as PDPContext return ( + + + {/* SEO */} + +const withTax = ( + price: number, + tax: number = 0, + unitMultiplier: number = 1 +) => { + const unitTax = tax / unitMultiplier + return Math.round((price + unitTax) * 100) / 100 +} + +const getHighPrice = ( + offers: Root[], + options: { includeTaxes: boolean } = { includeTaxes: false } +) => { + const availableOffers = offers.filter(inStock) + const highOffer = availableOffers[availableOffers.length - 1] + const highPrice = highOffer ? price(highOffer) : 0 + if (!options.includeTaxes) { + return highPrice + } + + return withTax(highPrice, highOffer?.Tax, highOffer?.product?.unitMultiplier) +} + +const getLowPrice = ( + offers: Root[], + options: { includeTaxes: boolean } = { includeTaxes: false } +) => { + const [lowOffer] = offers.filter(inStock) + + const lowPrice = lowOffer ? price(lowOffer) : 0 + + if (!options.includeTaxes) { + return lowPrice + } + + return withTax(lowPrice, lowOffer?.Tax, lowOffer?.product?.unitMultiplier) +} + +export function aggregateOffer(offers: Root[]) { + return { + highPrice: getHighPrice(offers), + lowPrice: getLowPrice(offers), + lowPriceWithTaxes: getLowPrice(offers, { includeTaxes: true }), + offerCount: offers.length, + } +} diff --git a/packages/core/src/sdk/offer/enhance.ts b/packages/core/src/sdk/offer/enhance.ts new file mode 100644 index 0000000000..e3d83ec465 --- /dev/null +++ b/packages/core/src/sdk/offer/enhance.ts @@ -0,0 +1,20 @@ +import { CommertialOffer } from '@faststore/api' + +export type EnhancedCommercialOffer = CommertialOffer & { + seller: S + product: P +} + +export const enhanceCommercialOffer = ({ + offer, + seller, + product, +}: { + offer: CommertialOffer + seller: S + product: P +}): EnhancedCommercialOffer => ({ + ...offer, + product, + seller, +}) diff --git a/packages/core/src/sdk/offer/fetcher.ts b/packages/core/src/sdk/offer/fetcher.ts new file mode 100644 index 0000000000..1618d1dd34 --- /dev/null +++ b/packages/core/src/sdk/offer/fetcher.ts @@ -0,0 +1,19 @@ +import { ProductSearchResult } from '@faststore/api' +import { api, storeUrl } from '../../../faststore.config' + +const IS_PROD = process.env.NODE_ENV === 'production' + +export function getUrl(skuId: string) { + const base = IS_PROD + ? storeUrl + : `https://${api.storeId}.${api.environment}.com.br` + const url = new URL(`${base}/api/intelligent-search/product_search`) + url.searchParams.append('query', `sku.id:${skuId}`) + return url.toString() +} + +export async function fetcher(skuId: string) { + return fetch(getUrl(skuId)).then((res) => + res.json() + ) as Promise +} diff --git a/packages/core/src/sdk/offer/index.ts b/packages/core/src/sdk/offer/index.ts new file mode 100644 index 0000000000..ad086486bb --- /dev/null +++ b/packages/core/src/sdk/offer/index.ts @@ -0,0 +1,45 @@ +import useSWR from 'swr' +import { aggregateOffer } from './aggregate' +import { enhanceCommercialOffer } from './enhance' +import { fetcher } from './fetcher' +import { bestOfferFirst } from './sort' +export { getUrl as getOfferUrl } from './fetcher' + +const ERROR_DATA = { offers: {}, isValidating: false } + +export function useOffer(args: { skuId: string }) { + const { data, error, isValidating } = useSWR(args.skuId, fetcher) + + if (error || !data || data.products.length === 0) { + console.warn('Error or no data fetching offer to SKU', args.skuId, error) + return ERROR_DATA + } + + const product = data.products[0] + + if (!product || product.items.length === 0) { + console.warn('Product not found or has no items for SKU', args.skuId) + return ERROR_DATA + } + + const item = product.items.find((item) => item.itemId === args.skuId) + + if (!item) { + console.warn('Item not found for SKU', args.skuId) + return ERROR_DATA + } + + const sellers = item.sellers + .map((seller) => + enhanceCommercialOffer({ + offer: seller.commertialOffer, + seller, + product: item, + }) + ) + .sort(bestOfferFirst) + + const offers = aggregateOffer(sellers) + + return { offers, isValidating } +} diff --git a/packages/core/src/sdk/offer/sort.ts b/packages/core/src/sdk/offer/sort.ts new file mode 100644 index 0000000000..410530b0c1 --- /dev/null +++ b/packages/core/src/sdk/offer/sort.ts @@ -0,0 +1,28 @@ +import type { CommertialOffer } from '@faststore/api' + +export const inStock = (offer: Pick) => + offer.AvailableQuantity > 0 + +export const price = (offer: Pick) => + offer.spotPrice ?? 0 + +export const availability = (available: boolean) => + available ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock' + +export const bestOfferFirst = ( + a: Pick, + b: Pick +) => { + if (inStock(a) && !inStock(b)) { + return -1 + } + + if (!inStock(a) && inStock(b)) { + return 1 + } + + return price(a) - price(b) +} + +export const inStockOrderFormItem = (itemAvailability: string) => + itemAvailability === 'available'