@decocms/apps 0.20.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.
Files changed (113) hide show
  1. package/.github/workflows/release.yml +34 -0
  2. package/.releaserc.json +25 -0
  3. package/commerce/components/Image.tsx +209 -0
  4. package/commerce/components/JsonLd.tsx +285 -0
  5. package/commerce/sdk/analytics.ts +24 -0
  6. package/commerce/sdk/formatPrice.ts +23 -0
  7. package/commerce/sdk/url.ts +9 -0
  8. package/commerce/sdk/useOffer.ts +75 -0
  9. package/commerce/sdk/useVariantPossibilities.ts +43 -0
  10. package/commerce/types/commerce.ts +1105 -0
  11. package/commerce/utils/canonical.ts +11 -0
  12. package/commerce/utils/constants.ts +9 -0
  13. package/commerce/utils/filters.ts +10 -0
  14. package/commerce/utils/productToAnalyticsItem.ts +67 -0
  15. package/commerce/utils/stateByZip.ts +50 -0
  16. package/knip.json +19 -0
  17. package/package.json +77 -0
  18. package/shopify/actions/cart/addItems.ts +37 -0
  19. package/shopify/actions/cart/updateCoupons.ts +32 -0
  20. package/shopify/actions/cart/updateItems.ts +32 -0
  21. package/shopify/actions/user/signIn.ts +45 -0
  22. package/shopify/actions/user/signUp.ts +36 -0
  23. package/shopify/client.ts +58 -0
  24. package/shopify/index.ts +32 -0
  25. package/shopify/init.ts +40 -0
  26. package/shopify/loaders/ProductDetailsPage.ts +35 -0
  27. package/shopify/loaders/ProductList.ts +101 -0
  28. package/shopify/loaders/ProductListingPage.ts +180 -0
  29. package/shopify/loaders/RelatedProducts.ts +45 -0
  30. package/shopify/loaders/cart.ts +73 -0
  31. package/shopify/loaders/shop.ts +40 -0
  32. package/shopify/loaders/user.ts +44 -0
  33. package/shopify/utils/admin/admin.ts +57 -0
  34. package/shopify/utils/admin/queries.ts +29 -0
  35. package/shopify/utils/cart.ts +28 -0
  36. package/shopify/utils/cookies.ts +85 -0
  37. package/shopify/utils/enums.ts +438 -0
  38. package/shopify/utils/graphql.ts +69 -0
  39. package/shopify/utils/storefront/queries.ts +530 -0
  40. package/shopify/utils/storefront/storefront.graphql.gen.ts +113 -0
  41. package/shopify/utils/transform.ts +436 -0
  42. package/shopify/utils/types.ts +191 -0
  43. package/shopify/utils/user.ts +23 -0
  44. package/shopify/utils/utils.ts +164 -0
  45. package/tsconfig.json +11 -0
  46. package/vtex/README.md +6 -0
  47. package/vtex/actions/address.ts +211 -0
  48. package/vtex/actions/auth.ts +337 -0
  49. package/vtex/actions/checkout.ts +497 -0
  50. package/vtex/actions/index.ts +11 -0
  51. package/vtex/actions/masterData.ts +170 -0
  52. package/vtex/actions/misc.ts +196 -0
  53. package/vtex/actions/newsletter.ts +108 -0
  54. package/vtex/actions/orders.ts +37 -0
  55. package/vtex/actions/profile.ts +119 -0
  56. package/vtex/actions/session.ts +87 -0
  57. package/vtex/actions/trigger.ts +43 -0
  58. package/vtex/actions/wishlist.ts +116 -0
  59. package/vtex/client.ts +423 -0
  60. package/vtex/hooks/index.ts +4 -0
  61. package/vtex/hooks/useAutocomplete.ts +89 -0
  62. package/vtex/hooks/useCart.ts +219 -0
  63. package/vtex/hooks/useUser.ts +78 -0
  64. package/vtex/hooks/useWishlist.ts +119 -0
  65. package/vtex/index.ts +14 -0
  66. package/vtex/inline-loaders/productDetailsPage.ts +75 -0
  67. package/vtex/inline-loaders/productList.ts +163 -0
  68. package/vtex/inline-loaders/productListingPage.ts +447 -0
  69. package/vtex/inline-loaders/relatedProducts.ts +83 -0
  70. package/vtex/inline-loaders/suggestions.ts +49 -0
  71. package/vtex/inline-loaders/workflowProducts.ts +68 -0
  72. package/vtex/invoke.ts +202 -0
  73. package/vtex/loaders/address.ts +120 -0
  74. package/vtex/loaders/brands.ts +51 -0
  75. package/vtex/loaders/cart.ts +49 -0
  76. package/vtex/loaders/catalog.ts +165 -0
  77. package/vtex/loaders/collections.ts +57 -0
  78. package/vtex/loaders/index.ts +19 -0
  79. package/vtex/loaders/legacy.ts +671 -0
  80. package/vtex/loaders/logistics.ts +115 -0
  81. package/vtex/loaders/navbar.ts +29 -0
  82. package/vtex/loaders/orders.ts +103 -0
  83. package/vtex/loaders/pageType.ts +62 -0
  84. package/vtex/loaders/payment.ts +107 -0
  85. package/vtex/loaders/profile.ts +138 -0
  86. package/vtex/loaders/promotion.ts +33 -0
  87. package/vtex/loaders/search.ts +127 -0
  88. package/vtex/loaders/session.ts +91 -0
  89. package/vtex/loaders/user.ts +89 -0
  90. package/vtex/loaders/wishlist.ts +89 -0
  91. package/vtex/loaders/wishlistProducts.ts +81 -0
  92. package/vtex/loaders/workflow.ts +323 -0
  93. package/vtex/logo.png +0 -0
  94. package/vtex/middleware.ts +229 -0
  95. package/vtex/types.ts +248 -0
  96. package/vtex/utils/batch.ts +21 -0
  97. package/vtex/utils/cookies.ts +76 -0
  98. package/vtex/utils/enrichment.ts +540 -0
  99. package/vtex/utils/fetchCache.ts +150 -0
  100. package/vtex/utils/index.ts +17 -0
  101. package/vtex/utils/intelligentSearch.ts +84 -0
  102. package/vtex/utils/legacy.ts +155 -0
  103. package/vtex/utils/pickAndOmit.ts +30 -0
  104. package/vtex/utils/proxy.ts +196 -0
  105. package/vtex/utils/resourceRange.ts +10 -0
  106. package/vtex/utils/segment.ts +163 -0
  107. package/vtex/utils/similars.ts +38 -0
  108. package/vtex/utils/sitemap.ts +133 -0
  109. package/vtex/utils/slugCache.ts +32 -0
  110. package/vtex/utils/slugify.ts +13 -0
  111. package/vtex/utils/transform.ts +1331 -0
  112. package/vtex/utils/types.ts +1884 -0
  113. package/vtex/utils/vtexId.ts +103 -0
@@ -0,0 +1,1331 @@
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 "../../commerce/types/commerce";
23
+ import { DEFAULT_IMAGE } from "../../commerce/utils/constants";
24
+ import { formatRange } from "../../commerce/utils/filters";
25
+ import { pick } from "./pickAndOmit";
26
+ import { slugify } from "./slugify";
27
+ import type {
28
+ Address,
29
+ Brand as BrandVTEX,
30
+ Category,
31
+ Facet as FacetVTEX,
32
+ FacetValueBoolean,
33
+ FacetValueRange,
34
+ Item as SkuVTEX,
35
+ LegacyFacet,
36
+ LegacyItem as LegacySkuVTEX,
37
+ LegacyProduct,
38
+ LegacyProduct as LegacyProductVTEX,
39
+ Maybe,
40
+ OrderForm,
41
+ PageType as PageTypeVTEX,
42
+ PickupHolidays,
43
+ PickupPoint,
44
+ Product as ProductVTEX,
45
+ ProductInventoryData,
46
+ ProductRating,
47
+ ProductReviewData,
48
+ SelectedFacet,
49
+ Seller as SellerVTEX,
50
+ Teasers,
51
+ } from "./types";
52
+
53
+ interface PickupPointVCS {
54
+ name?: string;
55
+ address?: {
56
+ country?: { acronym?: string };
57
+ location?: { latitude?: number; longitude?: number };
58
+ city?: string;
59
+ state?: string;
60
+ postalCode?: string;
61
+ street?: string;
62
+ };
63
+ pickupHolidays?: any[];
64
+ businessHours?: any[];
65
+ isActive?: boolean;
66
+ id?: string;
67
+ [key: string]: unknown;
68
+ }
69
+
70
+ const DEFAULT_CATEGORY_SEPARATOR = ">";
71
+
72
+ export const SCHEMA_LIST_PRICE: PriceTypeEnumeration =
73
+ "https://schema.org/ListPrice";
74
+ export const SCHEMA_SALE_PRICE: PriceTypeEnumeration =
75
+ "https://schema.org/SalePrice";
76
+ export const SCHEMA_SRP: PriceTypeEnumeration = "https://schema.org/SRP";
77
+ export const SCHEMA_INSTALLMENT: PriceComponentTypeEnumeration =
78
+ "https://schema.org/Installment";
79
+ export const SCHEMA_IN_STOCK: ItemAvailability = "https://schema.org/InStock";
80
+ export const SCHEMA_OUT_OF_STOCK: ItemAvailability =
81
+ "https://schema.org/OutOfStock";
82
+
83
+ const isLegacySku = (sku: LegacySkuVTEX | SkuVTEX): sku is LegacySkuVTEX =>
84
+ typeof (sku as LegacySkuVTEX).variations?.[0] === "string" ||
85
+ !!(sku as LegacySkuVTEX).Videos;
86
+
87
+ const isLegacyProduct = (
88
+ product: ProductVTEX | LegacyProductVTEX,
89
+ ): product is LegacyProductVTEX => product.origin !== "intelligent-search";
90
+
91
+ const getProductGroupURL = (
92
+ origin: string,
93
+ { linkText }: { linkText: string },
94
+ ) => new URL(`/${linkText}/p`, origin);
95
+
96
+ const getProductURL = (
97
+ origin: string,
98
+ product: { linkText: string },
99
+ skuId?: string,
100
+ ) => {
101
+ const canonicalUrl = getProductGroupURL(origin, product);
102
+
103
+ if (skuId) {
104
+ canonicalUrl.searchParams.set("skuId", skuId);
105
+ }
106
+
107
+ return canonicalUrl;
108
+ };
109
+
110
+ const nonEmptyArray = <T>(
111
+ array: T[] | null | undefined,
112
+ ) => (Array.isArray(array) && array.length > 0 ? array : null);
113
+
114
+ interface ProductOptions {
115
+ baseUrl: string;
116
+ /** Price coded currency, e.g.: USD, BRL */
117
+ priceCurrency: string;
118
+ imagesByKey?: Map<string, string>;
119
+ /** Original attributes to be included in the transformed product */
120
+ includeOriginalAttributes?: string[];
121
+ }
122
+
123
+ /** Returns first available sku */
124
+ const findFirstAvailable = (items: Array<LegacySkuVTEX | SkuVTEX>) =>
125
+ items?.find((item) =>
126
+ Boolean(
127
+ item?.sellers?.find((s) => s.commertialOffer?.AvailableQuantity > 0),
128
+ )
129
+ );
130
+
131
+ export const pickSku = <T extends ProductVTEX | LegacyProductVTEX>(
132
+ product: T,
133
+ maybeSkuId?: string,
134
+ ): T["items"][number] => {
135
+ const skuId = maybeSkuId ?? findFirstAvailable(product.items)?.itemId ??
136
+ product.items[0]?.itemId;
137
+ for (const item of product.items) {
138
+ if (item.itemId === skuId) {
139
+ return item;
140
+ }
141
+ }
142
+
143
+ return product.items[0];
144
+ };
145
+
146
+ const toAccessoryOrSparePartFor = <T extends ProductVTEX | LegacyProductVTEX>(
147
+ sku: T["items"][number],
148
+ kitItems: T[],
149
+ options: ProductOptions,
150
+ ) => {
151
+ const productBySkuId = kitItems.reduce((map, product) => {
152
+ product.items.forEach((item) => map.set(item.itemId, product));
153
+
154
+ return map;
155
+ }, new Map<string, T>());
156
+
157
+ return sku.kitItems
158
+ ?.map(({ itemId }) => {
159
+ const product = productBySkuId.get(itemId);
160
+
161
+ /** Sometimes VTEX does not return what I've asked for */
162
+ if (!product) return;
163
+
164
+ const sku = pickSku(product, itemId);
165
+
166
+ return toProduct(product, sku, 0, options);
167
+ })
168
+ .filter((p): p is Product => typeof p !== "undefined");
169
+ };
170
+
171
+ export const forceHttpsOnAssets = (orderForm: OrderForm) => {
172
+ orderForm.items.forEach((item) => {
173
+ if (item.imageUrl) {
174
+ item.imageUrl = item.imageUrl.startsWith("http://")
175
+ ? item.imageUrl.replace("http://", "https://")
176
+ : item.imageUrl;
177
+ }
178
+ });
179
+ return orderForm;
180
+ };
181
+
182
+ export const toProductPage = <T extends ProductVTEX | LegacyProductVTEX>(
183
+ product: T,
184
+ sku: T["items"][number],
185
+ kitItems: T[],
186
+ options: ProductOptions,
187
+ ): Omit<ProductDetailsPage, "seo"> => {
188
+ const partialProduct = toProduct(product, sku, 0, options);
189
+ // This is deprecated. Compose this loader at loaders > product > extension > detailsPage.ts
190
+ const isAccessoryOrSparePartFor = toAccessoryOrSparePartFor(
191
+ sku,
192
+ kitItems,
193
+ options,
194
+ );
195
+
196
+ return {
197
+ "@type": "ProductDetailsPage",
198
+ breadcrumbList: toBreadcrumbList(product, options),
199
+ product: { ...partialProduct, isAccessoryOrSparePartFor },
200
+ };
201
+ };
202
+
203
+ export const inStock = (offer: Offer) => offer.availability === SCHEMA_IN_STOCK;
204
+
205
+ // Smallest Available Spot Price First
206
+ export const bestOfferFirst = (a: Offer, b: Offer) => {
207
+ if (inStock(a) && !inStock(b)) {
208
+ return -1;
209
+ }
210
+
211
+ if (!inStock(a) && inStock(b)) {
212
+ return 1;
213
+ }
214
+
215
+ return a.price - b.price;
216
+ };
217
+
218
+ const getHighPriceIndex = (offers: Offer[]) => {
219
+ let it = offers.length - 1;
220
+ for (; it > 0 && !inStock(offers[it]); it--);
221
+ return it;
222
+ };
223
+
224
+ const splitCategory = (firstCategory: string) =>
225
+ firstCategory.split("/").filter(Boolean);
226
+
227
+ const toAdditionalPropertyCategories = <
228
+ P extends LegacyProductVTEX | ProductVTEX,
229
+ >(
230
+ product: P,
231
+ ): Product["additionalProperty"] => {
232
+ const categories = new Set<string>();
233
+ const categoryIds = new Set<string>();
234
+
235
+ product.categories.forEach((productCategory, i) => {
236
+ const category = splitCategory(productCategory);
237
+ const categoryId = splitCategory(product.categoriesIds[i]);
238
+
239
+ category.forEach((splitCategoryItem, j) => {
240
+ categories.add(splitCategoryItem);
241
+ categoryIds.add(categoryId[j]);
242
+ });
243
+ });
244
+
245
+ const categoriesArray = Array.from(categories);
246
+ const categoryIdsArray = Array.from(categoryIds);
247
+
248
+ return categoriesArray.map((category, index) =>
249
+ toAdditionalPropertyCategory({
250
+ propertyID: categoryIdsArray[index],
251
+ value: category || "",
252
+ })
253
+ );
254
+ };
255
+
256
+ export const toAdditionalPropertyCategory = ({
257
+ propertyID,
258
+ value,
259
+ }: {
260
+ propertyID: string;
261
+ value: string;
262
+ }): PropertyValue => ({
263
+ "@type": "PropertyValue" as const,
264
+ name: "category",
265
+ propertyID,
266
+ value,
267
+ });
268
+
269
+ const toAdditionalPropertyClusters = <
270
+ P extends LegacyProductVTEX | ProductVTEX,
271
+ >(
272
+ product: P,
273
+ ): Product["additionalProperty"] => {
274
+ const mapEntriesToIdName = ([id, name]: [string, unknown]) => ({
275
+ id,
276
+ name: name as string,
277
+ });
278
+
279
+ const allClusters = isLegacyProduct(product)
280
+ ? Object.entries(product.productClusters).map(mapEntriesToIdName)
281
+ : product.productClusters;
282
+
283
+ const highlightsSet = isLegacyProduct(product)
284
+ ? new Set(Object.keys(product.clusterHighlights))
285
+ : new Set(product.clusterHighlights.map(({ id }) => id));
286
+
287
+ return allClusters.map((cluster) =>
288
+ toAdditionalPropertyCluster({
289
+ propertyID: cluster.id,
290
+ value: cluster.name || "",
291
+ }, highlightsSet)
292
+ );
293
+ };
294
+
295
+ export const toAdditionalPropertyCluster = (
296
+ { propertyID, value }: { propertyID: string; value: string },
297
+ highlights?: Set<string>,
298
+ ): PropertyValue => ({
299
+ "@type": "PropertyValue",
300
+ name: "cluster",
301
+ value,
302
+ propertyID,
303
+ description: highlights?.has(propertyID) ? "highlight" : undefined,
304
+ });
305
+
306
+ const toAdditionalPropertyReferenceIds = (
307
+ referenceId: Array<{ Key: string; Value: string }>,
308
+ ): Product["additionalProperty"] => {
309
+ return referenceId.map(({ Key, Value }) =>
310
+ toAdditionalPropertyReferenceId({ name: Key, value: Value })
311
+ );
312
+ };
313
+
314
+ export const toAdditionalPropertyReferenceId = ({
315
+ name,
316
+ value,
317
+ }: {
318
+ name: string;
319
+ value: string;
320
+ }): PropertyValue => ({
321
+ "@type": "PropertyValue",
322
+ name,
323
+ value,
324
+ valueReference: "ReferenceID",
325
+ });
326
+
327
+ const getImageKey = (src = "") => {
328
+ return src;
329
+
330
+ // TODO: figure out how we can improve this
331
+ // const match = new URLPattern({
332
+ // pathname: "/arquivos/ids/:skuId/:imageId",
333
+ // }).exec(src);
334
+
335
+ // if (match == null) {
336
+ // return src;
337
+ // }
338
+
339
+ // return `${match.pathname.groups.imageId}${match.search.input}`;
340
+ };
341
+
342
+ export const aggregateOffers = (
343
+ offers: Offer[],
344
+ priceCurrency?: string,
345
+ ): AggregateOffer | undefined => {
346
+ const sorted = offers.sort(bestOfferFirst);
347
+
348
+ if (sorted.length === 0) return;
349
+
350
+ const highPriceIndex = getHighPriceIndex(sorted);
351
+ const lowPriceIndex = 0;
352
+
353
+ return {
354
+ "@type": "AggregateOffer",
355
+ priceCurrency,
356
+ highPrice: sorted[highPriceIndex]?.price ?? null,
357
+ lowPrice: sorted[lowPriceIndex]?.price ?? null,
358
+ offerCount: sorted.length,
359
+ offers: sorted,
360
+ };
361
+ };
362
+
363
+ export const toProduct = <P extends LegacyProductVTEX | ProductVTEX>(
364
+ product: P,
365
+ sku: P["items"][number],
366
+ level = 0, // prevent inifinte loop while self referencing the product
367
+ options: ProductOptions,
368
+ ): Product => {
369
+ const { baseUrl, priceCurrency } = options;
370
+ const {
371
+ brand,
372
+ brandId,
373
+ brandImageUrl,
374
+ productId,
375
+ productReference,
376
+ description,
377
+ releaseDate,
378
+ items,
379
+ } = product;
380
+ const {
381
+ name,
382
+ ean,
383
+ itemId: skuId,
384
+ referenceId = [],
385
+ kitItems,
386
+ estimatedDateArrival,
387
+ } = sku;
388
+
389
+ const videos = isLegacySku(sku) ? sku.Videos : sku.videos;
390
+ const nonEmptyVideos = nonEmptyArray(videos);
391
+ const imagesByKey = options.imagesByKey ??
392
+ items
393
+ .flatMap((i) => i.images)
394
+ .reduce((map, img) => {
395
+ img?.imageUrl && map.set(getImageKey(img.imageUrl), img.imageUrl);
396
+ return map;
397
+ }, new Map<string, string>());
398
+
399
+ const groupAdditionalProperty = isLegacyProduct(product)
400
+ ? legacyToProductGroupAdditionalProperties(product)
401
+ : toProductGroupAdditionalProperties(product);
402
+ const originalAttributesAdditionalProperties =
403
+ toOriginalAttributesAdditionalProperties(
404
+ options.includeOriginalAttributes,
405
+ product,
406
+ );
407
+ const specificationsAdditionalProperty = isLegacySku(sku)
408
+ ? toAdditionalPropertiesLegacy(sku)
409
+ : toAdditionalProperties(sku);
410
+ const referenceIdAdditionalProperty = toAdditionalPropertyReferenceIds(
411
+ referenceId,
412
+ );
413
+ const images = nonEmptyArray(sku.images);
414
+ const offers = (sku.sellers ?? []).map(
415
+ isLegacyProduct(product) ? toOfferLegacy : toOffer,
416
+ );
417
+
418
+ const variantOptions = imagesByKey !== options.imagesByKey
419
+ ? { ...options, imagesByKey }
420
+ : options;
421
+ const isVariantOf = level < 1
422
+ ? ({
423
+ "@type": "ProductGroup",
424
+ productGroupID: productId,
425
+ hasVariant: items.map((sku) =>
426
+ toProduct(product, sku, 1, variantOptions)
427
+ ),
428
+ url: getProductGroupURL(baseUrl, product).href,
429
+ name: product.productName,
430
+ additionalProperty: [
431
+ ...groupAdditionalProperty,
432
+ ...originalAttributesAdditionalProperties,
433
+ ],
434
+ model: productReference,
435
+ } satisfies ProductGroup)
436
+ : undefined;
437
+
438
+ const finalImages = images?.map(({ imageUrl, imageText, imageLabel }) => {
439
+ const url = imagesByKey.get(getImageKey(imageUrl)) ?? imageUrl;
440
+ const alternateName = imageText || imageLabel || "";
441
+ const name = imageLabel || "";
442
+ const encodingFormat = "image";
443
+
444
+ return {
445
+ "@type": "ImageObject" as const,
446
+ alternateName,
447
+ url,
448
+ name,
449
+ encodingFormat,
450
+ };
451
+ }) ?? [DEFAULT_IMAGE];
452
+
453
+ const finalVideos = nonEmptyVideos?.map((video) => {
454
+ const url = video;
455
+ const alternateName = "Product video";
456
+ const name = "Product video";
457
+ const encodingFormat = "video";
458
+ return {
459
+ "@type": "VideoObject" as const,
460
+ alternateName,
461
+ contentUrl: url,
462
+ name,
463
+ encodingFormat,
464
+ };
465
+ });
466
+
467
+ // From schema.org: A category for the item. Greater signs or slashes can be used to informally indicate a category hierarchy
468
+ const categoriesString = splitCategory(product.categories[0]).join(
469
+ DEFAULT_CATEGORY_SEPARATOR,
470
+ );
471
+
472
+ const categoryAdditionalProperties = toAdditionalPropertyCategories(product);
473
+ const clusterAdditionalProperties = toAdditionalPropertyClusters(product);
474
+
475
+ const additionalProperty: PropertyValue[] = [
476
+ ...specificationsAdditionalProperty,
477
+ ];
478
+ if (categoryAdditionalProperties) {
479
+ additionalProperty.push(...categoryAdditionalProperties);
480
+ }
481
+ if (clusterAdditionalProperties) {
482
+ additionalProperty.push(...clusterAdditionalProperties);
483
+ }
484
+ if (referenceIdAdditionalProperty) {
485
+ additionalProperty.push(...referenceIdAdditionalProperty);
486
+ }
487
+
488
+ estimatedDateArrival && additionalProperty.push({
489
+ "@type": "PropertyValue",
490
+ name: "Estimated Date Arrival",
491
+ value: estimatedDateArrival,
492
+ });
493
+
494
+ if (sku.modalType) {
495
+ additionalProperty.push({
496
+ "@type": "PropertyValue",
497
+ name: "Modal Type",
498
+ value: sku.modalType,
499
+ });
500
+ }
501
+
502
+ return {
503
+ "@type": "Product",
504
+ category: categoriesString,
505
+ productID: skuId,
506
+ url: getProductURL(baseUrl, product, sku.itemId).href,
507
+ name,
508
+ alternateName: sku.complementName,
509
+ description,
510
+ brand: {
511
+ "@type": "Brand",
512
+ "@id": brandId?.toString(),
513
+ name: brand,
514
+ logo: brandImageUrl,
515
+ },
516
+ isAccessoryOrSparePartFor: kitItems?.map(({ itemId }) => ({
517
+ "@type": "Product",
518
+ productID: itemId,
519
+ sku: itemId,
520
+ })),
521
+ inProductGroupWithID: productId,
522
+ sku: skuId,
523
+ gtin: ean,
524
+ releaseDate,
525
+ additionalProperty,
526
+ isVariantOf,
527
+ image: finalImages,
528
+ video: finalVideos,
529
+ offers: aggregateOffers(offers, priceCurrency),
530
+ };
531
+ };
532
+
533
+ const toBreadcrumbList = (
534
+ product: ProductVTEX | LegacyProductVTEX,
535
+ { baseUrl }: ProductOptions,
536
+ ): BreadcrumbList => {
537
+ const { categories, productName } = product;
538
+ const names = categories[0]?.split("/").filter(Boolean);
539
+
540
+ const segments = names.map(slugify);
541
+
542
+ return {
543
+ "@type": "BreadcrumbList",
544
+ itemListElement: [
545
+ ...names.map((name, index) => {
546
+ const position = index + 1;
547
+
548
+ return {
549
+ "@type": "ListItem" as const,
550
+ name,
551
+ item:
552
+ new URL(`/${segments.slice(0, position).join("/")}`, baseUrl).href,
553
+ position,
554
+ };
555
+ }),
556
+ {
557
+ "@type": "ListItem",
558
+ name: productName,
559
+ item: getProductGroupURL(baseUrl, product).href,
560
+ position: categories.length + 1,
561
+ },
562
+ ],
563
+ numberOfItems: categories.length + 1,
564
+ };
565
+ };
566
+
567
+ const legacyToProductGroupAdditionalProperties = (
568
+ product: LegacyProductVTEX,
569
+ ) => {
570
+ const groups = product.allSpecificationsGroups ?? [];
571
+ const allSpecifications = product.allSpecifications ?? [];
572
+
573
+ const specByGroup: Record<string, string> = {};
574
+
575
+ groups.forEach((group) => {
576
+ const groupSpecs = (product as unknown as Record<string, string[]>)[group];
577
+ groupSpecs.forEach((specName) => {
578
+ specByGroup[specName] = group;
579
+ });
580
+ });
581
+
582
+ return allSpecifications.flatMap((name) => {
583
+ const values = (product as unknown as Record<string, string[]>)[name];
584
+ return values.map((value) =>
585
+ toAdditionalPropertySpecification({
586
+ name,
587
+ value,
588
+ propertyID: specByGroup[name],
589
+ })
590
+ );
591
+ });
592
+ };
593
+
594
+ const toProductGroupAdditionalProperties = (
595
+ { specificationGroups = [] }: ProductVTEX,
596
+ ) =>
597
+ specificationGroups.flatMap(({ name: groupName, specifications }) =>
598
+ specifications.flatMap(({ name, values }) =>
599
+ values.map(
600
+ (value) =>
601
+ ({
602
+ "@type": "PropertyValue",
603
+ name,
604
+ value,
605
+ propertyID: groupName,
606
+ valueReference: "PROPERTY" as string,
607
+ }) as const,
608
+ )
609
+ )
610
+ );
611
+
612
+ const toOriginalAttributesAdditionalProperties = (
613
+ originalAttributes: Maybe<string[]>,
614
+ product: ProductVTEX | LegacyProduct,
615
+ ) => {
616
+ if (!originalAttributes) {
617
+ return [];
618
+ }
619
+
620
+ const attributes =
621
+ pick(originalAttributes as Array<keyof typeof product>, product) ?? {};
622
+
623
+ return Object.entries(attributes).map(([name, value]) =>
624
+ ({
625
+ "@type": "PropertyValue",
626
+ name,
627
+ value,
628
+ valueReference: "ORIGINAL_PROPERTY" as string,
629
+ }) as const
630
+ ) as unknown as PropertyValue[];
631
+ };
632
+
633
+ const toAdditionalProperties = (sku: SkuVTEX): PropertyValue[] =>
634
+ sku.variations?.flatMap(({ name, values }) =>
635
+ values.map((value) => toAdditionalPropertySpecification({ name, value }))
636
+ ) ?? [];
637
+
638
+ export const toAdditionalPropertySpecification = ({
639
+ name,
640
+ value,
641
+ propertyID,
642
+ }: {
643
+ name: string;
644
+ value: string;
645
+ propertyID?: string;
646
+ }): PropertyValue => ({
647
+ "@type": "PropertyValue",
648
+ name,
649
+ value,
650
+ propertyID,
651
+ valueReference: "SPECIFICATION",
652
+ });
653
+
654
+ const toAdditionalPropertiesLegacy = (sku: LegacySkuVTEX): PropertyValue[] => {
655
+ const { variations = [], attachments = [] } = sku;
656
+
657
+ const specificationProperties = variations.flatMap((variation) =>
658
+ sku[variation].map((value) =>
659
+ toAdditionalPropertySpecification({ name: variation, value })
660
+ )
661
+ );
662
+
663
+ const attachmentProperties = attachments.map(
664
+ (attachment) =>
665
+ ({
666
+ "@type": "PropertyValue",
667
+ propertyID: `${attachment.id}`,
668
+ name: attachment.name,
669
+ value: attachment.domainValues,
670
+ required: attachment.required,
671
+ valueReference: "ATTACHMENT",
672
+ }) as const,
673
+ );
674
+
675
+ if (attachmentProperties.length === 0) return specificationProperties;
676
+ specificationProperties.push(...attachmentProperties);
677
+ return specificationProperties;
678
+ };
679
+
680
+ const buildOffer = (
681
+ { commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
682
+ teasers: Teasers[],
683
+ ): Offer => ({
684
+ "@type": "Offer",
685
+ identifier: sellerDefault ? "default" : undefined,
686
+ price: offer.spotPrice ?? offer.Price,
687
+ seller: sellerId,
688
+ sellerName,
689
+ priceValidUntil: offer.PriceValidUntil,
690
+ inventoryLevel: { value: offer.AvailableQuantity },
691
+ giftSkuIds: offer.GiftSkuIds ?? [],
692
+ teasers,
693
+ priceSpecification: [
694
+ {
695
+ "@type": "UnitPriceSpecification",
696
+ priceType: SCHEMA_LIST_PRICE,
697
+ price: offer.ListPrice,
698
+ },
699
+ {
700
+ "@type": "UnitPriceSpecification",
701
+ priceType: SCHEMA_SALE_PRICE,
702
+ price: offer.Price,
703
+ },
704
+ {
705
+ "@type": "UnitPriceSpecification",
706
+ priceType: SCHEMA_SRP,
707
+ price: offer.PriceWithoutDiscount,
708
+ },
709
+ ...offer.Installments.map(
710
+ (installment): UnitPriceSpecification => ({
711
+ "@type": "UnitPriceSpecification",
712
+ priceType: SCHEMA_SALE_PRICE,
713
+ priceComponentType: SCHEMA_INSTALLMENT,
714
+ name: installment.PaymentSystemName,
715
+ description: installment.Name,
716
+ billingDuration: installment.NumberOfInstallments,
717
+ billingIncrement: installment.Value,
718
+ price: installment.TotalValuePlusInterestRate,
719
+ }),
720
+ ),
721
+ ],
722
+ availability: offer.AvailableQuantity > 0
723
+ ? SCHEMA_IN_STOCK
724
+ : SCHEMA_OUT_OF_STOCK,
725
+ });
726
+
727
+ const toOffer = (seller: SellerVTEX): Offer =>
728
+ buildOffer(seller, seller.commertialOffer.teasers ?? []);
729
+
730
+ const toOfferLegacy = (seller: SellerVTEX): Offer => {
731
+ const otherTeasers = seller.commertialOffer.DiscountHighLight?.map((i) => {
732
+ const discount = i as Record<string, string>;
733
+ const [_k__BackingField, discountName] = Object.entries(discount)?.[0] ??
734
+ [];
735
+
736
+ const teasers: Teasers = {
737
+ name: discountName,
738
+ conditions: {
739
+ minimumQuantity: 0,
740
+ parameters: [],
741
+ },
742
+ effects: {
743
+ parameters: [],
744
+ },
745
+ };
746
+
747
+ return teasers;
748
+ }) ?? [];
749
+
750
+ const legacyTeasers = (seller.commertialOffer.Teasers ?? []).map(
751
+ (teaser) => ({
752
+ name: teaser["<Name>k__BackingField"],
753
+ generalValues: teaser["<GeneralValues>k__BackingField"],
754
+ conditions: {
755
+ minimumQuantity: teaser["<Conditions>k__BackingField"][
756
+ "<MinimumQuantity>k__BackingField"
757
+ ],
758
+ parameters: teaser["<Conditions>k__BackingField"][
759
+ "<Parameters>k__BackingField"
760
+ ].map((parameter) => ({
761
+ name: parameter["<Name>k__BackingField"],
762
+ value: parameter["<Value>k__BackingField"],
763
+ })),
764
+ },
765
+ effects: {
766
+ parameters: teaser["<Effects>k__BackingField"][
767
+ "<Parameters>k__BackingField"
768
+ ].map((parameter) => ({
769
+ name: parameter["<Name>k__BackingField"],
770
+ value: parameter["<Value>k__BackingField"],
771
+ })),
772
+ },
773
+ }),
774
+ );
775
+
776
+ return buildOffer(seller, [...otherTeasers, ...legacyTeasers]);
777
+ };
778
+
779
+ export const legacyFacetToFilter = (
780
+ name: string,
781
+ facets: LegacyFacet[],
782
+ url: URL,
783
+ map: string,
784
+ term: string,
785
+ behavior: "dynamic" | "static",
786
+ ignoreCaseSelected?: boolean,
787
+ fullPath = false,
788
+ ): Filter | null => {
789
+ const mapSegments = map.split(",").filter((x) => x.length > 0);
790
+ const pathSegments = term.replace(/^\//, "").split("/").slice(
791
+ 0,
792
+ mapSegments.length,
793
+ );
794
+
795
+ const mapSet = new Set(
796
+ mapSegments.map((i) => ignoreCaseSelected ? i.toLowerCase() : i),
797
+ );
798
+ const pathSet = new Set(
799
+ pathSegments.map((i) => ignoreCaseSelected ? i.toLowerCase() : i),
800
+ );
801
+
802
+ // for productClusterIds, we have to use the full path
803
+ // example:
804
+ // category2/123?map=c,productClusterIds -> DO NOT WORK
805
+ // category1/category2/123?map=c,c,productClusterIds -> WORK
806
+ const hasProductClusterIds = mapSegments.includes("productClusterIds");
807
+ const hasToBeFullpath = fullPath ||
808
+ hasProductClusterIds ||
809
+ mapSegments.includes("ft") ||
810
+ mapSegments.includes("b");
811
+
812
+ const getLink = (facet: LegacyFacet, selected: boolean) => {
813
+ const index = pathSegments.findIndex((s) => {
814
+ if (ignoreCaseSelected) {
815
+ return s.toLowerCase() === facet.Value.toLowerCase();
816
+ }
817
+
818
+ return s === facet.Value;
819
+ });
820
+
821
+ const map = hasToBeFullpath
822
+ ? facet.Link.split("map=")[1].split(",")
823
+ : [facet.Map];
824
+ const value = hasToBeFullpath
825
+ ? facet.Link.split("?")[0].slice(1).split("/")
826
+ : [facet.Value];
827
+
828
+ const pathSegmentsFiltered = hasProductClusterIds
829
+ ? [pathSegments[mapSegments.indexOf("productClusterIds")]]
830
+ : [];
831
+ const mapSegmentsFiltered = hasProductClusterIds
832
+ ? ["productClusterIds"]
833
+ : [];
834
+
835
+ const _mapSegments = hasToBeFullpath ? mapSegmentsFiltered : mapSegments;
836
+ const _pathSegments = hasToBeFullpath ? pathSegmentsFiltered : pathSegments;
837
+
838
+ const newMap = selected
839
+ ? [...mapSegments.filter((_, i) => i !== index)]
840
+ : [..._mapSegments, ...map];
841
+ const newPath = selected
842
+ ? [...pathSegments.filter((_, i) => i !== index)]
843
+ : [..._pathSegments, ...value];
844
+
845
+ // Insertion-sort like algorithm. Uses the c-continuum theorem
846
+ const zipped: [string, string][] = [];
847
+ for (let it = 0; it < newMap.length; it++) {
848
+ let i = 0;
849
+ while (
850
+ i < zipped.length && (zipped[i][0] === "c" || zipped[i][0] === "C")
851
+ ) i++;
852
+
853
+ zipped.splice(i, 0, [newMap[it], newPath[it]]);
854
+ }
855
+
856
+ const link = new URL(`/${zipped.map(([, s]) => s).join("/")}`, url);
857
+ link.searchParams.set("map", zipped.map(([m]) => m).join(","));
858
+ if (behavior === "static") {
859
+ link.searchParams.set(
860
+ "fmap",
861
+ url.searchParams.get("fmap") || mapSegments.join(","),
862
+ );
863
+ }
864
+ const currentQuery = url.searchParams.get("q");
865
+ if (currentQuery) {
866
+ link.searchParams.set("q", currentQuery);
867
+ }
868
+
869
+ return `${link.pathname}${link.search}`;
870
+ };
871
+
872
+ return {
873
+ "@type": "FilterToggle",
874
+ quantity: facets?.length,
875
+ label: name,
876
+ key: name,
877
+ values: facets.map((facet) => {
878
+ const normalizedFacet = name !== "PriceRanges"
879
+ ? facet
880
+ : normalizeFacet(facet);
881
+
882
+ const selected = mapSet.has(
883
+ ignoreCaseSelected
884
+ ? normalizedFacet.Map.toLowerCase()
885
+ : normalizedFacet.Map,
886
+ ) &&
887
+ pathSet.has(
888
+ ignoreCaseSelected
889
+ ? normalizedFacet.Value.toLowerCase()
890
+ : normalizedFacet.Value,
891
+ );
892
+
893
+ return {
894
+ value: normalizedFacet.Value,
895
+ quantity: normalizedFacet.Quantity,
896
+ url: getLink(normalizedFacet, selected),
897
+ label: normalizedFacet.Name,
898
+ selected,
899
+ children: facet.Children?.length > 0
900
+ ? legacyFacetToFilter(
901
+ normalizedFacet.Name,
902
+ facet.Children,
903
+ url,
904
+ map,
905
+ term,
906
+ behavior,
907
+ ignoreCaseSelected,
908
+ fullPath,
909
+ )
910
+ : undefined,
911
+ };
912
+ }),
913
+ };
914
+ };
915
+
916
+ export const filtersToSearchParams = (
917
+ selectedFacets: SelectedFacet[],
918
+ paramsToPersist?: URLSearchParams,
919
+ ) => {
920
+ const searchParams = new URLSearchParams(paramsToPersist);
921
+
922
+ for (const { key, value } of selectedFacets) {
923
+ searchParams.append(`filter.${key}`, value);
924
+ }
925
+
926
+ return searchParams;
927
+ };
928
+
929
+ const fromLegacyMap: Record<string, string> = {
930
+ priceFrom: "price",
931
+ productClusterSearchableIds: "productClusterIds",
932
+ };
933
+
934
+ export const legacyFacetsNormalize = (map: string, path: string) => {
935
+ // Replace legacy price path param to IS price facet format
936
+ // exemple: de-34,90-a-56,90 turns to 34.90:56.90
937
+ // may this regex have to be adjusted for international stores
938
+ const value = path.replace(
939
+ /de-(?<from>\d+[,]?[\d]+)-a-(?<to>\d+[,]?[\d]+)/,
940
+ (_match, from, to) => {
941
+ return `${from.replace(",", ".")}:${to.replace(",", ".")}`;
942
+ },
943
+ );
944
+
945
+ const key = fromLegacyMap[map] || map;
946
+
947
+ return { key, value };
948
+ };
949
+
950
+ /**
951
+ * Transform ?map urls into selected facets. This happens when a store is migrating
952
+ * to Deco and also migrating from VTEX Legacy to VTEX Intelligent Search.
953
+ */
954
+ export const legacyFacetsFromURL = (url: URL) => {
955
+ const mapSegments = url.searchParams.get("map")?.split(",") ?? [];
956
+ const pathSegments = url.pathname.split("/").slice(1); // Remove first slash
957
+ const length = Math.min(mapSegments.length, pathSegments.length);
958
+
959
+ const selectedFacets: SelectedFacet[] = [];
960
+ for (let it = 0; it < length; it++) {
961
+ const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
962
+
963
+ selectedFacets.push(facet);
964
+ }
965
+
966
+ return selectedFacets;
967
+ };
968
+
969
+ export const filtersFromURL = (url: URL) => {
970
+ const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
971
+
972
+ url.searchParams.forEach((value, name) => {
973
+ const [filter, key] = name.split(".");
974
+
975
+ if (filter === "filter" && typeof key === "string") {
976
+ selectedFacets.push({ key, value });
977
+ }
978
+ });
979
+
980
+ return selectedFacets;
981
+ };
982
+
983
+ export const mergeFacets = (
984
+ f1: SelectedFacet[],
985
+ f2: SelectedFacet[],
986
+ ): SelectedFacet[] => {
987
+ const facetKey = (facet: SelectedFacet) =>
988
+ `key:${facet.key}-value:${facet.value}`;
989
+ const merged = new Map<string, SelectedFacet>();
990
+
991
+ for (const f of f1) {
992
+ merged.set(facetKey(f), f);
993
+ }
994
+ for (const f of f2) {
995
+ merged.set(facetKey(f), f);
996
+ }
997
+
998
+ return [...merged.values()];
999
+ };
1000
+
1001
+ const isValueRange = (
1002
+ facet: FacetValueRange | FacetValueBoolean,
1003
+ ): facet is FacetValueRange =>
1004
+ // deno-lint-ignore no-explicit-any
1005
+ Boolean((facet as any).range);
1006
+
1007
+ const facetToToggle = (
1008
+ selectedFacets: SelectedFacet[],
1009
+ key: string,
1010
+ paramsToPersist?: URLSearchParams,
1011
+ ) =>
1012
+ (item: FacetValueRange | FacetValueBoolean): FilterToggleValue => {
1013
+ const { quantity, selected } = item;
1014
+ const isRange = isValueRange(item);
1015
+
1016
+ const value = isRange
1017
+ ? formatRange(item.range.from, item.range.to)
1018
+ : item.value;
1019
+ const label = isRange ? value : item.name;
1020
+ const facet = { key, value };
1021
+
1022
+ const filters = selected
1023
+ ? selectedFacets.filter((f) => f.key !== key || f.value !== value)
1024
+ : [...selectedFacets, facet];
1025
+
1026
+ return {
1027
+ value,
1028
+ quantity,
1029
+ selected,
1030
+ url: `?${filtersToSearchParams(filters, paramsToPersist)}`,
1031
+ label,
1032
+ };
1033
+ };
1034
+
1035
+ export const toFilter =
1036
+ (selectedFacets: SelectedFacet[], paramsToPersist?: URLSearchParams) =>
1037
+ ({ key, name, quantity, values }: FacetVTEX): Filter => ({
1038
+ "@type": "FilterToggle",
1039
+ key,
1040
+ label: name,
1041
+ quantity: quantity,
1042
+ values: values.map(facetToToggle(selectedFacets, key, paramsToPersist)),
1043
+ });
1044
+
1045
+ function nodeToNavbar(node: Category): SiteNavigationElement {
1046
+ const url = new URL(node.url, "https://example.com");
1047
+
1048
+ return {
1049
+ "@type": "SiteNavigationElement",
1050
+ url: `${url.pathname}${url.search}`,
1051
+ name: node.name,
1052
+ children: node.children.map(nodeToNavbar),
1053
+ };
1054
+ }
1055
+
1056
+ export const categoryTreeToNavbar = (
1057
+ tree: Category[],
1058
+ ): SiteNavigationElement[] => tree.map(nodeToNavbar);
1059
+
1060
+ export const toBrand = (
1061
+ { id, name, imageUrl, metaTagDescription }: BrandVTEX,
1062
+ baseUrl: string,
1063
+ ): Brand => ({
1064
+ "@type": "Brand",
1065
+ "@id": `${id}`,
1066
+ name,
1067
+ logo: imageUrl?.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`,
1068
+ description: metaTagDescription,
1069
+ });
1070
+
1071
+ export const normalizeFacet = (facet: LegacyFacet) => {
1072
+ return {
1073
+ ...facet,
1074
+ Map: "priceFrom",
1075
+ Value: facet.Slug!,
1076
+ };
1077
+ };
1078
+
1079
+ export const toReview = (
1080
+ products: Product[],
1081
+ ratings: ProductRating[],
1082
+ reviews: ProductReviewData[],
1083
+ ): Product[] => {
1084
+ return products.map((p, index) => {
1085
+ const ratingsCount = ratings[index].totalCount || 0;
1086
+ const productReviews = reviews[index].data || [];
1087
+
1088
+ return {
1089
+ ...p,
1090
+ aggregateRating: {
1091
+ "@type": "AggregateRating",
1092
+ reviewCount: ratingsCount,
1093
+ ratingCount: ratingsCount,
1094
+ ratingValue: ratings[index]?.average || 0,
1095
+ },
1096
+ review: productReviews.map((_, reviewIndex) => ({
1097
+ "@type": "Review",
1098
+ id: productReviews[reviewIndex]?.id?.toString(),
1099
+ author: [{
1100
+ "@type": "Author",
1101
+ name: productReviews[reviewIndex]?.reviewerName,
1102
+ verifiedBuyer: productReviews[reviewIndex]?.verifiedPurchaser,
1103
+ }],
1104
+ itemReviewed: productReviews[reviewIndex]?.productId,
1105
+ datePublished: productReviews[reviewIndex]?.reviewDateTime,
1106
+ reviewHeadline: productReviews[reviewIndex]?.title,
1107
+ reviewBody: productReviews[reviewIndex]?.text,
1108
+ reviewRating: {
1109
+ "@type": "AggregateRating",
1110
+ ratingValue: productReviews[reviewIndex]?.rating || 0,
1111
+ },
1112
+ })),
1113
+ };
1114
+ });
1115
+ };
1116
+
1117
+ export const toInventories = (
1118
+ products: Product[],
1119
+ inventoriesData: ProductInventoryData[],
1120
+ ): Product[] => {
1121
+ return products.map((p, index) => {
1122
+ const balance = inventoriesData[index].balance || [];
1123
+
1124
+ const additionalProperty = Array.from(p.additionalProperty || []);
1125
+
1126
+ const inventories: PropertyValue[] = balance.map((b) => ({
1127
+ "@type": "PropertyValue",
1128
+ valueReference: "INVENTORY",
1129
+ propertyID: b.warehouseId,
1130
+ name: b.warehouseName,
1131
+ value: b.totalQuantity?.toString(),
1132
+ }));
1133
+
1134
+ return {
1135
+ ...p,
1136
+ additionalProperty: [...additionalProperty, ...inventories],
1137
+ };
1138
+ });
1139
+ };
1140
+
1141
+ type ProductMap = Record<string, Product>;
1142
+
1143
+ export const sortProducts = (
1144
+ products: Product[],
1145
+ orderOfIdsOrSkus: string[],
1146
+ prop: "sku" | "inProductGroupWithID",
1147
+ ) => {
1148
+ const productMap: ProductMap = {};
1149
+
1150
+ products.forEach((product) => {
1151
+ productMap[product[prop] || product["sku"]] = product;
1152
+ });
1153
+
1154
+ return orderOfIdsOrSkus.map((id) => productMap[id]);
1155
+ };
1156
+
1157
+ export const parsePageType = (p: PageTypeVTEX): PageType => {
1158
+ const type = p.pageType;
1159
+
1160
+ // Search or Busca vazia
1161
+ if (type === "FullText") {
1162
+ return "Search";
1163
+ }
1164
+
1165
+ // A page that vtex doesn't recognize
1166
+ if (type === "NotFound") {
1167
+ return "Unknown";
1168
+ }
1169
+
1170
+ return type;
1171
+ };
1172
+
1173
+ function dayOfWeekIndexToString(day?: number): DayOfWeek | undefined {
1174
+ switch (day) {
1175
+ case 0:
1176
+ return "Sunday";
1177
+ case 1:
1178
+ return "Monday";
1179
+ case 2:
1180
+ return "Tuesday";
1181
+ case 3:
1182
+ return "Wednesday";
1183
+ case 4:
1184
+ return "Thursday";
1185
+ case 5:
1186
+ return "Friday";
1187
+ case 6:
1188
+ return "Saturday";
1189
+ default:
1190
+ return undefined;
1191
+ }
1192
+ }
1193
+
1194
+ interface Hours {
1195
+ dayOfWeek?: number;
1196
+ openingTime?: string;
1197
+ closingTime?: string;
1198
+ }
1199
+
1200
+ function toHoursSpecification(hours: Hours): OpeningHoursSpecification {
1201
+ return {
1202
+ "@type": "OpeningHoursSpecification",
1203
+ opens: hours.openingTime,
1204
+ closes: hours.closingTime,
1205
+ dayOfWeek: dayOfWeekIndexToString(hours.dayOfWeek),
1206
+ };
1207
+ }
1208
+
1209
+ function toSpecialHoursSpecification(
1210
+ holiday: PickupHolidays,
1211
+ ): OpeningHoursSpecification {
1212
+ const dateHoliday = new Date(holiday.date ?? "");
1213
+ // VTEX provide date in ISO format, at 00h on the day
1214
+ const validThrough = dateHoliday.setDate(dateHoliday.getDate() + 1)
1215
+ .toString();
1216
+
1217
+ return {
1218
+ "@type": "OpeningHoursSpecification",
1219
+ opens: holiday.hourBegin,
1220
+ closes: holiday.hourEnd,
1221
+ validFrom: holiday.date,
1222
+ validThrough,
1223
+ };
1224
+ }
1225
+
1226
+ function isPickupPointVCS(
1227
+ pickupPoint: PickupPoint | PickupPointVCS,
1228
+ ): pickupPoint is PickupPointVCS {
1229
+ return "name" in pickupPoint;
1230
+ }
1231
+
1232
+ interface ToPlaceOptions {
1233
+ isActive?: boolean;
1234
+ }
1235
+
1236
+ export function toPlace(
1237
+ pickupPoint: PickupPoint & { distance?: number } | PickupPointVCS,
1238
+ options?: ToPlaceOptions,
1239
+ ): Place {
1240
+ const {
1241
+ name,
1242
+ country,
1243
+ latitude,
1244
+ longitude,
1245
+ openingHoursSpecification,
1246
+ specialOpeningHoursSpecification,
1247
+ isActive,
1248
+ } = isPickupPointVCS(pickupPoint)
1249
+ ? {
1250
+ name: pickupPoint.name,
1251
+ country: pickupPoint.address?.country?.acronym,
1252
+ latitude: pickupPoint.address?.location?.latitude,
1253
+ longitude: pickupPoint.address?.location?.longitude,
1254
+ specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
1255
+ toSpecialHoursSpecification,
1256
+ ),
1257
+ openingHoursSpecification: pickupPoint.businessHours?.map(
1258
+ toHoursSpecification,
1259
+ ),
1260
+ isActive: pickupPoint.isActive,
1261
+ }
1262
+ : {
1263
+ name: pickupPoint.friendlyName,
1264
+ country: pickupPoint.address?.country,
1265
+ latitude: pickupPoint.address?.geoCoordinates?.[0],
1266
+ longitude: pickupPoint.address?.geoCoordinates?.[1],
1267
+ specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
1268
+ toSpecialHoursSpecification,
1269
+ ),
1270
+ openingHoursSpecification: pickupPoint.businessHours?.map((
1271
+ { ClosingTime, DayOfWeek, OpeningTime },
1272
+ ) =>
1273
+ toHoursSpecification({
1274
+ closingTime: ClosingTime,
1275
+ dayOfWeek: DayOfWeek,
1276
+ openingTime: OpeningTime,
1277
+ })
1278
+ ),
1279
+ isActive: options?.isActive,
1280
+ };
1281
+
1282
+ return {
1283
+ "@id": pickupPoint.id,
1284
+ "@type": "Place",
1285
+ address: {
1286
+ "@type": "PostalAddress",
1287
+ addressCountry: country,
1288
+ addressLocality: pickupPoint.address?.city,
1289
+ addressRegion: pickupPoint.address?.state,
1290
+ postalCode: pickupPoint.address?.postalCode,
1291
+ streetAddress: pickupPoint.address?.street,
1292
+ },
1293
+ latitude,
1294
+ longitude,
1295
+ name,
1296
+ specialOpeningHoursSpecification,
1297
+ openingHoursSpecification,
1298
+ additionalProperty: [
1299
+ {
1300
+ "@type": "PropertyValue",
1301
+ name: "distance",
1302
+ value: `${pickupPoint.distance}`,
1303
+ },
1304
+ {
1305
+ "@type": "PropertyValue",
1306
+ name: "isActive",
1307
+ value: typeof isActive === "boolean" ? `${isActive}` : undefined,
1308
+ },
1309
+ ],
1310
+ };
1311
+ }
1312
+
1313
+ export const toPostalAddress = (address: Address): PostalAddress => {
1314
+ return {
1315
+ "@type": "PostalAddress",
1316
+ "@id": address.addressId,
1317
+ addressCountry: address.country,
1318
+ addressLocality: address.city,
1319
+ addressRegion: address.state,
1320
+ areaServed: address.neighborhood || undefined,
1321
+ postalCode: address.postalCode,
1322
+ streetAddress: address.street,
1323
+ identifier: address.number || undefined,
1324
+ name: address.addressName || undefined,
1325
+ alternateName: address.receiverName || undefined,
1326
+ description: address.complement || undefined,
1327
+ disambiguatingDescription: address.reference || undefined,
1328
+ latitude: address.geoCoordinates?.[0] || undefined,
1329
+ longitude: address.geoCoordinates?.[1] || undefined,
1330
+ };
1331
+ };