@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -52
- package/src/index.ts +10 -28
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +75 -65
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +354 -343
- package/src/README.md +0 -6
- package/src/__conformance__/upstream.test.ts +0 -158
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -724
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -56
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
- package/src/loaders/intelligentSearch/productList.ts +0 -170
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
- package/src/loaders/intelligentSearch/suggestions.ts +0 -44
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
- package/src/loaders/legacy.ts +0 -710
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -157
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -62
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.cacheHeaders.test.ts +0 -44
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -173
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
- package/src/utils/__tests__/transform.test.ts +0 -1111
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/buildOfferShelf.test.ts +0 -59
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -556
- package/src/utils/fetch.ts +0 -111
- package/src/utils/fetchCache.ts +0 -55
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -98
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -495
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -35
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -283
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1691
- package/src/utils/vtexId.ts +0 -151
- package/src/vtexClient.test.ts +0 -204
- package/tsconfig.json +0 -7
package/src/utils/transform.ts
DELETED
|
@@ -1,1691 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AggregateOffer,
|
|
3
|
-
Brand,
|
|
4
|
-
BreadcrumbList,
|
|
5
|
-
DayOfWeek,
|
|
6
|
-
Filter,
|
|
7
|
-
FilterToggleValue,
|
|
8
|
-
ItemAvailability,
|
|
9
|
-
Offer,
|
|
10
|
-
OpeningHoursSpecification,
|
|
11
|
-
PageType,
|
|
12
|
-
Place,
|
|
13
|
-
PostalAddress,
|
|
14
|
-
PriceComponentTypeEnumeration,
|
|
15
|
-
PriceTypeEnumeration,
|
|
16
|
-
Product,
|
|
17
|
-
ProductDetailsPage,
|
|
18
|
-
ProductGroup,
|
|
19
|
-
PropertyValue,
|
|
20
|
-
SiteNavigationElement,
|
|
21
|
-
UnitPriceSpecification,
|
|
22
|
-
} from "@decocms/apps-commerce/types";
|
|
23
|
-
import { bestInstallment } from "@decocms/apps-commerce/sdk/useOffer";
|
|
24
|
-
import { DEFAULT_IMAGE } from "@decocms/apps-commerce/utils/constants";
|
|
25
|
-
import { formatRange } from "@decocms/apps-commerce/utils/filters";
|
|
26
|
-
import { pick } from "./pickAndOmit";
|
|
27
|
-
import { slugify } from "./slugify";
|
|
28
|
-
import type {
|
|
29
|
-
Address,
|
|
30
|
-
Brand as BrandVTEX,
|
|
31
|
-
Category,
|
|
32
|
-
FacetValueBoolean,
|
|
33
|
-
FacetValueRange,
|
|
34
|
-
Facet as FacetVTEX,
|
|
35
|
-
LegacyFacet,
|
|
36
|
-
LegacyProduct,
|
|
37
|
-
LegacyProduct as LegacyProductVTEX,
|
|
38
|
-
LegacyItem as LegacySkuVTEX,
|
|
39
|
-
Maybe,
|
|
40
|
-
OrderForm,
|
|
41
|
-
PageType as PageTypeVTEX,
|
|
42
|
-
PickupHolidays,
|
|
43
|
-
PickupPoint,
|
|
44
|
-
ProductInventoryData,
|
|
45
|
-
ProductRating,
|
|
46
|
-
ProductReviewData,
|
|
47
|
-
Product as ProductVTEX,
|
|
48
|
-
SelectedFacet,
|
|
49
|
-
Seller as SellerVTEX,
|
|
50
|
-
Item as SkuVTEX,
|
|
51
|
-
Teasers,
|
|
52
|
-
} from "./types";
|
|
53
|
-
|
|
54
|
-
interface PickupPointVCS {
|
|
55
|
-
name?: string;
|
|
56
|
-
address?: {
|
|
57
|
-
country?: { acronym?: string };
|
|
58
|
-
location?: { latitude?: number; longitude?: number };
|
|
59
|
-
city?: string;
|
|
60
|
-
state?: string;
|
|
61
|
-
postalCode?: string;
|
|
62
|
-
street?: string;
|
|
63
|
-
};
|
|
64
|
-
pickupHolidays?: any[];
|
|
65
|
-
businessHours?: any[];
|
|
66
|
-
isActive?: boolean;
|
|
67
|
-
id?: string;
|
|
68
|
-
[key: string]: unknown;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const DEFAULT_CATEGORY_SEPARATOR = ">";
|
|
72
|
-
|
|
73
|
-
export const SCHEMA_LIST_PRICE: PriceTypeEnumeration = "https://schema.org/ListPrice";
|
|
74
|
-
export const SCHEMA_SALE_PRICE: PriceTypeEnumeration = "https://schema.org/SalePrice";
|
|
75
|
-
export const SCHEMA_SRP: PriceTypeEnumeration = "https://schema.org/SRP";
|
|
76
|
-
export const SCHEMA_INSTALLMENT: PriceComponentTypeEnumeration = "https://schema.org/Installment";
|
|
77
|
-
export const SCHEMA_IN_STOCK: ItemAvailability = "https://schema.org/InStock";
|
|
78
|
-
export const SCHEMA_OUT_OF_STOCK: ItemAvailability = "https://schema.org/OutOfStock";
|
|
79
|
-
|
|
80
|
-
const isLegacySku = (sku: LegacySkuVTEX | SkuVTEX): sku is LegacySkuVTEX =>
|
|
81
|
-
typeof (sku as LegacySkuVTEX).variations?.[0] === "string" || !!(sku as LegacySkuVTEX).Videos;
|
|
82
|
-
|
|
83
|
-
const isLegacyProduct = (product: ProductVTEX | LegacyProductVTEX): product is LegacyProductVTEX =>
|
|
84
|
-
product.origin !== "intelligent-search";
|
|
85
|
-
|
|
86
|
-
const getProductGroupURL = (origin: string, { linkText }: { linkText: string }) =>
|
|
87
|
-
new URL(`/${linkText}/p`, origin);
|
|
88
|
-
|
|
89
|
-
const getProductURL = (origin: string, product: { linkText: string }, skuId?: string) => {
|
|
90
|
-
const canonicalUrl = getProductGroupURL(origin, product);
|
|
91
|
-
|
|
92
|
-
if (skuId) {
|
|
93
|
-
canonicalUrl.searchParams.set("skuId", skuId);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return canonicalUrl;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const nonEmptyArray = <T>(array: T[] | null | undefined) =>
|
|
100
|
-
Array.isArray(array) && array.length > 0 ? array : null;
|
|
101
|
-
|
|
102
|
-
export interface ProductOptions {
|
|
103
|
-
baseUrl: string;
|
|
104
|
-
/** Price coded currency, e.g.: USD, BRL */
|
|
105
|
-
priceCurrency: string;
|
|
106
|
-
imagesByKey?: Map<string, string>;
|
|
107
|
-
/** Original attributes to be included in the transformed product */
|
|
108
|
-
includeOriginalAttributes?: string[];
|
|
109
|
-
/** Use lean toProductVariant for hasVariant[] instead of full toProduct at level=1 */
|
|
110
|
-
leanVariants?: boolean;
|
|
111
|
-
/**
|
|
112
|
-
* With `leanVariants`, keep the payment ladder on the ONE variant the card
|
|
113
|
-
* actually renders. Receives the raw SKU list and returns that SKU's
|
|
114
|
-
* `itemId` (or undefined to leave every variant lean).
|
|
115
|
-
*
|
|
116
|
-
* Why this exists: `leanVariants` assumes the card renders the ROOT sku, so
|
|
117
|
-
* `buildOfferVariant` empties `priceSpecification` on every entry. Cards that
|
|
118
|
-
* instead pick a representative variant out of `isVariantOf.hasVariant` (e.g.
|
|
119
|
-
* "cheapest in stock") read that variant's own offer for list price and
|
|
120
|
-
* installments — with the ladder emptied, those render blank.
|
|
121
|
-
*
|
|
122
|
-
* The kept entry stays on the LEAN variant shape and only its offer is
|
|
123
|
-
* upgraded, to {@link buildOfferShelf} (ListPrice/SalePrice/SRP + PIX + the
|
|
124
|
-
* one installment `useOffer` would pick). A full `toProduct` here would
|
|
125
|
-
* instead re-emit `description` and the whole 48-entry ladder the parent
|
|
126
|
-
* already carries: measured on a real listing, 17.5 KB of the 53.5 KB in
|
|
127
|
-
* every product, for a card that reads price and installment only.
|
|
128
|
-
*
|
|
129
|
-
* Undefined (default) preserves the current behaviour byte for byte.
|
|
130
|
-
*/
|
|
131
|
-
displayedVariantId?: (items: Array<LegacySkuVTEX | SkuVTEX>) => string | undefined;
|
|
132
|
-
/**
|
|
133
|
-
* Rewrite `priceSpecification` on the offers this transform emits. Runs on
|
|
134
|
-
* the ROOT offer and on the offer of the variant `displayedVariantId` picks.
|
|
135
|
-
*
|
|
136
|
-
* A listing renders ONE installment string per card, while the Catalog API
|
|
137
|
-
* returns every payment method the store accepts: measured on a real page,
|
|
138
|
-
* 48 `UnitPriceSpecification` entries = 12.0 KB per product, 22% of the
|
|
139
|
-
* listing payload — built, cached and re-serialized in full.
|
|
140
|
-
*
|
|
141
|
-
* Deliberately a caller-supplied function and not a boolean: WHICH rungs a
|
|
142
|
-
* card reads is store policy, not ours. {@link buildOfferShelf} keeps the
|
|
143
|
-
* `bestInstallment` one (lowest total, tie-broken by highest
|
|
144
|
-
* `billingDuration`) — and on a store with a boleto discount that resolves to
|
|
145
|
-
* "Boleto Bancário 1x", measured against a storefront that renders
|
|
146
|
-
* "Visa 10x" on all 36 cards of the page. Same data, different string. Pass
|
|
147
|
-
* `(specs) => [buildOfferShelf({ ...offer, priceSpecification: specs }).…]`-style
|
|
148
|
-
* logic, or your own predicate; the transform only applies it.
|
|
149
|
-
*
|
|
150
|
-
* Undefined (default) keeps the full ladder — a PDP needs it.
|
|
151
|
-
*/
|
|
152
|
-
priceSpecifications?: (specs: UnitPriceSpecification[]) => UnitPriceSpecification[];
|
|
153
|
-
/**
|
|
154
|
-
* Internal: set by `toProduct`/`toProductShelf` on the ONE variant
|
|
155
|
-
* `displayedVariantId` picked, so `toProductVariant` emits a real ladder for
|
|
156
|
-
* it instead of `buildOfferVariant`'s empty one. Not meant for callers.
|
|
157
|
-
*/
|
|
158
|
-
variantKeepLadder?: boolean;
|
|
159
|
-
/**
|
|
160
|
-
* Cap `image[]` to the first N entries, in path order. Undefined (default)
|
|
161
|
-
* keeps every image.
|
|
162
|
-
*
|
|
163
|
-
* Listings render at most a couple of images per card, while the Catalog API
|
|
164
|
-
* returns every asset registered on the SKU (3 on average, up to 5 measured).
|
|
165
|
-
*
|
|
166
|
-
* NOTE: this truncates by POSITION, so images the consumer selects BY NAME
|
|
167
|
-
* can fall outside the cap. Measured on a real listing page: the `vira`
|
|
168
|
-
* (hover) image sits at index 1 in only 5 of the 19 products that have one —
|
|
169
|
-
* index 2 in 12 of them, index 3 in 2. A cap of 2 therefore drops the hover
|
|
170
|
-
* image on most cards that use one. Callers that select by name should keep
|
|
171
|
-
* the named entries through {@link keepImageNames}.
|
|
172
|
-
*/
|
|
173
|
-
maxImages?: number;
|
|
174
|
-
/**
|
|
175
|
-
* `imageLabel`s that survive the positional cap. An image whose label is
|
|
176
|
-
* listed here is kept even when it falls outside {@link maxImages} (and
|
|
177
|
-
* outside the shelf transform's own default cap), in its original order.
|
|
178
|
-
*
|
|
179
|
-
* This is the escape hatch the `maxImages` note asks for. A storefront that
|
|
180
|
-
* picks the card image BY NAME — a "still"/packshot asset, a colour
|
|
181
|
-
* thumbnail — cannot use a positional cap on its own: those assets are
|
|
182
|
-
* registered last on the SKU, so any cap drops them and the card silently
|
|
183
|
-
* falls back to the first photo. Pass the labels the card selects and the
|
|
184
|
-
* payload stays lean everywhere else.
|
|
185
|
-
*
|
|
186
|
-
* Undefined (default) preserves the current behaviour byte for byte.
|
|
187
|
-
*/
|
|
188
|
-
keepImageNames?: string[];
|
|
189
|
-
/** Property names to keep on lean variant additionalProperty. Defaults to VARIANT_PROPERTY_NAMES. */
|
|
190
|
-
variantPropertyNames?: Set<string>;
|
|
191
|
-
/** When leanVariants is true, still include image[0] on each variant entry. Default true. */
|
|
192
|
-
variantIncludeImage?: boolean;
|
|
193
|
-
/** When leanVariants is true, still include inventoryLevel on each variant offer. Default true. */
|
|
194
|
-
variantIncludeInventory?: boolean;
|
|
195
|
-
/**
|
|
196
|
-
* Shelf: include every SKU as a lean variant in isVariantOf.hasVariant[] (via
|
|
197
|
-
* toProductVariant) instead of a single in-stock one. Lets shelf cards render
|
|
198
|
-
* the full size/color grid without pulling the heavy toProduct payload.
|
|
199
|
-
* Default false — preserves the lean single-variant shelf other sites rely on.
|
|
200
|
-
*/
|
|
201
|
-
shelfCompleteVariants?: boolean;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Apply the positional image cap, then add back the entries the caller selects
|
|
206
|
-
* by name. Order is preserved: the capped head first, then the named tail in
|
|
207
|
-
* catalog order. No cap means no work.
|
|
208
|
-
*/
|
|
209
|
-
const capImages = <I extends { imageLabel?: string | null }>(
|
|
210
|
-
images: I[] | null | undefined,
|
|
211
|
-
maxImages: number | undefined,
|
|
212
|
-
keepImageNames: string[] | undefined,
|
|
213
|
-
): I[] | null | undefined => {
|
|
214
|
-
if (!images || typeof maxImages !== "number") return images;
|
|
215
|
-
|
|
216
|
-
const capped = images.slice(0, maxImages);
|
|
217
|
-
|
|
218
|
-
if (!keepImageNames?.length) return capped;
|
|
219
|
-
|
|
220
|
-
const keep = new Set(keepImageNames);
|
|
221
|
-
const named = images.slice(maxImages).filter((image) => keep.has(image.imageLabel ?? ""));
|
|
222
|
-
|
|
223
|
-
return named.length > 0 ? [...capped, ...named] : capped;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
/** Returns first available sku */
|
|
227
|
-
const findFirstAvailable = (items: Array<LegacySkuVTEX | SkuVTEX>) =>
|
|
228
|
-
items?.find((item) =>
|
|
229
|
-
Boolean(item?.sellers?.find((s) => s.commertialOffer?.AvailableQuantity > 0)),
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
export const pickSku = <T extends ProductVTEX | LegacyProductVTEX>(
|
|
233
|
-
product: T,
|
|
234
|
-
maybeSkuId?: string,
|
|
235
|
-
): T["items"][number] => {
|
|
236
|
-
const skuId = maybeSkuId ?? findFirstAvailable(product.items)?.itemId ?? product.items[0]?.itemId;
|
|
237
|
-
for (const item of product.items) {
|
|
238
|
-
if (item.itemId === skuId) {
|
|
239
|
-
return item;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return product.items[0];
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const toAccessoryOrSparePartFor = <T extends ProductVTEX | LegacyProductVTEX>(
|
|
247
|
-
sku: T["items"][number],
|
|
248
|
-
kitItems: T[],
|
|
249
|
-
options: ProductOptions,
|
|
250
|
-
) => {
|
|
251
|
-
const productBySkuId = kitItems.reduce((map, product) => {
|
|
252
|
-
product.items.forEach((item) => map.set(item.itemId, product));
|
|
253
|
-
|
|
254
|
-
return map;
|
|
255
|
-
}, new Map<string, T>());
|
|
256
|
-
|
|
257
|
-
return sku.kitItems
|
|
258
|
-
?.map(({ itemId }) => {
|
|
259
|
-
const product = productBySkuId.get(itemId);
|
|
260
|
-
|
|
261
|
-
/** Sometimes VTEX does not return what I've asked for */
|
|
262
|
-
if (!product) return;
|
|
263
|
-
|
|
264
|
-
const sku = pickSku(product, itemId);
|
|
265
|
-
|
|
266
|
-
return toProduct(product, sku, 0, options);
|
|
267
|
-
})
|
|
268
|
-
.filter((p): p is Product => typeof p !== "undefined");
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export const forceHttpsOnAssets = (orderForm: OrderForm) => {
|
|
272
|
-
orderForm.items.forEach((item) => {
|
|
273
|
-
if (item.imageUrl) {
|
|
274
|
-
item.imageUrl = item.imageUrl.startsWith("http://")
|
|
275
|
-
? item.imageUrl.replace("http://", "https://")
|
|
276
|
-
: item.imageUrl;
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
return orderForm;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
export const toProductPage = <T extends ProductVTEX | LegacyProductVTEX>(
|
|
283
|
-
product: T,
|
|
284
|
-
sku: T["items"][number],
|
|
285
|
-
kitItems: T[],
|
|
286
|
-
options: ProductOptions,
|
|
287
|
-
): Omit<ProductDetailsPage, "seo"> => {
|
|
288
|
-
const partialProduct = toProduct(product, sku, 0, options);
|
|
289
|
-
// This is deprecated. Compose this loader at loaders > product > extension > detailsPage.ts
|
|
290
|
-
const isAccessoryOrSparePartFor = toAccessoryOrSparePartFor(sku, kitItems, options);
|
|
291
|
-
|
|
292
|
-
return {
|
|
293
|
-
"@type": "ProductDetailsPage",
|
|
294
|
-
breadcrumbList: toBreadcrumbList(product, options),
|
|
295
|
-
product: { ...partialProduct, isAccessoryOrSparePartFor },
|
|
296
|
-
};
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
export const inStock = (offer: Offer) => offer.availability === SCHEMA_IN_STOCK;
|
|
300
|
-
|
|
301
|
-
// Smallest Available Spot Price First
|
|
302
|
-
export const bestOfferFirst = (a: Offer, b: Offer) => {
|
|
303
|
-
if (inStock(a) && !inStock(b)) {
|
|
304
|
-
return -1;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (!inStock(a) && inStock(b)) {
|
|
308
|
-
return 1;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return a.price - b.price;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const getHighPriceIndex = (offers: Offer[]) => {
|
|
315
|
-
let it = offers.length - 1;
|
|
316
|
-
for (; it > 0 && !inStock(offers[it]); it--);
|
|
317
|
-
return it;
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const splitCategory = (firstCategory: string) => firstCategory.split("/").filter(Boolean);
|
|
321
|
-
|
|
322
|
-
const toAdditionalPropertyCategories = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
323
|
-
product: P,
|
|
324
|
-
): Product["additionalProperty"] => {
|
|
325
|
-
const categories = new Set<string>();
|
|
326
|
-
const categoryIds = new Set<string>();
|
|
327
|
-
|
|
328
|
-
product.categories.forEach((productCategory, i) => {
|
|
329
|
-
const category = splitCategory(productCategory);
|
|
330
|
-
const categoryId = splitCategory(product.categoriesIds[i]);
|
|
331
|
-
|
|
332
|
-
category.forEach((splitCategoryItem, j) => {
|
|
333
|
-
categories.add(splitCategoryItem);
|
|
334
|
-
categoryIds.add(categoryId[j]);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
const categoriesArray = Array.from(categories);
|
|
339
|
-
const categoryIdsArray = Array.from(categoryIds);
|
|
340
|
-
|
|
341
|
-
return categoriesArray.map((category, index) =>
|
|
342
|
-
toAdditionalPropertyCategory({
|
|
343
|
-
propertyID: categoryIdsArray[index],
|
|
344
|
-
value: category || "",
|
|
345
|
-
}),
|
|
346
|
-
);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
export const toAdditionalPropertyCategory = ({
|
|
350
|
-
propertyID,
|
|
351
|
-
value,
|
|
352
|
-
}: {
|
|
353
|
-
propertyID: string;
|
|
354
|
-
value: string;
|
|
355
|
-
}): PropertyValue => ({
|
|
356
|
-
"@type": "PropertyValue" as const,
|
|
357
|
-
name: "category",
|
|
358
|
-
propertyID,
|
|
359
|
-
value,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const toAdditionalPropertyClusters = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
363
|
-
product: P,
|
|
364
|
-
): Product["additionalProperty"] => {
|
|
365
|
-
const mapEntriesToIdName = ([id, name]: [string, unknown]) => ({
|
|
366
|
-
id,
|
|
367
|
-
name: name as string,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const allClusters = isLegacyProduct(product)
|
|
371
|
-
? Object.entries(product.productClusters).map(mapEntriesToIdName)
|
|
372
|
-
: product.productClusters;
|
|
373
|
-
|
|
374
|
-
const highlightsSet = isLegacyProduct(product)
|
|
375
|
-
? new Set(Object.keys(product.clusterHighlights))
|
|
376
|
-
: new Set(product.clusterHighlights.map(({ id }) => id));
|
|
377
|
-
|
|
378
|
-
return allClusters.map((cluster) =>
|
|
379
|
-
toAdditionalPropertyCluster(
|
|
380
|
-
{
|
|
381
|
-
propertyID: cluster.id,
|
|
382
|
-
value: cluster.name || "",
|
|
383
|
-
},
|
|
384
|
-
highlightsSet,
|
|
385
|
-
),
|
|
386
|
-
);
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
export const toAdditionalPropertyCluster = (
|
|
390
|
-
{ propertyID, value }: { propertyID: string; value: string },
|
|
391
|
-
highlights?: Set<string>,
|
|
392
|
-
): PropertyValue => ({
|
|
393
|
-
"@type": "PropertyValue",
|
|
394
|
-
name: "cluster",
|
|
395
|
-
value,
|
|
396
|
-
propertyID,
|
|
397
|
-
description: highlights?.has(propertyID) ? "highlight" : undefined,
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const toAdditionalPropertyReferenceIds = (
|
|
401
|
-
referenceId: Array<{ Key: string; Value: string }>,
|
|
402
|
-
): Product["additionalProperty"] => {
|
|
403
|
-
return referenceId.map(({ Key, Value }) =>
|
|
404
|
-
toAdditionalPropertyReferenceId({ name: Key, value: Value }),
|
|
405
|
-
);
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
export const toAdditionalPropertyReferenceId = ({
|
|
409
|
-
name,
|
|
410
|
-
value,
|
|
411
|
-
}: {
|
|
412
|
-
name: string;
|
|
413
|
-
value: string;
|
|
414
|
-
}): PropertyValue => ({
|
|
415
|
-
"@type": "PropertyValue",
|
|
416
|
-
name,
|
|
417
|
-
value,
|
|
418
|
-
valueReference: "ReferenceID",
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const getImageKey = (src = "") => {
|
|
422
|
-
return src;
|
|
423
|
-
|
|
424
|
-
// TODO: figure out how we can improve this
|
|
425
|
-
// const match = new URLPattern({
|
|
426
|
-
// pathname: "/arquivos/ids/:skuId/:imageId",
|
|
427
|
-
// }).exec(src);
|
|
428
|
-
|
|
429
|
-
// if (match == null) {
|
|
430
|
-
// return src;
|
|
431
|
-
// }
|
|
432
|
-
|
|
433
|
-
// return `${match.pathname.groups.imageId}${match.search.input}`;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
export const aggregateOffers = (
|
|
437
|
-
offers: Offer[],
|
|
438
|
-
priceCurrency?: string,
|
|
439
|
-
): AggregateOffer | undefined => {
|
|
440
|
-
const sorted = offers.sort(bestOfferFirst);
|
|
441
|
-
|
|
442
|
-
if (sorted.length === 0) return;
|
|
443
|
-
|
|
444
|
-
const highPriceIndex = getHighPriceIndex(sorted);
|
|
445
|
-
const lowPriceIndex = 0;
|
|
446
|
-
|
|
447
|
-
return {
|
|
448
|
-
"@type": "AggregateOffer",
|
|
449
|
-
priceCurrency,
|
|
450
|
-
highPrice: sorted[highPriceIndex]?.price ?? null,
|
|
451
|
-
lowPrice: sorted[lowPriceIndex]?.price ?? null,
|
|
452
|
-
offerCount: sorted.length,
|
|
453
|
-
offers: sorted,
|
|
454
|
-
};
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
export const toProduct = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
458
|
-
product: P,
|
|
459
|
-
sku: P["items"][number],
|
|
460
|
-
level = 0, // prevent inifinte loop while self referencing the product
|
|
461
|
-
options: ProductOptions,
|
|
462
|
-
): Product => {
|
|
463
|
-
const { baseUrl, priceCurrency } = options;
|
|
464
|
-
const {
|
|
465
|
-
brand,
|
|
466
|
-
brandId,
|
|
467
|
-
brandImageUrl,
|
|
468
|
-
productId,
|
|
469
|
-
productReference,
|
|
470
|
-
description,
|
|
471
|
-
releaseDate,
|
|
472
|
-
items,
|
|
473
|
-
} = product;
|
|
474
|
-
const { name, ean, itemId: skuId, referenceId = [], kitItems, estimatedDateArrival } = sku;
|
|
475
|
-
|
|
476
|
-
const videos = isLegacySku(sku) ? sku.Videos : sku.videos;
|
|
477
|
-
const nonEmptyVideos = nonEmptyArray(videos);
|
|
478
|
-
const imagesByKey =
|
|
479
|
-
options.imagesByKey ??
|
|
480
|
-
items
|
|
481
|
-
.flatMap((i) => i.images)
|
|
482
|
-
.reduce((map, img) => {
|
|
483
|
-
img?.imageUrl && map.set(getImageKey(img.imageUrl), img.imageUrl);
|
|
484
|
-
return map;
|
|
485
|
-
}, new Map<string, string>());
|
|
486
|
-
|
|
487
|
-
const groupAdditionalProperty = isLegacyProduct(product)
|
|
488
|
-
? legacyToProductGroupAdditionalProperties(product)
|
|
489
|
-
: toProductGroupAdditionalProperties(product);
|
|
490
|
-
const originalAttributesAdditionalProperties = toOriginalAttributesAdditionalProperties(
|
|
491
|
-
options.includeOriginalAttributes,
|
|
492
|
-
product,
|
|
493
|
-
);
|
|
494
|
-
const specificationsAdditionalProperty = isLegacySku(sku)
|
|
495
|
-
? toAdditionalPropertiesLegacy(sku)
|
|
496
|
-
: toAdditionalProperties(sku);
|
|
497
|
-
const referenceIdAdditionalProperty = toAdditionalPropertyReferenceIds(referenceId);
|
|
498
|
-
const images = nonEmptyArray(sku.images);
|
|
499
|
-
const rawOffers = (sku.sellers ?? []).map(isLegacyProduct(product) ? toOfferLegacy : toOffer);
|
|
500
|
-
const offers = applyPriceSpecifications(rawOffers, options);
|
|
501
|
-
|
|
502
|
-
// `variantKeepLadder` must NOT be inherited: while it leaked through, every
|
|
503
|
-
// lean variant emitted a ladder instead of an empty one — measured on a real
|
|
504
|
-
// listing, `isVariantOf` GREW from 38.6 KB to 49.6 KB, so the option made
|
|
505
|
-
// the payload bigger. Only the entry `displayedVariantId` picks turns it on,
|
|
506
|
-
// explicitly, below.
|
|
507
|
-
const variantOptions = { ...options, imagesByKey, variantKeepLadder: false };
|
|
508
|
-
const isVariantOf =
|
|
509
|
-
level < 1
|
|
510
|
-
? ({
|
|
511
|
-
"@type": "ProductGroup",
|
|
512
|
-
productGroupID: productId,
|
|
513
|
-
hasVariant: options.leanVariants
|
|
514
|
-
? ((keepId) =>
|
|
515
|
-
items.map((sku) =>
|
|
516
|
-
toProductVariant(
|
|
517
|
-
product,
|
|
518
|
-
sku,
|
|
519
|
-
keepId !== undefined && sku.itemId === keepId
|
|
520
|
-
? { ...variantOptions, variantKeepLadder: true }
|
|
521
|
-
: variantOptions,
|
|
522
|
-
),
|
|
523
|
-
))(options.displayedVariantId?.(items))
|
|
524
|
-
: items.map((sku) => toProduct(product, sku, 1, variantOptions)),
|
|
525
|
-
url: getProductGroupURL(baseUrl, product).href,
|
|
526
|
-
name: product.productName,
|
|
527
|
-
additionalProperty: [
|
|
528
|
-
...groupAdditionalProperty,
|
|
529
|
-
...originalAttributesAdditionalProperties,
|
|
530
|
-
],
|
|
531
|
-
model: productReference,
|
|
532
|
-
} satisfies ProductGroup)
|
|
533
|
-
: undefined;
|
|
534
|
-
|
|
535
|
-
const cappedImages = capImages(images, options.maxImages, options.keepImageNames);
|
|
536
|
-
const finalImages = cappedImages?.map(({ imageUrl, imageText, imageLabel }) => {
|
|
537
|
-
const url = imagesByKey.get(getImageKey(imageUrl)) ?? imageUrl;
|
|
538
|
-
const alternateName = imageText || imageLabel || "";
|
|
539
|
-
const name = imageLabel || "";
|
|
540
|
-
const encodingFormat = "image";
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
"@type": "ImageObject" as const,
|
|
544
|
-
alternateName,
|
|
545
|
-
url,
|
|
546
|
-
name,
|
|
547
|
-
encodingFormat,
|
|
548
|
-
};
|
|
549
|
-
}) ?? [DEFAULT_IMAGE];
|
|
550
|
-
|
|
551
|
-
const finalVideos = nonEmptyVideos?.map((video) => {
|
|
552
|
-
const url = video;
|
|
553
|
-
const alternateName = "Product video";
|
|
554
|
-
const name = "Product video";
|
|
555
|
-
const encodingFormat = "video";
|
|
556
|
-
return {
|
|
557
|
-
"@type": "VideoObject" as const,
|
|
558
|
-
alternateName,
|
|
559
|
-
contentUrl: url,
|
|
560
|
-
name,
|
|
561
|
-
encodingFormat,
|
|
562
|
-
};
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// From schema.org: A category for the item. Greater signs or slashes can be used to informally indicate a category hierarchy
|
|
566
|
-
const categoriesString = splitCategory(product.categories[0]).join(DEFAULT_CATEGORY_SEPARATOR);
|
|
567
|
-
|
|
568
|
-
const categoryAdditionalProperties = toAdditionalPropertyCategories(product);
|
|
569
|
-
const clusterAdditionalProperties = toAdditionalPropertyClusters(product);
|
|
570
|
-
|
|
571
|
-
const additionalProperty: PropertyValue[] = [...specificationsAdditionalProperty];
|
|
572
|
-
if (categoryAdditionalProperties) {
|
|
573
|
-
additionalProperty.push(...categoryAdditionalProperties);
|
|
574
|
-
}
|
|
575
|
-
if (clusterAdditionalProperties) {
|
|
576
|
-
additionalProperty.push(...clusterAdditionalProperties);
|
|
577
|
-
}
|
|
578
|
-
if (referenceIdAdditionalProperty) {
|
|
579
|
-
additionalProperty.push(...referenceIdAdditionalProperty);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
estimatedDateArrival &&
|
|
583
|
-
additionalProperty.push({
|
|
584
|
-
"@type": "PropertyValue",
|
|
585
|
-
name: "Estimated Date Arrival",
|
|
586
|
-
value: estimatedDateArrival,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
if (sku.modalType) {
|
|
590
|
-
additionalProperty.push({
|
|
591
|
-
"@type": "PropertyValue",
|
|
592
|
-
name: "Modal Type",
|
|
593
|
-
value: sku.modalType,
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return {
|
|
598
|
-
"@type": "Product",
|
|
599
|
-
category: categoriesString,
|
|
600
|
-
productID: skuId,
|
|
601
|
-
url: getProductURL(baseUrl, product, sku.itemId).href,
|
|
602
|
-
name,
|
|
603
|
-
alternateName: sku.complementName,
|
|
604
|
-
description,
|
|
605
|
-
brand: {
|
|
606
|
-
"@type": "Brand",
|
|
607
|
-
"@id": brandId?.toString(),
|
|
608
|
-
name: brand,
|
|
609
|
-
logo: brandImageUrl,
|
|
610
|
-
},
|
|
611
|
-
isAccessoryOrSparePartFor: kitItems?.map(({ itemId }) => ({
|
|
612
|
-
"@type": "Product",
|
|
613
|
-
productID: itemId,
|
|
614
|
-
sku: itemId,
|
|
615
|
-
})),
|
|
616
|
-
inProductGroupWithID: productId,
|
|
617
|
-
sku: skuId,
|
|
618
|
-
gtin: ean,
|
|
619
|
-
releaseDate,
|
|
620
|
-
additionalProperty,
|
|
621
|
-
isVariantOf,
|
|
622
|
-
image: finalImages,
|
|
623
|
-
video: finalVideos,
|
|
624
|
-
offers: aggregateOffers(offers, priceCurrency),
|
|
625
|
-
};
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Build a lean offer for shelf display. Keeps only:
|
|
630
|
-
* - ListPrice, SalePrice, SRP price types
|
|
631
|
-
* - PIX installment (name?.toUpperCase() === "PIX")
|
|
632
|
-
* - Best installment, chosen with the SAME `bestInstallment` heuristic `useOffer`
|
|
633
|
-
* applies on PLP/PDP (lowest total price, tie-broken by highest billingDuration).
|
|
634
|
-
* Reusing it means shelf cards and detail pages always show the same installment.
|
|
635
|
-
* Drops: inventoryLevel, giftSkuIds, priceValidUntil
|
|
636
|
-
*/
|
|
637
|
-
export const buildOfferShelf = (offer: Offer): Offer => {
|
|
638
|
-
const leanSpecs: UnitPriceSpecification[] = [];
|
|
639
|
-
const installmentSpecs: UnitPriceSpecification[] = [];
|
|
640
|
-
|
|
641
|
-
for (const spec of offer.priceSpecification ?? []) {
|
|
642
|
-
// Keep base (non-installment) price types
|
|
643
|
-
if (
|
|
644
|
-
(spec.priceType === SCHEMA_LIST_PRICE ||
|
|
645
|
-
spec.priceType === SCHEMA_SALE_PRICE ||
|
|
646
|
-
spec.priceType === SCHEMA_SRP) &&
|
|
647
|
-
spec.priceComponentType !== SCHEMA_INSTALLMENT
|
|
648
|
-
) {
|
|
649
|
-
leanSpecs.push(spec);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Keep PIX installment (also feeds the bestInstallment reducer below via leanSpecs)
|
|
654
|
-
if (spec.priceComponentType === SCHEMA_INSTALLMENT && spec.name?.toUpperCase() === "PIX") {
|
|
655
|
-
leanSpecs.push(spec);
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (spec.priceComponentType === SCHEMA_INSTALLMENT) {
|
|
660
|
-
installmentSpecs.push(spec);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Keep the single installment useOffer would pick from the full list. PIX is kept
|
|
665
|
-
// separately above, so useOffer on the lean offer still reduces over {PIX, best}
|
|
666
|
-
// and matches the detail page's reduce over the full set.
|
|
667
|
-
const best = installmentSpecs.reduce(bestInstallment, null as UnitPriceSpecification | null);
|
|
668
|
-
if (best) {
|
|
669
|
-
leanSpecs.push(best);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return {
|
|
673
|
-
"@type": "Offer",
|
|
674
|
-
identifier: offer.identifier,
|
|
675
|
-
price: offer.price,
|
|
676
|
-
seller: offer.seller,
|
|
677
|
-
sellerName: offer.sellerName,
|
|
678
|
-
teasers: offer.teasers,
|
|
679
|
-
priceSpecification: leanSpecs,
|
|
680
|
-
availability: offer.availability,
|
|
681
|
-
inventoryLevel: { value: 0 },
|
|
682
|
-
};
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Apply `options.priceSpecifications` to every offer, if the caller supplied it.
|
|
687
|
-
* Identity (same array reference) when absent, so the default output is
|
|
688
|
-
* byte-for-byte unchanged.
|
|
689
|
-
*/
|
|
690
|
-
const applyPriceSpecifications = (offers: Offer[], options: ProductOptions): Offer[] =>
|
|
691
|
-
options.priceSpecifications
|
|
692
|
-
? offers.map((offer) => ({
|
|
693
|
-
...offer,
|
|
694
|
-
priceSpecification: options.priceSpecifications!(offer.priceSpecification ?? []),
|
|
695
|
-
}))
|
|
696
|
-
: offers;
|
|
697
|
-
|
|
698
|
-
/** Default positional image cap for the shelf transform (front + back). */
|
|
699
|
-
const SHELF_MAX_IMAGES = 2;
|
|
700
|
-
|
|
701
|
-
/** Property names commonly used by ProductCard/Shelf components */
|
|
702
|
-
const SHELF_PROPERTY_NAMES = new Set([
|
|
703
|
-
"category",
|
|
704
|
-
"cluster",
|
|
705
|
-
"Campanha",
|
|
706
|
-
"Cor",
|
|
707
|
-
"Tamanho",
|
|
708
|
-
"Voltagem",
|
|
709
|
-
"sellerId",
|
|
710
|
-
]);
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Lean product transform for shelf/card display. Same signature as toProduct().
|
|
714
|
-
*
|
|
715
|
-
* Differences from toProduct():
|
|
716
|
-
* - Images: capped at 2 per SKU (front + back), overridable via `maxImages`;
|
|
717
|
-
* `keepImageNames` adds back labelled assets that fall outside the cap
|
|
718
|
-
* - Offers: best seller only (in-stock first, then cheapest), stripped installments (keeps ListPrice, SalePrice, SRP, PIX, best no-interest)
|
|
719
|
-
* - isVariantOf: single in-stock variant at level 0
|
|
720
|
-
* - additionalProperty: filtered to known-used property names
|
|
721
|
-
* - Drops: description, video, isAccessoryOrSparePartFor, alternateName, gtin, releaseDate
|
|
722
|
-
*/
|
|
723
|
-
export const toProductShelf = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
724
|
-
product: P,
|
|
725
|
-
sku: P["items"][number],
|
|
726
|
-
level = 0,
|
|
727
|
-
options: ProductOptions,
|
|
728
|
-
): Product => {
|
|
729
|
-
const { baseUrl, priceCurrency } = options;
|
|
730
|
-
const { productId, items, productReference } = product;
|
|
731
|
-
const { name, itemId: skuId } = sku;
|
|
732
|
-
|
|
733
|
-
// Images: cap at 2, keeping any label the caller selects by name
|
|
734
|
-
const rawImages = nonEmptyArray(sku.images);
|
|
735
|
-
const cappedImages =
|
|
736
|
-
capImages(rawImages, options.maxImages ?? SHELF_MAX_IMAGES, options.keepImageNames) ?? [];
|
|
737
|
-
const mappedImages = cappedImages.map(({ imageUrl, imageText, imageLabel }) => ({
|
|
738
|
-
"@type": "ImageObject" as const,
|
|
739
|
-
alternateName: imageText || imageLabel || "",
|
|
740
|
-
url: imageUrl,
|
|
741
|
-
name: imageLabel || "",
|
|
742
|
-
encodingFormat: "image",
|
|
743
|
-
}));
|
|
744
|
-
const finalImages = mappedImages.length > 0 ? mappedImages : [DEFAULT_IMAGE];
|
|
745
|
-
|
|
746
|
-
// Offers: best seller (in-stock first, then cheapest), lean.
|
|
747
|
-
// Must consider ALL sellers so marketplace products where sellers[0]
|
|
748
|
-
// is OOS but another seller has stock still show as available.
|
|
749
|
-
const offerConverter = isLegacyProduct(product) ? toOfferLegacy : toOffer;
|
|
750
|
-
const allOffers = (sku.sellers ?? []).map(offerConverter).sort(bestOfferFirst);
|
|
751
|
-
const bestOffer = allOffers[0];
|
|
752
|
-
const leanOffers = bestOffer ? [buildOfferShelf(bestOffer)] : [];
|
|
753
|
-
|
|
754
|
-
// additionalProperty: filter to known-used names
|
|
755
|
-
const specificationsAdditionalProperty = isLegacySku(sku)
|
|
756
|
-
? toAdditionalPropertiesLegacy(sku)
|
|
757
|
-
: toAdditionalProperties(sku);
|
|
758
|
-
const categoryAdditionalProperties = toAdditionalPropertyCategories(product) ?? [];
|
|
759
|
-
const clusterAdditionalProperties = toAdditionalPropertyClusters(product) ?? [];
|
|
760
|
-
|
|
761
|
-
const additionalProperty = [
|
|
762
|
-
...specificationsAdditionalProperty,
|
|
763
|
-
...categoryAdditionalProperties,
|
|
764
|
-
...clusterAdditionalProperties,
|
|
765
|
-
].filter((prop) => SHELF_PROPERTY_NAMES.has(prop.name ?? ""));
|
|
766
|
-
|
|
767
|
-
// Product-group specs (from specificationGroups) filtered to shelf-safe names.
|
|
768
|
-
// Launch markers like "Campanha" live here, NOT in sku.variations, so they only
|
|
769
|
-
// reach the card through the ProductGroup's additionalProperty (mirrors toProduct).
|
|
770
|
-
const groupAdditionalProperty = (
|
|
771
|
-
isLegacyProduct(product)
|
|
772
|
-
? legacyToProductGroupAdditionalProperties(product)
|
|
773
|
-
: toProductGroupAdditionalProperties(product)
|
|
774
|
-
).filter((prop) => SHELF_PROPERTY_NAMES.has(prop.name ?? ""));
|
|
775
|
-
|
|
776
|
-
// isVariantOf: single in-stock variant at level 0
|
|
777
|
-
const isVariantOf =
|
|
778
|
-
level < 1
|
|
779
|
-
? (() => {
|
|
780
|
-
const inStockSku = findFirstAvailable(items) ?? items[0];
|
|
781
|
-
// Opt-in: every SKU as a lean variant (size/color grid on shelf cards).
|
|
782
|
-
// Default: a single in-stock variant (lean shelf payload).
|
|
783
|
-
const keepId = options.displayedVariantId?.(items);
|
|
784
|
-
const hasVariant = options.shelfCompleteVariants
|
|
785
|
-
? items.map((variantSku) =>
|
|
786
|
-
toProductVariant(
|
|
787
|
-
product,
|
|
788
|
-
variantSku,
|
|
789
|
-
keepId !== undefined && variantSku.itemId === keepId
|
|
790
|
-
? { ...options, variantKeepLadder: true }
|
|
791
|
-
: options,
|
|
792
|
-
),
|
|
793
|
-
)
|
|
794
|
-
: inStockSku
|
|
795
|
-
? [toProductShelf(product, inStockSku, 1, options)]
|
|
796
|
-
: [];
|
|
797
|
-
return {
|
|
798
|
-
"@type": "ProductGroup" as const,
|
|
799
|
-
productGroupID: productId,
|
|
800
|
-
hasVariant,
|
|
801
|
-
url: getProductGroupURL(baseUrl, product).href,
|
|
802
|
-
name: product.productName,
|
|
803
|
-
// Carry the shelf-safe group specs (incl. "Campanha") so ProductCard
|
|
804
|
-
// flags that read isVariantOf.additionalProperty (e.g. ReleaseFlag ->
|
|
805
|
-
// "Lançamento") still fire on shelf/carousel cards.
|
|
806
|
-
additionalProperty: groupAdditionalProperty,
|
|
807
|
-
// The product reference (VTEX `productReference`). Analytics reads it
|
|
808
|
-
// off `isVariantOf.model` — GA4 `dimension1` on view_item_list /
|
|
809
|
-
// select_item, and the equivalent on productImpression /
|
|
810
|
-
// productClick. Dropping it silently blanked that dimension on every
|
|
811
|
-
// shelf, PLP and search result. One short string per product.
|
|
812
|
-
model: productReference,
|
|
813
|
-
} satisfies ProductGroup;
|
|
814
|
-
})()
|
|
815
|
-
: undefined;
|
|
816
|
-
|
|
817
|
-
const categoriesString = splitCategory(product.categories[0]).join(DEFAULT_CATEGORY_SEPARATOR);
|
|
818
|
-
|
|
819
|
-
return {
|
|
820
|
-
"@type": "Product",
|
|
821
|
-
category: categoriesString,
|
|
822
|
-
productID: skuId,
|
|
823
|
-
url: getProductURL(baseUrl, product, sku.itemId).href,
|
|
824
|
-
name,
|
|
825
|
-
brand: { "@type": "Brand", name: product.brand },
|
|
826
|
-
inProductGroupWithID: productId,
|
|
827
|
-
sku: skuId,
|
|
828
|
-
image: finalImages,
|
|
829
|
-
offers: aggregateOffers(leanOffers, priceCurrency),
|
|
830
|
-
isVariantOf,
|
|
831
|
-
additionalProperty,
|
|
832
|
-
};
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
/** Property names that differentiate SKU variants (used by variant selectors) */
|
|
836
|
-
const VARIANT_PROPERTY_NAMES = new Set(["Cor", "Voltagem", "Tamanho"]);
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Build a minimal offer for variant display. Keeps only availability and seller.
|
|
840
|
-
* No priceSpecification, no teasers. inventoryLevel is preserved from the real
|
|
841
|
-
* offer unless `includeInventory` is false (variant selectors rely on it to
|
|
842
|
-
* decide stock state per SKU — dropping it hard-zeroes every variant).
|
|
843
|
-
*/
|
|
844
|
-
const buildOfferVariant = (offer: Offer, includeInventory: boolean): Offer => ({
|
|
845
|
-
"@type": "Offer",
|
|
846
|
-
identifier: offer.identifier,
|
|
847
|
-
price: offer.price,
|
|
848
|
-
seller: offer.seller,
|
|
849
|
-
availability: offer.availability,
|
|
850
|
-
priceSpecification: [],
|
|
851
|
-
inventoryLevel: includeInventory ? offer.inventoryLevel : { value: 0 },
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Minimal product transform for variant entries inside isVariantOf.hasVariant[].
|
|
856
|
-
*
|
|
857
|
-
* Keeps only what variant selectors need:
|
|
858
|
-
* - url, productID, sku, name, inProductGroupWithID
|
|
859
|
-
* - additionalProperty filtered to variant-differentiating props (Cor, Voltagem, Tamanho)
|
|
860
|
-
* - image[0] (when variantIncludeImage is not false) — selectors render thumbnails
|
|
861
|
-
* - offers with availability + seller + real inventoryLevel (when variantIncludeInventory
|
|
862
|
-
* is not false) — selectors decide stock state per SKU from inventoryLevel.value
|
|
863
|
-
*
|
|
864
|
-
* Drops: description, video, brand, category, gtin, releaseDate,
|
|
865
|
-
* alternateName, isAccessoryOrSparePartFor, isVariantOf
|
|
866
|
-
*/
|
|
867
|
-
export const toProductVariant = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
868
|
-
product: P,
|
|
869
|
-
sku: P["items"][number],
|
|
870
|
-
options: ProductOptions,
|
|
871
|
-
): Product => {
|
|
872
|
-
const { baseUrl, priceCurrency } = options;
|
|
873
|
-
const { productId, items } = product;
|
|
874
|
-
const { name, itemId: skuId } = sku;
|
|
875
|
-
const variantProps = options.variantPropertyNames ?? VARIANT_PROPERTY_NAMES;
|
|
876
|
-
const includeImage = options.variantIncludeImage !== false;
|
|
877
|
-
const includeInventory = options.variantIncludeInventory !== false;
|
|
878
|
-
|
|
879
|
-
// additionalProperty: variant-differentiating specs, plus the SKU reference
|
|
880
|
-
// ids. Analytics reads the SKU's `RefId` off this list — GA4 `dimension2` on
|
|
881
|
-
// view_item_list / select_item, and the equivalent on productImpression /
|
|
882
|
-
// productClick. It lives in `referenceId`, not in `variations`, so the
|
|
883
|
-
// `variantProps` filter dropped it and blanked that dimension on every shelf
|
|
884
|
-
// card rendered with `shelfCompleteVariants`. One short string per SKU.
|
|
885
|
-
const specificationsAdditionalProperty = isLegacySku(sku)
|
|
886
|
-
? toAdditionalPropertiesLegacy(sku)
|
|
887
|
-
: toAdditionalProperties(sku);
|
|
888
|
-
const additionalProperty = [
|
|
889
|
-
...specificationsAdditionalProperty.filter((prop) => variantProps.has(prop.name ?? "")),
|
|
890
|
-
...(toAdditionalPropertyReferenceIds(sku.referenceId ?? []) ?? []),
|
|
891
|
-
];
|
|
892
|
-
|
|
893
|
-
// Offers: best seller, lean (availability + seller; optional inventoryLevel)
|
|
894
|
-
const offerConverter = isLegacyProduct(product) ? toOfferLegacy : toOffer;
|
|
895
|
-
const allOffers = (sku.sellers ?? []).map(offerConverter).sort(bestOfferFirst);
|
|
896
|
-
const bestOffer = allOffers[0];
|
|
897
|
-
// `variantKeepLadder` marks the ONE variant the card renders (see
|
|
898
|
-
// displayedVariantId): it keeps the LEAN shape but needs a real ladder,
|
|
899
|
-
// through the same `priceSpecifications` hook the root offer uses.
|
|
900
|
-
const leanOffers = bestOffer
|
|
901
|
-
? [
|
|
902
|
-
options.variantKeepLadder
|
|
903
|
-
? {
|
|
904
|
-
...buildOfferVariant(bestOffer, includeInventory),
|
|
905
|
-
priceSpecification: applyPriceSpecifications([bestOffer], options)[0]
|
|
906
|
-
.priceSpecification,
|
|
907
|
-
}
|
|
908
|
-
: buildOfferVariant(bestOffer, includeInventory),
|
|
909
|
-
]
|
|
910
|
-
: [];
|
|
911
|
-
|
|
912
|
-
// image[0] only — selectors render a single thumbnail. Reuse the same
|
|
913
|
-
// imagesByKey lookup toProduct uses so URLs stay consistent across variants.
|
|
914
|
-
const imagesByKey =
|
|
915
|
-
options.imagesByKey ??
|
|
916
|
-
items
|
|
917
|
-
.flatMap((i) => i.images)
|
|
918
|
-
.reduce((map, img) => {
|
|
919
|
-
img?.imageUrl && map.set(getImageKey(img.imageUrl), img.imageUrl);
|
|
920
|
-
return map;
|
|
921
|
-
}, new Map<string, string>());
|
|
922
|
-
|
|
923
|
-
const image = includeImage
|
|
924
|
-
? nonEmptyArray(sku.images)
|
|
925
|
-
?.slice(0, 1)
|
|
926
|
-
.map(({ imageUrl, imageText, imageLabel }) => ({
|
|
927
|
-
"@type": "ImageObject" as const,
|
|
928
|
-
alternateName: imageText || imageLabel || "",
|
|
929
|
-
url: imagesByKey.get(getImageKey(imageUrl)) ?? imageUrl,
|
|
930
|
-
name: imageLabel || "",
|
|
931
|
-
encodingFormat: "image",
|
|
932
|
-
}))
|
|
933
|
-
: undefined;
|
|
934
|
-
|
|
935
|
-
return {
|
|
936
|
-
"@type": "Product",
|
|
937
|
-
productID: skuId,
|
|
938
|
-
sku: skuId,
|
|
939
|
-
name,
|
|
940
|
-
url: getProductURL(baseUrl, product, sku.itemId).href,
|
|
941
|
-
inProductGroupWithID: productId,
|
|
942
|
-
additionalProperty,
|
|
943
|
-
...(image ? { image } : {}),
|
|
944
|
-
offers: aggregateOffers(leanOffers, priceCurrency),
|
|
945
|
-
};
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
const toBreadcrumbList = (
|
|
949
|
-
product: ProductVTEX | LegacyProductVTEX,
|
|
950
|
-
{ baseUrl }: ProductOptions,
|
|
951
|
-
): BreadcrumbList => {
|
|
952
|
-
const { categories, productName } = product;
|
|
953
|
-
const names = categories[0]?.split("/").filter(Boolean);
|
|
954
|
-
|
|
955
|
-
const segments = names.map(slugify);
|
|
956
|
-
|
|
957
|
-
return {
|
|
958
|
-
"@type": "BreadcrumbList",
|
|
959
|
-
itemListElement: [
|
|
960
|
-
...names.map((name, index) => {
|
|
961
|
-
const position = index + 1;
|
|
962
|
-
|
|
963
|
-
return {
|
|
964
|
-
"@type": "ListItem" as const,
|
|
965
|
-
name,
|
|
966
|
-
item: new URL(`/${segments.slice(0, position).join("/")}`, baseUrl).href,
|
|
967
|
-
position,
|
|
968
|
-
};
|
|
969
|
-
}),
|
|
970
|
-
{
|
|
971
|
-
"@type": "ListItem",
|
|
972
|
-
name: productName,
|
|
973
|
-
item: getProductGroupURL(baseUrl, product).href,
|
|
974
|
-
position: categories.length + 1,
|
|
975
|
-
},
|
|
976
|
-
],
|
|
977
|
-
numberOfItems: categories.length + 1,
|
|
978
|
-
};
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
const legacyToProductGroupAdditionalProperties = (product: LegacyProductVTEX) => {
|
|
982
|
-
const groups = product.allSpecificationsGroups ?? [];
|
|
983
|
-
const allSpecifications = product.allSpecifications ?? [];
|
|
984
|
-
|
|
985
|
-
const specByGroup: Record<string, string> = {};
|
|
986
|
-
|
|
987
|
-
groups.forEach((group) => {
|
|
988
|
-
const groupSpecs = (product as unknown as Record<string, string[]>)[group];
|
|
989
|
-
groupSpecs.forEach((specName) => {
|
|
990
|
-
specByGroup[specName] = group;
|
|
991
|
-
});
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
return allSpecifications.flatMap((name) => {
|
|
995
|
-
const values = (product as unknown as Record<string, string[]>)[name];
|
|
996
|
-
return values.map((value) =>
|
|
997
|
-
toAdditionalPropertySpecification({
|
|
998
|
-
name,
|
|
999
|
-
value,
|
|
1000
|
-
propertyID: specByGroup[name],
|
|
1001
|
-
}),
|
|
1002
|
-
);
|
|
1003
|
-
});
|
|
1004
|
-
};
|
|
1005
|
-
|
|
1006
|
-
const toProductGroupAdditionalProperties = ({ specificationGroups = [] }: ProductVTEX) =>
|
|
1007
|
-
specificationGroups.flatMap(({ name: groupName, specifications }) =>
|
|
1008
|
-
specifications.flatMap(({ name, values }) =>
|
|
1009
|
-
values.map(
|
|
1010
|
-
(value) =>
|
|
1011
|
-
({
|
|
1012
|
-
"@type": "PropertyValue",
|
|
1013
|
-
name,
|
|
1014
|
-
value,
|
|
1015
|
-
propertyID: groupName,
|
|
1016
|
-
valueReference: "PROPERTY" as string,
|
|
1017
|
-
}) as const,
|
|
1018
|
-
),
|
|
1019
|
-
),
|
|
1020
|
-
);
|
|
1021
|
-
|
|
1022
|
-
const toOriginalAttributesAdditionalProperties = (
|
|
1023
|
-
originalAttributes: Maybe<string[]>,
|
|
1024
|
-
product: ProductVTEX | LegacyProduct,
|
|
1025
|
-
) => {
|
|
1026
|
-
if (!originalAttributes) {
|
|
1027
|
-
return [];
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const attributes = pick(originalAttributes as Array<keyof typeof product>, product) ?? {};
|
|
1031
|
-
|
|
1032
|
-
return Object.entries(attributes).map(
|
|
1033
|
-
([name, value]) =>
|
|
1034
|
-
({
|
|
1035
|
-
"@type": "PropertyValue",
|
|
1036
|
-
name,
|
|
1037
|
-
value,
|
|
1038
|
-
valueReference: "ORIGINAL_PROPERTY" as string,
|
|
1039
|
-
}) as const,
|
|
1040
|
-
) as unknown as PropertyValue[];
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
const toAdditionalProperties = (sku: SkuVTEX): PropertyValue[] =>
|
|
1044
|
-
sku.variations?.flatMap(({ name, values }) =>
|
|
1045
|
-
values.map((value) => toAdditionalPropertySpecification({ name, value })),
|
|
1046
|
-
) ?? [];
|
|
1047
|
-
|
|
1048
|
-
export const toAdditionalPropertySpecification = ({
|
|
1049
|
-
name,
|
|
1050
|
-
value,
|
|
1051
|
-
propertyID,
|
|
1052
|
-
}: {
|
|
1053
|
-
name: string;
|
|
1054
|
-
value: string;
|
|
1055
|
-
propertyID?: string;
|
|
1056
|
-
}): PropertyValue => ({
|
|
1057
|
-
"@type": "PropertyValue",
|
|
1058
|
-
name,
|
|
1059
|
-
value,
|
|
1060
|
-
propertyID,
|
|
1061
|
-
valueReference: "SPECIFICATION",
|
|
1062
|
-
});
|
|
1063
|
-
|
|
1064
|
-
const toAdditionalPropertiesLegacy = (sku: LegacySkuVTEX): PropertyValue[] => {
|
|
1065
|
-
const { variations = [], attachments = [] } = sku;
|
|
1066
|
-
|
|
1067
|
-
const specificationProperties = variations.flatMap((variation) =>
|
|
1068
|
-
sku[variation].map((value) => toAdditionalPropertySpecification({ name: variation, value })),
|
|
1069
|
-
);
|
|
1070
|
-
|
|
1071
|
-
const attachmentProperties = attachments.map(
|
|
1072
|
-
(attachment) =>
|
|
1073
|
-
({
|
|
1074
|
-
"@type": "PropertyValue",
|
|
1075
|
-
propertyID: `${attachment.id}`,
|
|
1076
|
-
name: attachment.name,
|
|
1077
|
-
value: attachment.domainValues,
|
|
1078
|
-
required: attachment.required,
|
|
1079
|
-
valueReference: "ATTACHMENT",
|
|
1080
|
-
}) as const,
|
|
1081
|
-
);
|
|
1082
|
-
|
|
1083
|
-
if (attachmentProperties.length === 0) return specificationProperties;
|
|
1084
|
-
specificationProperties.push(...attachmentProperties);
|
|
1085
|
-
return specificationProperties;
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
|
-
const buildOffer = (
|
|
1089
|
-
{ commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
|
|
1090
|
-
teasers: Teasers[],
|
|
1091
|
-
): Offer => ({
|
|
1092
|
-
"@type": "Offer",
|
|
1093
|
-
identifier: sellerDefault ? "default" : undefined,
|
|
1094
|
-
price: offer.spotPrice ?? offer.Price,
|
|
1095
|
-
seller: sellerId,
|
|
1096
|
-
sellerName,
|
|
1097
|
-
priceValidUntil: offer.PriceValidUntil,
|
|
1098
|
-
inventoryLevel: { value: offer.AvailableQuantity },
|
|
1099
|
-
giftSkuIds: offer.GiftSkuIds ?? [],
|
|
1100
|
-
teasers,
|
|
1101
|
-
priceSpecification: [
|
|
1102
|
-
{
|
|
1103
|
-
"@type": "UnitPriceSpecification",
|
|
1104
|
-
priceType: SCHEMA_LIST_PRICE,
|
|
1105
|
-
price: offer.ListPrice,
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
"@type": "UnitPriceSpecification",
|
|
1109
|
-
priceType: SCHEMA_SALE_PRICE,
|
|
1110
|
-
price: offer.Price,
|
|
1111
|
-
},
|
|
1112
|
-
{
|
|
1113
|
-
"@type": "UnitPriceSpecification",
|
|
1114
|
-
priceType: SCHEMA_SRP,
|
|
1115
|
-
price: offer.PriceWithoutDiscount,
|
|
1116
|
-
},
|
|
1117
|
-
...offer.Installments.map(
|
|
1118
|
-
(installment): UnitPriceSpecification => ({
|
|
1119
|
-
"@type": "UnitPriceSpecification",
|
|
1120
|
-
priceType: SCHEMA_SALE_PRICE,
|
|
1121
|
-
priceComponentType: SCHEMA_INSTALLMENT,
|
|
1122
|
-
name: installment.PaymentSystemName,
|
|
1123
|
-
description: installment.Name,
|
|
1124
|
-
billingDuration: installment.NumberOfInstallments,
|
|
1125
|
-
billingIncrement: installment.Value,
|
|
1126
|
-
price: installment.TotalValuePlusInterestRate,
|
|
1127
|
-
}),
|
|
1128
|
-
),
|
|
1129
|
-
],
|
|
1130
|
-
availability: offer.AvailableQuantity > 0 ? SCHEMA_IN_STOCK : SCHEMA_OUT_OF_STOCK,
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
const toOffer = (seller: SellerVTEX): Offer =>
|
|
1134
|
-
buildOffer(seller, seller.commertialOffer.teasers ?? []);
|
|
1135
|
-
|
|
1136
|
-
const toOfferLegacy = (seller: SellerVTEX): Offer => {
|
|
1137
|
-
const otherTeasers =
|
|
1138
|
-
seller.commertialOffer.DiscountHighLight?.map((i) => {
|
|
1139
|
-
const discount = i as Record<string, string>;
|
|
1140
|
-
const [_k__BackingField, discountName] = Object.entries(discount)?.[0] ?? [];
|
|
1141
|
-
|
|
1142
|
-
const teasers: Teasers = {
|
|
1143
|
-
name: discountName,
|
|
1144
|
-
conditions: {
|
|
1145
|
-
minimumQuantity: 0,
|
|
1146
|
-
parameters: [],
|
|
1147
|
-
},
|
|
1148
|
-
effects: {
|
|
1149
|
-
parameters: [],
|
|
1150
|
-
},
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
return teasers;
|
|
1154
|
-
}) ?? [];
|
|
1155
|
-
|
|
1156
|
-
const legacyTeasers = (seller.commertialOffer.Teasers ?? []).map((teaser) => ({
|
|
1157
|
-
name: teaser["<Name>k__BackingField"],
|
|
1158
|
-
generalValues: teaser["<GeneralValues>k__BackingField"],
|
|
1159
|
-
conditions: {
|
|
1160
|
-
minimumQuantity: teaser["<Conditions>k__BackingField"]["<MinimumQuantity>k__BackingField"],
|
|
1161
|
-
parameters: teaser["<Conditions>k__BackingField"]["<Parameters>k__BackingField"].map(
|
|
1162
|
-
(parameter) => ({
|
|
1163
|
-
name: parameter["<Name>k__BackingField"],
|
|
1164
|
-
value: parameter["<Value>k__BackingField"],
|
|
1165
|
-
}),
|
|
1166
|
-
),
|
|
1167
|
-
},
|
|
1168
|
-
effects: {
|
|
1169
|
-
parameters: teaser["<Effects>k__BackingField"]["<Parameters>k__BackingField"].map(
|
|
1170
|
-
(parameter) => ({
|
|
1171
|
-
name: parameter["<Name>k__BackingField"],
|
|
1172
|
-
value: parameter["<Value>k__BackingField"],
|
|
1173
|
-
}),
|
|
1174
|
-
),
|
|
1175
|
-
},
|
|
1176
|
-
}));
|
|
1177
|
-
|
|
1178
|
-
return buildOffer(seller, [...otherTeasers, ...legacyTeasers]);
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
export const legacyFacetToFilter = (
|
|
1182
|
-
name: string,
|
|
1183
|
-
facets: LegacyFacet[],
|
|
1184
|
-
url: URL,
|
|
1185
|
-
map: string,
|
|
1186
|
-
term: string,
|
|
1187
|
-
behavior: "dynamic" | "static",
|
|
1188
|
-
ignoreCaseSelected?: boolean,
|
|
1189
|
-
fullPath = false,
|
|
1190
|
-
): Filter | null => {
|
|
1191
|
-
const mapSegments = map.split(",").filter((x) => x.length > 0);
|
|
1192
|
-
const pathSegments = term.replace(/^\//, "").split("/").slice(0, mapSegments.length);
|
|
1193
|
-
|
|
1194
|
-
const mapSet = new Set(mapSegments.map((i) => (ignoreCaseSelected ? i.toLowerCase() : i)));
|
|
1195
|
-
const pathSet = new Set(pathSegments.map((i) => (ignoreCaseSelected ? i.toLowerCase() : i)));
|
|
1196
|
-
|
|
1197
|
-
// for productClusterIds, we have to use the full path
|
|
1198
|
-
// example:
|
|
1199
|
-
// category2/123?map=c,productClusterIds -> DO NOT WORK
|
|
1200
|
-
// category1/category2/123?map=c,c,productClusterIds -> WORK
|
|
1201
|
-
const hasProductClusterIds = mapSegments.includes("productClusterIds");
|
|
1202
|
-
const hasToBeFullpath =
|
|
1203
|
-
fullPath || hasProductClusterIds || mapSegments.includes("ft") || mapSegments.includes("b");
|
|
1204
|
-
|
|
1205
|
-
const getLink = (facet: LegacyFacet, selected: boolean) => {
|
|
1206
|
-
const index = pathSegments.findIndex((s) => {
|
|
1207
|
-
if (ignoreCaseSelected) {
|
|
1208
|
-
return s.toLowerCase() === facet.Value.toLowerCase();
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
return s === facet.Value;
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
const map = hasToBeFullpath ? facet.Link.split("map=")[1].split(",") : [facet.Map];
|
|
1215
|
-
const value = hasToBeFullpath ? facet.Link.split("?")[0].slice(1).split("/") : [facet.Value];
|
|
1216
|
-
|
|
1217
|
-
const pathSegmentsFiltered = hasProductClusterIds
|
|
1218
|
-
? [pathSegments[mapSegments.indexOf("productClusterIds")]]
|
|
1219
|
-
: [];
|
|
1220
|
-
const mapSegmentsFiltered = hasProductClusterIds ? ["productClusterIds"] : [];
|
|
1221
|
-
|
|
1222
|
-
const _mapSegments = hasToBeFullpath ? mapSegmentsFiltered : mapSegments;
|
|
1223
|
-
const _pathSegments = hasToBeFullpath ? pathSegmentsFiltered : pathSegments;
|
|
1224
|
-
|
|
1225
|
-
const newMap = selected
|
|
1226
|
-
? [...mapSegments.filter((_, i) => i !== index)]
|
|
1227
|
-
: [..._mapSegments, ...map];
|
|
1228
|
-
const newPath = selected
|
|
1229
|
-
? [...pathSegments.filter((_, i) => i !== index)]
|
|
1230
|
-
: [..._pathSegments, ...value];
|
|
1231
|
-
|
|
1232
|
-
// Insertion-sort like algorithm. Uses the c-continuum theorem
|
|
1233
|
-
const zipped: [string, string][] = [];
|
|
1234
|
-
for (let it = 0; it < newMap.length; it++) {
|
|
1235
|
-
let i = 0;
|
|
1236
|
-
while (i < zipped.length && (zipped[i][0] === "c" || zipped[i][0] === "C")) i++;
|
|
1237
|
-
|
|
1238
|
-
zipped.splice(i, 0, [newMap[it], newPath[it]]);
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
const link = new URL(`/${zipped.map(([, s]) => s).join("/")}`, url);
|
|
1242
|
-
link.searchParams.set("map", zipped.map(([m]) => m).join(","));
|
|
1243
|
-
if (behavior === "static") {
|
|
1244
|
-
link.searchParams.set("fmap", url.searchParams.get("fmap") || mapSegments.join(","));
|
|
1245
|
-
}
|
|
1246
|
-
const currentQuery = url.searchParams.get("q");
|
|
1247
|
-
if (currentQuery) {
|
|
1248
|
-
link.searchParams.set("q", currentQuery);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
return `${link.pathname}${link.search}`;
|
|
1252
|
-
};
|
|
1253
|
-
|
|
1254
|
-
return {
|
|
1255
|
-
"@type": "FilterToggle",
|
|
1256
|
-
quantity: facets?.length,
|
|
1257
|
-
label: name,
|
|
1258
|
-
key: name,
|
|
1259
|
-
values: facets.map((facet) => {
|
|
1260
|
-
const normalizedFacet = name !== "PriceRanges" ? facet : normalizeFacet(facet);
|
|
1261
|
-
|
|
1262
|
-
const selected =
|
|
1263
|
-
mapSet.has(ignoreCaseSelected ? normalizedFacet.Map.toLowerCase() : normalizedFacet.Map) &&
|
|
1264
|
-
pathSet.has(
|
|
1265
|
-
ignoreCaseSelected ? normalizedFacet.Value.toLowerCase() : normalizedFacet.Value,
|
|
1266
|
-
);
|
|
1267
|
-
|
|
1268
|
-
return {
|
|
1269
|
-
value: normalizedFacet.Value,
|
|
1270
|
-
quantity: normalizedFacet.Quantity,
|
|
1271
|
-
url: getLink(normalizedFacet, selected),
|
|
1272
|
-
label: normalizedFacet.Name,
|
|
1273
|
-
selected,
|
|
1274
|
-
children:
|
|
1275
|
-
facet.Children?.length > 0
|
|
1276
|
-
? legacyFacetToFilter(
|
|
1277
|
-
normalizedFacet.Name,
|
|
1278
|
-
facet.Children,
|
|
1279
|
-
url,
|
|
1280
|
-
map,
|
|
1281
|
-
term,
|
|
1282
|
-
behavior,
|
|
1283
|
-
ignoreCaseSelected,
|
|
1284
|
-
fullPath,
|
|
1285
|
-
)
|
|
1286
|
-
: undefined,
|
|
1287
|
-
};
|
|
1288
|
-
}),
|
|
1289
|
-
};
|
|
1290
|
-
};
|
|
1291
|
-
|
|
1292
|
-
export const filtersToSearchParams = (
|
|
1293
|
-
selectedFacets: SelectedFacet[],
|
|
1294
|
-
paramsToPersist?: URLSearchParams,
|
|
1295
|
-
) => {
|
|
1296
|
-
const searchParams = new URLSearchParams(paramsToPersist);
|
|
1297
|
-
|
|
1298
|
-
for (const { key, value } of selectedFacets) {
|
|
1299
|
-
searchParams.append(`filter.${key}`, value);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
return searchParams;
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
|
-
const fromLegacyMap: Record<string, string> = {
|
|
1306
|
-
priceFrom: "price",
|
|
1307
|
-
productClusterSearchableIds: "productClusterIds",
|
|
1308
|
-
};
|
|
1309
|
-
|
|
1310
|
-
export const legacyFacetsNormalize = (map: string, path: string) => {
|
|
1311
|
-
// Replace legacy price path param to IS price facet format
|
|
1312
|
-
// exemple: de-34,90-a-56,90 turns to 34.90:56.90
|
|
1313
|
-
// may this regex have to be adjusted for international stores
|
|
1314
|
-
const value = path.replace(
|
|
1315
|
-
/de-(?<from>\d+[,]?[\d]+)-a-(?<to>\d+[,]?[\d]+)/,
|
|
1316
|
-
(_match, from, to) => {
|
|
1317
|
-
return `${from.replace(",", ".")}:${to.replace(",", ".")}`;
|
|
1318
|
-
},
|
|
1319
|
-
);
|
|
1320
|
-
|
|
1321
|
-
const key = fromLegacyMap[map] || map;
|
|
1322
|
-
|
|
1323
|
-
return { key, value };
|
|
1324
|
-
};
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* Transform ?map urls into selected facets. This happens when a store is migrating
|
|
1328
|
-
* to Deco and also migrating from VTEX Legacy to VTEX Intelligent Search.
|
|
1329
|
-
*/
|
|
1330
|
-
export const legacyFacetsFromURL = (url: URL) => {
|
|
1331
|
-
const mapSegments = url.searchParams.get("map")?.split(",") ?? [];
|
|
1332
|
-
const pathSegments = url.pathname.split("/").slice(1); // Remove first slash
|
|
1333
|
-
const length = Math.min(mapSegments.length, pathSegments.length);
|
|
1334
|
-
|
|
1335
|
-
const selectedFacets: SelectedFacet[] = [];
|
|
1336
|
-
for (let it = 0; it < length; it++) {
|
|
1337
|
-
const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
|
|
1338
|
-
|
|
1339
|
-
selectedFacets.push(facet);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
return selectedFacets;
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
export const filtersFromURL = (url: URL) => {
|
|
1346
|
-
const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
|
|
1347
|
-
|
|
1348
|
-
url.searchParams.forEach((value, name) => {
|
|
1349
|
-
const [filter, key] = name.split(".");
|
|
1350
|
-
|
|
1351
|
-
if (filter === "filter" && typeof key === "string") {
|
|
1352
|
-
selectedFacets.push({ key, value });
|
|
1353
|
-
}
|
|
1354
|
-
});
|
|
1355
|
-
|
|
1356
|
-
return selectedFacets;
|
|
1357
|
-
};
|
|
1358
|
-
|
|
1359
|
-
export const mergeFacets = (f1: SelectedFacet[], f2: SelectedFacet[]): SelectedFacet[] => {
|
|
1360
|
-
const facetKey = (facet: SelectedFacet) => `key:${facet.key}-value:${facet.value}`;
|
|
1361
|
-
const merged = new Map<string, SelectedFacet>();
|
|
1362
|
-
|
|
1363
|
-
for (const f of f1) {
|
|
1364
|
-
merged.set(facetKey(f), f);
|
|
1365
|
-
}
|
|
1366
|
-
for (const f of f2) {
|
|
1367
|
-
merged.set(facetKey(f), f);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
return [...merged.values()];
|
|
1371
|
-
};
|
|
1372
|
-
|
|
1373
|
-
const isValueRange = (facet: FacetValueRange | FacetValueBoolean): facet is FacetValueRange =>
|
|
1374
|
-
// deno-lint-ignore no-explicit-any
|
|
1375
|
-
Boolean((facet as any).range);
|
|
1376
|
-
|
|
1377
|
-
const facetToToggle =
|
|
1378
|
-
(selectedFacets: SelectedFacet[], key: string, paramsToPersist?: URLSearchParams) =>
|
|
1379
|
-
(item: FacetValueRange | FacetValueBoolean): FilterToggleValue => {
|
|
1380
|
-
const { quantity, selected } = item;
|
|
1381
|
-
const isRange = isValueRange(item);
|
|
1382
|
-
|
|
1383
|
-
const value = isRange ? formatRange(item.range.from, item.range.to) : item.value;
|
|
1384
|
-
const label = isRange ? value : item.name;
|
|
1385
|
-
const facet = { key, value };
|
|
1386
|
-
|
|
1387
|
-
const filters = selected
|
|
1388
|
-
? selectedFacets.filter((f) => f.key !== key || f.value !== value)
|
|
1389
|
-
: [...selectedFacets, facet];
|
|
1390
|
-
|
|
1391
|
-
return {
|
|
1392
|
-
value,
|
|
1393
|
-
quantity,
|
|
1394
|
-
selected,
|
|
1395
|
-
url: `?${filtersToSearchParams(filters, paramsToPersist)}`,
|
|
1396
|
-
label,
|
|
1397
|
-
};
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
export const toFilter =
|
|
1401
|
-
(selectedFacets: SelectedFacet[], paramsToPersist?: URLSearchParams) =>
|
|
1402
|
-
({ key, name, quantity, values }: FacetVTEX): Filter => ({
|
|
1403
|
-
"@type": "FilterToggle",
|
|
1404
|
-
key,
|
|
1405
|
-
label: name,
|
|
1406
|
-
quantity: quantity,
|
|
1407
|
-
values: values.map(facetToToggle(selectedFacets, key, paramsToPersist)),
|
|
1408
|
-
});
|
|
1409
|
-
|
|
1410
|
-
function nodeToNavbar(node: Category): SiteNavigationElement {
|
|
1411
|
-
const url = new URL(node.url, "https://example.com");
|
|
1412
|
-
|
|
1413
|
-
return {
|
|
1414
|
-
"@type": "SiteNavigationElement",
|
|
1415
|
-
url: `${url.pathname}${url.search}`,
|
|
1416
|
-
name: node.name,
|
|
1417
|
-
children: node.children.map(nodeToNavbar),
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
export const categoryTreeToNavbar = (tree: Category[]): SiteNavigationElement[] =>
|
|
1422
|
-
tree.map(nodeToNavbar);
|
|
1423
|
-
|
|
1424
|
-
export const toBrand = (
|
|
1425
|
-
{ id, name, imageUrl, metaTagDescription }: BrandVTEX,
|
|
1426
|
-
baseUrl: string,
|
|
1427
|
-
): Brand => ({
|
|
1428
|
-
"@type": "Brand",
|
|
1429
|
-
"@id": `${id}`,
|
|
1430
|
-
name,
|
|
1431
|
-
logo: imageUrl?.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`,
|
|
1432
|
-
description: metaTagDescription,
|
|
1433
|
-
});
|
|
1434
|
-
|
|
1435
|
-
export const normalizeFacet = (facet: LegacyFacet) => {
|
|
1436
|
-
return {
|
|
1437
|
-
...facet,
|
|
1438
|
-
Map: "priceFrom",
|
|
1439
|
-
Value: facet.Slug!,
|
|
1440
|
-
};
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
export const toReview = (
|
|
1444
|
-
products: Product[],
|
|
1445
|
-
ratings: ProductRating[],
|
|
1446
|
-
reviews: ProductReviewData[],
|
|
1447
|
-
): Product[] => {
|
|
1448
|
-
return products.map((p, index) => {
|
|
1449
|
-
const ratingsCount = ratings[index].totalCount || 0;
|
|
1450
|
-
const productReviews = reviews[index].data || [];
|
|
1451
|
-
|
|
1452
|
-
return {
|
|
1453
|
-
...p,
|
|
1454
|
-
aggregateRating: {
|
|
1455
|
-
"@type": "AggregateRating",
|
|
1456
|
-
reviewCount: ratingsCount,
|
|
1457
|
-
ratingCount: ratingsCount,
|
|
1458
|
-
ratingValue: ratings[index]?.average || 0,
|
|
1459
|
-
},
|
|
1460
|
-
review: productReviews.map((_, reviewIndex) => ({
|
|
1461
|
-
"@type": "Review",
|
|
1462
|
-
id: productReviews[reviewIndex]?.id?.toString(),
|
|
1463
|
-
author: [
|
|
1464
|
-
{
|
|
1465
|
-
"@type": "Author",
|
|
1466
|
-
name: productReviews[reviewIndex]?.reviewerName,
|
|
1467
|
-
verifiedBuyer: productReviews[reviewIndex]?.verifiedPurchaser,
|
|
1468
|
-
},
|
|
1469
|
-
],
|
|
1470
|
-
itemReviewed: productReviews[reviewIndex]?.productId,
|
|
1471
|
-
datePublished: productReviews[reviewIndex]?.reviewDateTime,
|
|
1472
|
-
reviewHeadline: productReviews[reviewIndex]?.title,
|
|
1473
|
-
reviewBody: productReviews[reviewIndex]?.text,
|
|
1474
|
-
reviewRating: {
|
|
1475
|
-
"@type": "AggregateRating",
|
|
1476
|
-
ratingValue: productReviews[reviewIndex]?.rating || 0,
|
|
1477
|
-
},
|
|
1478
|
-
})),
|
|
1479
|
-
};
|
|
1480
|
-
});
|
|
1481
|
-
};
|
|
1482
|
-
|
|
1483
|
-
export const toInventories = (
|
|
1484
|
-
products: Product[],
|
|
1485
|
-
inventoriesData: ProductInventoryData[],
|
|
1486
|
-
): Product[] => {
|
|
1487
|
-
return products.map((p, index) => {
|
|
1488
|
-
const balance = inventoriesData[index].balance || [];
|
|
1489
|
-
|
|
1490
|
-
const additionalProperty = Array.from(p.additionalProperty || []);
|
|
1491
|
-
|
|
1492
|
-
const inventories: PropertyValue[] = balance.map((b) => ({
|
|
1493
|
-
"@type": "PropertyValue",
|
|
1494
|
-
valueReference: "INVENTORY",
|
|
1495
|
-
propertyID: b.warehouseId,
|
|
1496
|
-
name: b.warehouseName,
|
|
1497
|
-
value: b.totalQuantity?.toString(),
|
|
1498
|
-
}));
|
|
1499
|
-
|
|
1500
|
-
return {
|
|
1501
|
-
...p,
|
|
1502
|
-
additionalProperty: [...additionalProperty, ...inventories],
|
|
1503
|
-
};
|
|
1504
|
-
});
|
|
1505
|
-
};
|
|
1506
|
-
|
|
1507
|
-
type ProductMap = Record<string, Product>;
|
|
1508
|
-
|
|
1509
|
-
export const sortProducts = (
|
|
1510
|
-
products: Product[],
|
|
1511
|
-
orderOfIdsOrSkus: string[],
|
|
1512
|
-
prop: "sku" | "inProductGroupWithID",
|
|
1513
|
-
) => {
|
|
1514
|
-
const productMap: ProductMap = {};
|
|
1515
|
-
|
|
1516
|
-
products.forEach((product) => {
|
|
1517
|
-
productMap[product[prop] || product.sku] = product;
|
|
1518
|
-
});
|
|
1519
|
-
|
|
1520
|
-
return orderOfIdsOrSkus.map((id) => productMap[id]);
|
|
1521
|
-
};
|
|
1522
|
-
|
|
1523
|
-
export const parsePageType = (p: PageTypeVTEX): PageType => {
|
|
1524
|
-
const type = p.pageType;
|
|
1525
|
-
|
|
1526
|
-
// Search or Busca vazia
|
|
1527
|
-
if (type === "FullText") {
|
|
1528
|
-
return "Search";
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// A page that vtex doesn't recognize
|
|
1532
|
-
if (type === "NotFound") {
|
|
1533
|
-
return "Unknown";
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
return type;
|
|
1537
|
-
};
|
|
1538
|
-
|
|
1539
|
-
function dayOfWeekIndexToString(day?: number): DayOfWeek | undefined {
|
|
1540
|
-
switch (day) {
|
|
1541
|
-
case 0:
|
|
1542
|
-
return "Sunday";
|
|
1543
|
-
case 1:
|
|
1544
|
-
return "Monday";
|
|
1545
|
-
case 2:
|
|
1546
|
-
return "Tuesday";
|
|
1547
|
-
case 3:
|
|
1548
|
-
return "Wednesday";
|
|
1549
|
-
case 4:
|
|
1550
|
-
return "Thursday";
|
|
1551
|
-
case 5:
|
|
1552
|
-
return "Friday";
|
|
1553
|
-
case 6:
|
|
1554
|
-
return "Saturday";
|
|
1555
|
-
default:
|
|
1556
|
-
return undefined;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
interface Hours {
|
|
1561
|
-
dayOfWeek?: number;
|
|
1562
|
-
openingTime?: string;
|
|
1563
|
-
closingTime?: string;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function toHoursSpecification(hours: Hours): OpeningHoursSpecification {
|
|
1567
|
-
return {
|
|
1568
|
-
"@type": "OpeningHoursSpecification",
|
|
1569
|
-
opens: hours.openingTime,
|
|
1570
|
-
closes: hours.closingTime,
|
|
1571
|
-
dayOfWeek: dayOfWeekIndexToString(hours.dayOfWeek),
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function toSpecialHoursSpecification(holiday: PickupHolidays): OpeningHoursSpecification {
|
|
1576
|
-
const dateHoliday = new Date(holiday.date ?? "");
|
|
1577
|
-
// VTEX provide date in ISO format, at 00h on the day
|
|
1578
|
-
const validThrough = dateHoliday.setDate(dateHoliday.getDate() + 1).toString();
|
|
1579
|
-
|
|
1580
|
-
return {
|
|
1581
|
-
"@type": "OpeningHoursSpecification",
|
|
1582
|
-
opens: holiday.hourBegin,
|
|
1583
|
-
closes: holiday.hourEnd,
|
|
1584
|
-
validFrom: holiday.date,
|
|
1585
|
-
validThrough,
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
function isPickupPointVCS(
|
|
1590
|
-
pickupPoint: PickupPoint | PickupPointVCS,
|
|
1591
|
-
): pickupPoint is PickupPointVCS {
|
|
1592
|
-
return "name" in pickupPoint;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
interface ToPlaceOptions {
|
|
1596
|
-
isActive?: boolean;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
export function toPlace(
|
|
1600
|
-
pickupPoint: (PickupPoint & { distance?: number }) | PickupPointVCS,
|
|
1601
|
-
options?: ToPlaceOptions,
|
|
1602
|
-
): Place {
|
|
1603
|
-
const {
|
|
1604
|
-
name,
|
|
1605
|
-
country,
|
|
1606
|
-
latitude,
|
|
1607
|
-
longitude,
|
|
1608
|
-
openingHoursSpecification,
|
|
1609
|
-
specialOpeningHoursSpecification,
|
|
1610
|
-
isActive,
|
|
1611
|
-
} = isPickupPointVCS(pickupPoint)
|
|
1612
|
-
? {
|
|
1613
|
-
name: pickupPoint.name,
|
|
1614
|
-
country: pickupPoint.address?.country?.acronym,
|
|
1615
|
-
latitude: pickupPoint.address?.location?.latitude,
|
|
1616
|
-
longitude: pickupPoint.address?.location?.longitude,
|
|
1617
|
-
specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
|
|
1618
|
-
toSpecialHoursSpecification,
|
|
1619
|
-
),
|
|
1620
|
-
openingHoursSpecification: pickupPoint.businessHours?.map(toHoursSpecification),
|
|
1621
|
-
isActive: pickupPoint.isActive,
|
|
1622
|
-
}
|
|
1623
|
-
: {
|
|
1624
|
-
name: pickupPoint.friendlyName,
|
|
1625
|
-
country: pickupPoint.address?.country,
|
|
1626
|
-
latitude: pickupPoint.address?.geoCoordinates?.[0],
|
|
1627
|
-
longitude: pickupPoint.address?.geoCoordinates?.[1],
|
|
1628
|
-
specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
|
|
1629
|
-
toSpecialHoursSpecification,
|
|
1630
|
-
),
|
|
1631
|
-
openingHoursSpecification: pickupPoint.businessHours?.map(
|
|
1632
|
-
({ ClosingTime, DayOfWeek, OpeningTime }) =>
|
|
1633
|
-
toHoursSpecification({
|
|
1634
|
-
closingTime: ClosingTime,
|
|
1635
|
-
dayOfWeek: DayOfWeek,
|
|
1636
|
-
openingTime: OpeningTime,
|
|
1637
|
-
}),
|
|
1638
|
-
),
|
|
1639
|
-
isActive: options?.isActive,
|
|
1640
|
-
};
|
|
1641
|
-
|
|
1642
|
-
return {
|
|
1643
|
-
"@id": pickupPoint.id,
|
|
1644
|
-
"@type": "Place",
|
|
1645
|
-
address: {
|
|
1646
|
-
"@type": "PostalAddress",
|
|
1647
|
-
addressCountry: country,
|
|
1648
|
-
addressLocality: pickupPoint.address?.city,
|
|
1649
|
-
addressRegion: pickupPoint.address?.state,
|
|
1650
|
-
postalCode: pickupPoint.address?.postalCode,
|
|
1651
|
-
streetAddress: pickupPoint.address?.street,
|
|
1652
|
-
},
|
|
1653
|
-
latitude,
|
|
1654
|
-
longitude,
|
|
1655
|
-
name,
|
|
1656
|
-
specialOpeningHoursSpecification,
|
|
1657
|
-
openingHoursSpecification,
|
|
1658
|
-
additionalProperty: [
|
|
1659
|
-
{
|
|
1660
|
-
"@type": "PropertyValue",
|
|
1661
|
-
name: "distance",
|
|
1662
|
-
value: `${pickupPoint.distance}`,
|
|
1663
|
-
},
|
|
1664
|
-
{
|
|
1665
|
-
"@type": "PropertyValue",
|
|
1666
|
-
name: "isActive",
|
|
1667
|
-
value: typeof isActive === "boolean" ? `${isActive}` : undefined,
|
|
1668
|
-
},
|
|
1669
|
-
],
|
|
1670
|
-
};
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
export const toPostalAddress = (address: Address): PostalAddress => {
|
|
1674
|
-
return {
|
|
1675
|
-
"@type": "PostalAddress",
|
|
1676
|
-
"@id": address.addressId,
|
|
1677
|
-
addressCountry: address.country,
|
|
1678
|
-
addressLocality: address.city,
|
|
1679
|
-
addressRegion: address.state,
|
|
1680
|
-
areaServed: address.neighborhood || undefined,
|
|
1681
|
-
postalCode: address.postalCode,
|
|
1682
|
-
streetAddress: address.street,
|
|
1683
|
-
identifier: address.number || undefined,
|
|
1684
|
-
name: address.addressName || undefined,
|
|
1685
|
-
alternateName: address.receiverName || undefined,
|
|
1686
|
-
description: address.complement || undefined,
|
|
1687
|
-
disambiguatingDescription: address.reference || undefined,
|
|
1688
|
-
latitude: address.geoCoordinates?.[0] || undefined,
|
|
1689
|
-
longitude: address.geoCoordinates?.[1] || undefined,
|
|
1690
|
-
};
|
|
1691
|
-
};
|