@decocms/apps 0.23.3 → 0.25.0

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