@decocms/apps-salesforce 7.2.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/package.json +43 -0
- package/src/__tests__/httpClient.test.ts +201 -0
- package/src/__tests__/parseUserCookie.test.ts +89 -0
- package/src/__tests__/transform.test.ts +227 -0
- package/src/index.ts +33 -0
- package/src/loaders/products/list.ts +139 -0
- package/src/loaders/products/listCart.ts +151 -0
- package/src/loaders/products/listRecomended.ts +121 -0
- package/src/types.ts +108 -0
- package/src/utils/httpClient.ts +154 -0
- package/src/utils/parseUserCookie.ts +41 -0
- package/src/utils/transform.ts +156 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode the JSON-encoded user identifier cookie set by Salesforce
|
|
3
|
+
* Marketing Cloud Personalization (Evergage) on the browser.
|
|
4
|
+
*
|
|
5
|
+
* Evergage stores `{ puid?: string, uuid?: string }` URL-encoded:
|
|
6
|
+
* - `puid` (persistent user id) is present after the user signs in
|
|
7
|
+
* - `uuid` (anonymous device id) is dropped on first visit
|
|
8
|
+
*
|
|
9
|
+
* The personalization API expects EITHER `encryptedId` (mapped from
|
|
10
|
+
* `puid`) OR `anonymousId` (mapped from `uuid`). When the cookie is
|
|
11
|
+
* missing or malformed, we fall back to the literal "anonymous" so
|
|
12
|
+
* the API still returns a default campaign instead of erroring.
|
|
13
|
+
*/
|
|
14
|
+
import type { ParsedUserCookie } from "../types";
|
|
15
|
+
|
|
16
|
+
const ANONYMOUS_FALLBACK: ParsedUserCookie = { anonymousId: "anonymous" };
|
|
17
|
+
|
|
18
|
+
export function parseUserCookie(rawCookie: string | undefined | null): ParsedUserCookie {
|
|
19
|
+
if (!rawCookie?.trim()) return ANONYMOUS_FALLBACK;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const decoded = decodeURIComponent(rawCookie);
|
|
23
|
+
const parsed: unknown = JSON.parse(decoded);
|
|
24
|
+
if (typeof parsed !== "object" || parsed === null) return ANONYMOUS_FALLBACK;
|
|
25
|
+
|
|
26
|
+
const { puid, uuid } = parsed as { puid?: unknown; uuid?: unknown };
|
|
27
|
+
|
|
28
|
+
// puid wins over uuid when both are present — once a user signs in,
|
|
29
|
+
// every request should be attributed to their persistent identity
|
|
30
|
+
// so cross-device personalization works.
|
|
31
|
+
if (typeof puid === "string" && puid.length > 0) {
|
|
32
|
+
return { encryptedId: puid };
|
|
33
|
+
}
|
|
34
|
+
if (typeof uuid === "string" && uuid.length > 0) {
|
|
35
|
+
return { anonymousId: uuid };
|
|
36
|
+
}
|
|
37
|
+
return ANONYMOUS_FALLBACK;
|
|
38
|
+
} catch {
|
|
39
|
+
return ANONYMOUS_FALLBACK;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a Salesforce Personalization product into a schema.org `Product`
|
|
3
|
+
* suitable for ProductShelf / ProductCard / PDP components.
|
|
4
|
+
*
|
|
5
|
+
* Most fields (id, name, price, images, currency) are common to every
|
|
6
|
+
* Evergage dataset. The `additionalProperty` list, however, is fully
|
|
7
|
+
* dataset-specific — each customer chooses which catalog columns are
|
|
8
|
+
* exposed by Evergage (`Marca`, `Volume`, `Linha`, `freeShipping` etc.).
|
|
9
|
+
* Sites pass a `propertyMapper` to project their custom columns into
|
|
10
|
+
* `schema.org/PropertyValue[]`; the default mapper produces the
|
|
11
|
+
* standard fields that exist on every Evergage product (`itemType`,
|
|
12
|
+
* `categories`).
|
|
13
|
+
*/
|
|
14
|
+
import type { Offer, Product, PropertyValue } from "@decocms/apps-commerce/types";
|
|
15
|
+
import type { SalesforceProduct } from "../types";
|
|
16
|
+
|
|
17
|
+
const IN_STOCK = "https://schema.org/InStock";
|
|
18
|
+
const OUT_OF_STOCK = "https://schema.org/OutOfStock";
|
|
19
|
+
|
|
20
|
+
export type PropertyMapper = (product: SalesforceProduct) => PropertyValue[];
|
|
21
|
+
|
|
22
|
+
export interface ProductTransformerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Project dataset-specific Evergage columns into schema.org
|
|
25
|
+
* `PropertyValue[]`. Receives the raw Evergage product (including
|
|
26
|
+
* any custom fields via the index signature). Returns an empty
|
|
27
|
+
* array by default — sites that want brand / volume / line tags on
|
|
28
|
+
* their cards pass a mapper.
|
|
29
|
+
*/
|
|
30
|
+
propertyMapper?: PropertyMapper;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_PROPERTY_MAPPER: PropertyMapper = (product) => {
|
|
34
|
+
const out: PropertyValue[] = [];
|
|
35
|
+
if (product.itemType) {
|
|
36
|
+
out.push({ "@type": "PropertyValue", name: "itemType", value: product.itemType });
|
|
37
|
+
}
|
|
38
|
+
if (product.categories?.length) {
|
|
39
|
+
out.push({
|
|
40
|
+
"@type": "PropertyValue",
|
|
41
|
+
name: "category",
|
|
42
|
+
value: product.categories.join(", "),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function toOffer({
|
|
49
|
+
product,
|
|
50
|
+
currencyCode,
|
|
51
|
+
}: {
|
|
52
|
+
product: SalesforceProduct;
|
|
53
|
+
currencyCode?: string;
|
|
54
|
+
}): Offer[] {
|
|
55
|
+
const productPrice = product.price;
|
|
56
|
+
const productSalePrice = product.salePrice || productPrice;
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
"@type": "Offer",
|
|
60
|
+
availability: product.inventoryCount > 0 ? IN_STOCK : OUT_OF_STOCK,
|
|
61
|
+
inventoryLevel: { value: product.inventoryCount },
|
|
62
|
+
itemCondition: "https://schema.org/NewCondition",
|
|
63
|
+
price: productSalePrice,
|
|
64
|
+
priceCurrency: currencyCode,
|
|
65
|
+
priceSpecification: [
|
|
66
|
+
{
|
|
67
|
+
"@type": "UnitPriceSpecification",
|
|
68
|
+
priceType: "https://schema.org/ListPrice",
|
|
69
|
+
price: productPrice,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"@type": "UnitPriceSpecification",
|
|
73
|
+
priceType: "https://schema.org/SalePrice",
|
|
74
|
+
price: productSalePrice,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
sku: product.id,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function toImages(product: SalesforceProduct) {
|
|
83
|
+
return product.imageUrls.map((url) => ({
|
|
84
|
+
"@type": "ImageObject" as const,
|
|
85
|
+
encodingFormat: "image",
|
|
86
|
+
alternateName: url,
|
|
87
|
+
url,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns a `toProduct` function bound to the dataset's property
|
|
93
|
+
* mapper. Sites typically construct one transformer at module level
|
|
94
|
+
* and reuse it across loaders.
|
|
95
|
+
*/
|
|
96
|
+
export function createProductTransformer(
|
|
97
|
+
options: ProductTransformerOptions = {},
|
|
98
|
+
): (input: { product: SalesforceProduct; options: { currencyCode?: string } }) => Product {
|
|
99
|
+
const mapProperties = options.propertyMapper ?? DEFAULT_PROPERTY_MAPPER;
|
|
100
|
+
|
|
101
|
+
return ({ product, options: opts }) => {
|
|
102
|
+
const offers = toOffer({ product, currencyCode: opts.currencyCode ?? product.currency });
|
|
103
|
+
const sku = product.id;
|
|
104
|
+
// `idMagento` (cross-system identifier) wins when present —
|
|
105
|
+
// downstream code keys product detail pages off `productID`.
|
|
106
|
+
const productID = (product.idMagento as string | undefined) ?? sku;
|
|
107
|
+
const productPrice = product.price;
|
|
108
|
+
const productSalePrice = product.salePrice || productPrice;
|
|
109
|
+
const productUrl = product.url;
|
|
110
|
+
const additionalProperty = mapProperties(product);
|
|
111
|
+
|
|
112
|
+
const variantTemplate: Product = {
|
|
113
|
+
"@type": "Product",
|
|
114
|
+
productID,
|
|
115
|
+
sku,
|
|
116
|
+
url: productUrl,
|
|
117
|
+
name: product.name.trim(),
|
|
118
|
+
gtin: sku,
|
|
119
|
+
offers: {
|
|
120
|
+
"@type": "AggregateOffer",
|
|
121
|
+
highPrice: productPrice,
|
|
122
|
+
lowPrice: productSalePrice,
|
|
123
|
+
offerCount: offers.length,
|
|
124
|
+
offers,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"@type": "Product",
|
|
130
|
+
productID,
|
|
131
|
+
sku,
|
|
132
|
+
url: productUrl,
|
|
133
|
+
name: product.name.trim(),
|
|
134
|
+
gtin: sku,
|
|
135
|
+
aggregateRating: { "@type": "AggregateRating", reviewCount: undefined },
|
|
136
|
+
isVariantOf: {
|
|
137
|
+
"@type": "ProductGroup",
|
|
138
|
+
productGroupID: productID,
|
|
139
|
+
url: productUrl,
|
|
140
|
+
name: product.name.trim(),
|
|
141
|
+
model: "",
|
|
142
|
+
additionalProperty,
|
|
143
|
+
hasVariant: [variantTemplate],
|
|
144
|
+
},
|
|
145
|
+
additionalProperty,
|
|
146
|
+
image: toImages(product),
|
|
147
|
+
offers: {
|
|
148
|
+
"@type": "AggregateOffer",
|
|
149
|
+
highPrice: productPrice,
|
|
150
|
+
lowPrice: productSalePrice,
|
|
151
|
+
offerCount: offers.length,
|
|
152
|
+
offers,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
}
|