@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 ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@decocms/apps-salesforce",
3
+ "version": "7.2.0",
4
+ "type": "module",
5
+ "description": "Deco commerce app: Salesforce Commerce Cloud integration",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/decocms/blocks.git",
9
+ "directory": "packages/apps-salesforce"
10
+ },
11
+ "main": "./src/index.ts",
12
+ "exports": {
13
+ ".": "./src/index.ts",
14
+ "./types": "./src/types.ts",
15
+ "./utils/*": "./src/utils/*.ts",
16
+ "./loaders/products/*": "./src/loaders/products/*.ts"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "vitest run --root ../.. packages/apps-salesforce/",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint:unused": "knip"
23
+ },
24
+ "dependencies": {
25
+ "@decocms/blocks": "7.2.0",
26
+ "@decocms/apps-commerce": "7.2.0"
27
+ },
28
+ "peerDependencies": {
29
+ "@tanstack/react-start": ">=1.0.0",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.0.0",
35
+ "@types/react-dom": "^19.0.0",
36
+ "knip": "^5.86.0",
37
+ "typescript": "^5.9.0"
38
+ },
39
+ "publishConfig": {
40
+ "registry": "https://registry.npmjs.org",
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Tests for utils/httpClient.ts.
3
+ *
4
+ * The legacy `apps/utils/http.ts` indexed-route Proxy is what every
5
+ * Deno-era loader called into, so this test file pins the call shapes
6
+ * existing site code still uses:
7
+ * - `client["POST /api2/event/:dataset"]({ dataset }, { body })`
8
+ * - `client.get(path)` / `client.post(path, body)` convenience methods
9
+ * - `:name` placeholder substitution and `*name` legacy Deno-era syntax
10
+ * - default `x-requested-with` header propagation
11
+ * - `{ json, ok, status, headers }` response shape
12
+ *
13
+ * `fetch` is mocked so we never touch the network.
14
+ */
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+ import { createHttpClient } from "../utils/httpClient";
17
+
18
+ interface CallRecord {
19
+ url: string;
20
+ init: {
21
+ method?: string;
22
+ body?: string;
23
+ headers: Record<string, string>;
24
+ };
25
+ }
26
+
27
+ type MockedFetch = typeof fetch & {
28
+ mock: { calls: [string, CallRecord["init"]][] };
29
+ };
30
+
31
+ function makeFetch(
32
+ jsonBody: unknown = { ok: true },
33
+ init: { status?: number; headers?: Record<string, string> } = {},
34
+ ): MockedFetch {
35
+ return vi.fn(async () => ({
36
+ json: async () => jsonBody,
37
+ ok: (init.status ?? 200) < 400,
38
+ status: init.status ?? 200,
39
+ headers: new Headers(init.headers ?? {}),
40
+ })) as unknown as MockedFetch;
41
+ }
42
+
43
+ function callAt(fetcher: MockedFetch, idx: number): CallRecord {
44
+ const call = fetcher.mock.calls[idx];
45
+ return { url: call[0], init: call[1] };
46
+ }
47
+
48
+ describe("createHttpClient", () => {
49
+ let originalFetch: typeof fetch;
50
+
51
+ beforeEach(() => {
52
+ originalFetch = globalThis.fetch;
53
+ });
54
+
55
+ afterEach(() => {
56
+ globalThis.fetch = originalFetch;
57
+ });
58
+
59
+ it("strips trailing slash from base URL", async () => {
60
+ const fetcher = makeFetch();
61
+ const client = createHttpClient({ base: "https://api.example.com/", fetcher });
62
+ await client.get("/health");
63
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/health");
64
+ });
65
+
66
+ it("uses global fetch when no fetcher is provided", async () => {
67
+ globalThis.fetch = makeFetch({ ok: true });
68
+ const client = createHttpClient({ base: "https://api.example.com" });
69
+ const result = await client.get("/health");
70
+ expect(result).toEqual({ ok: true });
71
+ expect(globalThis.fetch).toHaveBeenCalledOnce();
72
+ });
73
+
74
+ describe(".get / .post convenience methods", () => {
75
+ it(".get returns parsed JSON directly", async () => {
76
+ const fetcher = makeFetch({ items: [1, 2, 3] });
77
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
78
+ const result = await client.get("/things");
79
+ expect(result).toEqual({ items: [1, 2, 3] });
80
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/things");
81
+ });
82
+
83
+ it(".post sends JSON body with Content-Type header", async () => {
84
+ const fetcher = makeFetch({ created: true });
85
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
86
+ await client.post("/things", { name: "thing" });
87
+ const { init } = callAt(fetcher, 0);
88
+ expect(init.method).toBe("POST");
89
+ expect(init.body).toBe(JSON.stringify({ name: "thing" }));
90
+ expect(init.headers["Content-Type"]).toBe("application/json");
91
+ });
92
+
93
+ it("merges default headers from options into request", async () => {
94
+ const fetcher = makeFetch();
95
+ const client = createHttpClient({
96
+ base: "https://api.example.com",
97
+ fetcher,
98
+ headers: { "x-requested-with": "XMLHttpRequest" },
99
+ });
100
+ await client.post("/things", { name: "x" });
101
+ const { init } = callAt(fetcher, 0);
102
+ expect(init.headers["x-requested-with"]).toBe("XMLHttpRequest");
103
+ });
104
+
105
+ it("accepts a Headers object for default headers", async () => {
106
+ const fetcher = makeFetch();
107
+ const client = createHttpClient({
108
+ base: "https://api.example.com",
109
+ fetcher,
110
+ headers: new Headers({ "x-token": "abc" }),
111
+ });
112
+ await client.get("/things");
113
+ const { init } = callAt(fetcher, 0);
114
+ expect(init.headers["x-token"]).toBe("abc");
115
+ });
116
+ });
117
+
118
+ describe("indexed-route Proxy syntax", () => {
119
+ it("replaces `:name` path placeholders with the matching param", async () => {
120
+ const fetcher = makeFetch({ campaignResponses: [] });
121
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
122
+ await client["POST /api2/event/:dataset"]({ dataset: "production" }, { body: { foo: 1 } });
123
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/api2/event/production");
124
+ });
125
+
126
+ it("url-encodes placeholder values", async () => {
127
+ const fetcher = makeFetch();
128
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
129
+ await client["GET /catalog/:slug"]({ slug: "shoes & boots" });
130
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/catalog/shoes%20%26%20boots");
131
+ });
132
+
133
+ it("removes leftover `*name` placeholders when no value is provided", async () => {
134
+ const fetcher = makeFetch();
135
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
136
+ await client["GET /catalog/*path"]({});
137
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/catalog");
138
+ });
139
+
140
+ it("substitutes `*name` placeholders when value is present", async () => {
141
+ const fetcher = makeFetch();
142
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
143
+ await client["GET /catalog/*path"]({ path: "shoes/boots" });
144
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/catalog/shoes/boots");
145
+ });
146
+
147
+ it("serialises remaining params as the body on non-GET requests", async () => {
148
+ const fetcher = makeFetch();
149
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
150
+ await client["POST /api2/event/:dataset"]({
151
+ dataset: "production",
152
+ extraField: "value",
153
+ });
154
+ const { init } = callAt(fetcher, 0);
155
+ expect(JSON.parse(init.body ?? "")).toEqual({ extraField: "value" });
156
+ });
157
+
158
+ it("prefers explicit { body } over remaining params", async () => {
159
+ const fetcher = makeFetch();
160
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
161
+ await client["POST /api2/event/:dataset"](
162
+ { dataset: "production", ignored: "yes" },
163
+ { body: { real: "body" } },
164
+ );
165
+ const { init } = callAt(fetcher, 0);
166
+ expect(JSON.parse(init.body ?? "")).toEqual({ real: "body" });
167
+ });
168
+
169
+ it("encodes remaining params as querystring on GET", async () => {
170
+ const fetcher = makeFetch();
171
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
172
+ await client["GET /search"]({ q: "shoes", limit: 12 });
173
+ const call = callAt(fetcher, 0);
174
+ expect(call.url).toBe("https://api.example.com/search?q=shoes&limit=12");
175
+ expect(call.init.body).toBeUndefined();
176
+ });
177
+
178
+ it("returns { json, ok, status, headers } from indexed routes", async () => {
179
+ const fetcher = makeFetch({ result: 1 }, { status: 201, headers: { etag: "abc" } });
180
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
181
+ const response = await client["POST /things"]({ name: "x" });
182
+ expect(response.status).toBe(201);
183
+ expect(response.ok).toBe(true);
184
+ expect(response.headers.get("etag")).toBe("abc");
185
+ expect(await response.json()).toEqual({ result: 1 });
186
+ });
187
+
188
+ it("appends query when no params remain but route already has `?`", async () => {
189
+ const fetcher = makeFetch();
190
+ const client = createHttpClient({ base: "https://api.example.com", fetcher });
191
+ await client["GET /search?fixed=1"]({ q: "shoes" });
192
+ expect(callAt(fetcher, 0).url).toBe("https://api.example.com/search?fixed=1&q=shoes");
193
+ });
194
+
195
+ it("returns undefined for unknown property accesses", () => {
196
+ const client = createHttpClient({ base: "https://api.example.com" });
197
+ // biome-ignore lint/suspicious/noExplicitAny: type-erased access for test
198
+ expect((client as any).somethingMadeUp).toBeUndefined();
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tests for utils/parseUserCookie.ts.
3
+ *
4
+ * Locks the contract that downstream loaders depend on:
5
+ * - puid wins over uuid (signed-in identity > anonymous device id)
6
+ * - uuid is used when puid is missing
7
+ * - empty / missing / malformed cookies fall back to "anonymous"
8
+ *
9
+ * The fallback is load-bearing — Evergage rejects requests with a
10
+ * missing user identifier, so an unparseable cookie must not surface
11
+ * as `{}`.
12
+ */
13
+ import { describe, expect, it } from "vitest";
14
+ import { parseUserCookie } from "../utils/parseUserCookie";
15
+
16
+ const encode = (obj: unknown) => encodeURIComponent(JSON.stringify(obj));
17
+
18
+ describe("parseUserCookie", () => {
19
+ it("returns anonymous fallback when cookie is undefined", () => {
20
+ expect(parseUserCookie(undefined)).toEqual({ anonymousId: "anonymous" });
21
+ });
22
+
23
+ it("returns anonymous fallback when cookie is null", () => {
24
+ expect(parseUserCookie(null)).toEqual({ anonymousId: "anonymous" });
25
+ });
26
+
27
+ it("returns anonymous fallback when cookie is empty string", () => {
28
+ expect(parseUserCookie("")).toEqual({ anonymousId: "anonymous" });
29
+ });
30
+
31
+ it("returns anonymous fallback when cookie is whitespace only", () => {
32
+ expect(parseUserCookie(" ")).toEqual({ anonymousId: "anonymous" });
33
+ });
34
+
35
+ it("returns anonymous fallback when cookie is not valid JSON", () => {
36
+ expect(parseUserCookie("not-json")).toEqual({ anonymousId: "anonymous" });
37
+ });
38
+
39
+ it("returns anonymous fallback when JSON value is not an object", () => {
40
+ expect(parseUserCookie(encode("string-value"))).toEqual({ anonymousId: "anonymous" });
41
+ expect(parseUserCookie(encode(42))).toEqual({ anonymousId: "anonymous" });
42
+ expect(parseUserCookie(encode(null))).toEqual({ anonymousId: "anonymous" });
43
+ });
44
+
45
+ it("maps puid to encryptedId", () => {
46
+ expect(parseUserCookie(encode({ puid: "user-123" }))).toEqual({
47
+ encryptedId: "user-123",
48
+ });
49
+ });
50
+
51
+ it("maps uuid to anonymousId", () => {
52
+ expect(parseUserCookie(encode({ uuid: "device-abc" }))).toEqual({
53
+ anonymousId: "device-abc",
54
+ });
55
+ });
56
+
57
+ it("prefers puid over uuid when both are present", () => {
58
+ expect(parseUserCookie(encode({ puid: "user-123", uuid: "device-abc" }))).toEqual({
59
+ encryptedId: "user-123",
60
+ });
61
+ });
62
+
63
+ it("returns anonymous fallback when puid is empty string", () => {
64
+ expect(parseUserCookie(encode({ puid: "", uuid: "device-abc" }))).toEqual({
65
+ anonymousId: "device-abc",
66
+ });
67
+ });
68
+
69
+ it("returns anonymous fallback when both puid and uuid are empty/missing", () => {
70
+ expect(parseUserCookie(encode({}))).toEqual({ anonymousId: "anonymous" });
71
+ expect(parseUserCookie(encode({ puid: "", uuid: "" }))).toEqual({
72
+ anonymousId: "anonymous",
73
+ });
74
+ });
75
+
76
+ it("returns anonymous fallback when puid/uuid are non-string types", () => {
77
+ expect(parseUserCookie(encode({ puid: 42, uuid: true }))).toEqual({
78
+ anonymousId: "anonymous",
79
+ });
80
+ });
81
+
82
+ it("decodes URL-encoded values", () => {
83
+ // Evergage encodes the JSON with encodeURIComponent — make sure
84
+ // embedded special chars (e.g. = / : in base64 ids) round-trip.
85
+ const puid = "abc=def+ghi/jkl";
86
+ const cookie = encodeURIComponent(JSON.stringify({ puid }));
87
+ expect(parseUserCookie(cookie)).toEqual({ encryptedId: puid });
88
+ });
89
+ });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Tests for utils/transform.ts.
3
+ *
4
+ * The Salesforce transformer is the only spot in the upstream package
5
+ * where consumer-specific behavior (the dataset's custom column shape)
6
+ * shows through, via the `propertyMapper` hook. The tests here lock:
7
+ *
8
+ * - default mapper outputs the always-present Evergage columns
9
+ * (itemType, categories) without dragging in dataset-specific
10
+ * fields,
11
+ * - a custom propertyMapper sees the raw Evergage product (including
12
+ * custom columns via the index signature),
13
+ * - schema.org shape is stable (productID precedence, AggregateOffer
14
+ * surface, isVariantOf hasVariant).
15
+ *
16
+ * These match the regressions the legacy `apps/salesforce/utils/
17
+ * transform.ts` watched for over its lifetime.
18
+ */
19
+ import { describe, expect, it } from "vitest";
20
+ import type { SalesforceProduct } from "../types";
21
+ import {
22
+ createProductTransformer,
23
+ type PropertyMapper,
24
+ toImages,
25
+ toOffer,
26
+ } from "../utils/transform";
27
+
28
+ const baseProduct = (overrides: Partial<SalesforceProduct> = {}): SalesforceProduct => ({
29
+ id: "SKU-42",
30
+ name: "Test Product",
31
+ price: 100,
32
+ salePrice: 80,
33
+ inventoryCount: 7,
34
+ imageUrls: ["https://cdn.example.com/p/42.jpg"],
35
+ url: "https://loja.example.com/p/test",
36
+ currency: "BRL",
37
+ itemType: "simple",
38
+ categories: ["category-a", "category-b"],
39
+ ...overrides,
40
+ });
41
+
42
+ describe("toOffer", () => {
43
+ it("emits InStock when inventoryCount > 0", () => {
44
+ const [offer] = toOffer({ product: baseProduct(), currencyCode: "BRL" });
45
+ expect(offer.availability).toBe("https://schema.org/InStock");
46
+ expect(offer.inventoryLevel?.value).toBe(7);
47
+ });
48
+
49
+ it("emits OutOfStock when inventoryCount is 0", () => {
50
+ const [offer] = toOffer({
51
+ product: baseProduct({ inventoryCount: 0 }),
52
+ currencyCode: "BRL",
53
+ });
54
+ expect(offer.availability).toBe("https://schema.org/OutOfStock");
55
+ });
56
+
57
+ it("falls back to price when salePrice is missing", () => {
58
+ const [offer] = toOffer({
59
+ product: baseProduct({ salePrice: undefined }),
60
+ currencyCode: "BRL",
61
+ });
62
+ expect(offer.price).toBe(100);
63
+ });
64
+
65
+ it("uses salePrice as the primary price when present", () => {
66
+ const [offer] = toOffer({ product: baseProduct(), currencyCode: "BRL" });
67
+ expect(offer.price).toBe(80);
68
+ });
69
+
70
+ it("threads currencyCode into priceCurrency", () => {
71
+ const [offer] = toOffer({ product: baseProduct(), currencyCode: "USD" });
72
+ expect(offer.priceCurrency).toBe("USD");
73
+ });
74
+
75
+ it("emits ListPrice + SalePrice priceSpecification entries", () => {
76
+ const [offer] = toOffer({ product: baseProduct(), currencyCode: "BRL" });
77
+ expect(offer.priceSpecification).toEqual([
78
+ {
79
+ "@type": "UnitPriceSpecification",
80
+ priceType: "https://schema.org/ListPrice",
81
+ price: 100,
82
+ },
83
+ {
84
+ "@type": "UnitPriceSpecification",
85
+ priceType: "https://schema.org/SalePrice",
86
+ price: 80,
87
+ },
88
+ ]);
89
+ });
90
+ });
91
+
92
+ describe("toImages", () => {
93
+ it("maps each imageUrl to a schema.org ImageObject", () => {
94
+ const images = toImages(
95
+ baseProduct({
96
+ imageUrls: ["https://cdn.example.com/a.jpg", "https://cdn.example.com/b.jpg"],
97
+ }),
98
+ );
99
+ expect(images).toEqual([
100
+ {
101
+ "@type": "ImageObject",
102
+ encodingFormat: "image",
103
+ alternateName: "https://cdn.example.com/a.jpg",
104
+ url: "https://cdn.example.com/a.jpg",
105
+ },
106
+ {
107
+ "@type": "ImageObject",
108
+ encodingFormat: "image",
109
+ alternateName: "https://cdn.example.com/b.jpg",
110
+ url: "https://cdn.example.com/b.jpg",
111
+ },
112
+ ]);
113
+ });
114
+
115
+ it("returns an empty array when imageUrls is empty", () => {
116
+ expect(toImages(baseProduct({ imageUrls: [] }))).toEqual([]);
117
+ });
118
+ });
119
+
120
+ describe("createProductTransformer", () => {
121
+ it("uses default mapper when no propertyMapper is passed", () => {
122
+ const transform = createProductTransformer();
123
+ const out = transform({ product: baseProduct(), options: { currencyCode: "BRL" } });
124
+ expect(out.additionalProperty).toEqual([
125
+ { "@type": "PropertyValue", name: "itemType", value: "simple" },
126
+ { "@type": "PropertyValue", name: "category", value: "category-a, category-b" },
127
+ ]);
128
+ });
129
+
130
+ it("default mapper skips itemType when absent", () => {
131
+ const transform = createProductTransformer();
132
+ const out = transform({
133
+ product: baseProduct({ itemType: undefined }),
134
+ options: { currencyCode: "BRL" },
135
+ });
136
+ expect(out.additionalProperty).toEqual([
137
+ { "@type": "PropertyValue", name: "category", value: "category-a, category-b" },
138
+ ]);
139
+ });
140
+
141
+ it("default mapper skips categories when empty", () => {
142
+ const transform = createProductTransformer();
143
+ const out = transform({
144
+ product: baseProduct({ categories: [] }),
145
+ options: { currencyCode: "BRL" },
146
+ });
147
+ expect(out.additionalProperty).toEqual([
148
+ { "@type": "PropertyValue", name: "itemType", value: "simple" },
149
+ ]);
150
+ });
151
+
152
+ it("custom propertyMapper sees site-specific extras via index signature", () => {
153
+ const granadoMapper: PropertyMapper = (product) => [
154
+ { "@type": "PropertyValue", name: "marca", value: String(product.Marca ?? "") },
155
+ { "@type": "PropertyValue", name: "volume", value: String(product.Volume ?? "") },
156
+ ];
157
+ const transform = createProductTransformer({ propertyMapper: granadoMapper });
158
+ const out = transform({
159
+ product: baseProduct({ Marca: "Granado", Volume: "200ml" }),
160
+ options: { currencyCode: "BRL" },
161
+ });
162
+ expect(out.additionalProperty).toEqual([
163
+ { "@type": "PropertyValue", name: "marca", value: "Granado" },
164
+ { "@type": "PropertyValue", name: "volume", value: "200ml" },
165
+ ]);
166
+ });
167
+
168
+ it("prefers idMagento over id for productID when present", () => {
169
+ const transform = createProductTransformer();
170
+ const out = transform({
171
+ product: baseProduct({ idMagento: "9999" }),
172
+ options: { currencyCode: "BRL" },
173
+ });
174
+ expect(out.productID).toBe("9999");
175
+ expect(out.sku).toBe("SKU-42");
176
+ });
177
+
178
+ it("uses id for productID when idMagento is missing", () => {
179
+ const transform = createProductTransformer();
180
+ const out = transform({ product: baseProduct(), options: { currencyCode: "BRL" } });
181
+ expect(out.productID).toBe("SKU-42");
182
+ });
183
+
184
+ it("trims whitespace from name", () => {
185
+ const transform = createProductTransformer();
186
+ const out = transform({
187
+ product: baseProduct({ name: " Padded Name " }),
188
+ options: { currencyCode: "BRL" },
189
+ });
190
+ expect(out.name).toBe("Padded Name");
191
+ expect(out.isVariantOf?.name).toBe("Padded Name");
192
+ });
193
+
194
+ it("falls back to product.currency when options.currencyCode is omitted", () => {
195
+ const transform = createProductTransformer();
196
+ const out = transform({
197
+ product: baseProduct({ currency: "EUR" }),
198
+ options: {},
199
+ });
200
+ const firstOffer = out.offers?.offers[0];
201
+ expect(firstOffer?.priceCurrency).toBe("EUR");
202
+ });
203
+
204
+ it("places one variant under isVariantOf.hasVariant", () => {
205
+ const transform = createProductTransformer();
206
+ const out = transform({ product: baseProduct(), options: { currencyCode: "BRL" } });
207
+ expect(out.isVariantOf?.hasVariant).toHaveLength(1);
208
+ expect(out.isVariantOf?.hasVariant?.[0].sku).toBe("SKU-42");
209
+ });
210
+
211
+ it("AggregateOffer surfaces high/low correctly when on sale", () => {
212
+ const transform = createProductTransformer();
213
+ const out = transform({ product: baseProduct(), options: { currencyCode: "BRL" } });
214
+ expect(out.offers?.highPrice).toBe(100);
215
+ expect(out.offers?.lowPrice).toBe(80);
216
+ });
217
+
218
+ it("AggregateOffer high === low when no salePrice is set", () => {
219
+ const transform = createProductTransformer();
220
+ const out = transform({
221
+ product: baseProduct({ salePrice: undefined }),
222
+ options: { currencyCode: "BRL" },
223
+ });
224
+ expect(out.offers?.highPrice).toBe(100);
225
+ expect(out.offers?.lowPrice).toBe(100);
226
+ });
227
+ });
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Salesforce Marketing Cloud Personalization (Evergage) app entry.
3
+ *
4
+ * Unlike `magento` / `algolia`, the Salesforce loaders here are
5
+ * stateless — every loader takes its `baseUrl` / `dataset` /
6
+ * `campaignId` / `cookieName` via props so the same package can power
7
+ * multiple Evergage datasets in a single worker without a global
8
+ * configure step. Sites just import the loader(s) they need.
9
+ *
10
+ * For loaders, use sub-path imports:
11
+ * import list from "@decocms/apps/salesforce/loaders/products/list"
12
+ * import listRecomended from "@decocms/apps/salesforce/loaders/products/listRecomended"
13
+ * import listCart from "@decocms/apps/salesforce/loaders/products/listCart"
14
+ *
15
+ * For the transformer (sites typically build their own `propertyMapper`
16
+ * over a dataset's custom columns):
17
+ * import { createProductTransformer } from "@decocms/apps/salesforce/utils/transform"
18
+ */
19
+ export type {
20
+ CampaignResponse,
21
+ ParsedUserCookie,
22
+ PersonalizationBody,
23
+ PersonalizationLineItem,
24
+ PersonalizationResponse,
25
+ SalesforceProduct,
26
+ } from "./types";
27
+ export { createHttpClient, type HttpClientOptions } from "./utils/httpClient";
28
+ export { parseUserCookie } from "./utils/parseUserCookie";
29
+ export {
30
+ createProductTransformer,
31
+ type ProductTransformerOptions,
32
+ type PropertyMapper,
33
+ } from "./utils/transform";