@openlup/platform 0.22.0 → 0.24.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/CHANGELOG.md +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -0
- package/dist/invoicing/contracts/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +39 -0
- package/dist/invoicing/index.js +37 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -0
- package/dist/subscriptions/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +23 -3
- package/release-gates.json +74 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +71 -0
- package/src/invoicing/contracts/host.ts +84 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +102 -0
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +39 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -0
- package/src/subscriptions/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
|
|
2
|
-
import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, LocalStock, StockSnapshot, StockMovement } from "./contracts/index.js";
|
|
2
|
+
import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, LocalStock, StockSnapshot, StockMovement, SyncCursor } from "./contracts/index.js";
|
|
3
3
|
type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
|
|
4
4
|
interface StockSyncResult {
|
|
5
5
|
ok: boolean; checked: number; updated: number; mismatches: number; reservationCoverage: number;
|
|
6
6
|
unclassified: number; lowStock: number; movements: number; replayed: number; failures: number;
|
|
7
7
|
providerCalls: number; readBacks: number; skipped: boolean; syncRunId: string;
|
|
8
8
|
lastMovementOccurredAt: string | null; reason?: string;
|
|
9
|
+
rejected: number; items: NonNullable<SyncCursor["items"]>[number][];
|
|
10
|
+
providerError?: NonNullable<SyncCursor["error"]>["providerError"];
|
|
11
|
+
refusal?: NonNullable<SyncCursor["refusal"]>;
|
|
9
12
|
}
|
|
10
13
|
export async function runFulfillmentStockSyncWorker(input: {
|
|
11
14
|
port: FulfillmentStockSyncPort;
|
|
@@ -26,6 +29,8 @@ export async function runFulfillmentStockSyncWorker(input: {
|
|
|
26
29
|
const syncRunId = input.compatibility.syncRunId(now);
|
|
27
30
|
const result: StockSyncResult = {
|
|
28
31
|
ok: true,
|
|
32
|
+
rejected: 0,
|
|
33
|
+
items: [],
|
|
29
34
|
checked: 0,
|
|
30
35
|
updated: 0,
|
|
31
36
|
mismatches: 0,
|
|
@@ -42,152 +47,218 @@ export async function runFulfillmentStockSyncWorker(input: {
|
|
|
42
47
|
lastMovementOccurredAt: null,
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
const alerts: NonNullable<SyncCursor["alerts"]>[number][] = [];
|
|
51
|
+
let stage = "cursor_read_failed";
|
|
52
|
+
let operationalReason: string | undefined;
|
|
53
|
+
let staleSync: SyncCursor["staleSync"];
|
|
54
|
+
let recoverStaleSync = false;
|
|
55
|
+
let previousCursor: SyncCursor | null = null;
|
|
56
|
+
let rejectedStockResponse = false;
|
|
57
|
+
const currentCoverageGapSkus = new Set<string>();
|
|
58
|
+
try {
|
|
59
|
+
previousCursor = await input.port.readCursor();
|
|
60
|
+
|
|
61
|
+
// Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
|
|
62
|
+
// low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
|
|
63
|
+
// below. A never-synced state (no prior cursor) is not "stale".
|
|
64
|
+
const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
|
|
65
|
+
const staleGapHours =
|
|
66
|
+
lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
|
|
67
|
+
const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
|
|
68
|
+
recoverStaleSync = !syncIsStale;
|
|
69
|
+
if (syncIsStale) {
|
|
70
|
+
staleSync = { gapHours: Math.round(staleGapHours! * 10) / 10, thresholdHours: staleThresholdHours, detectedAt: now.toISOString() };
|
|
71
|
+
stage = "stock_evidence_write_failed";
|
|
72
|
+
alerts.push({ skuRef: input.compatibility.staleEvidenceSku, thresholdKind: "stale_sync", severity: "warning",
|
|
73
|
+
providerForSaleQuantity: 0, localAvailableQuantity: 0, ...staleSync });
|
|
74
|
+
await input.port.recordLowStockEvidence({
|
|
75
|
+
requestKey: input.compatibility.staleKey(previousCursor!.lastStockSyncedAt!),
|
|
76
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
77
|
+
thresholdKind: "stale_sync",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
providerForSaleQuantity: 0,
|
|
80
|
+
localAvailableQuantity: 0,
|
|
81
|
+
syncRunId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
stage = "cursor_write_failed";
|
|
86
|
+
await input.port.recordCursor({
|
|
62
87
|
syncRunId,
|
|
88
|
+
status: "running",
|
|
89
|
+
lastStockSyncedAt: null,
|
|
90
|
+
lastMovementOccurredAt: null,
|
|
91
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
92
|
+
coverageGapSkus: previousCursor?.coverageGapSkus ?? [], refusal: null,
|
|
63
93
|
});
|
|
64
|
-
}
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
stage = "stock_read_failed";
|
|
96
|
+
result.providerCalls += 1;
|
|
97
|
+
const [localStock, providerStock] = await Promise.all([
|
|
98
|
+
input.port.readLocalInventoryStock(now.toISOString()),
|
|
99
|
+
input.provider.getStock(),
|
|
100
|
+
]);
|
|
101
|
+
const stockItems: { stock: StockSnapshot; itemIndex: number }[] = [];
|
|
102
|
+
for (const [itemIndex, item] of providerStock.entries()) {
|
|
103
|
+
if ("kind" in item && item.kind === "rejected") {
|
|
104
|
+
result.rejected += 1;
|
|
105
|
+
result.failures += 1;
|
|
106
|
+
result.items.push({ kind: "stock", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
|
|
107
|
+
} else if ("skuRef" in item && item.skuRef.trim()) {
|
|
108
|
+
stockItems.push({ stock: item, itemIndex });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const skus = [...new Set(stockItems.map(({ stock }) => stock.skuRef))];
|
|
112
|
+
rejectedStockResponse = providerStock.length > 0 && stockItems.length === 0;
|
|
113
|
+
stage = "stock_inventory_read_failed";
|
|
114
|
+
const [inventoryClasses, activeReservations] = await Promise.all([
|
|
115
|
+
input.port.readSkuInventoryClasses(skus),
|
|
116
|
+
input.port.readActiveReservations(skus, now.toISOString()),
|
|
117
|
+
]);
|
|
73
118
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
input.provider.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const coverageCandidates: Array<{
|
|
94
|
-
stock: StockSnapshot;
|
|
95
|
-
local: LocalStock;
|
|
96
|
-
activeReserved: number;
|
|
97
|
-
}> = [];
|
|
98
|
-
const currentCoverageGapSkus = new Set<string>();
|
|
99
|
-
const packagingSkus = new Set<string>();
|
|
100
|
-
const previousCoverageGapSkus = previousCursor?.status === "succeeded"
|
|
101
|
-
? readStringSet(previousCursor.coverageGapSkus)
|
|
102
|
-
: new Set<string>();
|
|
119
|
+
stage = "stock_movements_read_failed";
|
|
120
|
+
result.providerCalls += 1;
|
|
121
|
+
const movementRows = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
|
|
122
|
+
const movements: StockMovement[] = [];
|
|
123
|
+
for (const item of movementRows) {
|
|
124
|
+
if ("kind" in item && item.kind === "rejected") {
|
|
125
|
+
result.rejected += 1;
|
|
126
|
+
result.failures += 1;
|
|
127
|
+
result.items.push({ kind: "movement", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
|
|
128
|
+
} else if ("skuRef" in item) {
|
|
129
|
+
movements.push(item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (rejectedStockResponse || (result.rejected > 0 && stockItems.length + movements.length === 0)) {
|
|
133
|
+
result.ok = false;
|
|
134
|
+
result.reason = "invalid_sku_ref";
|
|
135
|
+
}
|
|
136
|
+
result.movements = movements.length;
|
|
137
|
+
result.lastMovementOccurredAt = latestOccurredAt(movements);
|
|
103
138
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
const localBySku = aggregateAtpCompatibleStock(localStock, now);
|
|
140
|
+
const coverageCandidates: Array<{
|
|
141
|
+
stock: StockSnapshot;
|
|
142
|
+
local: LocalStock;
|
|
143
|
+
activeReserved: number;
|
|
144
|
+
}> = [];
|
|
145
|
+
const packagingSkus = new Set<string>();
|
|
146
|
+
const previousCoverageGapSkus = previousCursor?.status === "succeeded"
|
|
147
|
+
? readStringSet(previousCursor.coverageGapSkus)
|
|
148
|
+
: new Set<string>();
|
|
149
|
+
|
|
150
|
+
for (const { stock, itemIndex } of stockItems) {
|
|
151
|
+
try {
|
|
152
|
+
const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
|
|
153
|
+
const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
|
|
154
|
+
const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
|
|
155
|
+
const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
|
|
156
|
+
stage = "stock_current_write_failed";
|
|
157
|
+
const current = await input.port.recordProviderStockCurrent({
|
|
158
|
+
requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
|
|
159
|
+
snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
|
|
160
|
+
});
|
|
161
|
+
stage = "snapshot_write_failed";
|
|
162
|
+
const snapshot = await input.port.recordStockSnapshot({
|
|
163
|
+
requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
|
|
164
|
+
snapshot: stock, local, mismatchKind: "none", inventoryClass,
|
|
165
|
+
});
|
|
118
166
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
167
|
+
result.items.push({ itemIndex, skuRef: stock.skuRef, status: "recorded", activeReserved,
|
|
168
|
+
sellableNow: inventoryClass === "sellable" ? Math.max(0, stock.forSaleQuantity - activeReserved - local.safetyStock) : null,
|
|
169
|
+
coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
|
|
170
|
+
result.checked += 1;
|
|
171
|
+
result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
|
|
172
|
+
if (snapshot.replayed) result.replayed += 1;
|
|
173
|
+
if (current.replayed) result.replayed += 1;
|
|
174
|
+
if (snapshot.readBack) result.readBacks += 1;
|
|
175
|
+
if (current.readBack) result.readBacks += 1;
|
|
176
|
+
if (inventoryClass === null) result.unclassified += 1;
|
|
177
|
+
if (inventoryClass === "packaging") packagingSkus.add(stock.skuRef);
|
|
178
|
+
if (inventoryClass === "sellable") {
|
|
179
|
+
coverageCandidates.push({ stock, local, activeReserved });
|
|
180
|
+
if (activeReserved > stock.forSaleQuantity) currentCoverageGapSkus.add(stock.skuRef);
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
result.ok = false;
|
|
184
|
+
result.failures += 1;
|
|
185
|
+
const reason = safeReason(error, stage);
|
|
186
|
+
operationalReason ??= reason;
|
|
187
|
+
result.items.push({ itemIndex, skuRef: stock.skuRef, status: "failed", reason });
|
|
130
188
|
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
result.ok = false;
|
|
133
|
-
result.failures += 1;
|
|
134
|
-
result.reason = result.reason ?? "stock_sync_operation_failed";
|
|
135
189
|
}
|
|
136
|
-
}
|
|
137
190
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
191
|
+
if (result.ok) {
|
|
192
|
+
for (const { stock, local, activeReserved } of coverageCandidates) {
|
|
193
|
+
const localAvailable = availableQuantity(local);
|
|
194
|
+
const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
|
|
195
|
+
const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
|
|
196
|
+
const activeThresholdKinds: Threshold[] = lowStockSignals.map((signal) => signal.thresholdKind);
|
|
197
|
+
if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
|
|
198
|
+
activeThresholdKinds.push("reservation_coverage");
|
|
199
|
+
}
|
|
200
|
+
for (const signal of lowStockSignals) {
|
|
201
|
+
alerts.push({ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
202
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, detectedAt: now.toISOString(),
|
|
203
|
+
activeReserved, coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
|
|
204
|
+
stage = "stock_evidence_write_failed";
|
|
205
|
+
const evidence = await input.port.recordLowStockEvidence({
|
|
206
|
+
requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
|
|
207
|
+
skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
208
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
|
|
209
|
+
});
|
|
210
|
+
if (evidence.replayed) result.replayed += 1;
|
|
211
|
+
result.lowStock += evidence.replayed ? 0 : 1;
|
|
212
|
+
if (signal.thresholdKind === "reservation_coverage") result.reservationCoverage += 1;
|
|
213
|
+
}
|
|
214
|
+
stage = "stock_evidence_resolve_failed";
|
|
215
|
+
await input.port.resolveLowStockEvidence({
|
|
216
|
+
skuRef: stock.skuRef,
|
|
217
|
+
activeThresholdKinds,
|
|
218
|
+
syncRunId,
|
|
219
|
+
});
|
|
146
220
|
}
|
|
147
|
-
for (const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
skuRef:
|
|
151
|
-
|
|
221
|
+
for (const sku of packagingSkus) {
|
|
222
|
+
stage = "stock_evidence_resolve_failed";
|
|
223
|
+
await input.port.resolveLowStockEvidence({
|
|
224
|
+
skuRef: sku,
|
|
225
|
+
activeThresholdKinds: [],
|
|
226
|
+
syncRunId,
|
|
152
227
|
});
|
|
153
|
-
if (evidence.replayed) result.replayed += 1;
|
|
154
|
-
result.lowStock += evidence.replayed ? 0 : 1;
|
|
155
|
-
if (signal.thresholdKind === "reservation_coverage") result.reservationCoverage += 1;
|
|
156
228
|
}
|
|
157
|
-
await input.port.resolveLowStockEvidence({
|
|
158
|
-
skuRef: stock.skuRef,
|
|
159
|
-
activeThresholdKinds,
|
|
160
|
-
syncRunId,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
for (const sku of packagingSkus) {
|
|
164
|
-
await input.port.resolveLowStockEvidence({
|
|
165
|
-
skuRef: sku,
|
|
166
|
-
activeThresholdKinds: [],
|
|
167
|
-
syncRunId,
|
|
168
|
-
});
|
|
169
229
|
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
result.ok = false;
|
|
232
|
+
result.failures += 1;
|
|
233
|
+
result.providerError = providerDiagnostic(error);
|
|
234
|
+
if (stage === "stock_read_failed" || stage === "stock_movements_read_failed") result.refusal = admissionRefusal(error);
|
|
235
|
+
operationalReason ??= result.providerError ? `provider_${result.providerError.operation}_failed:${result.providerError.code}` : result.refusal?.reason ?? safeReason(error, stage);
|
|
170
236
|
}
|
|
171
237
|
|
|
238
|
+
result.reason = operationalReason ?? result.reason;
|
|
239
|
+
if (!result.ok) result.refusal ??= { reason: result.failures === result.rejected ? "malformed" : "io_failure", phase: "sync", retryable: result.providerError?.retryable ?? result.failures > result.rejected };
|
|
172
240
|
await input.port.recordCursor({
|
|
173
|
-
status: result.ok ? "succeeded" : "failed",
|
|
241
|
+
status: result.ok ? "succeeded" : "failed", syncRunId,
|
|
174
242
|
lastStockSyncedAt: result.ok ? now.toISOString() : null,
|
|
175
243
|
lastMovementOccurredAt: result.lastMovementOccurredAt,
|
|
176
244
|
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
177
|
-
coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : [],
|
|
178
|
-
refusal: result.
|
|
245
|
+
coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : rejectedStockResponse ? previousCursor?.coverageGapSkus ?? [] : [],
|
|
246
|
+
refusal: result.refusal ?? null,
|
|
247
|
+
error: result.ok ? null : { reason: result.reason!, ...(result.providerError ? { providerError: result.providerError } : {}) },
|
|
248
|
+
counts: { checked: result.checked, updated: result.updated, movements: result.movements, rejected: result.rejected,
|
|
249
|
+
failures: result.failures, providerCalls: result.providerCalls, readBacks: result.readBacks },
|
|
250
|
+
items: result.items, alerts, staleSync,
|
|
179
251
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
252
|
+
// Recovery follows the durable succeeded cursor; a failed cleanup leaves the completed run intact.
|
|
253
|
+
if (result.ok && recoverStaleSync) {
|
|
254
|
+
try {
|
|
255
|
+
await input.port.resolveLowStockEvidence({
|
|
256
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
257
|
+
activeThresholdKinds: [],
|
|
258
|
+
syncRunId,
|
|
259
|
+
});
|
|
260
|
+
} catch { /* Best-effort cleanup must not undo the completed run. */ }
|
|
189
261
|
}
|
|
190
|
-
|
|
191
262
|
return result;
|
|
192
263
|
}
|
|
193
264
|
|
|
@@ -220,3 +291,28 @@ function latestOccurredAt(movements: StockMovement[]): string | null {
|
|
|
220
291
|
.filter((value): value is string => Boolean(value))
|
|
221
292
|
.sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
|
|
222
293
|
}
|
|
294
|
+
|
|
295
|
+
/** Keep the operation's closed identity and SQLSTATE only, never arbitrary exception prose. */
|
|
296
|
+
function safeReason(error: unknown, operation: string): string {
|
|
297
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
298
|
+
if (typeof code === "string" && /^[0-9A-Z]{5}$/.test(code)) return `${operation}:${code}`;
|
|
299
|
+
const known = error instanceof Error ? error.message.match(new RegExp(`^${operation}:([0-9A-Z]{5})$`)) : null;
|
|
300
|
+
return known ? `${operation}:${known[1]}` : operation;
|
|
301
|
+
}
|
|
302
|
+
function providerDiagnostic(error: unknown): StockSyncResult["providerError"] {
|
|
303
|
+
if (!error || typeof error !== "object" || !("providerError" in error)) return undefined;
|
|
304
|
+
const detail = error.providerError;
|
|
305
|
+
if (!detail || typeof detail !== "object" || !("operation" in detail) || !("code" in detail) || !("status" in detail) || !("retryable" in detail)) return undefined;
|
|
306
|
+
if (detail.operation !== "get_stock" && detail.operation !== "get_stock_movements") return undefined;
|
|
307
|
+
if (typeof detail.code !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(detail.code)) return undefined;
|
|
308
|
+
return { operation: detail.operation, code: detail.code,
|
|
309
|
+
status: typeof detail.status === "number" && Number.isInteger(detail.status) && detail.status >= 100 && detail.status <= 599 ? detail.status : null,
|
|
310
|
+
retryable: detail.retryable === true };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function admissionRefusal(error: unknown): StockSyncResult["refusal"] {
|
|
314
|
+
if (!error || typeof error !== "object" || !("reason" in error) || !("phase" in error) || !("retryable" in error)) return undefined;
|
|
315
|
+
if (error.phase !== "sync" || typeof error.retryable !== "boolean") return undefined;
|
|
316
|
+
if (error.reason !== "disabled" && error.reason !== "not_ready" && error.reason !== "malformed") return undefined;
|
|
317
|
+
return { reason: error.reason, phase: error.phase, retryable: error.retryable };
|
|
318
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FulfillmentTrackingReference } from "./contracts/index.js";
|
|
1
|
+
import type { FulfillmentTrackingReference, FulfillmentObservationCodecs } from "./contracts/index.js";
|
|
2
2
|
|
|
3
3
|
export function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean {
|
|
4
4
|
return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
|
|
@@ -7,7 +7,8 @@ export function trackingReferenceComplete(ref: FulfillmentTrackingReference): bo
|
|
|
7
7
|
export function enrichTrackingReference(
|
|
8
8
|
ref: FulfillmentTrackingReference,
|
|
9
9
|
deliveryCarrier: { carrierRef: string | null; serviceRef: string | null } | null,
|
|
10
|
-
codecs: {
|
|
10
|
+
codecs: FulfillmentObservationCodecs & {
|
|
11
|
+
approveTrackingUrl?(url: string): boolean;
|
|
11
12
|
normalizeCarrier(input: { carrierRef: string | null; serviceRef: string | null }): string | null;
|
|
12
13
|
publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
|
|
13
14
|
},
|
|
@@ -1,20 +1,35 @@
|
|
|
1
|
-
import type { FulfillmentObservation, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
|
|
1
|
+
import type { FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
|
|
2
|
+
import { safeDiagnosticCode } from "./dispatchProjection.js";
|
|
2
3
|
import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
3
4
|
import { observationStatusPolicy, projectObservation } from "./observationProjection.js";
|
|
4
5
|
import { uniqueTrackingReferences } from "./providerTrackingReferences.js";
|
|
5
6
|
|
|
6
7
|
type EffectInput = Parameters<typeof applyFulfillmentObservationEffects<FulfillmentObservation["fact"]["canonicalStatus"], "in_transit" | "delivered">>[0];
|
|
7
8
|
type DispatchRef = { orderRef: string; fulfillmentRef: string; dispatchRef: string; externalRef: string | null };
|
|
9
|
+
export interface FulfillmentIngressCounts { invalidTimestamps: number; rejectedCorrelationRefs: number; rejectedTrackingRefs: number; rejectedTrackingUrls: number }
|
|
10
|
+
export type FulfillmentIngressResult = { counts: FulfillmentIngressCounts } & (
|
|
11
|
+
| { kind: "refused"; receiptId: null; refusal: FulfillmentRefusal; diagnostics?: { invalidTimestamp?: true; rejectedCorrelationRefs?: number } }
|
|
12
|
+
| { kind: "processed" | "ignored" | "superseded"; receiptId: string; observation: FulfillmentObservation; replayed: boolean; trackingRefs: number; trackingReadBacks: number; supersededCause?: string });
|
|
13
|
+
/** Safe response classes only; the host owns HTTP and localized message text. */
|
|
14
|
+
export function fulfillmentIngressHttpResult(result: FulfillmentIngressResult): { status: 200 | 400 | 403 | 503; messageClass: "accepted" | "bad_request" | "forbidden" | "upstream_unavailable" } {
|
|
15
|
+
if (result.kind !== "refused") return { status: 200, messageClass: "accepted" };
|
|
16
|
+
if (result.refusal.reason === "untrusted") return { status: 403, messageClass: "forbidden" };
|
|
17
|
+
if (result.refusal.reason === "disabled" || result.refusal.reason === "not_ready" || result.refusal.retryable)
|
|
18
|
+
return { status: 503, messageClass: "upstream_unavailable" };
|
|
19
|
+
return { status: 400, messageClass: "bad_request" };
|
|
20
|
+
}
|
|
8
21
|
/** Adapter verification owns auth-before-parse; the host owns durable replay. */
|
|
9
22
|
export async function verifyAndNormalizeFulfillmentObservation(input: {
|
|
10
23
|
enabled: boolean;
|
|
24
|
+
approveTrackingUrl?(url: string): boolean;
|
|
25
|
+
observationCodecs?: FulfillmentObservationCodecs;
|
|
11
26
|
provider: FulfillmentObservationPort;
|
|
12
27
|
verification: Parameters<FulfillmentObservationPort["verifyAndNormalize"]>[0];
|
|
13
28
|
localPersistence: {
|
|
14
29
|
/** G17: atomic insert by eventKey; retries keep the first stored firstReceivedAt. */
|
|
15
|
-
recordObservation(observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
|
|
30
|
+
recordObservation(observation: FulfillmentObservation): Promise<{ replayed: boolean; receiptId?: string }>;
|
|
16
31
|
/** Idempotent; only called after completion or a classified superseding conflict. */
|
|
17
|
-
markProcessed(eventKey: string, outcome: { status: "processed" | "ignored"; refusal?: FulfillmentRefusal }): Promise<void>;
|
|
32
|
+
markProcessed(eventKey: string, outcome: { status: "processed" | "ignored"; refusal?: FulfillmentRefusal; supersededCause?: string }): Promise<void>;
|
|
18
33
|
};
|
|
19
34
|
lifecycle: {
|
|
20
35
|
findDispatchRef(observation: FulfillmentObservation): Promise<DispatchRef | null>;
|
|
@@ -23,19 +38,34 @@ export async function verifyAndNormalizeFulfillmentObservation(input: {
|
|
|
23
38
|
effects: EffectInput["port"];
|
|
24
39
|
keys: { stock(ref: string): string; handoff(ref: string): string; tracking(eventKey: string, number: string): string };
|
|
25
40
|
issueAccountingInvoice?(ref: string): Promise<void>;
|
|
41
|
+
/** Return a machine identity, never exception prose. */
|
|
42
|
+
supersededCause?(error: unknown): string | null;
|
|
26
43
|
classifyFailure(error: unknown): "stock_contention" | "state_conflict" | "other";
|
|
27
44
|
annotateStockContention?(eventKey: string, fulfillmentRef: string): Promise<void>;
|
|
28
45
|
};
|
|
29
|
-
}): Promise<
|
|
30
|
-
|
|
46
|
+
}): Promise<FulfillmentIngressResult> {
|
|
47
|
+
const counts: FulfillmentIngressCounts = { invalidTimestamps: 0, rejectedCorrelationRefs: 0, rejectedTrackingRefs: 0, rejectedTrackingUrls: 0 };
|
|
48
|
+
if (!input.enabled) return { kind: "refused", receiptId: null, counts, refusal: { reason: "disabled", phase: "verify", retryable: false } };
|
|
31
49
|
const verified = await input.provider.verifyAndNormalize(input.verification);
|
|
32
|
-
if (verified.kind === "refused")
|
|
33
|
-
|
|
50
|
+
if (verified.kind === "refused") {
|
|
51
|
+
if (verified.diagnostics?.invalidTimestamp) counts.invalidTimestamps++;
|
|
52
|
+
const rejected = verified.diagnostics?.rejectedCorrelationRefs;
|
|
53
|
+
if (typeof rejected === "number" && Number.isSafeInteger(rejected) && rejected > 0) counts.rejectedCorrelationRefs = rejected;
|
|
54
|
+
return { kind: "refused", receiptId: null, refusal: verified.refusal, counts,
|
|
55
|
+
...(counts.invalidTimestamps || counts.rejectedCorrelationRefs ? { diagnostics: {
|
|
56
|
+
...(counts.invalidTimestamps ? { invalidTimestamp: true as const } : {}),
|
|
57
|
+
...(counts.rejectedCorrelationRefs ? { rejectedCorrelationRefs: counts.rejectedCorrelationRefs } : {}),
|
|
58
|
+
} } : {}) };
|
|
59
|
+
}
|
|
60
|
+
const observation = projectObservation(verified.observation, input.approveTrackingUrl, input.observationCodecs);
|
|
61
|
+
counts.rejectedTrackingRefs = observation.diagnostics?.admission?.rejectedTrackingRefs ?? 0;
|
|
62
|
+
counts.rejectedTrackingUrls = observation.diagnostics?.admission?.rejectedTrackingUrls ?? 0;
|
|
34
63
|
const inbound = await input.localPersistence.recordObservation(observation);
|
|
64
|
+
const receiptId = inbound.receiptId ?? observation.eventKey;
|
|
35
65
|
const ref = await input.lifecycle.findDispatchRef(observation);
|
|
36
66
|
if (!ref) {
|
|
37
67
|
await input.localPersistence.markProcessed(observation.eventKey, { status: "ignored", refusal: { reason: "local_missing", phase: "apply", retryable: false } });
|
|
38
|
-
return { kind: "ignored", observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
|
|
68
|
+
return { kind: "ignored", receiptId, counts, observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
|
|
39
69
|
}
|
|
40
70
|
const externalRef = observation.correlation.externalRef?.trim() || ref.externalRef?.trim();
|
|
41
71
|
if (!externalRef) throw new TypeError("fulfillment_dispatch_acceptance_external_ref_missing");
|
|
@@ -71,10 +101,13 @@ export async function verifyAndNormalizeFulfillmentObservation(input: {
|
|
|
71
101
|
try { await applyFulfillmentObservationEffects(effects, observationStatusPolicy); }
|
|
72
102
|
catch (error) {
|
|
73
103
|
if (input.lifecycle.classifyFailure(error) !== "state_conflict") throw error;
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
const cause = input.lifecycle.supersededCause ? input.lifecycle.supersededCause(error)
|
|
105
|
+
: error && typeof error === "object" && "reason" in error ? error.reason : null;
|
|
106
|
+
const supersededCause = safeDiagnosticCode(cause) ?? "fulfillment_state_conflict";
|
|
107
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed", supersededCause });
|
|
108
|
+
return { kind: "superseded", receiptId, counts, supersededCause, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
76
109
|
}
|
|
77
110
|
}
|
|
78
111
|
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
|
|
79
|
-
return { kind: "processed", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
112
|
+
return { kind: "processed", receiptId, counts, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
80
113
|
}
|