@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.
@@ -0,0 +1,749 @@
1
+ /**
2
+ * Mirror-stock allocations for a final cart. Catalog availability is not a hold.
3
+ * One allocation per tenant, stage, order, sku, and location. Payment confirmation
4
+ * moves `reserved` to `confirmed` once; that quantity stays the decrement.
5
+ * Expiry and timeout release only `reserved`. Cancel before handoff releases once.
6
+ * A refund does not restock. There is no on-hand ledger in this schema; `onHand`
7
+ * is the mirror balance the caller resolved, never a catalog quantity.
8
+ */
9
+ import {
10
+ decideCommerceCommand,
11
+ type CommerceEvidence,
12
+ type CommerceStage,
13
+ type FulfillmentState,
14
+ type LabelState,
15
+ type PaymentState,
16
+ type StockState,
17
+ } from "@tot/public-runtime";
18
+
19
+ export const RESERVATION_AUTHORITY_HEADER = "x-tot-reservation-authority";
20
+ export const RESERVATION_AUTHORITY_NONE = "none";
21
+ /** Catalog `available` / `quantity` never authorizes a hold. */
22
+ export const CATALOG_IS_RESERVATION_AUTHORITY = false;
23
+ export const VERIFICATION_PROVIDER_OWNER = "tot-9577-tot-verification-provider";
24
+
25
+ const SKU_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,80}$/;
26
+ const ID_RE = /^[a-z0-9][a-z0-9-]{0,80}$/;
27
+ const LOCATION_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,80}$/;
28
+ const ISSUER_RE = /^[A-Za-z0-9._:/?#&=%+-]{1,200}$/;
29
+ const JTI_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,128}$/;
30
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
31
+
32
+ export interface SqlStatement {
33
+ sql: string;
34
+ params: readonly unknown[];
35
+ }
36
+
37
+ export interface StatementRunner {
38
+ all(sql: string, params: readonly unknown[]): Promise<readonly Record<string, unknown>[]>;
39
+ batch(statements: readonly SqlStatement[]): Promise<readonly (readonly Record<string, unknown>[])[]>;
40
+ }
41
+
42
+ interface SqliteStatement {
43
+ all(...params: unknown[]): unknown[];
44
+ }
45
+
46
+ export interface SqliteDatabase {
47
+ exec(sql: string): void;
48
+ prepare(sql: string): SqliteStatement;
49
+ }
50
+
51
+ interface D1Bound {
52
+ all(): Promise<unknown>;
53
+ }
54
+
55
+ export interface D1CommerceDatabase {
56
+ prepare(sql: string): { bind(...vals: unknown[]): D1Bound };
57
+ batch?(statements: unknown[]): Promise<unknown[]>;
58
+ }
59
+
60
+ export interface MirrorStockAuthority {
61
+ provider: "mirror";
62
+ readable: true;
63
+ locationId: string;
64
+ sku: string;
65
+ onHand: number;
66
+ asOf: string;
67
+ }
68
+
69
+ export interface ReserveHold {
70
+ sku: string;
71
+ locationId: string;
72
+ quantity: number;
73
+ onHand: number;
74
+ }
75
+
76
+ export interface ReserveInput {
77
+ tenantId: string;
78
+ stage: CommerceStage;
79
+ orderId: string;
80
+ cartDigest: string;
81
+ expiresAt: string;
82
+ now: string;
83
+ currency: string;
84
+ amountMinor: number;
85
+ issuer: string;
86
+ jti: string;
87
+ holds: readonly ReserveHold[];
88
+ }
89
+
90
+ export type ReserveResult =
91
+ | { ok: true; authorizationId: string; repeated: boolean; expiresAt: string }
92
+ | { ok: false; finding: "caller-replay" | "stock-unavailable" | "malformed-request" };
93
+
94
+ export interface PolicyResult {
95
+ released: number;
96
+ restock: false;
97
+ finding: string;
98
+ /** Units moved from reserved to confirmed. Present only for payment confirmation. */
99
+ decremented?: number;
100
+ }
101
+
102
+ type PolicyKind = "refund" | "cancel" | "payment-failure" | "confirm" | "expiry" | "timeout" | "restart";
103
+
104
+ export interface PolicyInput {
105
+ kind: PolicyKind;
106
+ tenantId: string;
107
+ stage: CommerceStage;
108
+ orderId?: string;
109
+ now?: string;
110
+ evidence?: CommerceEvidence;
111
+ providerOutcome?: "intent" | "accepted" | "unknown" | "voided" | "refunded" | null;
112
+ }
113
+
114
+ const FRESH_EVIDENCE: CommerceEvidence = {
115
+ verification: "fresh",
116
+ tax: "fresh",
117
+ risk: "fresh",
118
+ stock: "fresh",
119
+ };
120
+
121
+ let testOnHand: ((sku: string, locationId: string) => number | null) | null = null;
122
+
123
+ /**
124
+ * Non-production mirror balance. Production has no on-hand ledger in this schema,
125
+ * so the read stays empty and authorization fails closed.
126
+ */
127
+ export function __setMirrorOnHandForTests(reader: ((sku: string, locationId: string) => number | null) | null): void {
128
+ testOnHand = reader;
129
+ }
130
+
131
+ export function mirrorOnHand(sku: string, locationId: string): number | null {
132
+ if (process.env.NODE_ENV === "production") return null;
133
+ if (!testOnHand) return null;
134
+ const onHand = testOnHand(sku, locationId);
135
+ if (typeof onHand !== "number" || !Number.isSafeInteger(onHand) || onHand < 0) return null;
136
+ return onHand;
137
+ }
138
+
139
+ export function reservationAuthorityHeader(): typeof RESERVATION_AUTHORITY_NONE {
140
+ if (CATALOG_IS_RESERVATION_AUTHORITY) {
141
+ throw new Error("catalog availability is not reservation authority");
142
+ }
143
+ return RESERVATION_AUTHORITY_NONE;
144
+ }
145
+
146
+ export function readMirrorAuthority(value: unknown): MirrorStockAuthority | null {
147
+ if (!value || typeof value !== "object") return null;
148
+ const record = value as Record<string, unknown>;
149
+ if (record.provider !== "mirror" || record.readable !== true) return null;
150
+ if (typeof record.locationId !== "string" || !LOCATION_RE.test(record.locationId)) return null;
151
+ if (typeof record.sku !== "string" || !SKU_RE.test(record.sku)) return null;
152
+ if (typeof record.onHand !== "number" || !Number.isSafeInteger(record.onHand) || record.onHand < 0) return null;
153
+ if (typeof record.asOf !== "string" || record.asOf.trim() === "" || record.asOf.length > 40) return null;
154
+ return {
155
+ provider: "mirror",
156
+ readable: true,
157
+ locationId: record.locationId,
158
+ sku: record.sku,
159
+ onHand: record.onHand,
160
+ asOf: record.asOf,
161
+ };
162
+ }
163
+
164
+ const chains = new WeakMap<object, Promise<unknown>>();
165
+
166
+ function enqueue<T>(key: object, job: () => Promise<T> | T): Promise<T> {
167
+ const previous = chains.get(key) ?? Promise.resolve();
168
+ const run = previous.then(job, job);
169
+ chains.set(
170
+ key,
171
+ run.then(
172
+ () => undefined,
173
+ () => undefined,
174
+ ),
175
+ );
176
+ return run;
177
+ }
178
+
179
+ function asRows(value: unknown): Record<string, unknown>[] {
180
+ if (Array.isArray(value)) return value as Record<string, unknown>[];
181
+ if (value && typeof value === "object" && Array.isArray((value as { results?: unknown }).results)) {
182
+ return (value as { results: Record<string, unknown>[] }).results;
183
+ }
184
+ return [];
185
+ }
186
+
187
+ export function sqliteCommerceRunner(db: SqliteDatabase): StatementRunner {
188
+ return {
189
+ all(sql, params) {
190
+ return enqueue(db, () => db.prepare(sql).all(...params) as Record<string, unknown>[]);
191
+ },
192
+ batch(statements) {
193
+ return enqueue(db, () => {
194
+ db.exec("BEGIN IMMEDIATE");
195
+ try {
196
+ const results = statements.map(
197
+ (statement) => db.prepare(statement.sql).all(...statement.params) as Record<string, unknown>[],
198
+ );
199
+ db.exec("COMMIT");
200
+ return results;
201
+ } catch (error) {
202
+ try {
203
+ db.exec("ROLLBACK");
204
+ } catch {
205
+ /* the failed statement already aborted the transaction */
206
+ }
207
+ throw error;
208
+ }
209
+ });
210
+ },
211
+ };
212
+ }
213
+
214
+ export function d1CommerceRunner(db: D1CommerceDatabase): StatementRunner {
215
+ return {
216
+ async all(sql, params) {
217
+ return asRows(await db.prepare(sql).bind(...params).all());
218
+ },
219
+ async batch(statements) {
220
+ if (!db.batch) {
221
+ const results: Record<string, unknown>[][] = [];
222
+ for (const statement of statements) {
223
+ results.push(asRows(await db.prepare(statement.sql).bind(...statement.params).all()));
224
+ }
225
+ return results;
226
+ }
227
+ const bound: unknown[] = statements.map((statement) => db.prepare(statement.sql).bind(...statement.params));
228
+ const results = await db.batch(bound);
229
+ return results.map((result) => asRows(result));
230
+ },
231
+ };
232
+ }
233
+
234
+ function sumQuantity(rows: readonly Record<string, unknown>[]): number {
235
+ let total = 0;
236
+ for (const row of rows) {
237
+ const quantity = row.quantity;
238
+ if (typeof quantity === "number" && Number.isSafeInteger(quantity) && quantity > 0) total += quantity;
239
+ }
240
+ return total;
241
+ }
242
+
243
+ function text(row: Record<string, unknown>, key: string): string {
244
+ const value = row[key];
245
+ return typeof value === "string" ? value : "";
246
+ }
247
+
248
+ function reserveShape(input: ReserveInput): ReserveResult | null {
249
+ if (!ID_RE.test(input.tenantId) || !ID_RE.test(input.orderId)) return { ok: false, finding: "malformed-request" };
250
+ if (input.stage !== "preview" && input.stage !== "publication") return { ok: false, finding: "malformed-request" };
251
+ if (!DIGEST_RE.test(input.cartDigest) || !/^[A-Z]{3}$/.test(input.currency)) return { ok: false, finding: "malformed-request" };
252
+ if (!ISSUER_RE.test(input.issuer) || !JTI_RE.test(input.jti)) return { ok: false, finding: "malformed-request" };
253
+ if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor < 0) return { ok: false, finding: "malformed-request" };
254
+ if (input.holds.length < 1) return { ok: false, finding: "malformed-request" };
255
+ for (const hold of input.holds) {
256
+ if (!SKU_RE.test(hold.sku) || !LOCATION_RE.test(hold.locationId)) return { ok: false, finding: "malformed-request" };
257
+ if (!Number.isSafeInteger(hold.quantity) || hold.quantity < 1) return { ok: false, finding: "malformed-request" };
258
+ if (!Number.isSafeInteger(hold.onHand) || hold.onHand < 0) return { ok: false, finding: "malformed-request" };
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function allocationInsert(input: ReserveInput, hold: ReserveHold, seenAt: string): SqlStatement {
264
+ return {
265
+ sql: `INSERT INTO scrappy_allocations (tenant_id, stage, order_id, sku, location_id, quantity, state)
266
+ SELECT ?, ?, ?, ?, ?, ?, 'reserved'
267
+ WHERE (
268
+ SELECT COALESCE(SUM(quantity), 0) FROM scrappy_allocations
269
+ WHERE tenant_id = ? AND stage = ? AND sku = ? AND location_id = ? AND state IN ('reserved', 'confirmed')
270
+ ) + ? <= ?
271
+ AND NOT EXISTS (
272
+ SELECT 1 FROM scrappy_allocations
273
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND sku = ? AND location_id = ?
274
+ AND state IN ('reserved', 'confirmed')
275
+ )
276
+ AND NOT EXISTS (
277
+ SELECT 1 FROM scrappy_final_cart_authorizations
278
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
279
+ )
280
+ AND EXISTS (
281
+ SELECT 1 FROM scrappy_caller_replay
282
+ WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ? AND seen_at = ?
283
+ )
284
+ AND EXISTS (
285
+ SELECT 1 FROM scrappy_commerce_orders
286
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
287
+ )
288
+ ON CONFLICT DO NOTHING
289
+ RETURNING order_id`,
290
+ params: [
291
+ input.tenantId,
292
+ input.stage,
293
+ input.orderId,
294
+ hold.sku,
295
+ hold.locationId,
296
+ hold.quantity,
297
+ input.tenantId,
298
+ input.stage,
299
+ hold.sku,
300
+ hold.locationId,
301
+ hold.quantity,
302
+ hold.onHand,
303
+ input.tenantId,
304
+ input.stage,
305
+ input.orderId,
306
+ hold.sku,
307
+ hold.locationId,
308
+ input.tenantId,
309
+ input.stage,
310
+ input.cartDigest,
311
+ input.tenantId,
312
+ input.stage,
313
+ input.issuer,
314
+ input.jti,
315
+ seenAt,
316
+ input.tenantId,
317
+ input.stage,
318
+ input.orderId,
319
+ ],
320
+ };
321
+ }
322
+
323
+ /** Admit one hold per sku. A second active digest returns the existing authorization. */
324
+ export async function reserveAllocation(runner: StatementRunner, input: ReserveInput): Promise<ReserveResult> {
325
+ const malformed = reserveShape(input);
326
+ if (malformed) return malformed;
327
+ const seen = await runner.all(
328
+ `SELECT jti FROM scrappy_caller_replay WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ?`,
329
+ [input.tenantId, input.stage, input.issuer, input.jti],
330
+ );
331
+ if (seen.length > 0) return { ok: false, finding: "caller-replay" };
332
+
333
+ const seenAt = `${input.now}|${crypto.randomUUID()}`;
334
+ const holds = [...input.holds].sort((left, right) => left.sku.localeCompare(right.sku) || left.locationId.localeCompare(right.locationId));
335
+ const statements: SqlStatement[] = [
336
+ {
337
+ sql: `INSERT INTO scrappy_caller_replay (tenant_id, stage, issuer, jti, seen_at)
338
+ VALUES (?, ?, ?, ?, ?)
339
+ ON CONFLICT DO NOTHING
340
+ RETURNING jti`,
341
+ params: [input.tenantId, input.stage, input.issuer, input.jti, seenAt],
342
+ },
343
+ {
344
+ sql: `INSERT INTO scrappy_commerce_orders (
345
+ tenant_id, stage, order_id, source, source_order_id, currency, placed_at,
346
+ payment_state, compliance_state, stock_state, fulfillment_state, label_state,
347
+ amount_minor, event_version
348
+ ) VALUES (?, ?, ?, 'scrappy', ?, ?, ?, 'pending', 'UNKNOWN', 'unallocated', 'held', 'none', ?, 1)
349
+ ON CONFLICT DO NOTHING
350
+ RETURNING order_id`,
351
+ params: [input.tenantId, input.stage, input.orderId, input.orderId, input.currency, input.now, input.amountMinor],
352
+ },
353
+ ...holds.map((hold) => allocationInsert(input, hold, seenAt)),
354
+ {
355
+ sql: `INSERT INTO scrappy_final_cart_authorizations (
356
+ tenant_id, stage, authorization_id, cart_digest, expires_at, state
357
+ )
358
+ SELECT ?, ?, ?, ?, ?, 'active'
359
+ WHERE (
360
+ SELECT COUNT(*) FROM scrappy_allocations
361
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
362
+ ) = ?
363
+ AND NOT EXISTS (
364
+ SELECT 1 FROM scrappy_final_cart_authorizations
365
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
366
+ )
367
+ AND EXISTS (
368
+ SELECT 1 FROM scrappy_caller_replay
369
+ WHERE tenant_id = ? AND stage = ? AND issuer = ? AND jti = ? AND seen_at = ?
370
+ )
371
+ ON CONFLICT DO NOTHING
372
+ RETURNING authorization_id`,
373
+ params: [
374
+ input.tenantId,
375
+ input.stage,
376
+ input.orderId,
377
+ input.cartDigest,
378
+ input.expiresAt,
379
+ input.tenantId,
380
+ input.stage,
381
+ input.orderId,
382
+ holds.length,
383
+ input.tenantId,
384
+ input.stage,
385
+ input.cartDigest,
386
+ input.tenantId,
387
+ input.stage,
388
+ input.issuer,
389
+ input.jti,
390
+ seenAt,
391
+ ],
392
+ },
393
+ {
394
+ sql: `UPDATE scrappy_allocations
395
+ SET state = 'released'
396
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
397
+ AND NOT EXISTS (
398
+ SELECT 1 FROM scrappy_final_cart_authorizations
399
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
400
+ )
401
+ RETURNING quantity`,
402
+ params: [input.tenantId, input.stage, input.orderId, input.tenantId, input.stage, input.orderId],
403
+ },
404
+ {
405
+ sql: `UPDATE scrappy_commerce_orders
406
+ SET stock_state = 'reserved'
407
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
408
+ AND EXISTS (
409
+ SELECT 1 FROM scrappy_final_cart_authorizations
410
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
411
+ )
412
+ RETURNING order_id`,
413
+ params: [input.tenantId, input.stage, input.orderId, input.tenantId, input.stage, input.orderId],
414
+ },
415
+ {
416
+ sql: `SELECT authorization_id, expires_at FROM scrappy_final_cart_authorizations
417
+ WHERE tenant_id = ? AND stage = ? AND cart_digest = ? AND state IN ('active', 'consumed')
418
+ ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END
419
+ LIMIT 1`,
420
+ params: [input.tenantId, input.stage, input.cartDigest],
421
+ },
422
+ ];
423
+
424
+ const results = await runner.batch(statements);
425
+ const replayRows = results[0] ?? [];
426
+ const authRows = results[2 + holds.length] ?? [];
427
+ const selected = results[results.length - 1] ?? [];
428
+ if (authRows.length > 0) {
429
+ return { ok: true, authorizationId: input.orderId, repeated: false, expiresAt: input.expiresAt };
430
+ }
431
+ if (replayRows.length === 0) return { ok: false, finding: "caller-replay" };
432
+ const existing = selected[0];
433
+ if (existing) {
434
+ return {
435
+ ok: true,
436
+ authorizationId: text(existing, "authorization_id"),
437
+ repeated: true,
438
+ expiresAt: text(existing, "expires_at"),
439
+ };
440
+ }
441
+ return { ok: false, finding: "stock-unavailable" };
442
+ }
443
+
444
+ export async function releaseHoldOnce(
445
+ runner: StatementRunner,
446
+ input: {
447
+ tenantId: string;
448
+ stage: CommerceStage;
449
+ orderId: string;
450
+ allocationState: "released" | "expired";
451
+ authState: "denied" | "expired";
452
+ orderStock: StockState;
453
+ },
454
+ ): Promise<number> {
455
+ const scope = [input.tenantId, input.stage, input.orderId];
456
+ const results = await runner.batch([
457
+ {
458
+ sql: `UPDATE scrappy_final_cart_authorizations
459
+ SET state = ?
460
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state IN ('active', 'consumed')
461
+ RETURNING authorization_id`,
462
+ params: [input.authState, ...scope],
463
+ },
464
+ {
465
+ sql: `UPDATE scrappy_allocations
466
+ SET state = ?
467
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state IN ('reserved', 'confirmed')
468
+ RETURNING quantity`,
469
+ params: [input.allocationState, ...scope],
470
+ },
471
+ {
472
+ sql: `UPDATE scrappy_commerce_orders
473
+ SET stock_state = ?
474
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND stock_state IN ('reserved', 'confirmed')
475
+ RETURNING order_id`,
476
+ params: [input.orderStock, ...scope],
477
+ },
478
+ ]);
479
+ return sumQuantity(results[1] ?? []);
480
+ }
481
+
482
+ export async function settleExpired(
483
+ runner: StatementRunner,
484
+ input: { tenantId: string; stage: CommerceStage; now: string; orderId?: string; force: boolean },
485
+ ): Promise<number> {
486
+ const orderId = input.orderId ?? "";
487
+ const force = input.force ? 1 : 0;
488
+ const due = [input.tenantId, input.stage, orderId, orderId, input.now, force];
489
+ const results = await runner.batch([
490
+ {
491
+ sql: `UPDATE scrappy_final_cart_authorizations
492
+ SET state = 'expired'
493
+ WHERE tenant_id = ? AND stage = ? AND state = 'active'
494
+ AND (? = '' OR authorization_id = ?)
495
+ AND (expires_at <= ? OR ? = 1)
496
+ RETURNING authorization_id`,
497
+ params: due,
498
+ },
499
+ {
500
+ sql: `UPDATE scrappy_allocations
501
+ SET state = 'expired'
502
+ WHERE tenant_id = ? AND stage = ? AND state = 'reserved'
503
+ AND order_id IN (
504
+ SELECT authorization_id FROM scrappy_final_cart_authorizations
505
+ WHERE tenant_id = ? AND stage = ? AND state = 'expired'
506
+ AND (? = '' OR authorization_id = ?)
507
+ AND (expires_at <= ? OR ? = 1)
508
+ )
509
+ RETURNING quantity`,
510
+ params: [input.tenantId, input.stage, ...due],
511
+ },
512
+ {
513
+ sql: `UPDATE scrappy_commerce_orders
514
+ SET stock_state = 'expired'
515
+ WHERE tenant_id = ? AND stage = ? AND stock_state IN ('reserved', 'confirmed')
516
+ AND order_id IN (
517
+ SELECT authorization_id FROM scrappy_final_cart_authorizations
518
+ WHERE tenant_id = ? AND stage = ? AND state = 'expired'
519
+ AND (? = '' OR authorization_id = ?)
520
+ AND (expires_at <= ? OR ? = 1)
521
+ )
522
+ RETURNING order_id`,
523
+ params: [input.tenantId, input.stage, ...due],
524
+ },
525
+ ]);
526
+ return sumQuantity(results[1] ?? []);
527
+ }
528
+
529
+ export async function releaseOrphanAllocations(
530
+ runner: StatementRunner,
531
+ input: { tenantId: string; stage: CommerceStage },
532
+ ): Promise<number> {
533
+ const results = await runner.batch([
534
+ {
535
+ sql: `UPDATE scrappy_allocations
536
+ SET state = 'released'
537
+ WHERE tenant_id = ? AND stage = ?
538
+ AND state IN ('reserved', 'confirmed')
539
+ AND NOT EXISTS (
540
+ SELECT 1 FROM scrappy_final_cart_authorizations AS auth
541
+ WHERE auth.tenant_id = scrappy_allocations.tenant_id
542
+ AND auth.stage = scrappy_allocations.stage
543
+ AND auth.authorization_id = scrappy_allocations.order_id
544
+ AND auth.state IN ('active', 'consumed')
545
+ )
546
+ RETURNING quantity`,
547
+ params: [input.tenantId, input.stage],
548
+ },
549
+ ]);
550
+ return sumQuantity(results[0] ?? []);
551
+ }
552
+
553
+ interface OrderRow {
554
+ payment: PaymentState;
555
+ stock: StockState;
556
+ fulfillment: FulfillmentState;
557
+ label: LabelState;
558
+ }
559
+
560
+ async function loadOrder(
561
+ runner: StatementRunner,
562
+ tenantId: string,
563
+ stage: CommerceStage,
564
+ orderId: string,
565
+ ): Promise<OrderRow | null> {
566
+ const rows = await runner.all(
567
+ `SELECT payment_state, stock_state, fulfillment_state, label_state
568
+ FROM scrappy_commerce_orders WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
569
+ [tenantId, stage, orderId],
570
+ );
571
+ const row = rows[0];
572
+ if (!row) return null;
573
+ const allocs = await runner.all(
574
+ `SELECT state FROM scrappy_allocations WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
575
+ [tenantId, stage, orderId],
576
+ );
577
+ const states = allocs.map((alloc) => text(alloc, "state"));
578
+ const stock = states.includes("reserved")
579
+ ? "reserved"
580
+ : states.includes("confirmed")
581
+ ? "confirmed"
582
+ : (text(row, "stock_state") as StockState);
583
+ return {
584
+ payment: text(row, "payment_state") as PaymentState,
585
+ stock,
586
+ fulfillment: text(row, "fulfillment_state") as FulfillmentState,
587
+ label: text(row, "label_state") as LabelState,
588
+ };
589
+ }
590
+
591
+ function commandFinding(reason: string): string {
592
+ switch (reason) {
593
+ case "stock":
594
+ return "stock-unknown";
595
+ case "fulfillment":
596
+ return "fulfillment-refused";
597
+ case "reconcile":
598
+ return "payment-reconcile";
599
+ case "payment":
600
+ return "payment-refused";
601
+ case "evidence":
602
+ return "evidence-refused";
603
+ default:
604
+ return "fulfillment-refused";
605
+ }
606
+ }
607
+
608
+ /** Move reserved units to confirmed once. A second call decrements nothing. */
609
+ export async function confirmHoldOnce(
610
+ runner: StatementRunner,
611
+ input: { tenantId: string; stage: CommerceStage; orderId: string },
612
+ ): Promise<{ decremented: number; states: string[] }> {
613
+ const scope = [input.tenantId, input.stage, input.orderId];
614
+ const results = await runner.batch([
615
+ {
616
+ sql: `UPDATE scrappy_allocations
617
+ SET state = 'confirmed'
618
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'reserved'
619
+ AND EXISTS (
620
+ SELECT 1 FROM scrappy_commerce_orders
621
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
622
+ AND fulfillment_state IN ('held', 'ready')
623
+ AND payment_state IN ('pending', 'authorized', 'captured')
624
+ )
625
+ AND EXISTS (
626
+ SELECT 1 FROM scrappy_final_cart_authorizations
627
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
628
+ )
629
+ RETURNING quantity`,
630
+ params: [...scope, ...scope, ...scope],
631
+ },
632
+ {
633
+ sql: `UPDATE scrappy_final_cart_authorizations
634
+ SET state = 'consumed'
635
+ WHERE tenant_id = ? AND stage = ? AND authorization_id = ? AND state = 'active'
636
+ AND EXISTS (
637
+ SELECT 1 FROM scrappy_allocations
638
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'confirmed'
639
+ )
640
+ RETURNING authorization_id`,
641
+ params: [...scope, ...scope],
642
+ },
643
+ {
644
+ sql: `UPDATE scrappy_commerce_orders
645
+ SET payment_state = CASE
646
+ WHEN payment_state IN ('pending', 'authorized') THEN 'captured'
647
+ ELSE payment_state
648
+ END,
649
+ stock_state = 'confirmed'
650
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
651
+ AND fulfillment_state IN ('held', 'ready')
652
+ AND payment_state IN ('pending', 'authorized', 'captured')
653
+ AND EXISTS (
654
+ SELECT 1 FROM scrappy_allocations
655
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND state = 'confirmed'
656
+ )
657
+ RETURNING order_id`,
658
+ params: [...scope, ...scope],
659
+ },
660
+ {
661
+ sql: `SELECT state FROM scrappy_allocations
662
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
663
+ params: scope,
664
+ },
665
+ ]);
666
+ return {
667
+ decremented: sumQuantity(results[0] ?? []),
668
+ states: (results[3] ?? []).map((row) => text(row, "state")),
669
+ };
670
+ }
671
+
672
+ /** Apply the recorded release policy. `restock` stays false on every result. */
673
+ export async function applyAllocationPolicy(runner: StatementRunner, input: PolicyInput): Promise<PolicyResult> {
674
+ const scope = { tenantId: input.tenantId, stage: input.stage };
675
+ if (input.kind === "confirm") {
676
+ if (!input.orderId) return { released: 0, restock: false, decremented: 0, finding: "order-absent" };
677
+ const order = await loadOrder(runner, input.tenantId, input.stage, input.orderId);
678
+ if (!order) return { released: 0, restock: false, decremented: 0, finding: "order-absent" };
679
+ if (order.fulfillment === "handed_off") return { released: 0, restock: false, decremented: 0, finding: "shipped-out-of-scope" };
680
+ if (order.fulfillment === "cancelled" || order.fulfillment === "UNKNOWN") {
681
+ return { released: 0, restock: false, decremented: 0, finding: "fulfillment-refused" };
682
+ }
683
+ const confirmed = await confirmHoldOnce(runner, { tenantId: input.tenantId, stage: input.stage, orderId: input.orderId });
684
+ if (confirmed.decremented > 0) return { released: 0, restock: false, decremented: confirmed.decremented, finding: "confirmed" };
685
+ if (confirmed.states.includes("confirmed")) return { released: 0, restock: false, decremented: 0, finding: "already-confirmed" };
686
+ return { released: 0, restock: false, decremented: 0, finding: "already-released" };
687
+ }
688
+ if (input.kind === "restart") {
689
+ const released = await releaseOrphanAllocations(runner, scope);
690
+ return { released, restock: false, finding: "restart-reconciled" };
691
+ }
692
+ if (input.kind === "expiry" || input.kind === "timeout") {
693
+ const released = await settleExpired(runner, {
694
+ ...scope,
695
+ now: input.now ?? "",
696
+ orderId: input.orderId,
697
+ force: input.kind === "timeout",
698
+ });
699
+ return { released, restock: false, finding: released > 0 ? "expired" : "already-released" };
700
+ }
701
+ if (!input.orderId) return { released: 0, restock: false, finding: "order-absent" };
702
+ const order = await loadOrder(runner, input.tenantId, input.stage, input.orderId);
703
+ if (!order) return { released: 0, restock: false, finding: "order-absent" };
704
+
705
+ if (input.kind === "payment-failure") {
706
+ if (order.fulfillment === "handed_off") return { released: 0, restock: false, finding: "shipped-out-of-scope" };
707
+ if (order.stock === "confirmed" || order.payment === "captured") {
708
+ return { released: 0, restock: false, finding: "already-confirmed" };
709
+ }
710
+ const released = await releaseHoldOnce(runner, {
711
+ tenantId: input.tenantId,
712
+ stage: input.stage,
713
+ orderId: input.orderId,
714
+ allocationState: "released",
715
+ authState: "denied",
716
+ orderStock: "released",
717
+ });
718
+ return { released, restock: false, finding: released > 0 ? "released" : "already-released" };
719
+ }
720
+
721
+ const decision = decideCommerceCommand({
722
+ command: input.kind,
723
+ payment: order.payment,
724
+ stock: order.stock,
725
+ fulfillment: order.fulfillment,
726
+ label: order.label,
727
+ evidence: input.evidence ?? FRESH_EVIDENCE,
728
+ providerOutcome: input.providerOutcome ?? null,
729
+ });
730
+ if (input.kind === "refund") {
731
+ return {
732
+ released: 0,
733
+ restock: false,
734
+ finding: decision.admit ? "refund-no-restock" : commandFinding(decision.reason),
735
+ };
736
+ }
737
+ if (!decision.admit || !decision.releaseReservation) {
738
+ return { released: 0, restock: false, finding: decision.admit ? "cancel-no-release" : commandFinding(decision.reason) };
739
+ }
740
+ const released = await releaseHoldOnce(runner, {
741
+ tenantId: input.tenantId,
742
+ stage: input.stage,
743
+ orderId: input.orderId,
744
+ allocationState: "released",
745
+ authState: "denied",
746
+ orderStock: "released",
747
+ });
748
+ return { released, restock: false, finding: released > 0 ? "released" : "already-released" };
749
+ }