@decocms/apps 0.23.3 → 0.24.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 (110) 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 +23 -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/productListingPage.ts +10 -34
  64. package/vtex/inline-loaders/relatedProducts.ts +1 -3
  65. package/vtex/inline-loaders/suggestions.ts +36 -39
  66. package/vtex/inline-loaders/workflowProducts.ts +45 -49
  67. package/vtex/invoke.ts +159 -194
  68. package/vtex/loaders/address.ts +49 -54
  69. package/vtex/loaders/brands.ts +19 -26
  70. package/vtex/loaders/cart.ts +24 -21
  71. package/vtex/loaders/catalog.ts +51 -53
  72. package/vtex/loaders/collections.ts +25 -27
  73. package/vtex/loaders/legacy.ts +487 -534
  74. package/vtex/loaders/logistics.ts +33 -37
  75. package/vtex/loaders/navbar.ts +5 -8
  76. package/vtex/loaders/orders.ts +28 -39
  77. package/vtex/loaders/pageType.ts +41 -35
  78. package/vtex/loaders/payment.ts +27 -37
  79. package/vtex/loaders/profile.ts +38 -38
  80. package/vtex/loaders/promotion.ts +5 -8
  81. package/vtex/loaders/search.ts +56 -59
  82. package/vtex/loaders/session.ts +22 -30
  83. package/vtex/loaders/user.ts +39 -41
  84. package/vtex/loaders/wishlist.ts +35 -35
  85. package/vtex/loaders/wishlistProducts.ts +3 -15
  86. package/vtex/loaders/workflow.ts +220 -227
  87. package/vtex/middleware.ts +116 -119
  88. package/vtex/types.ts +201 -201
  89. package/vtex/utils/batch.ts +13 -16
  90. package/vtex/utils/cookies.ts +76 -80
  91. package/vtex/utils/enrichment.ts +62 -42
  92. package/vtex/utils/fetchCache.ts +1 -4
  93. package/vtex/utils/index.ts +6 -6
  94. package/vtex/utils/intelligentSearch.ts +48 -57
  95. package/vtex/utils/legacy.ts +108 -124
  96. package/vtex/utils/pickAndOmit.ts +15 -20
  97. package/vtex/utils/proxy.ts +136 -146
  98. package/vtex/utils/resourceRange.ts +3 -3
  99. package/vtex/utils/segment.ts +100 -111
  100. package/vtex/utils/similars.ts +1 -2
  101. package/vtex/utils/sitemap.ts +91 -91
  102. package/vtex/utils/slugCache.ts +2 -6
  103. package/vtex/utils/slugify.ts +9 -9
  104. package/vtex/utils/transform.ts +1012 -1105
  105. package/vtex/utils/types.ts +1381 -1381
  106. package/vtex/utils/vtexId.ts +44 -47
  107. package/.github/workflows/release.yml +0 -34
  108. package/.releaserc.json +0 -28
  109. package/knip.json +0 -19
  110. package/tsconfig.json +0 -11
@@ -1,950 +1,873 @@
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);
87
+
88
+ const getProductURL = (origin: string, product: { linkText: string }, skuId?: string) => {
89
+ const canonicalUrl = getProductGroupURL(origin, product);
102
90
 
103
- if (skuId) {
104
- canonicalUrl.searchParams.set("skuId", skuId);
105
- }
91
+ if (skuId) {
92
+ canonicalUrl.searchParams.set("skuId", skuId);
93
+ }
106
94
 
107
- return canonicalUrl;
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,
368
346
  ): 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
- };
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
+ };
531
493
  };
532
494
 
533
495
  const toBreadcrumbList = (
534
- product: ProductVTEX | LegacyProductVTEX,
535
- { baseUrl }: ProductOptions,
496
+ product: ProductVTEX | LegacyProductVTEX,
497
+ { baseUrl }: ProductOptions,
536
498
  ): 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
- };
499
+ const { categories, productName } = product;
500
+ const names = categories[0]?.split("/").filter(Boolean);
501
+
502
+ const segments = names.map(slugify);
503
+
504
+ return {
505
+ "@type": "BreadcrumbList",
506
+ itemListElement: [
507
+ ...names.map((name, index) => {
508
+ const position = index + 1;
509
+
510
+ return {
511
+ "@type": "ListItem" as const,
512
+ name,
513
+ item: new URL(`/${segments.slice(0, position).join("/")}`, baseUrl).href,
514
+ position,
515
+ };
516
+ }),
517
+ {
518
+ "@type": "ListItem",
519
+ name: productName,
520
+ item: getProductGroupURL(baseUrl, product).href,
521
+ position: categories.length + 1,
522
+ },
523
+ ],
524
+ numberOfItems: categories.length + 1,
525
+ };
565
526
  };
566
527
 
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
- });
528
+ const legacyToProductGroupAdditionalProperties = (product: LegacyProductVTEX) => {
529
+ const groups = product.allSpecificationsGroups ?? [];
530
+ const allSpecifications = product.allSpecifications ?? [];
531
+
532
+ const specByGroup: Record<string, string> = {};
533
+
534
+ groups.forEach((group) => {
535
+ const groupSpecs = (product as unknown as Record<string, string[]>)[group];
536
+ groupSpecs.forEach((specName) => {
537
+ specByGroup[specName] = group;
538
+ });
539
+ });
540
+
541
+ return allSpecifications.flatMap((name) => {
542
+ const values = (product as unknown as Record<string, string[]>)[name];
543
+ return values.map((value) =>
544
+ toAdditionalPropertySpecification({
545
+ name,
546
+ value,
547
+ propertyID: specByGroup[name],
548
+ }),
549
+ );
550
+ });
592
551
  };
593
552
 
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
- );
553
+ const toProductGroupAdditionalProperties = ({ specificationGroups = [] }: ProductVTEX) =>
554
+ specificationGroups.flatMap(({ name: groupName, specifications }) =>
555
+ specifications.flatMap(({ name, values }) =>
556
+ values.map(
557
+ (value) =>
558
+ ({
559
+ "@type": "PropertyValue",
560
+ name,
561
+ value,
562
+ propertyID: groupName,
563
+ valueReference: "PROPERTY" as string,
564
+ }) as const,
565
+ ),
566
+ ),
567
+ );
611
568
 
612
569
  const toOriginalAttributesAdditionalProperties = (
613
- originalAttributes: Maybe<string[]>,
614
- product: ProductVTEX | LegacyProduct,
570
+ originalAttributes: Maybe<string[]>,
571
+ product: ProductVTEX | LegacyProduct,
615
572
  ) => {
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[];
573
+ if (!originalAttributes) {
574
+ return [];
575
+ }
576
+
577
+ const attributes = pick(originalAttributes as Array<keyof typeof product>, product) ?? {};
578
+
579
+ return Object.entries(attributes).map(
580
+ ([name, value]) =>
581
+ ({
582
+ "@type": "PropertyValue",
583
+ name,
584
+ value,
585
+ valueReference: "ORIGINAL_PROPERTY" as string,
586
+ }) as const,
587
+ ) as unknown as PropertyValue[];
631
588
  };
632
589
 
633
590
  const toAdditionalProperties = (sku: SkuVTEX): PropertyValue[] =>
634
- sku.variations?.flatMap(({ name, values }) =>
635
- values.map((value) => toAdditionalPropertySpecification({ name, value }))
636
- ) ?? [];
591
+ sku.variations?.flatMap(({ name, values }) =>
592
+ values.map((value) => toAdditionalPropertySpecification({ name, value })),
593
+ ) ?? [];
637
594
 
638
595
  export const toAdditionalPropertySpecification = ({
639
- name,
640
- value,
641
- propertyID,
596
+ name,
597
+ value,
598
+ propertyID,
642
599
  }: {
643
- name: string;
644
- value: string;
645
- propertyID?: string;
600
+ name: string;
601
+ value: string;
602
+ propertyID?: string;
646
603
  }): PropertyValue => ({
647
- "@type": "PropertyValue",
648
- name,
649
- value,
650
- propertyID,
651
- valueReference: "SPECIFICATION",
604
+ "@type": "PropertyValue",
605
+ name,
606
+ value,
607
+ propertyID,
608
+ valueReference: "SPECIFICATION",
652
609
  });
653
610
 
654
611
  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;
612
+ const { variations = [], attachments = [] } = sku;
613
+
614
+ const specificationProperties = variations.flatMap((variation) =>
615
+ sku[variation].map((value) => toAdditionalPropertySpecification({ name: variation, value })),
616
+ );
617
+
618
+ const attachmentProperties = attachments.map(
619
+ (attachment) =>
620
+ ({
621
+ "@type": "PropertyValue",
622
+ propertyID: `${attachment.id}`,
623
+ name: attachment.name,
624
+ value: attachment.domainValues,
625
+ required: attachment.required,
626
+ valueReference: "ATTACHMENT",
627
+ }) as const,
628
+ );
629
+
630
+ if (attachmentProperties.length === 0) return specificationProperties;
631
+ specificationProperties.push(...attachmentProperties);
632
+ return specificationProperties;
678
633
  };
679
634
 
680
635
  const buildOffer = (
681
- { commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
682
- teasers: Teasers[],
636
+ { commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
637
+ teasers: Teasers[],
683
638
  ): 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,
639
+ "@type": "Offer",
640
+ identifier: sellerDefault ? "default" : undefined,
641
+ price: offer.spotPrice ?? offer.Price,
642
+ seller: sellerId,
643
+ sellerName,
644
+ priceValidUntil: offer.PriceValidUntil,
645
+ inventoryLevel: { value: offer.AvailableQuantity },
646
+ giftSkuIds: offer.GiftSkuIds ?? [],
647
+ teasers,
648
+ priceSpecification: [
649
+ {
650
+ "@type": "UnitPriceSpecification",
651
+ priceType: SCHEMA_LIST_PRICE,
652
+ price: offer.ListPrice,
653
+ },
654
+ {
655
+ "@type": "UnitPriceSpecification",
656
+ priceType: SCHEMA_SALE_PRICE,
657
+ price: offer.Price,
658
+ },
659
+ {
660
+ "@type": "UnitPriceSpecification",
661
+ priceType: SCHEMA_SRP,
662
+ price: offer.PriceWithoutDiscount,
663
+ },
664
+ ...offer.Installments.map(
665
+ (installment): UnitPriceSpecification => ({
666
+ "@type": "UnitPriceSpecification",
667
+ priceType: SCHEMA_SALE_PRICE,
668
+ priceComponentType: SCHEMA_INSTALLMENT,
669
+ name: installment.PaymentSystemName,
670
+ description: installment.Name,
671
+ billingDuration: installment.NumberOfInstallments,
672
+ billingIncrement: installment.Value,
673
+ price: installment.TotalValuePlusInterestRate,
674
+ }),
675
+ ),
676
+ ],
677
+ availability: offer.AvailableQuantity > 0 ? SCHEMA_IN_STOCK : SCHEMA_OUT_OF_STOCK,
725
678
  });
726
679
 
727
680
  const toOffer = (seller: SellerVTEX): Offer =>
728
- buildOffer(seller, seller.commertialOffer.teasers ?? []);
681
+ buildOffer(seller, seller.commertialOffer.teasers ?? []);
729
682
 
730
683
  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]);
684
+ const otherTeasers =
685
+ seller.commertialOffer.DiscountHighLight?.map((i) => {
686
+ const discount = i as Record<string, string>;
687
+ const [_k__BackingField, discountName] = Object.entries(discount)?.[0] ?? [];
688
+
689
+ const teasers: Teasers = {
690
+ name: discountName,
691
+ conditions: {
692
+ minimumQuantity: 0,
693
+ parameters: [],
694
+ },
695
+ effects: {
696
+ parameters: [],
697
+ },
698
+ };
699
+
700
+ return teasers;
701
+ }) ?? [];
702
+
703
+ const legacyTeasers = (seller.commertialOffer.Teasers ?? []).map((teaser) => ({
704
+ name: teaser["<Name>k__BackingField"],
705
+ generalValues: teaser["<GeneralValues>k__BackingField"],
706
+ conditions: {
707
+ minimumQuantity: teaser["<Conditions>k__BackingField"]["<MinimumQuantity>k__BackingField"],
708
+ parameters: teaser["<Conditions>k__BackingField"]["<Parameters>k__BackingField"].map(
709
+ (parameter) => ({
710
+ name: parameter["<Name>k__BackingField"],
711
+ value: parameter["<Value>k__BackingField"],
712
+ }),
713
+ ),
714
+ },
715
+ effects: {
716
+ parameters: teaser["<Effects>k__BackingField"]["<Parameters>k__BackingField"].map(
717
+ (parameter) => ({
718
+ name: parameter["<Name>k__BackingField"],
719
+ value: parameter["<Value>k__BackingField"],
720
+ }),
721
+ ),
722
+ },
723
+ }));
724
+
725
+ return buildOffer(seller, [...otherTeasers, ...legacyTeasers]);
777
726
  };
778
727
 
779
728
  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,
729
+ name: string,
730
+ facets: LegacyFacet[],
731
+ url: URL,
732
+ map: string,
733
+ term: string,
734
+ behavior: "dynamic" | "static",
735
+ ignoreCaseSelected?: boolean,
736
+ fullPath = false,
788
737
  ): 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
- };
738
+ const mapSegments = map.split(",").filter((x) => x.length > 0);
739
+ const pathSegments = term.replace(/^\//, "").split("/").slice(0, mapSegments.length);
740
+
741
+ const mapSet = new Set(mapSegments.map((i) => (ignoreCaseSelected ? i.toLowerCase() : i)));
742
+ const pathSet = new Set(pathSegments.map((i) => (ignoreCaseSelected ? i.toLowerCase() : i)));
743
+
744
+ // for productClusterIds, we have to use the full path
745
+ // example:
746
+ // category2/123?map=c,productClusterIds -> DO NOT WORK
747
+ // category1/category2/123?map=c,c,productClusterIds -> WORK
748
+ const hasProductClusterIds = mapSegments.includes("productClusterIds");
749
+ const hasToBeFullpath =
750
+ fullPath || hasProductClusterIds || mapSegments.includes("ft") || mapSegments.includes("b");
751
+
752
+ const getLink = (facet: LegacyFacet, selected: boolean) => {
753
+ const index = pathSegments.findIndex((s) => {
754
+ if (ignoreCaseSelected) {
755
+ return s.toLowerCase() === facet.Value.toLowerCase();
756
+ }
757
+
758
+ return s === facet.Value;
759
+ });
760
+
761
+ const map = hasToBeFullpath ? facet.Link.split("map=")[1].split(",") : [facet.Map];
762
+ const value = hasToBeFullpath ? facet.Link.split("?")[0].slice(1).split("/") : [facet.Value];
763
+
764
+ const pathSegmentsFiltered = hasProductClusterIds
765
+ ? [pathSegments[mapSegments.indexOf("productClusterIds")]]
766
+ : [];
767
+ const mapSegmentsFiltered = hasProductClusterIds ? ["productClusterIds"] : [];
768
+
769
+ const _mapSegments = hasToBeFullpath ? mapSegmentsFiltered : mapSegments;
770
+ const _pathSegments = hasToBeFullpath ? pathSegmentsFiltered : pathSegments;
771
+
772
+ const newMap = selected
773
+ ? [...mapSegments.filter((_, i) => i !== index)]
774
+ : [..._mapSegments, ...map];
775
+ const newPath = selected
776
+ ? [...pathSegments.filter((_, i) => i !== index)]
777
+ : [..._pathSegments, ...value];
778
+
779
+ // Insertion-sort like algorithm. Uses the c-continuum theorem
780
+ const zipped: [string, string][] = [];
781
+ for (let it = 0; it < newMap.length; it++) {
782
+ let i = 0;
783
+ while (i < zipped.length && (zipped[i][0] === "c" || zipped[i][0] === "C")) i++;
784
+
785
+ zipped.splice(i, 0, [newMap[it], newPath[it]]);
786
+ }
787
+
788
+ const link = new URL(`/${zipped.map(([, s]) => s).join("/")}`, url);
789
+ link.searchParams.set("map", zipped.map(([m]) => m).join(","));
790
+ if (behavior === "static") {
791
+ link.searchParams.set("fmap", url.searchParams.get("fmap") || mapSegments.join(","));
792
+ }
793
+ const currentQuery = url.searchParams.get("q");
794
+ if (currentQuery) {
795
+ link.searchParams.set("q", currentQuery);
796
+ }
797
+
798
+ return `${link.pathname}${link.search}`;
799
+ };
800
+
801
+ return {
802
+ "@type": "FilterToggle",
803
+ quantity: facets?.length,
804
+ label: name,
805
+ key: name,
806
+ values: facets.map((facet) => {
807
+ const normalizedFacet = name !== "PriceRanges" ? facet : normalizeFacet(facet);
808
+
809
+ const selected =
810
+ mapSet.has(ignoreCaseSelected ? normalizedFacet.Map.toLowerCase() : normalizedFacet.Map) &&
811
+ pathSet.has(
812
+ ignoreCaseSelected ? normalizedFacet.Value.toLowerCase() : normalizedFacet.Value,
813
+ );
814
+
815
+ return {
816
+ value: normalizedFacet.Value,
817
+ quantity: normalizedFacet.Quantity,
818
+ url: getLink(normalizedFacet, selected),
819
+ label: normalizedFacet.Name,
820
+ selected,
821
+ children:
822
+ facet.Children?.length > 0
823
+ ? legacyFacetToFilter(
824
+ normalizedFacet.Name,
825
+ facet.Children,
826
+ url,
827
+ map,
828
+ term,
829
+ behavior,
830
+ ignoreCaseSelected,
831
+ fullPath,
832
+ )
833
+ : undefined,
834
+ };
835
+ }),
836
+ };
914
837
  };
915
838
 
916
839
  export const filtersToSearchParams = (
917
- selectedFacets: SelectedFacet[],
918
- paramsToPersist?: URLSearchParams,
840
+ selectedFacets: SelectedFacet[],
841
+ paramsToPersist?: URLSearchParams,
919
842
  ) => {
920
- const searchParams = new URLSearchParams(paramsToPersist);
843
+ const searchParams = new URLSearchParams(paramsToPersist);
921
844
 
922
- for (const { key, value } of selectedFacets) {
923
- searchParams.append(`filter.${key}`, value);
924
- }
845
+ for (const { key, value } of selectedFacets) {
846
+ searchParams.append(`filter.${key}`, value);
847
+ }
925
848
 
926
- return searchParams;
849
+ return searchParams;
927
850
  };
928
851
 
929
852
  const fromLegacyMap: Record<string, string> = {
930
- priceFrom: "price",
931
- productClusterSearchableIds: "productClusterIds",
853
+ priceFrom: "price",
854
+ productClusterSearchableIds: "productClusterIds",
932
855
  };
933
856
 
934
857
  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 };
858
+ // Replace legacy price path param to IS price facet format
859
+ // exemple: de-34,90-a-56,90 turns to 34.90:56.90
860
+ // may this regex have to be adjusted for international stores
861
+ const value = path.replace(
862
+ /de-(?<from>\d+[,]?[\d]+)-a-(?<to>\d+[,]?[\d]+)/,
863
+ (_match, from, to) => {
864
+ return `${from.replace(",", ".")}:${to.replace(",", ".")}`;
865
+ },
866
+ );
867
+
868
+ const key = fromLegacyMap[map] || map;
869
+
870
+ return { key, value };
948
871
  };
949
872
 
950
873
  /**
@@ -952,380 +875,364 @@ export const legacyFacetsNormalize = (map: string, path: string) => {
952
875
  * to Deco and also migrating from VTEX Legacy to VTEX Intelligent Search.
953
876
  */
954
877
  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);
878
+ const mapSegments = url.searchParams.get("map")?.split(",") ?? [];
879
+ const pathSegments = url.pathname.split("/").slice(1); // Remove first slash
880
+ const length = Math.min(mapSegments.length, pathSegments.length);
958
881
 
959
- const selectedFacets: SelectedFacet[] = [];
960
- for (let it = 0; it < length; it++) {
961
- const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
882
+ const selectedFacets: SelectedFacet[] = [];
883
+ for (let it = 0; it < length; it++) {
884
+ const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
962
885
 
963
- selectedFacets.push(facet);
964
- }
886
+ selectedFacets.push(facet);
887
+ }
965
888
 
966
- return selectedFacets;
889
+ return selectedFacets;
967
890
  };
968
891
 
969
892
  export const filtersFromURL = (url: URL) => {
970
- const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
893
+ const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
971
894
 
972
- url.searchParams.forEach((value, name) => {
973
- const [filter, key] = name.split(".");
895
+ url.searchParams.forEach((value, name) => {
896
+ const [filter, key] = name.split(".");
974
897
 
975
- if (filter === "filter" && typeof key === "string") {
976
- selectedFacets.push({ key, value });
977
- }
978
- });
898
+ if (filter === "filter" && typeof key === "string") {
899
+ selectedFacets.push({ key, value });
900
+ }
901
+ });
979
902
 
980
- return selectedFacets;
903
+ return selectedFacets;
981
904
  };
982
905
 
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
- };
906
+ export const mergeFacets = (f1: SelectedFacet[], f2: SelectedFacet[]): SelectedFacet[] => {
907
+ const facetKey = (facet: SelectedFacet) => `key:${facet.key}-value:${facet.value}`;
908
+ const merged = new Map<string, SelectedFacet>();
1000
909
 
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
- };
910
+ for (const f of f1) {
911
+ merged.set(facetKey(f), f);
912
+ }
913
+ for (const f of f2) {
914
+ merged.set(facetKey(f), f);
915
+ }
916
+
917
+ return [...merged.values()];
1033
918
  };
1034
919
 
920
+ const isValueRange = (facet: FacetValueRange | FacetValueBoolean): facet is FacetValueRange =>
921
+ // deno-lint-ignore no-explicit-any
922
+ Boolean((facet as any).range);
923
+
924
+ const facetToToggle =
925
+ (selectedFacets: SelectedFacet[], key: string, paramsToPersist?: URLSearchParams) =>
926
+ (item: FacetValueRange | FacetValueBoolean): FilterToggleValue => {
927
+ const { quantity, selected } = item;
928
+ const isRange = isValueRange(item);
929
+
930
+ const value = isRange ? formatRange(item.range.from, item.range.to) : item.value;
931
+ const label = isRange ? value : item.name;
932
+ const facet = { key, value };
933
+
934
+ const filters = selected
935
+ ? selectedFacets.filter((f) => f.key !== key || f.value !== value)
936
+ : [...selectedFacets, facet];
937
+
938
+ return {
939
+ value,
940
+ quantity,
941
+ selected,
942
+ url: `?${filtersToSearchParams(filters, paramsToPersist)}`,
943
+ label,
944
+ };
945
+ };
946
+
1035
947
  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
- });
948
+ (selectedFacets: SelectedFacet[], paramsToPersist?: URLSearchParams) =>
949
+ ({ key, name, quantity, values }: FacetVTEX): Filter => ({
950
+ "@type": "FilterToggle",
951
+ key,
952
+ label: name,
953
+ quantity: quantity,
954
+ values: values.map(facetToToggle(selectedFacets, key, paramsToPersist)),
955
+ });
1044
956
 
1045
957
  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
- };
958
+ const url = new URL(node.url, "https://example.com");
959
+
960
+ return {
961
+ "@type": "SiteNavigationElement",
962
+ url: `${url.pathname}${url.search}`,
963
+ name: node.name,
964
+ children: node.children.map(nodeToNavbar),
965
+ };
1054
966
  }
1055
967
 
1056
- export const categoryTreeToNavbar = (
1057
- tree: Category[],
1058
- ): SiteNavigationElement[] => tree.map(nodeToNavbar);
968
+ export const categoryTreeToNavbar = (tree: Category[]): SiteNavigationElement[] =>
969
+ tree.map(nodeToNavbar);
1059
970
 
1060
971
  export const toBrand = (
1061
- { id, name, imageUrl, metaTagDescription }: BrandVTEX,
1062
- baseUrl: string,
972
+ { id, name, imageUrl, metaTagDescription }: BrandVTEX,
973
+ baseUrl: string,
1063
974
  ): Brand => ({
1064
- "@type": "Brand",
1065
- "@id": `${id}`,
1066
- name,
1067
- logo: imageUrl?.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`,
1068
- description: metaTagDescription,
975
+ "@type": "Brand",
976
+ "@id": `${id}`,
977
+ name,
978
+ logo: imageUrl?.startsWith("http") ? imageUrl : `${baseUrl}${imageUrl}`,
979
+ description: metaTagDescription,
1069
980
  });
1070
981
 
1071
982
  export const normalizeFacet = (facet: LegacyFacet) => {
1072
- return {
1073
- ...facet,
1074
- Map: "priceFrom",
1075
- Value: facet.Slug!,
1076
- };
983
+ return {
984
+ ...facet,
985
+ Map: "priceFrom",
986
+ Value: facet.Slug!,
987
+ };
1077
988
  };
1078
989
 
1079
990
  export const toReview = (
1080
- products: Product[],
1081
- ratings: ProductRating[],
1082
- reviews: ProductReviewData[],
991
+ products: Product[],
992
+ ratings: ProductRating[],
993
+ reviews: ProductReviewData[],
1083
994
  ): 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
- });
995
+ return products.map((p, index) => {
996
+ const ratingsCount = ratings[index].totalCount || 0;
997
+ const productReviews = reviews[index].data || [];
998
+
999
+ return {
1000
+ ...p,
1001
+ aggregateRating: {
1002
+ "@type": "AggregateRating",
1003
+ reviewCount: ratingsCount,
1004
+ ratingCount: ratingsCount,
1005
+ ratingValue: ratings[index]?.average || 0,
1006
+ },
1007
+ review: productReviews.map((_, reviewIndex) => ({
1008
+ "@type": "Review",
1009
+ id: productReviews[reviewIndex]?.id?.toString(),
1010
+ author: [
1011
+ {
1012
+ "@type": "Author",
1013
+ name: productReviews[reviewIndex]?.reviewerName,
1014
+ verifiedBuyer: productReviews[reviewIndex]?.verifiedPurchaser,
1015
+ },
1016
+ ],
1017
+ itemReviewed: productReviews[reviewIndex]?.productId,
1018
+ datePublished: productReviews[reviewIndex]?.reviewDateTime,
1019
+ reviewHeadline: productReviews[reviewIndex]?.title,
1020
+ reviewBody: productReviews[reviewIndex]?.text,
1021
+ reviewRating: {
1022
+ "@type": "AggregateRating",
1023
+ ratingValue: productReviews[reviewIndex]?.rating || 0,
1024
+ },
1025
+ })),
1026
+ };
1027
+ });
1115
1028
  };
1116
1029
 
1117
1030
  export const toInventories = (
1118
- products: Product[],
1119
- inventoriesData: ProductInventoryData[],
1031
+ products: Product[],
1032
+ inventoriesData: ProductInventoryData[],
1120
1033
  ): 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
- });
1034
+ return products.map((p, index) => {
1035
+ const balance = inventoriesData[index].balance || [];
1036
+
1037
+ const additionalProperty = Array.from(p.additionalProperty || []);
1038
+
1039
+ const inventories: PropertyValue[] = balance.map((b) => ({
1040
+ "@type": "PropertyValue",
1041
+ valueReference: "INVENTORY",
1042
+ propertyID: b.warehouseId,
1043
+ name: b.warehouseName,
1044
+ value: b.totalQuantity?.toString(),
1045
+ }));
1046
+
1047
+ return {
1048
+ ...p,
1049
+ additionalProperty: [...additionalProperty, ...inventories],
1050
+ };
1051
+ });
1139
1052
  };
1140
1053
 
1141
1054
  type ProductMap = Record<string, Product>;
1142
1055
 
1143
1056
  export const sortProducts = (
1144
- products: Product[],
1145
- orderOfIdsOrSkus: string[],
1146
- prop: "sku" | "inProductGroupWithID",
1057
+ products: Product[],
1058
+ orderOfIdsOrSkus: string[],
1059
+ prop: "sku" | "inProductGroupWithID",
1147
1060
  ) => {
1148
- const productMap: ProductMap = {};
1061
+ const productMap: ProductMap = {};
1149
1062
 
1150
- products.forEach((product) => {
1151
- productMap[product[prop] || product["sku"]] = product;
1152
- });
1063
+ products.forEach((product) => {
1064
+ productMap[product[prop] || product.sku] = product;
1065
+ });
1153
1066
 
1154
- return orderOfIdsOrSkus.map((id) => productMap[id]);
1067
+ return orderOfIdsOrSkus.map((id) => productMap[id]);
1155
1068
  };
1156
1069
 
1157
1070
  export const parsePageType = (p: PageTypeVTEX): PageType => {
1158
- const type = p.pageType;
1071
+ const type = p.pageType;
1159
1072
 
1160
- // Search or Busca vazia
1161
- if (type === "FullText") {
1162
- return "Search";
1163
- }
1073
+ // Search or Busca vazia
1074
+ if (type === "FullText") {
1075
+ return "Search";
1076
+ }
1164
1077
 
1165
- // A page that vtex doesn't recognize
1166
- if (type === "NotFound") {
1167
- return "Unknown";
1168
- }
1078
+ // A page that vtex doesn't recognize
1079
+ if (type === "NotFound") {
1080
+ return "Unknown";
1081
+ }
1169
1082
 
1170
- return type;
1083
+ return type;
1171
1084
  };
1172
1085
 
1173
1086
  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
- }
1087
+ switch (day) {
1088
+ case 0:
1089
+ return "Sunday";
1090
+ case 1:
1091
+ return "Monday";
1092
+ case 2:
1093
+ return "Tuesday";
1094
+ case 3:
1095
+ return "Wednesday";
1096
+ case 4:
1097
+ return "Thursday";
1098
+ case 5:
1099
+ return "Friday";
1100
+ case 6:
1101
+ return "Saturday";
1102
+ default:
1103
+ return undefined;
1104
+ }
1192
1105
  }
1193
1106
 
1194
1107
  interface Hours {
1195
- dayOfWeek?: number;
1196
- openingTime?: string;
1197
- closingTime?: string;
1108
+ dayOfWeek?: number;
1109
+ openingTime?: string;
1110
+ closingTime?: string;
1198
1111
  }
1199
1112
 
1200
1113
  function toHoursSpecification(hours: Hours): OpeningHoursSpecification {
1201
- return {
1202
- "@type": "OpeningHoursSpecification",
1203
- opens: hours.openingTime,
1204
- closes: hours.closingTime,
1205
- dayOfWeek: dayOfWeekIndexToString(hours.dayOfWeek),
1206
- };
1114
+ return {
1115
+ "@type": "OpeningHoursSpecification",
1116
+ opens: hours.openingTime,
1117
+ closes: hours.closingTime,
1118
+ dayOfWeek: dayOfWeekIndexToString(hours.dayOfWeek),
1119
+ };
1207
1120
  }
1208
1121
 
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
- };
1122
+ function toSpecialHoursSpecification(holiday: PickupHolidays): OpeningHoursSpecification {
1123
+ const dateHoliday = new Date(holiday.date ?? "");
1124
+ // VTEX provide date in ISO format, at 00h on the day
1125
+ const validThrough = dateHoliday.setDate(dateHoliday.getDate() + 1).toString();
1126
+
1127
+ return {
1128
+ "@type": "OpeningHoursSpecification",
1129
+ opens: holiday.hourBegin,
1130
+ closes: holiday.hourEnd,
1131
+ validFrom: holiday.date,
1132
+ validThrough,
1133
+ };
1224
1134
  }
1225
1135
 
1226
1136
  function isPickupPointVCS(
1227
- pickupPoint: PickupPoint | PickupPointVCS,
1137
+ pickupPoint: PickupPoint | PickupPointVCS,
1228
1138
  ): pickupPoint is PickupPointVCS {
1229
- return "name" in pickupPoint;
1139
+ return "name" in pickupPoint;
1230
1140
  }
1231
1141
 
1232
1142
  interface ToPlaceOptions {
1233
- isActive?: boolean;
1143
+ isActive?: boolean;
1234
1144
  }
1235
1145
 
1236
1146
  export function toPlace(
1237
- pickupPoint: PickupPoint & { distance?: number } | PickupPointVCS,
1238
- options?: ToPlaceOptions,
1147
+ pickupPoint: (PickupPoint & { distance?: number }) | PickupPointVCS,
1148
+ options?: ToPlaceOptions,
1239
1149
  ): 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
- };
1150
+ const {
1151
+ name,
1152
+ country,
1153
+ latitude,
1154
+ longitude,
1155
+ openingHoursSpecification,
1156
+ specialOpeningHoursSpecification,
1157
+ isActive,
1158
+ } = isPickupPointVCS(pickupPoint)
1159
+ ? {
1160
+ name: pickupPoint.name,
1161
+ country: pickupPoint.address?.country?.acronym,
1162
+ latitude: pickupPoint.address?.location?.latitude,
1163
+ longitude: pickupPoint.address?.location?.longitude,
1164
+ specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
1165
+ toSpecialHoursSpecification,
1166
+ ),
1167
+ openingHoursSpecification: pickupPoint.businessHours?.map(toHoursSpecification),
1168
+ isActive: pickupPoint.isActive,
1169
+ }
1170
+ : {
1171
+ name: pickupPoint.friendlyName,
1172
+ country: pickupPoint.address?.country,
1173
+ latitude: pickupPoint.address?.geoCoordinates?.[0],
1174
+ longitude: pickupPoint.address?.geoCoordinates?.[1],
1175
+ specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map(
1176
+ toSpecialHoursSpecification,
1177
+ ),
1178
+ openingHoursSpecification: pickupPoint.businessHours?.map(
1179
+ ({ ClosingTime, DayOfWeek, OpeningTime }) =>
1180
+ toHoursSpecification({
1181
+ closingTime: ClosingTime,
1182
+ dayOfWeek: DayOfWeek,
1183
+ openingTime: OpeningTime,
1184
+ }),
1185
+ ),
1186
+ isActive: options?.isActive,
1187
+ };
1188
+
1189
+ return {
1190
+ "@id": pickupPoint.id,
1191
+ "@type": "Place",
1192
+ address: {
1193
+ "@type": "PostalAddress",
1194
+ addressCountry: country,
1195
+ addressLocality: pickupPoint.address?.city,
1196
+ addressRegion: pickupPoint.address?.state,
1197
+ postalCode: pickupPoint.address?.postalCode,
1198
+ streetAddress: pickupPoint.address?.street,
1199
+ },
1200
+ latitude,
1201
+ longitude,
1202
+ name,
1203
+ specialOpeningHoursSpecification,
1204
+ openingHoursSpecification,
1205
+ additionalProperty: [
1206
+ {
1207
+ "@type": "PropertyValue",
1208
+ name: "distance",
1209
+ value: `${pickupPoint.distance}`,
1210
+ },
1211
+ {
1212
+ "@type": "PropertyValue",
1213
+ name: "isActive",
1214
+ value: typeof isActive === "boolean" ? `${isActive}` : undefined,
1215
+ },
1216
+ ],
1217
+ };
1311
1218
  }
1312
1219
 
1313
1220
  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
- };
1221
+ return {
1222
+ "@type": "PostalAddress",
1223
+ "@id": address.addressId,
1224
+ addressCountry: address.country,
1225
+ addressLocality: address.city,
1226
+ addressRegion: address.state,
1227
+ areaServed: address.neighborhood || undefined,
1228
+ postalCode: address.postalCode,
1229
+ streetAddress: address.street,
1230
+ identifier: address.number || undefined,
1231
+ name: address.addressName || undefined,
1232
+ alternateName: address.receiverName || undefined,
1233
+ description: address.complement || undefined,
1234
+ disambiguatingDescription: address.reference || undefined,
1235
+ latitude: address.geoCoordinates?.[0] || undefined,
1236
+ longitude: address.geoCoordinates?.[1] || undefined,
1237
+ };
1331
1238
  };