@tokenoftrust/storefront-runner 4.7.7 → 4.7.8

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.
@@ -51,9 +51,39 @@ export async function readCommerceAdmission(
51
51
  stage,
52
52
  authorizationId,
53
53
  );
54
- if (!row) return null;
54
+ return row ? parseCommerceAdmission(row.snapshot_json, tenantId, stage, authorizationId) : null;
55
+ }
56
+
57
+ /** Every admission snapshot one tenant and stage holds, keyed by authorization id. A snapshot that fails its own checks is absent. */
58
+ export async function readCommerceAdmissions(
59
+ db: Queryable,
60
+ tenantId: string,
61
+ stage: CommerceStage,
62
+ authorizationIds: ReadonlySet<string>,
63
+ ): Promise<Map<string, CommerceAdmissionSnapshot>> {
64
+ const admissions = new Map<string, CommerceAdmissionSnapshot>();
65
+ if (authorizationIds.size === 0) return admissions;
66
+ const rows = await db.all<{ authorization_id: string; snapshot_json: string }>(
67
+ `SELECT authorization_id,snapshot_json FROM commerce_admission_snapshots WHERE tenant_id=? AND stage=?`,
68
+ tenantId,
69
+ stage,
70
+ );
71
+ for (const row of rows) {
72
+ if (!authorizationIds.has(row.authorization_id)) continue;
73
+ const admission = parseCommerceAdmission(row.snapshot_json, tenantId, stage, row.authorization_id);
74
+ if (admission) admissions.set(row.authorization_id, admission);
75
+ }
76
+ return admissions;
77
+ }
78
+
79
+ function parseCommerceAdmission(
80
+ snapshotJson: string,
81
+ tenantId: string,
82
+ stage: CommerceStage,
83
+ authorizationId: string,
84
+ ): CommerceAdmissionSnapshot | null {
55
85
  try {
56
- const snapshot = JSON.parse(row.snapshot_json) as CommerceAdmissionSnapshot;
86
+ const snapshot = JSON.parse(snapshotJson) as CommerceAdmissionSnapshot;
57
87
  if (
58
88
  snapshot.tenantId !== tenantId ||
59
89
  snapshot.stage !== stage ||
@@ -106,3 +136,63 @@ export function admissionMatches(
106
136
  canonicalJson(sort(event.lines)) === canonicalJson(sort(snapshot.lines))
107
137
  );
108
138
  }
139
+
140
+ /** The fields of a provider order event that bind it to a final-cart admission. */
141
+ export interface AdmissionBoundEvent {
142
+ command: "observe" | "capture" | "refund" | "cancel";
143
+ eventVersion: number;
144
+ sourceEventId: string;
145
+ source: string;
146
+ sourceOrderId: string;
147
+ placedAt: string;
148
+ lines?: readonly { sku: string; quantity: number; unitMinor: number }[];
149
+ admissionReference?: string;
150
+ providerCartDigest?: string;
151
+ destinationDigest?: string;
152
+ }
153
+
154
+ const COMMAND_RANK = { observe: 0, capture: 1, refund: 2, cancel: 3 } as const;
155
+
156
+ /**
157
+ * The newest event that owns the order's cart binding. A paid or cart snapshot owns its lines,
158
+ * provider cart digest, destination digest and admission reference together; a status-only update
159
+ * keeps those facts but never replaces them.
160
+ */
161
+ export function currentCartEvent<E extends AdmissionBoundEvent>(events: readonly E[]): E | undefined {
162
+ return [...events]
163
+ .sort(
164
+ (a, b) =>
165
+ b.eventVersion - a.eventVersion ||
166
+ COMMAND_RANK[b.command] - COMMAND_RANK[a.command] ||
167
+ b.sourceEventId.localeCompare(a.sourceEventId),
168
+ )
169
+ .find(
170
+ (event) =>
171
+ event.command === "capture" ||
172
+ event.lines !== undefined ||
173
+ event.admissionReference !== undefined ||
174
+ event.providerCartDigest !== undefined ||
175
+ event.destinationDigest !== undefined,
176
+ );
177
+ }
178
+
179
+ /** True when this admission is the one the order's paid events were placed against: same cart, same totals, placed before it expired. */
180
+ export function admissionBindsOrder(
181
+ admission: CommerceAdmissionSnapshot,
182
+ events: readonly AdmissionBoundEvent[],
183
+ order: { source: string; sourceOrderId: string; currency: string; totals: MinorTotals },
184
+ ): boolean {
185
+ const cart = currentCartEvent(events);
186
+ if (!cart) return false;
187
+ return (
188
+ events.every((event) => event.source === order.source && event.sourceOrderId === order.sourceOrderId) &&
189
+ admissionMatches(admission, {
190
+ currency: order.currency,
191
+ totals: order.totals,
192
+ lines: cart.lines ?? [],
193
+ providerCartDigest: cart.providerCartDigest,
194
+ destinationDigest: cart.destinationDigest,
195
+ }) &&
196
+ Date.parse(cart.placedAt) <= Date.parse(admission.expiresAt)
197
+ );
198
+ }
@@ -2,7 +2,7 @@
2
2
  import { type CommerceEvidence, type CommerceStage } from "@tot/public-runtime";
3
3
  import type { Queryable } from "../catalog/d1/catalog.js";
4
4
  import { foldCommerceEvents, type StoredCommercePayload } from "../orders/data/orderMapper.js";
5
- import { admissionMatches, readCommerceAdmission } from "../checkout/commerce/commerceAdmission.js";
5
+ import { admissionBindsOrder, currentCartEvent, readCommerceAdmission } from "../checkout/commerce/commerceAdmission.js";
6
6
  import { FulfillmentService } from "./fulfillmentService.js";
7
7
  import { fulfillmentDbFromQueryable } from "./store.js";
8
8
  const UNKNOWN: CommerceEvidence = { verification: "UNKNOWN", tax: "UNKNOWN", risk: "UNKNOWN", stock: "UNKNOWN" };
@@ -20,44 +20,13 @@ export async function projectCommerceOrder(
20
20
  const events = rows.map((row) => (JSON.parse(row.payload) as StoredCommercePayload).source);
21
21
  if (!events.length) throw new Error("source-unavailable");
22
22
  const snapshot = foldCommerceEvents(events);
23
- const commandRank = { observe: 0, capture: 1, refund: 2, cancel: 3 };
24
- const sorted = [...events].sort(
25
- (a, b) =>
26
- b.eventVersion - a.eventVersion ||
27
- commandRank[b.command] - commandRank[a.command] ||
28
- b.sourceEventId.localeCompare(a.sourceEventId),
29
- );
30
- // A paid/cart snapshot owns all of its binding fields together. A status-only
31
- // update can retain those immutable facts, but cannot replace any of them.
32
- const currentCart = sorted.find(
33
- (event) =>
34
- event.command === "capture" ||
35
- event.lines !== undefined ||
36
- event.admissionReference !== undefined ||
37
- event.providerCartDigest !== undefined ||
38
- event.destinationDigest !== undefined,
39
- );
23
+ const currentCart = currentCartEvent(events);
40
24
  const lines = currentCart?.lines ?? [];
41
25
  const admission = currentCart?.admissionReference
42
26
  ? await readCommerceAdmission(db, input.tenantId, input.stage, currentCart.admissionReference)
43
27
  : null;
44
- const sameSourceOrder = events.every(
45
- (event) => event.source === snapshot.source && event.sourceOrderId === snapshot.sourceOrderId,
46
- );
47
28
  let trusted = false;
48
- if (
49
- admission &&
50
- currentCart &&
51
- sameSourceOrder &&
52
- admissionMatches(admission, {
53
- currency: snapshot.currency,
54
- totals: snapshot.totals,
55
- lines,
56
- providerCartDigest: currentCart.providerCartDigest,
57
- destinationDigest: currentCart.destinationDigest,
58
- }) &&
59
- Date.parse(currentCart.placedAt) <= Date.parse(admission.expiresAt)
60
- ) {
29
+ if (admission && currentCart && admissionBindsOrder(admission, events, snapshot)) {
61
30
  if (!db.batchResults) throw new Error("transaction-required");
62
31
  const results = await db.batchResults([
63
32
  {
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Orders workspace actions. Every button in the order detail answers: it opens the part of the
3
+ * order it names, moves to the tab that resolves it, loads the owner-only address, or says why
4
+ * nothing can be done from here. `planOrderAction` decides on the server; `mountOrderActions`
5
+ * carries the plan out in the browser. Neither ever leaves a click without a visible result.
6
+ */
7
+
8
+ export type OrderActionKind = "reveal" | "tab" | "address" | "explain";
9
+
10
+ export interface OrderActionPlan {
11
+ kind: OrderActionKind;
12
+ /** `reveal`: selector of the section inside the order record. `tab`: the admin tab id. */
13
+ target: string;
14
+ message: string;
15
+ }
16
+
17
+ export interface OrderActionInput {
18
+ /** The readiness check key, or `blocker` for the order-level Resolve hold / next-step buttons. */
19
+ check: string;
20
+ /** The state the check reports, e.g. `captured`, `UNKNOWN`, `fresh`. */
21
+ state: string;
22
+ /** The order's blocker label when the order is held. */
23
+ blocker?: string | null;
24
+ }
25
+
26
+ const CLEAR_STATES = new Set(["fresh", "not-required"]);
27
+
28
+ function evidenceMessage(label: string, state: string): string {
29
+ if (CLEAR_STATES.has(state)) return `${label} evidence is ${state}. Activity and evidence below shows how this order got here.`;
30
+ return `${label} evidence is ${state}, so this order stays held. Evidence is recorded when the shopper passes final cart; an order whose final cart is not on record cannot be cleared from admin.`;
31
+ }
32
+
33
+ export function planOrderAction(input: OrderActionInput): OrderActionPlan {
34
+ switch (input.check) {
35
+ case "payment_capture":
36
+ return {
37
+ kind: "explain",
38
+ target: "",
39
+ message:
40
+ input.state === "captured" || input.state === "Captured"
41
+ ? "Payment is captured."
42
+ : "Payment is captured by the checkout provider and recorded here when it confirms. Admin cannot capture it; check the transaction at the provider.",
43
+ };
44
+ case "tax_and_fees":
45
+ return { kind: "reveal", target: "[data-orders-totals]", message: "Items and totals are open below." };
46
+ case "identity_and_age":
47
+ return { kind: "reveal", target: "[data-orders-evidence]", message: evidenceMessage("Identity and age", input.state) };
48
+ case "risk":
49
+ return { kind: "reveal", target: "[data-orders-evidence]", message: evidenceMessage("Risk", input.state) };
50
+ case "address":
51
+ return { kind: "address", target: "[data-orders-address]", message: "" };
52
+ case "stock":
53
+ return {
54
+ kind: "explain",
55
+ target: "",
56
+ message:
57
+ input.state === "confirmed"
58
+ ? "Stock is confirmed for this order."
59
+ : `Stock is ${input.state} for this order. Stock is confirmed when the final-cart reservation matches the paid order.`,
60
+ };
61
+ case "fulfillment":
62
+ return { kind: "tab", target: "fulfillment", message: "Fulfillment holds are worked in the Fulfillment tab." };
63
+ case "blocker":
64
+ return blockerPlan(input);
65
+ default:
66
+ return { kind: "explain", target: "", message: "Nothing can be done for this check from here." };
67
+ }
68
+ }
69
+
70
+ function blockerPlan(input: OrderActionInput): OrderActionPlan {
71
+ if (!input.blocker) {
72
+ return { kind: "explain", target: "", message: "This order has no hold." };
73
+ }
74
+ if (input.blocker === "Payment capture") return planOrderAction({ check: "payment_capture", state: input.state });
75
+ if (input.blocker === "Evidence review" || input.blocker === "Age verification hold" || input.blocker === "Verification unknown") {
76
+ return planOrderAction({ check: "identity_and_age", state: input.state });
77
+ }
78
+ return { kind: "tab", target: "fulfillment", message: `${input.blocker}: holds are worked in the Fulfillment tab.` };
79
+ }
80
+
81
+ /** Owner address response, as stored. Only these fields are shown, in this order. */
82
+ const ADDRESS_FIELDS = ["name", "line1", "line2", "city", "region", "postalCode", "country"] as const;
83
+
84
+ export function addressLines(address: unknown): string[] {
85
+ if (address === null || typeof address !== "object" || Array.isArray(address)) return [];
86
+ const record = address as Record<string, unknown>;
87
+ const lines: string[] = [];
88
+ for (const key of ADDRESS_FIELDS) {
89
+ const value = record[key];
90
+ if (typeof value === "string" && value.trim() !== "") lines.push(value.trim());
91
+ }
92
+ return lines;
93
+ }
94
+
95
+ export const ADDRESS_NONE =
96
+ "No ship-to address is stored for this order. The checkout confirmation carries none and the order ledger keeps no customer data.";
97
+ export const ADDRESS_DENIED = "Only the store owner can view a ship-to address.";
98
+ export const ADDRESS_FAILED = "The address could not be loaded. Try again.";
99
+
100
+ export interface OrderActionDeps {
101
+ fetch: typeof fetch;
102
+ /** The fulfillment API root for the selected store, or null when this page cannot reach it. */
103
+ api: string | null;
104
+ navigate: (tab: string) => boolean;
105
+ }
106
+
107
+ function show(note: HTMLElement | null, lines: readonly string[]): void {
108
+ if (!note) return;
109
+ note.replaceChildren(...lines.map((line) => Object.assign(document.createElement("span"), { textContent: line })));
110
+ note.hidden = false;
111
+ }
112
+
113
+ async function loadAddress(orderId: string, note: HTMLElement | null, deps: OrderActionDeps): Promise<void> {
114
+ if (!deps.api) {
115
+ show(note, [ADDRESS_FAILED]);
116
+ return;
117
+ }
118
+ show(note, ["Loading address…"]);
119
+ try {
120
+ const response = await deps.fetch(`${deps.api}/orders/${encodeURIComponent(orderId)}/address`, {
121
+ credentials: "same-origin",
122
+ cache: "no-store",
123
+ });
124
+ if (response.status === 404) return show(note, [ADDRESS_NONE]);
125
+ if (response.status === 401 || response.status === 403) return show(note, [ADDRESS_DENIED]);
126
+ if (!response.ok) return show(note, [ADDRESS_FAILED]);
127
+ const lines = addressLines(await response.json());
128
+ show(note, lines.length ? lines : [ADDRESS_NONE]);
129
+ } catch {
130
+ show(note, [ADDRESS_FAILED]);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Wire every `[data-orders-action]` button under `root`. The button carries its plan as
136
+ * `data-orders-action-kind|target|message`; its note is the nearest `[data-orders-action-note]` in
137
+ * the same record. Order-level buttons (`data-orders-resolve-hold`) act through the selected record's
138
+ * `[data-orders-blocker-action]`.
139
+ */
140
+ export function mountOrderActions(root: HTMLElement, deps: OrderActionDeps): void {
141
+ const run = (button: HTMLElement, record: HTMLElement): void => {
142
+ const kind = button.dataset.ordersActionKind as OrderActionKind | undefined;
143
+ const target = button.dataset.ordersActionTarget ?? "";
144
+ const message = button.dataset.ordersActionMessage ?? "";
145
+ const orderId = record.dataset.ordersDetailRecord ?? "";
146
+ const scope = button.closest<HTMLElement>("[data-orders-action-scope]");
147
+ const note = scope
148
+ ? scope.querySelector<HTMLElement>("[data-orders-action-note]")
149
+ : record.querySelector<HTMLElement>("[data-orders-record-note]");
150
+ if (kind === "reveal") {
151
+ const section = record.querySelector<HTMLDetailsElement>(target);
152
+ if (section) {
153
+ section.open = true;
154
+ section.scrollIntoView?.({ block: "nearest" });
155
+ }
156
+ show(note, [message]);
157
+ } else if (kind === "tab") {
158
+ show(note, [deps.navigate(target) ? message : `${message} The ${target} tab is not available to this account.`]);
159
+ } else if (kind === "address") {
160
+ void loadAddress(orderId, record.querySelector<HTMLElement>(target) ?? note, deps);
161
+ } else {
162
+ show(note, [message || "Nothing can be done from here."]);
163
+ }
164
+ };
165
+
166
+ root.addEventListener("click", (event) => {
167
+ const origin = event.target instanceof Element ? event.target : null;
168
+ const button = origin?.closest<HTMLElement>("[data-orders-action]");
169
+ if (button && root.contains(button)) {
170
+ const record = button.closest<HTMLElement>("[data-orders-detail-record]");
171
+ if (record) run(button, record);
172
+ return;
173
+ }
174
+ const orderLevel = origin?.closest<HTMLElement>("[data-orders-resolve-hold], [data-orders-next-step]");
175
+ if (!orderLevel || !root.contains(orderLevel)) return;
176
+ const selected = orderLevel.dataset.ordersNextStep ?? root.querySelector<HTMLElement>("[data-orders-detail-root]")?.dataset.adminSelectedId ?? "";
177
+ const record = Array.from(root.querySelectorAll<HTMLElement>("[data-orders-detail-record]")).find(
178
+ (item) => item.dataset.ordersDetailRecord === selected,
179
+ );
180
+ const action = record?.querySelector<HTMLElement>("[data-orders-blocker-action]");
181
+ if (!record || !action) {
182
+ show(root.querySelector<HTMLElement>("[data-orders-root-note]"), ["Select an order first."]);
183
+ return;
184
+ }
185
+ record.hidden = false;
186
+ record.scrollIntoView?.({ block: "nearest" });
187
+ run(action, record);
188
+ });
189
+ }
190
+
191
+ /** Switch the admin shell to a tab by id; false when the tab is not in this page. */
192
+ export function navigateAdminTab(tab: string): boolean {
193
+ if (!document.getElementById(tab)) return false;
194
+ window.location.hash = `#${tab}`;
195
+ return true;
196
+ }
@@ -19,6 +19,7 @@ import type {
19
19
  CheckoutOrderListResponse,
20
20
  CheckoutOrderPayment,
21
21
  CheckoutOrderSummary,
22
+ CommerceEvidence,
22
23
  CommerceScope,
23
24
  CommerceStage,
24
25
  CustomizationEnv,
@@ -193,9 +194,10 @@ interface LedgerOrder {
193
194
  function ledgerOrder(entry: CommerceOrderBookEntry): LedgerOrder {
194
195
  const placedAt = isoInstant(entry.placedAt);
195
196
  const snapshot = entry.events.length > 0 ? foldCommerceEvents(entry.events) : null;
196
- const payment = snapshot?.payment ?? entry.payment;
197
- const fulfillment = snapshot?.fulfillment ?? entry.fulfillment;
198
- const evidence = snapshot?.verificationDetail ?? { verification: "UNKNOWN", tax: "UNKNOWN", risk: "UNKNOWN", stock: "UNKNOWN" };
197
+ // The order row is the state of record: provider events and fulfillment commands both write it.
198
+ const { payment, fulfillment } = entry;
199
+ const evidence: CommerceEvidence =
200
+ entry.admitted?.evidence ?? snapshot?.verificationDetail ?? { verification: "UNKNOWN", tax: "UNKNOWN", risk: "UNKNOWN", stock: "UNKNOWN" };
199
201
  const totals = snapshot?.totals ?? { subtotalMinor: entry.amountMinor, shippingMinor: 0, taxMinor: 0, totalMinor: entry.amountMinor };
200
202
  const lines =
201
203
  [...entry.events].sort((a, b) => b.eventVersion - a.eventVersion).find((event) => event.lines?.length)?.lines ?? [];
@@ -217,10 +219,10 @@ function ledgerOrder(entry: CommerceOrderBookEntry): LedgerOrder {
217
219
  const itemCount = lines.reduce((sum, line) => sum + line.quantity, 0);
218
220
  const itemsSummary =
219
221
  lines.length === 0 ? "Items not recorded" : lines.length === 1 ? lines[0]!.sku : `${lines[0]!.sku} +${lines.length - 1} more`;
220
- const verification: Record<string, string> = {};
222
+ const verification: Record<string, string> = { stockState: entry.stock };
221
223
  for (const key of ["verification", "tax", "risk", "stock"] as const) {
222
224
  verification[key] = evidence[key];
223
- verification[`${key}Source`] = "commerce-ledger";
225
+ verification[`${key}Source`] = entry.admitted?.provenance[key].source ?? "commerce-ledger";
224
226
  verification[`${key}AsOf`] = placedAt;
225
227
  }
226
228
 
@@ -34,6 +34,7 @@ import {
34
34
  paymentIsCaptured,
35
35
  publicCommerceEvent,
36
36
  readOwnerAddress,
37
+ STOCK_STATES,
37
38
  sumMinor,
38
39
  totalsMatch,
39
40
  } from "@tot/public-runtime";
@@ -209,6 +210,21 @@ function readSourcedEvidence(
209
210
  };
210
211
  }
211
212
 
213
+ /** Stock allocation state the order source recorded, never inferred from stock evidence. */
214
+ function readStockState(verification: CheckoutOrderVerification | null): SourcedState<StockState> {
215
+ const state = verification?.stockState;
216
+ if (typeof state !== "string" || !(STOCK_STATES as readonly string[]).includes(state)) {
217
+ return { state: "UNKNOWN", source: "absent", asOf: null };
218
+ }
219
+ const source = verification?.stockSource;
220
+ const asOf = verification?.stockAsOf;
221
+ return {
222
+ state: state as StockState,
223
+ source: typeof source === "string" && source.length > 0 ? source : "absent",
224
+ asOf: typeof asOf === "string" ? asOf : null,
225
+ };
226
+ }
227
+
212
228
  export function readinessColumns(order: ScrappyOrderRecord): OrderReadinessColumn[] {
213
229
  return [
214
230
  { key: "payment", state: order.payment.state, source: order.payment.source, asOf: order.payment.asOf },
@@ -279,7 +295,7 @@ function checkoutRecord(order: CheckoutOrderSummary, scope: CommerceScope): Scra
279
295
  tax: readSourcedEvidence(order.verification, "tax"),
280
296
  verification: readSourcedEvidence(order.verification, "verification"),
281
297
  risk: readSourcedEvidence(order.verification, "risk"),
282
- stock: { state: "UNKNOWN", source: "absent", asOf: null },
298
+ stock: readStockState(order.verification),
283
299
  stockEvidence: readSourcedEvidence(order.verification, "stock"),
284
300
  fulfillment: {
285
301
  state: fulfillmentFromCheckout(order.fulfillment),
@@ -17,9 +17,12 @@ import {
17
17
  idempotencyKey,
18
18
  paginateOrders,
19
19
  sha256Hex,
20
+ type CommerceEvidence,
21
+ type CommerceEvidenceProvenance,
20
22
  type CommerceStage,
21
23
  type PaymentState,
22
24
  } from "@tot/public-runtime";
25
+ import { admissionBindsOrder, currentCartEvent, readCommerceAdmissions } from "../../checkout/commerce/commerceAdmission.js";
23
26
  import {
24
27
  foldCommerceEvents,
25
28
  packCommercePayload,
@@ -230,8 +233,15 @@ export interface CommerceOrderBookEntry {
230
233
  currency: string;
231
234
  payment: PaymentState;
232
235
  fulfillment: CommerceSnapshot["fulfillment"];
236
+ stock: CommerceSnapshot["stock"];
233
237
  amountMinor: number;
234
238
  events: CommerceSourceEvent[];
239
+ /**
240
+ * The final-cart evidence this order was placed against, present only when the admission the paid
241
+ * events name is the same cart, totals and destination and had not expired at placement. The paid
242
+ * events carry no evidence of their own.
243
+ */
244
+ admitted: { evidence: CommerceEvidence; provenance: CommerceEvidenceProvenance } | null;
235
245
  }
236
246
 
237
247
  export interface CommerceAggregate {
@@ -504,16 +514,33 @@ export class ScrappyOrderLedger {
504
514
  list.push(parsed.source);
505
515
  events.set(row.order_id, list);
506
516
  }
507
- return rows.map((row) => ({
508
- orderId: row.order_id,
509
- source: row.source,
510
- placedAt: row.placed_at,
511
- currency: row.currency,
512
- payment: row.payment_state,
513
- fulfillment: row.fulfillment_state,
514
- amountMinor: row.amount_minor,
515
- events: events.get(row.order_id) ?? [],
516
- }));
517
+ const references = new Set<string>();
518
+ for (const list of events.values()) {
519
+ const reference = currentCartEvent(list)?.admissionReference;
520
+ if (reference) references.add(reference);
521
+ }
522
+ const admissions = await readCommerceAdmissions(this.db, tenantId, stage, references);
523
+ return rows.map((row) => {
524
+ const orderEvents = events.get(row.order_id) ?? [];
525
+ const reference = currentCartEvent(orderEvents)?.admissionReference;
526
+ const admission = reference ? admissions.get(reference) : undefined;
527
+ const bound =
528
+ admission !== undefined &&
529
+ orderEvents.length > 0 &&
530
+ admissionBindsOrder(admission, orderEvents, foldCommerceEvents(orderEvents));
531
+ return {
532
+ orderId: row.order_id,
533
+ source: row.source,
534
+ placedAt: row.placed_at,
535
+ currency: row.currency,
536
+ payment: row.payment_state,
537
+ fulfillment: row.fulfillment_state,
538
+ stock: row.stock_state,
539
+ amountMinor: row.amount_minor,
540
+ events: orderEvents,
541
+ admitted: bound ? { evidence: admission.evidence, provenance: admission.provenance } : null,
542
+ };
543
+ });
517
544
  }
518
545
 
519
546
  /** Reset one outbox row for this tenant and stage. Does not write another order. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.7.7",
3
+ "version": "4.7.8",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",