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/discovery.config.default.js b/packages/core/discovery.config.default.js
index 70aab9636f..1f63b07d41 100644
--- a/packages/core/discovery.config.default.js
+++ b/packages/core/discovery.config.default.js
@@ -102,5 +102,6 @@ module.exports = {
noRobots: false,
preact: false,
enableRedirects: false,
+ revalidate: 300, // Revalidate every 5 minutes
},
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 4eefe3c11d..11986a687e 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -80,7 +80,7 @@
"sass-loader": "^12.6.0",
"sharp": "^0.32.6",
"style-loader": "^3.3.1",
- "swr": "^1.3.0",
+ "swr": "^2.2.5",
"tsx": "^4.6.2",
"typescript": "4.7.3"
},
diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx
index cd77ef01ff..fe80b4d79e 100644
--- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx
+++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx
@@ -192,61 +192,77 @@ function ProductDetails({
{...ImageGallery.props}
images={productImages}
/>
-
-
- {!outOfStock && (
-
+
+
+ ) : (
+
+ >
+
+
+
+ {!outOfStock && (
+
+ )}
+
+ )}
{shouldDisplayProductDescription && (
+
+
+
{/* 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..39862981c5
--- /dev/null
+++ b/packages/core/src/sdk/offer/fetcher.ts
@@ -0,0 +1,23 @@
+import { ProductSearchResult } from '@faststore/api'
+import { api, storeUrl } from '../../../discovery.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}`)
+ if (IS_PROD) {
+ url.searchParams.append('workspace', 'chrs')
+ }
+
+ 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'
diff --git a/packages/core/src/sdk/product/useProductLink.ts b/packages/core/src/sdk/product/useProductLink.ts
index 289bbcddc8..e810f96d70 100644
--- a/packages/core/src/sdk/product/useProductLink.ts
+++ b/packages/core/src/sdk/product/useProductLink.ts
@@ -1,8 +1,6 @@
import type { CurrencyCode, SelectItemEvent } from '@faststore/sdk'
-import { useCallback } from 'react'
-
import type { ProductSummary_ProductFragment } from '@generated/graphql'
-
+import { useCallback } from 'react'
import type { AnalyticsItem, SearchSelectItemEvent } from '../analytics/types'
import { useSession } from '../session'
diff --git a/turbo.json b/turbo.json
index 2d6c20c26f..799820723d 100644
--- a/turbo.json
+++ b/turbo.json
@@ -22,6 +22,7 @@
"dependsOn": ["^build"]
},
"lint": {},
+ "serve": {},
"start": {
"outputs": ["dist/**"]
},
diff --git a/yarn.lock b/yarn.lock
index c732799755..8da4f3458f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16869,7 +16869,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -16887,15 +16887,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz"
@@ -16990,7 +16981,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17018,13 +17009,6 @@ strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz"
@@ -17303,10 +17287,13 @@ swap-case@^2.0.2:
dependencies:
tslib "^2.0.3"
-swr@^1.3.0:
- version "1.3.0"
- resolved "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz"
- integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
+swr@^2.2.5:
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.5.tgz#063eea0e9939f947227d5ca760cc53696f46446b"
+ integrity sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==
+ dependencies:
+ client-only "^0.0.1"
+ use-sync-external-store "^1.2.0"
symbol-observable@^1.1.0:
version "1.2.0"
@@ -18270,6 +18257,11 @@ urlpattern-polyfill@^8.0.0:
resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz"
integrity sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==
+use-sync-external-store@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
+ integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
+
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@@ -18701,7 +18693,7 @@ wordwrap@^1.0.0:
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -18735,15 +18727,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"