@decocms/apps-commerce 8.0.0 → 8.1.0-next.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/apps-commerce",
|
|
3
|
-
"version": "8.0.0",
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco commerce apps: shared types, app-registry, and portable commerce utilities",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"lint:unused": "knip"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@decocms/blocks": "8.0.0"
|
|
29
|
+
"@decocms/blocks": "8.1.0-next.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "^19.0.0",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-app guardrail: every commerce provider that makes upstream API calls
|
|
3
|
+
* MUST ship an instrumented fetch that routes egress through the framework's
|
|
4
|
+
* `createInstrumentedFetch` and emits the canonical
|
|
5
|
+
* `http.client.request.duration` histogram via `recordCommerceMetric`.
|
|
6
|
+
*
|
|
7
|
+
* Scope, stated honestly: this asserts the *factory exists* in the package
|
|
8
|
+
* (it greps `src/utils/instrumentedFetch.ts` for the two required symbols). It
|
|
9
|
+
* does NOT prove the fetch is actually reached at runtime — for VTEX/Shopify/
|
|
10
|
+
* Magento that still depends on the *site* calling `setXFetch(createXFetch())`
|
|
11
|
+
* at boot (they fall back to an uninstrumented `globalThis.fetch` otherwise).
|
|
12
|
+
* So this catches "a provider shipped with no instrumented fetch at all"; it
|
|
13
|
+
* does not catch "a site forgot to wire it".
|
|
14
|
+
*
|
|
15
|
+
* This test reads sibling package source from disk (it does NOT import app
|
|
16
|
+
* modules) so it stays within the one-way dependency graph. If you add a new
|
|
17
|
+
* commerce provider, add it to REQUIRED below and give it a
|
|
18
|
+
* `src/utils/instrumentedFetch.ts` that wires the two symbols.
|
|
19
|
+
*
|
|
20
|
+
* Thin clients (/next/upstream-clients) are checked separately below: each
|
|
21
|
+
* client module must build its requests on `createInstrumentedFetch` from
|
|
22
|
+
* `@decocms/blocks/fetch`, which measures every request itself, and must not
|
|
23
|
+
* call `fetch` in any form. A new thin client goes in V8_CLIENTS.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { describe, expect, it } from "vitest";
|
|
28
|
+
|
|
29
|
+
// Repo root = nearest ancestor of cwd that contains `packages/apps-commerce`.
|
|
30
|
+
// Robust whether the runner's cwd is the repo root (vitest --root .) or the
|
|
31
|
+
// package dir (`bun run --filter ... test`).
|
|
32
|
+
function findRepoRoot(): string {
|
|
33
|
+
let dir = process.cwd();
|
|
34
|
+
for (let i = 0; i < 8; i++) {
|
|
35
|
+
if (existsSync(join(dir, "packages", "apps-commerce"))) return dir;
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir) break;
|
|
38
|
+
dir = parent;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("could not locate repo root (packages/apps-commerce not found above cwd)");
|
|
41
|
+
}
|
|
42
|
+
const repoRoot = findRepoRoot();
|
|
43
|
+
|
|
44
|
+
/** Commerce apps whose upstream egress must be instrumented. */
|
|
45
|
+
const REQUIRED = ["apps-vtex", "apps-shopify", "apps-magento"] as const;
|
|
46
|
+
|
|
47
|
+
describe("commerce apps instrumentation guardrail", () => {
|
|
48
|
+
for (const app of REQUIRED) {
|
|
49
|
+
it(`${app} wires createInstrumentedFetch + recordCommerceMetric`, () => {
|
|
50
|
+
const file = join(repoRoot, "packages", app, "src", "utils", "instrumentedFetch.ts");
|
|
51
|
+
let src: string;
|
|
52
|
+
try {
|
|
53
|
+
src = readFileSync(file, "utf8");
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${app} is missing src/utils/instrumentedFetch.ts — every commerce app must ` +
|
|
57
|
+
`route egress through @decocms/blocks/sdk/instrumentedFetch. See apps-vtex for the pattern.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
expect(src, `${app} must call createInstrumentedFetch`).toContain("createInstrumentedFetch");
|
|
61
|
+
expect(src, `${app} must record the commerce histogram`).toContain("recordCommerceMetric");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* v8 thin clients (/next/upstream-clients): each client module creates its
|
|
68
|
+
* requests with `createInstrumentedFetch` from `@decocms/blocks/fetch` under
|
|
69
|
+
* its own provider name, and never calls a fetch around it. Unlike the v7
|
|
70
|
+
* factories above, nothing has to be wired at boot: the instrumented fetch is
|
|
71
|
+
* the only way the client reaches the network. Each client's own tests assert
|
|
72
|
+
* the operation labels.
|
|
73
|
+
*/
|
|
74
|
+
const V8_CLIENTS: Record<string, { file: string; provider: string }> = {
|
|
75
|
+
"apps-shopify": { file: "src/v8/client.ts", provider: "shopify" },
|
|
76
|
+
"apps-vtex": { file: "src/vtexClient.ts", provider: "vtex" },
|
|
77
|
+
"apps-wake": { file: "src/wakeClient.ts", provider: "wake" },
|
|
78
|
+
"apps-magento": { file: "src/magentoClient.ts", provider: "magento" },
|
|
79
|
+
"apps-algolia": { file: "src/index.ts", provider: "algolia" },
|
|
80
|
+
"apps-resend": { file: "src/emails.ts", provider: "resend" },
|
|
81
|
+
"apps-sfmc-personalization": { file: "src/index.ts", provider: "sfmc-personalization" },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
describe("v8 upstream clients use the instrumented fetch", () => {
|
|
85
|
+
for (const [app, { file, provider }] of Object.entries(V8_CLIENTS)) {
|
|
86
|
+
it(`${app} sends every request through createInstrumentedFetch as "${provider}"`, () => {
|
|
87
|
+
const src = readFileSync(join(repoRoot, "packages", app, file), "utf8");
|
|
88
|
+
const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
89
|
+
expect(src).toMatch(
|
|
90
|
+
/import\s*\{[^}]*\bcreateInstrumentedFetch\b[^}]*\}\s*from\s*"@decocms\/blocks\/fetch"/,
|
|
91
|
+
);
|
|
92
|
+
expect(src).toMatch(
|
|
93
|
+
new RegExp(`createInstrumentedFetch\\(\\s*\\{[^}]*\\bprovider:\\s*"${provider}"`),
|
|
94
|
+
);
|
|
95
|
+
expect(code, `${app} must not call fetch directly`).not.toMatch(/\bfetch\s*\(/);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
package/src/sdk/useOffer.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { AggregateOffer, UnitPriceSpecification } from "../types/commerce";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Reducer that selects the "best" installment: lowest total price, tie-broken by
|
|
5
|
+
* highest billingDuration. This is the single source of truth for installment
|
|
6
|
+
* selection — PLP/PDP use it here via `useOffer`, and the VTEX shelf transform
|
|
7
|
+
* (`buildOfferShelf`) reuses it so shelf cards and detail pages never disagree.
|
|
8
|
+
*/
|
|
9
|
+
export const bestInstallment = (
|
|
10
|
+
acc: UnitPriceSpecification | null,
|
|
11
|
+
curr: UnitPriceSpecification,
|
|
12
|
+
) => {
|
|
4
13
|
if (curr.priceComponentType !== "https://schema.org/Installment") {
|
|
5
14
|
return acc;
|
|
6
15
|
}
|