@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.
- 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 +23 -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/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 +1012 -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,873 @@
|
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
if (skuId) {
|
|
92
|
+
canonicalUrl.searchParams.set("skuId", skuId);
|
|
93
|
+
}
|
|
106
94
|
|
|
107
|
-
|
|
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,
|
|
368
346
|
): 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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
535
|
-
|
|
496
|
+
product: ProductVTEX | LegacyProductVTEX,
|
|
497
|
+
{ baseUrl }: ProductOptions,
|
|
536
498
|
): 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
|
-
};
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
596
|
-
) =>
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
614
|
-
|
|
570
|
+
originalAttributes: Maybe<string[]>,
|
|
571
|
+
product: ProductVTEX | LegacyProduct,
|
|
615
572
|
) => {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
591
|
+
sku.variations?.flatMap(({ name, values }) =>
|
|
592
|
+
values.map((value) => toAdditionalPropertySpecification({ name, value })),
|
|
593
|
+
) ?? [];
|
|
637
594
|
|
|
638
595
|
export const toAdditionalPropertySpecification = ({
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
596
|
+
name,
|
|
597
|
+
value,
|
|
598
|
+
propertyID,
|
|
642
599
|
}: {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
600
|
+
name: string;
|
|
601
|
+
value: string;
|
|
602
|
+
propertyID?: string;
|
|
646
603
|
}): PropertyValue => ({
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
604
|
+
"@type": "PropertyValue",
|
|
605
|
+
name,
|
|
606
|
+
value,
|
|
607
|
+
propertyID,
|
|
608
|
+
valueReference: "SPECIFICATION",
|
|
652
609
|
});
|
|
653
610
|
|
|
654
611
|
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;
|
|
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
|
-
|
|
682
|
-
|
|
636
|
+
{ commertialOffer: offer, sellerId, sellerName, sellerDefault }: SellerVTEX,
|
|
637
|
+
teasers: Teasers[],
|
|
683
638
|
): 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,
|
|
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
|
-
|
|
681
|
+
buildOffer(seller, seller.commertialOffer.teasers ?? []);
|
|
729
682
|
|
|
730
683
|
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]);
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
-
|
|
918
|
-
|
|
840
|
+
selectedFacets: SelectedFacet[],
|
|
841
|
+
paramsToPersist?: URLSearchParams,
|
|
919
842
|
) => {
|
|
920
|
-
|
|
843
|
+
const searchParams = new URLSearchParams(paramsToPersist);
|
|
921
844
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
845
|
+
for (const { key, value } of selectedFacets) {
|
|
846
|
+
searchParams.append(`filter.${key}`, value);
|
|
847
|
+
}
|
|
925
848
|
|
|
926
|
-
|
|
849
|
+
return searchParams;
|
|
927
850
|
};
|
|
928
851
|
|
|
929
852
|
const fromLegacyMap: Record<string, string> = {
|
|
930
|
-
|
|
931
|
-
|
|
853
|
+
priceFrom: "price",
|
|
854
|
+
productClusterSearchableIds: "productClusterIds",
|
|
932
855
|
};
|
|
933
856
|
|
|
934
857
|
export const legacyFacetsNormalize = (map: string, path: string) => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
882
|
+
const selectedFacets: SelectedFacet[] = [];
|
|
883
|
+
for (let it = 0; it < length; it++) {
|
|
884
|
+
const facet = legacyFacetsNormalize(mapSegments[it], pathSegments[it]);
|
|
962
885
|
|
|
963
|
-
|
|
964
|
-
|
|
886
|
+
selectedFacets.push(facet);
|
|
887
|
+
}
|
|
965
888
|
|
|
966
|
-
|
|
889
|
+
return selectedFacets;
|
|
967
890
|
};
|
|
968
891
|
|
|
969
892
|
export const filtersFromURL = (url: URL) => {
|
|
970
|
-
|
|
893
|
+
const selectedFacets: SelectedFacet[] = legacyFacetsFromURL(url);
|
|
971
894
|
|
|
972
|
-
|
|
973
|
-
|
|
895
|
+
url.searchParams.forEach((value, name) => {
|
|
896
|
+
const [filter, key] = name.split(".");
|
|
974
897
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
898
|
+
if (filter === "filter" && typeof key === "string") {
|
|
899
|
+
selectedFacets.push({ key, value });
|
|
900
|
+
}
|
|
901
|
+
});
|
|
979
902
|
|
|
980
|
-
|
|
903
|
+
return selectedFacets;
|
|
981
904
|
};
|
|
982
905
|
|
|
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
|
-
};
|
|
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
|
|
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
|
-
};
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1058
|
-
): SiteNavigationElement[] => tree.map(nodeToNavbar);
|
|
968
|
+
export const categoryTreeToNavbar = (tree: Category[]): SiteNavigationElement[] =>
|
|
969
|
+
tree.map(nodeToNavbar);
|
|
1059
970
|
|
|
1060
971
|
export const toBrand = (
|
|
1061
|
-
|
|
1062
|
-
|
|
972
|
+
{ id, name, imageUrl, metaTagDescription }: BrandVTEX,
|
|
973
|
+
baseUrl: string,
|
|
1063
974
|
): Brand => ({
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
983
|
+
return {
|
|
984
|
+
...facet,
|
|
985
|
+
Map: "priceFrom",
|
|
986
|
+
Value: facet.Slug!,
|
|
987
|
+
};
|
|
1077
988
|
};
|
|
1078
989
|
|
|
1079
990
|
export const toReview = (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
991
|
+
products: Product[],
|
|
992
|
+
ratings: ProductRating[],
|
|
993
|
+
reviews: ProductReviewData[],
|
|
1083
994
|
): 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
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1031
|
+
products: Product[],
|
|
1032
|
+
inventoriesData: ProductInventoryData[],
|
|
1120
1033
|
): Product[] => {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1057
|
+
products: Product[],
|
|
1058
|
+
orderOfIdsOrSkus: string[],
|
|
1059
|
+
prop: "sku" | "inProductGroupWithID",
|
|
1147
1060
|
) => {
|
|
1148
|
-
|
|
1061
|
+
const productMap: ProductMap = {};
|
|
1149
1062
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1063
|
+
products.forEach((product) => {
|
|
1064
|
+
productMap[product[prop] || product.sku] = product;
|
|
1065
|
+
});
|
|
1153
1066
|
|
|
1154
|
-
|
|
1067
|
+
return orderOfIdsOrSkus.map((id) => productMap[id]);
|
|
1155
1068
|
};
|
|
1156
1069
|
|
|
1157
1070
|
export const parsePageType = (p: PageTypeVTEX): PageType => {
|
|
1158
|
-
|
|
1071
|
+
const type = p.pageType;
|
|
1159
1072
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1073
|
+
// Search or Busca vazia
|
|
1074
|
+
if (type === "FullText") {
|
|
1075
|
+
return "Search";
|
|
1076
|
+
}
|
|
1164
1077
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1078
|
+
// A page that vtex doesn't recognize
|
|
1079
|
+
if (type === "NotFound") {
|
|
1080
|
+
return "Unknown";
|
|
1081
|
+
}
|
|
1169
1082
|
|
|
1170
|
-
|
|
1083
|
+
return type;
|
|
1171
1084
|
};
|
|
1172
1085
|
|
|
1173
1086
|
function dayOfWeekIndexToString(day?: number): DayOfWeek | undefined {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1108
|
+
dayOfWeek?: number;
|
|
1109
|
+
openingTime?: string;
|
|
1110
|
+
closingTime?: string;
|
|
1198
1111
|
}
|
|
1199
1112
|
|
|
1200
1113
|
function toHoursSpecification(hours: Hours): OpeningHoursSpecification {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1137
|
+
pickupPoint: PickupPoint | PickupPointVCS,
|
|
1228
1138
|
): pickupPoint is PickupPointVCS {
|
|
1229
|
-
|
|
1139
|
+
return "name" in pickupPoint;
|
|
1230
1140
|
}
|
|
1231
1141
|
|
|
1232
1142
|
interface ToPlaceOptions {
|
|
1233
|
-
|
|
1143
|
+
isActive?: boolean;
|
|
1234
1144
|
}
|
|
1235
1145
|
|
|
1236
1146
|
export function toPlace(
|
|
1237
|
-
|
|
1238
|
-
|
|
1147
|
+
pickupPoint: (PickupPoint & { distance?: number }) | PickupPointVCS,
|
|
1148
|
+
options?: ToPlaceOptions,
|
|
1239
1149
|
): 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
|
-
};
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
};
|