@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.
- package/LICENSE +21 -0
- package/commerce/components/Image.tsx +129 -143
- package/commerce/components/JsonLd.tsx +192 -201
- package/commerce/components/Picture.tsx +65 -75
- package/commerce/sdk/analytics.ts +15 -15
- package/commerce/sdk/formatPrice.ts +13 -16
- package/commerce/sdk/url.ts +7 -7
- package/commerce/sdk/useOffer.ts +46 -57
- package/commerce/sdk/useVariantPossibilities.ts +25 -25
- package/commerce/types/commerce.ts +868 -875
- package/commerce/utils/canonical.ts +5 -8
- package/commerce/utils/constants.ts +5 -6
- package/commerce/utils/filters.ts +4 -4
- package/commerce/utils/productToAnalyticsItem.ts +52 -56
- package/commerce/utils/stateByZip.ts +42 -42
- package/package.json +24 -4
- package/shopify/actions/cart/addItems.ts +24 -25
- package/shopify/actions/cart/updateCoupons.ts +19 -20
- package/shopify/actions/cart/updateItems.ts +19 -20
- package/shopify/actions/user/signIn.ts +25 -30
- package/shopify/actions/user/signUp.ts +19 -24
- package/shopify/client.ts +24 -24
- package/shopify/index.ts +20 -18
- package/shopify/init.ts +18 -21
- package/shopify/loaders/ProductDetailsPage.ts +16 -20
- package/shopify/loaders/ProductList.ts +66 -69
- package/shopify/loaders/ProductListingPage.ts +150 -158
- package/shopify/loaders/RelatedProducts.ts +24 -27
- package/shopify/loaders/cart.ts +53 -52
- package/shopify/loaders/shop.ts +22 -27
- package/shopify/loaders/user.ts +27 -32
- package/shopify/utils/admin/admin.ts +33 -34
- package/shopify/utils/admin/queries.ts +2 -2
- package/shopify/utils/cart.ts +18 -14
- package/shopify/utils/cookies.ts +62 -65
- package/shopify/utils/enums.ts +424 -424
- package/shopify/utils/graphql.ts +44 -55
- package/shopify/utils/storefront/queries.ts +24 -29
- package/shopify/utils/storefront/storefront.graphql.gen.ts +55 -55
- package/shopify/utils/transform.ts +370 -376
- package/shopify/utils/types.ts +118 -118
- package/shopify/utils/user.ts +11 -11
- package/shopify/utils/utils.ts +135 -140
- package/vtex/actions/address.ts +86 -86
- package/vtex/actions/auth.ts +14 -27
- package/vtex/actions/checkout.ts +36 -49
- package/vtex/actions/masterData.ts +10 -27
- package/vtex/actions/misc.ts +101 -111
- package/vtex/actions/newsletter.ts +48 -52
- package/vtex/actions/orders.ts +13 -16
- package/vtex/actions/profile.ts +55 -55
- package/vtex/actions/session.ts +36 -35
- package/vtex/actions/trigger.ts +25 -25
- package/vtex/actions/wishlist.ts +51 -53
- package/vtex/client.ts +14 -42
- package/vtex/hooks/index.ts +4 -4
- package/vtex/hooks/useAutocomplete.ts +42 -48
- package/vtex/hooks/useCart.ts +153 -165
- package/vtex/hooks/useUser.ts +40 -40
- package/vtex/hooks/useWishlist.ts +70 -70
- package/vtex/inline-loaders/productDetailsPage.ts +1 -3
- package/vtex/inline-loaders/productList.ts +121 -127
- package/vtex/inline-loaders/productListShelf.ts +159 -0
- package/vtex/inline-loaders/productListingPage.ts +10 -34
- package/vtex/inline-loaders/relatedProducts.ts +1 -3
- package/vtex/inline-loaders/suggestions.ts +36 -39
- package/vtex/inline-loaders/workflowProducts.ts +45 -49
- package/vtex/invoke.ts +159 -194
- package/vtex/loaders/address.ts +49 -54
- package/vtex/loaders/brands.ts +19 -26
- package/vtex/loaders/cart.ts +24 -21
- package/vtex/loaders/catalog.ts +51 -53
- package/vtex/loaders/collections.ts +25 -27
- package/vtex/loaders/legacy.ts +487 -534
- package/vtex/loaders/logistics.ts +33 -37
- package/vtex/loaders/navbar.ts +5 -8
- package/vtex/loaders/orders.ts +28 -39
- package/vtex/loaders/pageType.ts +41 -35
- package/vtex/loaders/payment.ts +27 -37
- package/vtex/loaders/profile.ts +38 -38
- package/vtex/loaders/promotion.ts +5 -8
- package/vtex/loaders/search.ts +56 -59
- package/vtex/loaders/session.ts +22 -30
- package/vtex/loaders/user.ts +39 -41
- package/vtex/loaders/wishlist.ts +35 -35
- package/vtex/loaders/wishlistProducts.ts +3 -15
- package/vtex/loaders/workflow.ts +220 -227
- package/vtex/middleware.ts +116 -119
- package/vtex/types.ts +201 -201
- package/vtex/utils/batch.ts +13 -16
- package/vtex/utils/cookies.ts +76 -80
- package/vtex/utils/enrichment.ts +62 -42
- package/vtex/utils/fetchCache.ts +1 -4
- package/vtex/utils/index.ts +6 -6
- package/vtex/utils/intelligentSearch.ts +48 -57
- package/vtex/utils/legacy.ts +108 -124
- package/vtex/utils/pickAndOmit.ts +15 -20
- package/vtex/utils/proxy.ts +136 -146
- package/vtex/utils/resourceRange.ts +3 -3
- package/vtex/utils/segment.ts +100 -111
- package/vtex/utils/similars.ts +1 -2
- package/vtex/utils/sitemap.ts +91 -91
- package/vtex/utils/slugCache.ts +2 -6
- package/vtex/utils/slugify.ts +9 -9
- package/vtex/utils/transform.ts +1178 -1105
- package/vtex/utils/types.ts +1381 -1381
- package/vtex/utils/vtexId.ts +44 -47
- package/.github/workflows/release.yml +0 -34
- package/.releaserc.json +0 -28
- package/knip.json +0 -19
- package/tsconfig.json +0 -11
package/vtex/utils/transform.ts
CHANGED
|
@@ -1,950 +1,1039 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
88
|
+
const getProductURL = (origin: string, product: { linkText: string }, skuId?: string) => {
|
|
89
|
+
const canonicalUrl = getProductGroupURL(origin, product);
|
|
106
90
|
|
|
107
|
-
|
|
91
|
+
if (skuId) {
|
|
92
|
+
canonicalUrl.searchParams.set("skuId", skuId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return canonicalUrl;
|
|
108
96
|
};
|
|
109
97
|
|
|
110
|
-
const nonEmptyArray = <T>(
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
117
|
+
product: T,
|
|
118
|
+
maybeSkuId?: string,
|
|
134
119
|
): T["items"][number] => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
sku: T["items"][number],
|
|
132
|
+
kitItems: T[],
|
|
133
|
+
options: ProductOptions,
|
|
150
134
|
) => {
|
|
151
|
-
|
|
152
|
-
|
|
135
|
+
const productBySkuId = kitItems.reduce((map, product) => {
|
|
136
|
+
product.items.forEach((item) => map.set(item.itemId, product));
|
|
153
137
|
|
|
154
|
-
|
|
155
|
-
|
|
138
|
+
return map;
|
|
139
|
+
}, new Map<string, T>());
|
|
156
140
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
141
|
+
return sku.kitItems
|
|
142
|
+
?.map(({ itemId }) => {
|
|
143
|
+
const product = productBySkuId.get(itemId);
|
|
160
144
|
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
/** Sometimes VTEX does not return what I've asked for */
|
|
146
|
+
if (!product) return;
|
|
163
147
|
|
|
164
|
-
|
|
148
|
+
const sku = pickSku(product, itemId);
|
|
165
149
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
167
|
+
product: T,
|
|
168
|
+
sku: T["items"][number],
|
|
169
|
+
kitItems: T[],
|
|
170
|
+
options: ProductOptions,
|
|
187
171
|
): Omit<ProductDetailsPage, "seo"> => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
187
|
+
if (inStock(a) && !inStock(b)) {
|
|
188
|
+
return -1;
|
|
189
|
+
}
|
|
210
190
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
if (!inStock(a) && inStock(b)) {
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
214
194
|
|
|
215
|
-
|
|
195
|
+
return a.price - b.price;
|
|
216
196
|
};
|
|
217
197
|
|
|
218
198
|
const getHighPriceIndex = (offers: Offer[]) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
229
|
-
>(
|
|
230
|
-
product: P,
|
|
206
|
+
const toAdditionalPropertyCategories = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
207
|
+
product: P,
|
|
231
208
|
): Product["additionalProperty"] => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
258
|
-
|
|
234
|
+
propertyID,
|
|
235
|
+
value,
|
|
259
236
|
}: {
|
|
260
|
-
|
|
261
|
-
|
|
237
|
+
propertyID: string;
|
|
238
|
+
value: string;
|
|
262
239
|
}): PropertyValue => ({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
240
|
+
"@type": "PropertyValue" as const,
|
|
241
|
+
name: "category",
|
|
242
|
+
propertyID,
|
|
243
|
+
value,
|
|
267
244
|
});
|
|
268
245
|
|
|
269
|
-
const toAdditionalPropertyClusters = <
|
|
270
|
-
|
|
271
|
-
>(
|
|
272
|
-
product: P,
|
|
246
|
+
const toAdditionalPropertyClusters = <P extends LegacyProductVTEX | ProductVTEX>(
|
|
247
|
+
product: P,
|
|
273
248
|
): Product["additionalProperty"] => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
297
|
-
|
|
274
|
+
{ propertyID, value }: { propertyID: string; value: string },
|
|
275
|
+
highlights?: Set<string>,
|
|
298
276
|
): PropertyValue => ({
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
285
|
+
referenceId: Array<{ Key: string; Value: string }>,
|
|
308
286
|
): Product["additionalProperty"] => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
287
|
+
return referenceId.map(({ Key, Value }) =>
|
|
288
|
+
toAdditionalPropertyReferenceId({ name: Key, value: Value }),
|
|
289
|
+
);
|
|
312
290
|
};
|
|
313
291
|
|
|
314
292
|
export const toAdditionalPropertyReferenceId = ({
|
|
315
|
-
|
|
316
|
-
|
|
293
|
+
name,
|
|
294
|
+
value,
|
|
317
295
|
}: {
|
|
318
|
-
|
|
319
|
-
|
|
296
|
+
name: string;
|
|
297
|
+
value: string;
|
|
320
298
|
}): PropertyValue => ({
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
299
|
+
"@type": "PropertyValue",
|
|
300
|
+
name,
|
|
301
|
+
value,
|
|
302
|
+
valueReference: "ReferenceID",
|
|
325
303
|
});
|
|
326
304
|
|
|
327
305
|
const getImageKey = (src = "") => {
|
|
328
|
-
|
|
306
|
+
return src;
|
|
329
307
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
313
|
+
// if (match == null) {
|
|
314
|
+
// return src;
|
|
315
|
+
// }
|
|
338
316
|
|
|
339
|
-
|
|
317
|
+
// return `${match.pathname.groups.imageId}${match.search.input}`;
|
|
340
318
|
};
|
|
341
319
|
|
|
342
320
|
export const aggregateOffers = (
|
|
343
|
-
|
|
344
|
-
|
|
321
|
+
offers: Offer[],
|
|
322
|
+
priceCurrency?: string,
|
|
345
323
|
): AggregateOffer | undefined => {
|
|
346
|
-
|
|
324
|
+
const sorted = offers.sort(bestOfferFirst);
|
|
347
325
|
|
|
348
|
-
|
|
326
|
+
if (sorted.length === 0) return;
|
|
349
327
|
|
|
350
|
-
|
|
351
|
-
|
|
328
|
+
const highPriceIndex = getHighPriceIndex(sorted);
|
|
329
|
+
const lowPriceIndex = 0;
|
|
352
330
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
535
|
-
|
|
662
|
+
product: ProductVTEX | LegacyProductVTEX,
|
|
663
|
+
{ baseUrl }: ProductOptions,
|
|
536
664
|
): BreadcrumbList => {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
596
|
-
) =>
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
614
|
-
|
|
736
|
+
originalAttributes: Maybe<string[]>,
|
|
737
|
+
product: ProductVTEX | LegacyProduct,
|
|
615
738
|
) => {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
757
|
+
sku.variations?.flatMap(({ name, values }) =>
|
|
758
|
+
values.map((value) => toAdditionalPropertySpecification({ name, value })),
|
|
759
|
+
) ?? [];
|
|
637
760
|
|
|
638
761
|
export const toAdditionalPropertySpecification = ({
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
762
|
+
name,
|
|
763
|
+
value,
|
|
764
|
+
propertyID,
|
|
642
765
|
}: {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
766
|
+
name: string;
|
|
767
|
+
value: string;
|
|
768
|
+
propertyID?: string;
|
|
646
769
|
}): PropertyValue => ({
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
770
|
+
"@type": "PropertyValue",
|
|
771
|
+
name,
|
|
772
|
+
value,
|
|
773
|
+
propertyID,
|
|
774
|
+
valueReference: "SPECIFICATION",
|
|
652
775
|
});
|
|
653
776
|
|
|
654
777
|
const toAdditionalPropertiesLegacy = (sku: LegacySkuVTEX): PropertyValue[] => {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
682
|
-
|
|
802
|
+
{ commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
|
|
803
|
+
teasers: Teasers[],
|
|
683
804
|
): Offer => ({
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
847
|
+
buildOffer(seller, seller.commertialOffer.teasers ?? []);
|
|
729
848
|
|
|
730
849
|
const toOfferLegacy = (seller: SellerVTEX): Offer => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
918
|
-
|
|
1006
|
+
selectedFacets: SelectedFacet[],
|
|
1007
|
+
paramsToPersist?: URLSearchParams,
|
|
919
1008
|
) => {
|
|
920
|
-
|
|
1009
|
+
const searchParams = new URLSearchParams(paramsToPersist);
|
|
921
1010
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1011
|
+
for (const { key, value } of selectedFacets) {
|
|
1012
|
+
searchParams.append(`filter.${key}`, value);
|
|
1013
|
+
}
|
|
925
1014
|
|
|
926
|
-
|
|
1015
|
+
return searchParams;
|
|
927
1016
|
};
|
|
928
1017
|
|
|
929
1018
|
const fromLegacyMap: Record<string, string> = {
|
|
930
|
-
|
|
931
|
-
|
|
1019
|
+
priceFrom: "price",
|
|
1020
|
+
productClusterSearchableIds: "productClusterIds",
|
|
932
1021
|
};
|
|
933
1022
|
|
|
934
1023
|
export const legacyFacetsNormalize = (map: string, path: string) => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1048
|
+
const selectedFacets: SelectedFacet[] = [];
|
|
1049
|
+
for (let it = 0; it < length; it++) {
|
|
1050
|
+
const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
|
|
962
1051
|
|
|
963
|
-
|
|
964
|
-
|
|
1052
|
+
selectedFacets.push(facet);
|
|
1053
|
+
}
|
|
965
1054
|
|
|
966
|
-
|
|
1055
|
+
return selectedFacets;
|
|
967
1056
|
};
|
|
968
1057
|
|
|
969
1058
|
export const filtersFromURL = (url: URL) => {
|
|
970
|
-
|
|
1059
|
+
const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
|
|
971
1060
|
|
|
972
|
-
|
|
973
|
-
|
|
1061
|
+
url.searchParams.forEach((value, name) => {
|
|
1062
|
+
const [filter, key] = name.split(".");
|
|
974
1063
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1064
|
+
if (filter === "filter" && typeof key === "string") {
|
|
1065
|
+
selectedFacets.push({ key, value });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
979
1068
|
|
|
980
|
-
|
|
1069
|
+
return selectedFacets;
|
|
981
1070
|
};
|
|
982
1071
|
|
|
983
|
-
export const mergeFacets = (
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1058
|
-
): SiteNavigationElement[] => tree.map(nodeToNavbar);
|
|
1134
|
+
export const categoryTreeToNavbar = (tree: Category[]): SiteNavigationElement[] =>
|
|
1135
|
+
tree.map(nodeToNavbar);
|
|
1059
1136
|
|
|
1060
1137
|
export const toBrand = (
|
|
1061
|
-
|
|
1062
|
-
|
|
1138
|
+
{ id, name, imageUrl, metaTagDescription }: BrandVTEX,
|
|
1139
|
+
baseUrl: string,
|
|
1063
1140
|
): Brand => ({
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1149
|
+
return {
|
|
1150
|
+
...facet,
|
|
1151
|
+
Map: "priceFrom",
|
|
1152
|
+
Value: facet.Slug!,
|
|
1153
|
+
};
|
|
1077
1154
|
};
|
|
1078
1155
|
|
|
1079
1156
|
export const toReview = (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1157
|
+
products: Product[],
|
|
1158
|
+
ratings: ProductRating[],
|
|
1159
|
+
reviews: ProductReviewData[],
|
|
1083
1160
|
): Product[] => {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1197
|
+
products: Product[],
|
|
1198
|
+
inventoriesData: ProductInventoryData[],
|
|
1120
1199
|
): Product[] => {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1223
|
+
products: Product[],
|
|
1224
|
+
orderOfIdsOrSkus: string[],
|
|
1225
|
+
prop: "sku" | "inProductGroupWithID",
|
|
1147
1226
|
) => {
|
|
1148
|
-
|
|
1227
|
+
const productMap: ProductMap = {};
|
|
1149
1228
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1229
|
+
products.forEach((product) => {
|
|
1230
|
+
productMap[product[prop] || product.sku] = product;
|
|
1231
|
+
});
|
|
1153
1232
|
|
|
1154
|
-
|
|
1233
|
+
return orderOfIdsOrSkus.map((id) => productMap[id]);
|
|
1155
1234
|
};
|
|
1156
1235
|
|
|
1157
1236
|
export const parsePageType = (p: PageTypeVTEX): PageType => {
|
|
1158
|
-
|
|
1237
|
+
const type = p.pageType;
|
|
1159
1238
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1239
|
+
// Search or Busca vazia
|
|
1240
|
+
if (type === "FullText") {
|
|
1241
|
+
return "Search";
|
|
1242
|
+
}
|
|
1164
1243
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1244
|
+
// A page that vtex doesn't recognize
|
|
1245
|
+
if (type === "NotFound") {
|
|
1246
|
+
return "Unknown";
|
|
1247
|
+
}
|
|
1169
1248
|
|
|
1170
|
-
|
|
1249
|
+
return type;
|
|
1171
1250
|
};
|
|
1172
1251
|
|
|
1173
1252
|
function dayOfWeekIndexToString(day?: number): DayOfWeek | undefined {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1274
|
+
dayOfWeek?: number;
|
|
1275
|
+
openingTime?: string;
|
|
1276
|
+
closingTime?: string;
|
|
1198
1277
|
}
|
|
1199
1278
|
|
|
1200
1279
|
function toHoursSpecification(hours: Hours): OpeningHoursSpecification {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1303
|
+
pickupPoint: PickupPoint | PickupPointVCS,
|
|
1228
1304
|
): pickupPoint is PickupPointVCS {
|
|
1229
|
-
|
|
1305
|
+
return "name" in pickupPoint;
|
|
1230
1306
|
}
|
|
1231
1307
|
|
|
1232
1308
|
interface ToPlaceOptions {
|
|
1233
|
-
|
|
1309
|
+
isActive?: boolean;
|
|
1234
1310
|
}
|
|
1235
1311
|
|
|
1236
1312
|
export function toPlace(
|
|
1237
|
-
|
|
1238
|
-
|
|
1313
|
+
pickupPoint: (PickupPoint & { distance?: number }) | PickupPointVCS,
|
|
1314
|
+
options?: ToPlaceOptions,
|
|
1239
1315
|
): Place {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
};
|