Skip to content

Commit

Permalink
feat: implement offer enhancement and fetching logic with aggregation
Browse files Browse the repository at this point in the history
  • Loading branch information
emersonlaurentino committed Feb 3, 2025
1 parent 4d5dc0d commit 23aa36c
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 89 deletions.
16 changes: 12 additions & 4 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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'
182 changes: 102 additions & 80 deletions packages/core/src/components/sections/ProductDetails/ProductDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,96 +144,118 @@ function ProductDetails({

return (
<Section className={`${styles.section} section-product-details`}>
<section data-fs-product-details>
<section data-fs-product-details-body data-fs-content="product-details">
<header data-fs-product-details-title data-fs-product-details-section>
<ProductTitle.Component
// TODO: We should review this prop. There's now way to override the title and use the dynamic name value.
// Maybe passing a ProductTitleHeader component as a prop would be better, as it would be overridable.
// Maybe now it's worth to make title always a h1 and receive only the name, as it would be easier for users to override.
title={<h1>{name}</h1>}
{...ProductTitle.props}
label={
showDiscountBadge && (
<DiscountBadge.Component
{...DiscountBadge.props}
size={discountBadgeSize ?? DiscountBadge.props.size}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
listPrice={listPrice}
spotPrice={lowPrice}
/>
)
}
refNumber={showRefNumber && productId}
/>
</header>
<ImageGallery.Component
data-fs-product-details-gallery
{...ImageGallery.props}
images={productImages}
/>
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
{isValidating ? (
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<p>Loading...</p>
</section>
</section>
) : (
<section data-fs-product-details>
<section
data-fs-product-details-body
data-fs-content="product-details"
>
<header
data-fs-product-details-title
data-fs-product-details-section
>
<ProductDetailsSettings
product={product}
isValidating={isValidating}
buyButtonTitle={buyButtonTitle}
quantity={quantity}
setQuantity={setQuantity}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
<ProductTitle.Component
// TODO: We should review this prop. There's now way to override the title and use the dynamic name value.
// Maybe passing a ProductTitleHeader component as a prop would be better, as it would be overridable.
// Maybe now it's worth to make title always a h1 and receive only the name, as it would be easier for users to override.
title={<h1>{name}</h1>}
{...ProductTitle.props}
label={
showDiscountBadge && (
<DiscountBadge.Component
{...DiscountBadge.props}
size={discountBadgeSize ?? DiscountBadge.props.size}
// Dynamic props shouldn't be overridable
// This decision can be reviewed later if needed
listPrice={listPrice}
spotPrice={lowPrice}
/>
)
}
refNumber={showRefNumber && productId}
/>
</header>
<ImageGallery.Component
data-fs-product-details-gallery
{...ImageGallery.props}
images={productImages}
/>
<section data-fs-product-details-info>
<section
data-fs-product-details-settings
data-fs-product-details-section
>
<ProductDetailsSettings
product={product}
isValidating={isValidating}
buyButtonTitle={buyButtonTitle}
quantity={quantity}
setQuantity={setQuantity}
buyButtonIcon={buyButtonIcon}
notAvailableButtonTitle={
notAvailableButtonTitle ?? NotAvailableButton.props.title
}
/>
</section>

{!outOfStock && (
<ShippingSimulation.Component
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={
shippingSimulatorTitle ?? ShippingSimulation.props.title
}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
/>
)}
</section>

{!outOfStock && (
<ShippingSimulation.Component
data-fs-product-details-section
data-fs-product-details-shipping
formatter={useFormattedPrice}
{...ShippingSimulation.props}
idkPostalCodeLinkProps={{
...ShippingSimulation.props.idkPostalCodeLinkProps,
href:
shippingSimulatorLinkUrl ??
ShippingSimulation.props.idkPostalCodeLinkProps?.href,
children:
shippingSimulatorLinkText ??
ShippingSimulation.props.idkPostalCodeLinkProps?.children,
}}
productShippingInfo={{
id,
quantity,
seller: seller.identifier,
}}
title={shippingSimulatorTitle ?? ShippingSimulation.props.title}
inputLabel={
shippingSimulatorInputLabel ??
ShippingSimulation.props.inputLabel
}
optionsLabel={
shippingSimulatorOptionsTableTitle ??
ShippingSimulation.props.optionsLabel
}
{shouldDisplayProductDescription && (
<ProductDescription
initiallyExpanded={productDescriptionInitiallyExpanded}
descriptionData={[
{
title: productDescriptionDetailsTitle,
content: description,
},
]}
/>
)}
</section>

{shouldDisplayProductDescription && (
<ProductDescription
initiallyExpanded={productDescriptionInitiallyExpanded}
descriptionData={[
{ title: productDescriptionDetailsTitle, content: description },
]}
/>
)}
</section>
</section>
)}
</Section>
)
}
Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/pages/[slug]/p.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 (
<GlobalSections {...globalSections}>
<Head>
<link
rel="preload"
href={getOfferUrl(product.sku)}
as="fetch"
crossOrigin="anonymous"
fetchPriority="high"
/>
</Head>
{/* SEO */}
<NextSeo
title={meta.title}
Expand Down Expand Up @@ -256,6 +265,7 @@ export const getStaticProps: GetStaticProps<
globalSections,
key: seo.canonical,
},
revalidate: 300,
}
}

Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/sdk/offer/aggregate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Item, Seller } from '@faststore/api'
import { EnhancedCommercialOffer } from './enhance'
import { inStock, price } from './sort'

type Root = EnhancedCommercialOffer<Seller, Item>

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,
}
}
20 changes: 20 additions & 0 deletions packages/core/src/sdk/offer/enhance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommertialOffer } from '@faststore/api'

export type EnhancedCommercialOffer<S, P> = CommertialOffer & {
seller: S
product: P
}

export const enhanceCommercialOffer = <S, P>({
offer,
seller,
product,
}: {
offer: CommertialOffer
seller: S
product: P
}): EnhancedCommercialOffer<S, P> => ({
...offer,
product,
seller,
})
19 changes: 19 additions & 0 deletions packages/core/src/sdk/offer/fetcher.ts
Original file line number Diff line number Diff line change
@@ -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<ProductSearchResult>
}
Loading

0 comments on commit 23aa36c

Please sign in to comment.