From 03d8bf28eaef9d7bf86e0311e56ebcd38763b6d9 Mon Sep 17 00:00:00 2001 From: Tiago Gimenes Date: Sat, 11 Jun 2022 11:47:17 -0300 Subject: [PATCH] pick best sku --- .../platforms/vtex/clients/commerce/index.ts | 15 ------ .../vtex/clients/commerce/types/Portal.ts | 2 +- .../src/platforms/vtex/resolvers/product.ts | 2 +- .../api/src/platforms/vtex/resolvers/query.ts | 49 +++++++++++++------ .../platforms/vtex/utils/orderStatistics.ts | 29 +++++++++++ packages/api/src/platforms/vtex/utils/sku.ts | 26 ++++++++++ 6 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 packages/api/src/platforms/vtex/utils/orderStatistics.ts create mode 100644 packages/api/src/platforms/vtex/utils/sku.ts diff --git a/packages/api/src/platforms/vtex/clients/commerce/index.ts b/packages/api/src/platforms/vtex/clients/commerce/index.ts index af9ba70537..ec9ec66af1 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/index.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/index.ts @@ -1,5 +1,4 @@ import { fetchAPI } from '../fetch' -import type { Product } from '../search/types/ProductSearchResult' import type { Context, Options } from '../../index' import type { Brand } from './types/Brand' import type { CategoryTree } from './types/CategoryTree' @@ -153,19 +152,5 @@ export const VtexCommerce = ( body: '{}', }) }, - search: { - slug: ( - slug: string, - options?: { simulation: boolean } - ): Promise => { - const params = new URLSearchParams({ - simulation: `${options?.simulation ?? false}`, // skip simulation for faster queries - }) - - return fetchAPI( - `${base}/api/catalog_system/pub/products/search/${slug}/p?${params.toString()}` - ) - }, - }, } } diff --git a/packages/api/src/platforms/vtex/clients/commerce/types/Portal.ts b/packages/api/src/platforms/vtex/clients/commerce/types/Portal.ts index b6cee9ae6c..c6a16394f1 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/types/Portal.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/types/Portal.ts @@ -6,7 +6,7 @@ export interface CollectionPageType { url: string title: string metaTagDescription: string - pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' + pageType: 'Brand' | 'Category' | 'Department' | 'Subcategory' | 'Product' } export interface FallbackPageType { diff --git a/packages/api/src/platforms/vtex/resolvers/product.ts b/packages/api/src/platforms/vtex/resolvers/product.ts index e0b197ab4d..216bf7d0cb 100644 --- a/packages/api/src/platforms/vtex/resolvers/product.ts +++ b/packages/api/src/platforms/vtex/resolvers/product.ts @@ -87,7 +87,7 @@ export const StoreProduct: Record> & { aggregateRating: () => ({}), offers: (root) => root.sellers - .flatMap((seller) => + .map((seller) => enhanceCommercialOffer({ offer: seller.commertialOffer, seller, diff --git a/packages/api/src/platforms/vtex/resolvers/query.ts b/packages/api/src/platforms/vtex/resolvers/query.ts index 145a602331..24af719693 100644 --- a/packages/api/src/platforms/vtex/resolvers/query.ts +++ b/packages/api/src/platforms/vtex/resolvers/query.ts @@ -1,4 +1,4 @@ -import { BadRequestError } from '../../errors' +import { NotFoundError, BadRequestError } from '../../errors' import { mutateChannelContext, mutateLocaleContext } from '../utils/contex' import { enhanceSku } from '../utils/enhanceSku' import { @@ -19,6 +19,7 @@ import type { } from '../../../__generated__/schema' import type { CategoryTree } from '../clients/commerce/types/CategoryTree' import type { Context } from '../index' +import { isValidSkuId, pickBestSku } from '../utils/sku' export const Query = { product: async (_: unknown, { locator }: QueryProductArgs, ctx: Context) => { @@ -38,28 +39,46 @@ export const Query = { const { loaders: { skuLoader }, - clients: { commerce }, + clients: { commerce, search }, } = ctx - const skuIdFromSlug = async (s: string) => { - // Standard VTEX PDP routes does not contain skuIds. - const [product] = await commerce.search.slug(s).catch(() => []) + try { + const skuId = id ?? slug?.split('-').pop() ?? '' - if (product) { - return product.items[0].itemId + if (!isValidSkuId(skuId)) { + throw new Error('Invalid SkuId') } - // We are not in a standard VTEX PDP route, this means we are in a /slug-skuId/p route - return s?.split('-').pop() ?? '' - } + const sku = await skuLoader.load(skuId) + + return sku + } catch (err) { + if (slug == null) { + throw new BadRequestError(`Missing slug or id`) + } - const skuId = slug ? await skuIdFromSlug(slug) : id + const route = await commerce.catalog.portal.pagetype(`${slug}/p`) - if (skuId !== null) { - return skuLoader.load(skuId) - } + if (route.pageType !== 'Product' || !route.id) { + throw new NotFoundError(`No product found for slug ${slug}`) + } - throw new BadRequestError(`Missing slug or id`) + const { + products: [product], + } = await search.products({ + page: 0, + count: 1, + query: `product:${route.id}`, + }) + + if (!product) { + throw new NotFoundError(`No product found for id ${route.id}`) + } + + const sku = pickBestSku(product.items) + + return enhanceSku(sku, product) + } }, collection: (_: unknown, { slug }: QueryCollectionArgs, ctx: Context) => { const { diff --git a/packages/api/src/platforms/vtex/utils/orderStatistics.ts b/packages/api/src/platforms/vtex/utils/orderStatistics.ts new file mode 100644 index 0000000000..06dd6508a9 --- /dev/null +++ b/packages/api/src/platforms/vtex/utils/orderStatistics.ts @@ -0,0 +1,29 @@ +/** + * More info at: https://en.wikipedia.org/wiki/Order_statistic + */ + +// O(n) search to find the max of an array +export const max = (array: T[], cmp: (a: T, b: T) => number) => { + let best = 0 + + for (let curr = 1; curr < array.length; curr++) { + if (cmp(array[best], array[curr]) < 0) { + best = curr + } + } + + return array[best] +} + +// O(n) search to find the max of an array +export const min = (array: T[], cmp: (a: T, b: T) => number) => { + let best = 0 + + for (let curr = 1; curr < array.length; curr++) { + if (cmp(array[best], array[curr]) > 0) { + best = curr + } + } + + return array[best] +} diff --git a/packages/api/src/platforms/vtex/utils/sku.ts b/packages/api/src/platforms/vtex/utils/sku.ts new file mode 100644 index 0000000000..dc0bd7fe34 --- /dev/null +++ b/packages/api/src/platforms/vtex/utils/sku.ts @@ -0,0 +1,26 @@ +import { min } from './orderStatistics' +import { bestOfferFirst } from './productStock' +import type { Item } from '../clients/search/types/ProductSearchResult' + +/** + * This function implements Portal heuristics for returning the best sku for a product. + * + * The best sku is the one with the best (cheapest available) offer + * */ +export const pickBestSku = (skus: Item[]) => { + const offersBySku = skus.flatMap((sku) => + sku.sellers.map((seller) => ({ + offer: seller.commertialOffer, + sku, + })) + ) + + const best = min(offersBySku, ({ offer: o1 }, { offer: o2 }) => + bestOfferFirst(o1, o2) + ) + + return best.sku +} + +export const isValidSkuId = (skuId: string) => + skuId !== '' && !Number.isNaN(Number(skuId))