diff --git a/vtex/utils/extensions/simulation.ts b/vtex/utils/extensions/simulation.ts index 3c9a77473..f4887b4e0 100644 --- a/vtex/utils/extensions/simulation.ts +++ b/vtex/utils/extensions/simulation.ts @@ -3,7 +3,11 @@ import { AppContext } from "../../mod.ts"; import { batch } from "../batch.ts"; import { OpenAPI } from "../openapi/vcs.openapi.gen.ts"; import { getSegmentFromBag, isAnonymous } from "../segment.ts"; -import { aggregateOffers } from "../transform.ts"; +import { + aggregateOffers, + SCHEMA_LIST_PRICE, + SCHEMA_SALE_PRICE, +} from "../transform.ts"; type Item = NonNullable< OpenAPI["POST /api/checkout/pub/orderForms/simulation"]["response"]["items"] @@ -91,50 +95,53 @@ export const extension = async (products: Product[], ctx: AppContext) => { } } - const fixOffer = (product: ProductLeaf): ProductLeaf => { - if (!product.offers) { - return product; - } - - const offers = product.offers.offers.map((o) => { - const offer = mapped.get(product.productID)?.get(o.seller!); + const fixOffer = (product: ProductLeaf): void => { + if (!product.offers) return; + + const skuOffers = mapped.get(product.productID); + if (!skuOffers) return; + + let changed = false; + for (const o of product.offers.offers) { + const simulated = skuOffers.get(o.seller!); + if (!simulated) continue; + + const salePrice = simulated.price != null + ? simulated.price / 100 + : o.price; + const listPrice = simulated.listPrice != null + ? simulated.listPrice / 100 + : undefined; + if (salePrice !== o.price) { + o.price = salePrice; + changed = true; + } - if (!offer) { - return o; + for (const spec of o.priceSpecification) { + if (spec.priceType === SCHEMA_SALE_PRICE) { + spec.price = salePrice; + } else if (spec.priceType === SCHEMA_LIST_PRICE) { + spec.price = listPrice ?? spec.price; + } } + } - const salePrice = offer.price ? offer.price / 100 : o.price; - const listPrice = offer.listPrice && offer.listPrice / 100; - - return { - ...o, - price: salePrice, - priceSpecification: o.priceSpecification.map((spec) => - spec.priceType === "https://schema.org/SalePrice" - ? ({ ...spec, price: salePrice }) - : spec.priceType === "https://schema.org/ListPrice" - ? ({ ...spec, price: listPrice ?? spec.price }) - : spec - ), - }; - }); - - const aggregated = aggregateOffers( - offers, - product.offers.priceCurrency, - ); - - return { - ...product, - offers: aggregated, - }; + if (changed) { + product.offers = aggregateOffers( + product.offers.offers, + product.offers.priceCurrency, + ); + } }; - return products?.map((p) => ({ - ...fixOffer(p), - isVariantOf: p.isVariantOf && { - ...p.isVariantOf, - hasVariant: p.isVariantOf.hasVariant.map(fixOffer), - }, - })) ?? null; + for (const p of products) { + fixOffer(p); + if (p.isVariantOf) { + for (const variant of p.isVariantOf.hasVariant) { + fixOffer(variant); + } + } + } + + return products; }; diff --git a/vtex/utils/transform.ts b/vtex/utils/transform.ts index c6dc0974c..63b87cf98 100644 --- a/vtex/utils/transform.ts +++ b/vtex/utils/transform.ts @@ -5,11 +5,14 @@ import type { DayOfWeek, Filter, FilterToggleValue, + ItemAvailability, Offer, OpeningHoursSpecification, PageType, Place, PostalAddress, + PriceComponentTypeEnumeration, + PriceTypeEnumeration, Product, ProductDetailsPage, ProductGroup, @@ -50,6 +53,17 @@ import type { const DEFAULT_CATEGORY_SEPARATOR = ">"; +export const SCHEMA_LIST_PRICE: PriceTypeEnumeration = + "https://schema.org/ListPrice"; +export const SCHEMA_SALE_PRICE: PriceTypeEnumeration = + "https://schema.org/SalePrice"; +export const SCHEMA_SRP: PriceTypeEnumeration = "https://schema.org/SRP"; +export const SCHEMA_INSTALLMENT: PriceComponentTypeEnumeration = + "https://schema.org/Installment"; +export const SCHEMA_IN_STOCK: ItemAvailability = "https://schema.org/InStock"; +export const SCHEMA_OUT_OF_STOCK: ItemAvailability = + "https://schema.org/OutOfStock"; + const isLegacySku = (sku: LegacySkuVTEX | SkuVTEX): sku is LegacySkuVTEX => typeof (sku as LegacySkuVTEX).variations?.[0] === "string" || !!(sku as LegacySkuVTEX).Videos; @@ -170,8 +184,7 @@ export const toProductPage = ( }; }; -export const inStock = (offer: Offer) => - offer.availability === "https://schema.org/InStock"; +export const inStock = (offer: Offer) => offer.availability === SCHEMA_IN_STOCK; // Smallest Available Spot Price First export const bestOfferFirst = (a: Offer, b: Offer) => { @@ -386,12 +399,15 @@ export const toProduct =

( isLegacyProduct(product) ? toOfferLegacy : toOffer, ); + const variantOptions = imagesByKey !== options.imagesByKey + ? { ...options, imagesByKey } + : options; const isVariantOf = level < 1 ? ({ "@type": "ProductGroup", productGroupID: productId, hasVariant: items.map((sku) => - toProduct(product, sku, 1, { ...options, imagesByKey }) + toProduct(product, sku, 1, variantOptions) ), url: getProductGroupURL(baseUrl, product).href, name: product.productName, @@ -440,10 +456,18 @@ export const toProduct =

( const categoryAdditionalProperties = toAdditionalPropertyCategories(product); const clusterAdditionalProperties = toAdditionalPropertyClusters(product); - const additionalProperty = specificationsAdditionalProperty - .concat(categoryAdditionalProperties ?? []) - .concat(clusterAdditionalProperties ?? []) - .concat(referenceIdAdditionalProperty ?? []); + const additionalProperty: PropertyValue[] = [ + ...specificationsAdditionalProperty, + ]; + if (categoryAdditionalProperties) { + additionalProperty.push(...categoryAdditionalProperties); + } + if (clusterAdditionalProperties) { + additionalProperty.push(...clusterAdditionalProperties); + } + if (referenceIdAdditionalProperty) { + additionalProperty.push(...referenceIdAdditionalProperty); + } estimatedDateArrival && additionalProperty.push({ "@type": "PropertyValue", @@ -632,15 +656,15 @@ const toAdditionalPropertiesLegacy = (sku: LegacySkuVTEX): PropertyValue[] => { }) as const, ); - return [...specificationProperties, ...attachmentProperties]; + if (attachmentProperties.length === 0) return specificationProperties; + specificationProperties.push(...attachmentProperties); + return specificationProperties; }; -const toOffer = ({ - commertialOffer: offer, - sellerId, - sellerName, - sellerDefault, -}: SellerVTEX): Offer => ({ +const buildOffer = ( + { commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX, + teasers: Teasers[], +): Offer => ({ "@type": "Offer", identifier: sellerDefault ? "default" : undefined, price: offer.spotPrice ?? offer.Price, @@ -649,28 +673,28 @@ const toOffer = ({ priceValidUntil: offer.PriceValidUntil, inventoryLevel: { value: offer.AvailableQuantity }, giftSkuIds: offer.GiftSkuIds ?? [], - teasers: offer.teasers ?? [], + teasers, priceSpecification: [ { "@type": "UnitPriceSpecification", - priceType: "https://schema.org/ListPrice", + priceType: SCHEMA_LIST_PRICE, price: offer.ListPrice, }, { "@type": "UnitPriceSpecification", - priceType: "https://schema.org/SalePrice", + priceType: SCHEMA_SALE_PRICE, price: offer.Price, }, { "@type": "UnitPriceSpecification", - priceType: "https://schema.org/SRP", + priceType: SCHEMA_SRP, price: offer.PriceWithoutDiscount, }, ...offer.Installments.map( (installment): UnitPriceSpecification => ({ "@type": "UnitPriceSpecification", - priceType: "https://schema.org/SalePrice", - priceComponentType: "https://schema.org/Installment", + priceType: SCHEMA_SALE_PRICE, + priceComponentType: SCHEMA_INSTALLMENT, name: installment.PaymentSystemName, description: installment.Name, billingDuration: installment.NumberOfInstallments, @@ -680,10 +704,13 @@ const toOffer = ({ ), ], availability: offer.AvailableQuantity > 0 - ? "https://schema.org/InStock" - : "https://schema.org/OutOfStock", + ? SCHEMA_IN_STOCK + : SCHEMA_OUT_OF_STOCK, }); +const toOffer = (seller: SellerVTEX): Offer => + buildOffer(seller, seller.commertialOffer.teasers ?? []); + const toOfferLegacy = (seller: SellerVTEX): Offer => { const otherTeasers = seller.commertialOffer.DiscountHighLight?.map((i) => { const discount = i as Record; @@ -704,35 +731,33 @@ const toOfferLegacy = (seller: SellerVTEX): Offer => { return teasers; }) ?? []; - return { - ...toOffer(seller), - teasers: [ - ...otherTeasers, - ...(seller.commertialOffer.Teasers ?? []).map((teaser) => ({ - name: teaser["k__BackingField"], - generalValues: teaser["k__BackingField"], - conditions: { - minimumQuantity: teaser["k__BackingField"][ - "k__BackingField" - ], - parameters: teaser["k__BackingField"][ - "k__BackingField" - ].map((parameter) => ({ - name: parameter["k__BackingField"], - value: parameter["k__BackingField"], - })), - }, - effects: { - parameters: teaser["k__BackingField"][ - "k__BackingField" - ].map((parameter) => ({ - name: parameter["k__BackingField"], - value: parameter["k__BackingField"], - })), - }, - })), - ], - }; + const legacyTeasers = (seller.commertialOffer.Teasers ?? []).map( + (teaser) => ({ + name: teaser["k__BackingField"], + generalValues: teaser["k__BackingField"], + conditions: { + minimumQuantity: teaser["k__BackingField"][ + "k__BackingField" + ], + parameters: teaser["k__BackingField"][ + "k__BackingField" + ].map((parameter) => ({ + name: parameter["k__BackingField"], + value: parameter["k__BackingField"], + })), + }, + effects: { + parameters: teaser["k__BackingField"][ + "k__BackingField" + ].map((parameter) => ({ + name: parameter["k__BackingField"], + value: parameter["k__BackingField"], + })), + }, + }), + ); + + return buildOffer(seller, [...otherTeasers, ...legacyTeasers]); }; export const legacyFacetToFilter = ( diff --git a/website/loaders/redirectsFromCsv.ts b/website/loaders/redirectsFromCsv.ts index 00e104cef..ab8ffb0c7 100644 --- a/website/loaders/redirectsFromCsv.ts +++ b/website/loaders/redirectsFromCsv.ts @@ -54,49 +54,37 @@ const getRedirectFromFile = async ( return []; } - const redirectsFromFiles: Redirects["redirects"] = redirectsRaw - ?.split(/\r\n|\r|\n/) - .slice(1) - .map((row) => { - // this regex is necessary to handle csv with comma as part of value - const parts = row.split(/,|;(?=(?:(?:[^"]*"){2})*[^"]*$)/); - - const type = findAndRemove(parts, REDIRECT_TYPE_ENUM) ?? - (forcePermanentRedirects ? "permanent" : "temporary"); - const discardQueryParameters = - findAndRemove(parts, CONCATENATE_PARAMS_VALUES) === "true"; - const from = parts[0]; - const to = parts[1]; - - return [ - from, - to, - type, - discardQueryParameters, - ]; - }) - .filter(([from, to]) => from && to && from !== to) - .map(([from, to, type, discardQueryParameters]) => ({ - from: from as string, - to: to as string, - type: type as Redirect["type"], - discardQueryParameters: discardQueryParameters as boolean, - })); - - return redirectsFromFiles.map(( - { from, to, type, discardQueryParameters }, - ) => ({ - pathTemplate: from, - isHref: true, - handler: { - value: { - __resolveType: "website/handlers/redirect.ts", - to, - type, - discardQueryParameters, + // this regex is necessary to handle csv with comma as part of value + const csvFieldSplit = /,|;(?=(?:(?:[^"]*"){2})*[^"]*$)/; + const lines = redirectsRaw.split(/\r\n|\r|\n/); + const routes: Route[] = []; + + for (let i = 1; i < lines.length; i++) { + const parts = lines[i].split(csvFieldSplit); + const type = findAndRemove(parts, REDIRECT_TYPE_ENUM) ?? + (forcePermanentRedirects ? "permanent" : "temporary"); + const discardQueryParameters = + findAndRemove(parts, CONCATENATE_PARAMS_VALUES) === "true"; + const from = parts[0]; + const to = parts[1]; + + if (!from || !to || from === to) continue; + + routes.push({ + pathTemplate: from, + isHref: true, + handler: { + value: { + __resolveType: "website/handlers/redirect.ts", + to, + type: type as Redirect["type"], + discardQueryParameters, + }, }, - }, - })); + }); + } + + return routes; }; export const removeTrailingSlash = (path: string) =>