@tokenoftrust/storefront-runner 2.4.50 → 2.4.52

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.
@@ -8,6 +8,9 @@
8
8
 
9
9
  export type OrderPaymentState = "authorized" | "captured" | "refunded";
10
10
 
11
+ /** Display risk. `unknown` is absent evidence, not a low-risk clearance. */
12
+ export type OrderRiskLevel = "low" | "medium" | "high" | "unknown";
13
+
11
14
  export type OrderShipState = "ready_to_ship" | "hold_review_required" | "unfulfilled" | "completed";
12
15
 
13
16
  /** The exact merchant-facing language the shell contract requires. */
@@ -54,7 +57,35 @@ export interface OrderRow {
54
57
  shipState: OrderShipState;
55
58
  blocker: string | null;
56
59
  nextStep: string;
57
- riskLevel: "low" | "medium" | "high";
60
+ riskLevel: OrderRiskLevel;
61
+ }
62
+
63
+ /** One independent readiness fact. Source and as-of belong to this column only. */
64
+ export interface OrderReadinessColumn {
65
+ key: "payment" | "tax" | "verification" | "risk" | "stock" | "fulfillment";
66
+ state: string;
67
+ source: string;
68
+ asOf: string | null;
69
+ }
70
+
71
+ /** Release is held unless every block is empty. `held` is not inferred from payment capture. */
72
+ export interface OrderRelease {
73
+ state: "held" | "ready";
74
+ ready: boolean;
75
+ blocks: readonly string[];
76
+ }
77
+
78
+ /** Storewide, filtered, and page money are separate scopes. Refunds are not values. */
79
+ export interface OrderMoneyScope {
80
+ count: number;
81
+ valueMinor: number;
82
+ refundMinor: number;
83
+ }
84
+
85
+ export interface OrderAccounting {
86
+ storewide: OrderMoneyScope;
87
+ filter: OrderMoneyScope;
88
+ page: OrderMoneyScope;
58
89
  }
59
90
 
60
91
  export interface OrderDetail extends OrderRow {
@@ -64,6 +95,9 @@ export interface OrderDetail extends OrderRow {
64
95
  shipping: number;
65
96
  estimatedTax: number;
66
97
  taxesAndFees: number;
98
+ /** Present when the order was assessed. Absent on the mock book. */
99
+ columns?: OrderReadinessColumn[];
100
+ release?: OrderRelease;
67
101
  }
68
102
 
69
103
  export type OrderQueueKey = "needs_attention" | "ready_to_ship" | "unfulfilled" | "all" | "completed";
@@ -89,4 +123,6 @@ export interface OrdersViewModel {
89
123
  rows: OrderRow[];
90
124
  detailsById: Record<string, OrderDetail>;
91
125
  today: OrdersTodaySummary;
126
+ /** Set by a scrappy book query. Never copied from the current page alone. */
127
+ accounting?: OrderAccounting;
92
128
  }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Final-cart authorization decision. Payment is admitted only when the caller
3
+ * token, the bound cart, fresh evidence, the verification seam, and mirror stock
4
+ * all agree. Catalog availability and a browser confirmation are not authority.
5
+ */
6
+ import {
7
+ admitCallerToken,
8
+ assertMinor,
9
+ decideCommerceCommand,
10
+ finalCartDigest,
11
+ finalCartStillBound,
12
+ sumMinor,
13
+ type CallerToken,
14
+ type CommerceEvidence,
15
+ type CommerceStage,
16
+ type EvidenceState,
17
+ type FinalCartBinding,
18
+ type FulfillmentState,
19
+ type LabelState,
20
+ type PaymentState,
21
+ type StockState,
22
+ } from "@tot/public-runtime";
23
+ import { readMirrorAuthority, VERIFICATION_PROVIDER_OWNER, type ReserveHold } from "@/lib/inventory/allocations.js";
24
+
25
+ export interface SeamView {
26
+ status: "clear" | "absent" | "blocked";
27
+ failedClosed?: boolean;
28
+ codes?: readonly string[];
29
+ }
30
+
31
+ export interface FinalCartRequest {
32
+ now: string;
33
+ routedTenantId: string;
34
+ quoted: FinalCartBinding;
35
+ presented: FinalCartBinding;
36
+ evidence: CommerceEvidence | null;
37
+ seam: SeamView | null;
38
+ catalogAvailable: boolean;
39
+ browserConfirmed: boolean;
40
+ token: CallerToken | null;
41
+ trustedIssuers: readonly string[];
42
+ expectedAudience: string;
43
+ activeKids: readonly string[];
44
+ authorities: readonly unknown[];
45
+ payment?: PaymentState;
46
+ stock?: StockState;
47
+ fulfillment?: FulfillmentState;
48
+ label?: LabelState;
49
+ }
50
+
51
+ export type FinalCartDecision =
52
+ | {
53
+ ok: true;
54
+ digest: string;
55
+ expiresAt: string;
56
+ amountMinor: number;
57
+ holds: ReserveHold[];
58
+ issuer: string;
59
+ jti: string;
60
+ tenantId: string;
61
+ stage: CommerceStage;
62
+ }
63
+ | { ok: false; finding: string; owner?: string };
64
+
65
+ const EVIDENCE_FIELDS = ["verification", "tax", "risk", "stock"] as const;
66
+
67
+ function sortedLines(cart: FinalCartBinding) {
68
+ return [...cart.lines].sort(
69
+ (left, right) => left.sku.localeCompare(right.sku) || left.unitMinor - right.unitMinor || left.quantity - right.quantity,
70
+ );
71
+ }
72
+
73
+ function cartFinding(quoted: FinalCartBinding, presented: FinalCartBinding, routedTenantId: string): string | null {
74
+ if (quoted.tenantId !== routedTenantId || presented.tenantId !== routedTenantId || quoted.tenantId !== presented.tenantId) {
75
+ return "tenant-changed";
76
+ }
77
+ if (quoted.stage !== presented.stage) return "stage-changed";
78
+ if (quoted.currency !== presented.currency) return "currency-changed";
79
+ if (quoted.destinationDigest !== presented.destinationDigest) return "destination-changed";
80
+ const left = sortedLines(quoted);
81
+ const right = sortedLines(presented);
82
+ if (left.length !== right.length || left.some((line, index) => line.sku !== right[index]?.sku)) return "product-changed";
83
+ if (left.some((line, index) => line.unitMinor !== right[index]?.unitMinor)) return "price-changed";
84
+ if (left.some((line, index) => line.quantity !== right[index]?.quantity)) return "quantity-changed";
85
+ if (quoted.verificationExpiresAt !== presented.verificationExpiresAt || quoted.taxExpiresAt !== presented.taxExpiresAt) {
86
+ return "evidence-window-changed";
87
+ }
88
+ return null;
89
+ }
90
+
91
+ function evidenceFinding(evidence: CommerceEvidence | null): string | null {
92
+ if (!evidence) return "missing-evidence";
93
+ for (const field of EVIDENCE_FIELDS) {
94
+ const state: EvidenceState | string = evidence[field];
95
+ if (state === "fresh") continue;
96
+ if (state === "UNKNOWN") return `evidence-${field}-unknown`;
97
+ if (state === "stale" || state === "unreadable" || state === "absent") return `evidence-${field}-${state}`;
98
+ return `evidence-${field}-unreadable`;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function seamFinding(seam: SeamView | null): { finding: string; owner?: string } | null {
104
+ if (!seam || seam.status === "absent") return { finding: "verification-seam-absent" };
105
+ if (seam.status === "clear") return null;
106
+ const codes = (seam.codes ?? []).filter((code) => /^[a-z0-9_-]+$/.test(code));
107
+ if (seam.failedClosed && codes.some((code) => code === "verification_unconfigured" || code === "ruleset_unavailable")) {
108
+ return { finding: "verification-provider-unconfigured", owner: VERIFICATION_PROVIDER_OWNER };
109
+ }
110
+ return { finding: codes[0] ? `verification-refused:${codes[0]}` : "verification-refused" };
111
+ }
112
+
113
+ function isCatalogShape(value: unknown): boolean {
114
+ return value == null || (typeof value === "object" && value !== null && "available" in value && !("provider" in value));
115
+ }
116
+
117
+ function commandFinding(reason: string): string {
118
+ switch (reason) {
119
+ case "stock":
120
+ return "stock-unknown";
121
+ case "fulfillment":
122
+ return "fulfillment-refused";
123
+ case "reconcile":
124
+ return "payment-reconcile";
125
+ case "payment":
126
+ return "payment-refused";
127
+ default:
128
+ return "evidence-refused";
129
+ }
130
+ }
131
+
132
+ function earlierExpiry(left: string, right: string): string {
133
+ return left <= right ? left : right;
134
+ }
135
+
136
+ /** Deny with one specific finding, or clear a mirror hold for each sku. */
137
+ export async function evaluateFinalCart(input: FinalCartRequest): Promise<FinalCartDecision> {
138
+ if (input.browserConfirmed && (!input.token || input.token.scheme !== "asymmetric-jwks" || !input.token.signatureValid)) {
139
+ return { ok: false, finding: "browser-not-authority" };
140
+ }
141
+ if (!input.token) return { ok: false, finding: "signature" };
142
+ const admitted = admitCallerToken({
143
+ token: input.token,
144
+ trustedIssuers: input.trustedIssuers,
145
+ expectedAudience: input.expectedAudience,
146
+ activeKids: input.activeKids,
147
+ now: input.now,
148
+ tenantId: input.routedTenantId,
149
+ stage: input.presented.stage,
150
+ });
151
+ if (!admitted.ok) return { ok: false, finding: admitted.reason };
152
+
153
+ const changed = cartFinding(input.quoted, input.presented, input.routedTenantId);
154
+ if (changed) return { ok: false, finding: changed };
155
+
156
+ if (input.presented.lines.length < 1) return { ok: false, finding: "malformed-request" };
157
+
158
+ const evidence = evidenceFinding(input.evidence);
159
+ if (evidence) return { ok: false, finding: evidence };
160
+
161
+ const expiresAt = earlierExpiry(input.presented.verificationExpiresAt, input.presented.taxExpiresAt);
162
+ if (
163
+ input.presented.verificationExpiresAt <= input.now ||
164
+ input.presented.taxExpiresAt <= input.now ||
165
+ expiresAt <= input.now
166
+ ) {
167
+ return { ok: false, finding: "evidence-expired" };
168
+ }
169
+
170
+ let digest: string;
171
+ let amountMinor: number;
172
+ try {
173
+ for (const line of input.presented.lines) {
174
+ assertMinor(line.quantity, "quantity");
175
+ assertMinor(line.unitMinor, "unitMinor");
176
+ }
177
+ amountMinor = sumMinor(input.presented.lines.map((line) => line.unitMinor * line.quantity));
178
+ digest = await finalCartDigest(input.presented);
179
+ const quotedDigest = await finalCartDigest(input.quoted);
180
+ if (!finalCartStillBound(quotedDigest, digest, input.now, expiresAt)) return { ok: false, finding: "cart-unbound" };
181
+ } catch {
182
+ return { ok: false, finding: "malformed-request" };
183
+ }
184
+
185
+ const seam = seamFinding(input.seam);
186
+ if (seam) return { ok: false, finding: seam.finding, ...(seam.owner ? { owner: seam.owner } : {}) };
187
+
188
+ const command = decideCommerceCommand({
189
+ command: "authorize-final-cart",
190
+ payment: input.payment ?? "pending",
191
+ stock: input.stock ?? "unallocated",
192
+ fulfillment: input.fulfillment ?? "held",
193
+ label: input.label ?? "none",
194
+ evidence: input.evidence!,
195
+ providerOutcome: null,
196
+ });
197
+ if (!command.admit) return { ok: false, finding: commandFinding(command.reason) };
198
+
199
+ const quantities = new Map<string, number>();
200
+ for (const line of input.presented.lines) {
201
+ quantities.set(line.sku, (quantities.get(line.sku) ?? 0) + line.quantity);
202
+ }
203
+ const holds: ReserveHold[] = [];
204
+ for (const sku of [...quantities.keys()].sort()) {
205
+ const quantity = quantities.get(sku)!;
206
+ const named =
207
+ input.authorities.find((value) => {
208
+ if (!value || typeof value !== "object") return false;
209
+ return (value as { sku?: unknown }).sku === sku;
210
+ }) ?? (quantities.size === 1 ? (input.authorities[0] ?? null) : null);
211
+ if (named && typeof named === "object" && "provider" in named && (named as { provider: unknown }).provider !== "mirror") {
212
+ return { ok: false, finding: "authority-unavailable" };
213
+ }
214
+ const mirror = readMirrorAuthority(named);
215
+ if (!mirror || mirror.sku !== sku) {
216
+ return { ok: false, finding: input.catalogAvailable && isCatalogShape(named) ? "catalog-not-authority" : "authority-unavailable" };
217
+ }
218
+ if (mirror.onHand < quantity) return { ok: false, finding: "stock-unavailable" };
219
+ holds.push({ sku, locationId: mirror.locationId, quantity, onHand: mirror.onHand });
220
+ }
221
+
222
+ if (input.token.scheme !== "asymmetric-jwks") return { ok: false, finding: "shared-secret" };
223
+ return {
224
+ ok: true,
225
+ digest,
226
+ expiresAt,
227
+ amountMinor,
228
+ holds,
229
+ issuer: input.token.issuer,
230
+ jti: input.token.jti,
231
+ tenantId: input.routedTenantId,
232
+ stage: input.presented.stage,
233
+ };
234
+ }