@openlup/platform 0.21.0 → 0.22.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 +210 -47
- package/README.md +27 -4
- package/dist/composition/composeBundle.d.ts +7 -0
- package/dist/composition/composeBundle.js +2 -0
- package/dist/composition/fulfillmentContribution.d.ts +21 -0
- package/dist/composition/fulfillmentContribution.js +19 -0
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/fulfillment/adapterConformance.d.ts +3 -0
- package/dist/fulfillment/adapterConformance.js +223 -0
- package/dist/fulfillment/bindings.d.ts +3 -0
- package/dist/fulfillment/bindings.js +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.d.ts +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.js +184 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.d.ts +86 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.js +131 -0
- package/dist/fulfillment/contracts/alignment.d.ts +25 -0
- package/dist/fulfillment/contracts/alignment.js +1 -0
- package/dist/fulfillment/contracts/bindings.d.ts +51 -0
- package/dist/fulfillment/contracts/bindings.js +1 -0
- package/dist/fulfillment/contracts/capabilities.d.ts +29 -0
- package/dist/fulfillment/contracts/capabilities.js +1 -0
- package/dist/fulfillment/contracts/conformance.d.ts +93 -0
- package/dist/fulfillment/contracts/conformance.js +1 -0
- package/dist/fulfillment/contracts/customerAlignment.d.ts +6 -0
- package/dist/fulfillment/contracts/customerAlignment.js +1 -0
- package/dist/fulfillment/contracts/dispatch.d.ts +105 -0
- package/dist/fulfillment/contracts/dispatch.js +1 -0
- package/dist/fulfillment/contracts/index.d.ts +18 -0
- package/dist/fulfillment/contracts/index.js +1 -0
- package/dist/fulfillment/contracts/label.d.ts +35 -0
- package/dist/fulfillment/contracts/label.js +1 -0
- package/dist/fulfillment/contracts/nativeUnsupported.d.ts +6 -0
- package/dist/fulfillment/contracts/nativeUnsupported.js +1 -0
- package/dist/fulfillment/contracts/observation.d.ts +52 -0
- package/dist/fulfillment/contracts/observation.js +1 -0
- package/dist/fulfillment/contracts/operator.d.ts +64 -0
- package/dist/fulfillment/contracts/operator.js +1 -0
- package/dist/fulfillment/contracts/read.d.ts +110 -0
- package/dist/fulfillment/contracts/read.js +1 -0
- package/dist/fulfillment/contracts/readback.d.ts +38 -0
- package/dist/fulfillment/contracts/readback.js +1 -0
- package/dist/fulfillment/contracts/replacement.d.ts +34 -0
- package/dist/fulfillment/contracts/replacement.js +1 -0
- package/dist/fulfillment/contracts/store.d.ts +86 -0
- package/dist/fulfillment/contracts/store.js +1 -0
- package/dist/fulfillment/contracts/sync.d.ts +170 -0
- package/dist/fulfillment/contracts/sync.js +1 -0
- package/dist/fulfillment/currentFulfillmentParcel.d.ts +2 -0
- package/dist/fulfillment/currentFulfillmentParcel.js +1 -0
- package/dist/fulfillment/delayReadModel.d.ts +11 -0
- package/dist/fulfillment/delayReadModel.js +45 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.d.ts +4 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.js +12 -0
- package/dist/fulfillment/dispatchProjection.d.ts +7 -0
- package/dist/fulfillment/dispatchProjection.js +18 -0
- package/dist/fulfillment/fulfillmentObservationEffects.d.ts +66 -0
- package/dist/fulfillment/fulfillmentObservationEffects.js +36 -0
- package/dist/fulfillment/handlerSchemas.d.ts +647 -0
- package/dist/fulfillment/handlerSchemas.js +213 -0
- package/dist/fulfillment/handlerSupport.d.ts +33 -0
- package/dist/fulfillment/handlerSupport.js +25 -0
- package/dist/fulfillment/index.d.ts +37 -0
- package/dist/fulfillment/index.js +37 -0
- package/dist/fulfillment/labelLifecycle.d.ts +24 -0
- package/dist/fulfillment/labelLifecycle.js +9 -0
- package/dist/fulfillment/lowStockEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/lowStockEvidenceHandler.js +46 -0
- package/dist/fulfillment/migrations.d.ts +27 -0
- package/dist/fulfillment/migrations.js +8 -0
- package/dist/fulfillment/nativeUnsupported.d.ts +10 -0
- package/dist/fulfillment/nativeUnsupported.js +18 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.d.ts +2 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +11 -0
- package/dist/fulfillment/observationProjection.js +37 -0
- package/dist/fulfillment/omnipack/capabilities.d.ts +3 -0
- package/dist/fulfillment/omnipack/capabilities.js +16 -0
- package/dist/fulfillment/omnipack/client.d.ts +37 -0
- package/dist/fulfillment/omnipack/client.js +162 -0
- package/dist/fulfillment/omnipack/index.d.ts +52 -0
- package/dist/fulfillment/omnipack/index.js +349 -0
- package/dist/fulfillment/omnipack/mappers.d.ts +73 -0
- package/dist/fulfillment/omnipack/mappers.js +177 -0
- package/dist/fulfillment/omnipack/observation.d.ts +21 -0
- package/dist/fulfillment/omnipack/observation.js +57 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.d.ts +36 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.js +63 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.d.ts +83 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.js +113 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.d.ts +11 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.js +38 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.d.ts +18 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.js +51 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.d.ts +1 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.js +3 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.d.ts +4 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.js +87 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.d.ts +9 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.js +76 -0
- package/dist/fulfillment/omnipack/productSyncMappers.d.ts +18 -0
- package/dist/fulfillment/omnipack/productSyncMappers.js +43 -0
- package/dist/fulfillment/omnipack/providerError.d.ts +25 -0
- package/dist/fulfillment/omnipack/providerError.js +47 -0
- package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
- package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
- package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
- package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
- package/dist/fulfillment/paidHandoff.d.ts +27 -0
- package/dist/fulfillment/paidHandoff.js +86 -0
- package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
- package/dist/fulfillment/paidHandoffPorts.js +3 -0
- package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
- package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
- package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
- package/dist/fulfillment/postgresDispatchStore.js +44 -0
- package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
- package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
- package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
- package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
- package/dist/fulfillment/postgresShipmentSpine.d.ts +63 -0
- package/dist/fulfillment/postgresShipmentSpine.js +232 -0
- package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
- package/dist/fulfillment/postgresStockSyncPort.js +63 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
- package/dist/fulfillment/providerCapabilities.d.ts +9 -0
- package/dist/fulfillment/providerCapabilities.js +17 -0
- package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
- package/dist/fulfillment/providerCommandLedger.js +14 -0
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +26 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +23 -0
- package/dist/fulfillment/providerDispatchError.d.ts +6 -0
- package/dist/fulfillment/providerDispatchError.js +5 -0
- package/dist/fulfillment/providerDispatchWorker.d.ts +31 -0
- package/dist/fulfillment/providerDispatchWorker.js +172 -0
- package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
- package/dist/fulfillment/providerProductSyncWorker.js +122 -0
- package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
- package/dist/fulfillment/providerReconciliationError.js +17 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
- package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
- package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
- package/dist/fulfillment/providerReconciliationWorker.d.ts +82 -0
- package/dist/fulfillment/providerReconciliationWorker.js +151 -0
- package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
- package/dist/fulfillment/providerStockAvailability.js +51 -0
- package/dist/fulfillment/providerStockSyncWorker.d.ts +37 -0
- package/dist/fulfillment/providerStockSyncWorker.js +186 -0
- package/dist/fulfillment/providerTrackingReferences.d.ts +15 -0
- package/dist/fulfillment/providerTrackingReferences.js +23 -0
- package/dist/fulfillment/replacementRefusals.d.ts +5 -0
- package/dist/fulfillment/replacementRefusals.js +38 -0
- package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
- package/dist/fulfillment/replacementShipmentHandler.js +46 -0
- package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
- package/dist/fulfillment/schemaCapabilities.js +7 -0
- package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
- package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
- package/dist/fulfillment/statusMap.d.ts +2 -0
- package/dist/fulfillment/statusMap.js +1 -0
- package/dist/fulfillment/trackingProgress.d.ts +1 -0
- package/dist/fulfillment/trackingProgress.js +1 -0
- package/dist/fulfillment/verifiedIngress.d.ts +52 -0
- package/dist/fulfillment/verifiedIngress.js +66 -0
- package/dist/payments/stripe/index.d.ts +25 -0
- package/dist/payments/stripe/index.js +25 -0
- package/dist/payments/tpay/index.d.ts +30 -0
- package/dist/payments/tpay/index.js +30 -0
- package/package.json +17 -2
- package/release-gates.json +44 -3
- package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
- package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
- package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
- package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
- package/src/composition/composeBundle.ts +4 -0
- package/src/composition/fulfillmentContribution.ts +28 -0
- package/src/composition/index.ts +1 -0
- package/src/fulfillment/adapterConformance.ts +195 -0
- package/src/fulfillment/bindings.ts +26 -0
- package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
- package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
- package/src/fulfillment/contracts/alignment.ts +13 -0
- package/src/fulfillment/contracts/bindings.ts +30 -0
- package/src/fulfillment/contracts/capabilities.ts +15 -0
- package/src/fulfillment/contracts/conformance.ts +61 -0
- package/src/fulfillment/contracts/customerAlignment.ts +4 -0
- package/src/fulfillment/contracts/dispatch.ts +58 -0
- package/src/fulfillment/contracts/index.ts +18 -0
- package/src/fulfillment/contracts/label.ts +9 -0
- package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
- package/src/fulfillment/contracts/observation.ts +18 -0
- package/src/fulfillment/contracts/operator.ts +31 -0
- package/src/fulfillment/contracts/read.ts +42 -0
- package/src/fulfillment/contracts/readback.ts +11 -0
- package/src/fulfillment/contracts/replacement.ts +15 -0
- package/src/fulfillment/contracts/store.ts +26 -0
- package/src/fulfillment/contracts/sync.ts +61 -0
- package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
- package/src/fulfillment/delayReadModel.ts +60 -0
- package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
- package/src/fulfillment/dispatchProjection.ts +20 -0
- package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
- package/src/fulfillment/handlerSchemas.ts +255 -0
- package/src/fulfillment/handlerSupport.ts +25 -0
- package/src/fulfillment/index.ts +38 -0
- package/src/fulfillment/labelLifecycle.ts +11 -0
- package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
- package/src/fulfillment/migrations.ts +8 -0
- package/src/fulfillment/nativeUnsupported.ts +23 -0
- package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
- package/src/fulfillment/observationProjection.ts +41 -0
- package/src/fulfillment/omnipack/capabilities.ts +18 -0
- package/src/fulfillment/omnipack/client.ts +258 -0
- package/src/fulfillment/omnipack/index.ts +312 -0
- package/src/fulfillment/omnipack/mappers.ts +261 -0
- package/src/fulfillment/omnipack/observation.ts +61 -0
- package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
- package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
- package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
- package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
- package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
- package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
- package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
- package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
- package/src/fulfillment/omnipack/providerError.ts +64 -0
- package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
- package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
- package/src/fulfillment/paidHandoff.ts +122 -0
- package/src/fulfillment/paidHandoffPorts.ts +21 -0
- package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
- package/src/fulfillment/postgresDispatchStore.ts +55 -0
- package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
- package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
- package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
- package/src/fulfillment/postgresShipmentSpine.ts +321 -0
- package/src/fulfillment/postgresStockSyncPort.ts +65 -0
- package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
- package/src/fulfillment/providerCapabilities.ts +18 -0
- package/src/fulfillment/providerCommandLedger.ts +53 -0
- package/src/fulfillment/providerDispatchAcceptance.ts +31 -0
- package/src/fulfillment/providerDispatchError.ts +7 -0
- package/src/fulfillment/providerDispatchWorker.ts +131 -0
- package/src/fulfillment/providerProductSyncWorker.ts +154 -0
- package/src/fulfillment/providerReconciliationError.ts +20 -0
- package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
- package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
- package/src/fulfillment/providerReconciliationWorker.ts +150 -0
- package/src/fulfillment/providerStockAvailability.ts +60 -0
- package/src/fulfillment/providerStockSyncWorker.ts +222 -0
- package/src/fulfillment/providerTrackingReferences.ts +31 -0
- package/src/fulfillment/replacementRefusals.ts +43 -0
- package/src/fulfillment/replacementShipmentHandler.ts +47 -0
- package/src/fulfillment/schemaCapabilities.ts +7 -0
- package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
- package/src/fulfillment/statusMap.ts +2 -0
- package/src/fulfillment/trackingProgress.ts +1 -0
- package/src/fulfillment/verifiedIngress.ts +80 -0
- package/src/payments/stripe/index.ts +30 -0
- package/src/payments/tpay/index.ts +36 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
|
|
2
|
+
import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, LocalStock, StockSnapshot, StockMovement } from "./contracts/index.js";
|
|
3
|
+
type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
|
|
4
|
+
interface StockSyncResult {
|
|
5
|
+
ok: boolean; checked: number; updated: number; mismatches: number; reservationCoverage: number;
|
|
6
|
+
unclassified: number; lowStock: number; movements: number; replayed: number; failures: number;
|
|
7
|
+
providerCalls: number; readBacks: number; skipped: boolean; syncRunId: string;
|
|
8
|
+
lastMovementOccurredAt: string | null; reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export async function runFulfillmentStockSyncWorker(input: {
|
|
11
|
+
port: FulfillmentStockSyncPort;
|
|
12
|
+
provider: FulfillmentStockSyncProvider;
|
|
13
|
+
movementBatchSize: number;
|
|
14
|
+
staleSyncThresholdHours?: number;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
compatibility: {
|
|
17
|
+
syncRunId(now: Date): string; staleEvidenceSku: string; timeZone: string;
|
|
18
|
+
currentKey(run: string, sku: string): string; snapshotKey(run: string, sku: string): string;
|
|
19
|
+
lowStockKey(sku: string, threshold: Threshold): string; staleKey(lastSyncedAt: string): string;
|
|
20
|
+
};
|
|
21
|
+
}): Promise<StockSyncResult> {
|
|
22
|
+
const now = input.now?.() ?? new Date();
|
|
23
|
+
const staleThresholdHours = input.staleSyncThresholdHours && input.staleSyncThresholdHours > 0
|
|
24
|
+
? input.staleSyncThresholdHours
|
|
25
|
+
: 6;
|
|
26
|
+
const syncRunId = input.compatibility.syncRunId(now);
|
|
27
|
+
const result: StockSyncResult = {
|
|
28
|
+
ok: true,
|
|
29
|
+
checked: 0,
|
|
30
|
+
updated: 0,
|
|
31
|
+
mismatches: 0,
|
|
32
|
+
reservationCoverage: 0,
|
|
33
|
+
unclassified: 0,
|
|
34
|
+
lowStock: 0,
|
|
35
|
+
movements: 0,
|
|
36
|
+
replayed: 0,
|
|
37
|
+
failures: 0,
|
|
38
|
+
providerCalls: 0,
|
|
39
|
+
readBacks: 0,
|
|
40
|
+
skipped: false,
|
|
41
|
+
syncRunId,
|
|
42
|
+
lastMovementOccurredAt: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const previousCursor = await input.port.readCursor();
|
|
46
|
+
|
|
47
|
+
// Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
|
|
48
|
+
// low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
|
|
49
|
+
// below. A never-synced state (no prior cursor) is not "stale".
|
|
50
|
+
const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
|
|
51
|
+
const staleGapHours =
|
|
52
|
+
lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
|
|
53
|
+
const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
|
|
54
|
+
if (syncIsStale) {
|
|
55
|
+
await input.port.recordLowStockEvidence({
|
|
56
|
+
requestKey: input.compatibility.staleKey(previousCursor!.lastStockSyncedAt!),
|
|
57
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
58
|
+
thresholdKind: "stale_sync",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
providerForSaleQuantity: 0,
|
|
61
|
+
localAvailableQuantity: 0,
|
|
62
|
+
syncRunId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await input.port.recordCursor({
|
|
67
|
+
status: "running",
|
|
68
|
+
lastStockSyncedAt: null,
|
|
69
|
+
lastMovementOccurredAt: null,
|
|
70
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
71
|
+
coverageGapSkus: [], refusal: null,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const [localStock, providerStock] = await Promise.all([
|
|
75
|
+
input.port.readLocalInventoryStock(now.toISOString()),
|
|
76
|
+
input.provider.getStock(),
|
|
77
|
+
]);
|
|
78
|
+
result.providerCalls += 1;
|
|
79
|
+
|
|
80
|
+
const stockItems = providerStock.filter((item) => item.skuRef.trim());
|
|
81
|
+
const skus = [...new Set(stockItems.map((item) => item.skuRef))];
|
|
82
|
+
const [inventoryClasses, activeReservations] = await Promise.all([
|
|
83
|
+
input.port.readSkuInventoryClasses(skus),
|
|
84
|
+
input.port.readActiveReservations(skus, now.toISOString()),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const movements = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
|
|
88
|
+
result.providerCalls += 1;
|
|
89
|
+
result.movements = movements.length;
|
|
90
|
+
result.lastMovementOccurredAt = latestOccurredAt(movements);
|
|
91
|
+
|
|
92
|
+
const localBySku = aggregateAtpCompatibleStock(localStock, now);
|
|
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>();
|
|
103
|
+
|
|
104
|
+
for (const stock of stockItems) {
|
|
105
|
+
try {
|
|
106
|
+
const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
|
|
107
|
+
const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
|
|
108
|
+
const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
|
|
109
|
+
const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
|
|
110
|
+
const current = await input.port.recordProviderStockCurrent({
|
|
111
|
+
requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
|
|
112
|
+
snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
|
|
113
|
+
});
|
|
114
|
+
const snapshot = await input.port.recordStockSnapshot({
|
|
115
|
+
requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
|
|
116
|
+
snapshot: stock, local, mismatchKind: "none", inventoryClass,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
result.checked += 1;
|
|
120
|
+
result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
|
|
121
|
+
if (snapshot.replayed) result.replayed += 1;
|
|
122
|
+
if (current.replayed) result.replayed += 1;
|
|
123
|
+
if (snapshot.readBack) result.readBacks += 1;
|
|
124
|
+
if (current.readBack) result.readBacks += 1;
|
|
125
|
+
if (inventoryClass === null) result.unclassified += 1;
|
|
126
|
+
if (inventoryClass === "packaging") packagingSkus.add(stock.skuRef);
|
|
127
|
+
if (inventoryClass === "sellable") {
|
|
128
|
+
coverageCandidates.push({ stock, local, activeReserved });
|
|
129
|
+
if (activeReserved > stock.forSaleQuantity) currentCoverageGapSkus.add(stock.skuRef);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
result.ok = false;
|
|
133
|
+
result.failures += 1;
|
|
134
|
+
result.reason = result.reason ?? "stock_sync_operation_failed";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
for (const { stock, local, activeReserved } of coverageCandidates) {
|
|
140
|
+
const localAvailable = availableQuantity(local);
|
|
141
|
+
const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
|
|
142
|
+
const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
|
|
143
|
+
const activeThresholdKinds: Threshold[] = lowStockSignals.map((signal) => signal.thresholdKind);
|
|
144
|
+
if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
|
|
145
|
+
activeThresholdKinds.push("reservation_coverage");
|
|
146
|
+
}
|
|
147
|
+
for (const signal of lowStockSignals) {
|
|
148
|
+
const evidence = await input.port.recordLowStockEvidence({
|
|
149
|
+
requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
|
|
150
|
+
skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
151
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
|
|
152
|
+
});
|
|
153
|
+
if (evidence.replayed) result.replayed += 1;
|
|
154
|
+
result.lowStock += evidence.replayed ? 0 : 1;
|
|
155
|
+
if (signal.thresholdKind === "reservation_coverage") result.reservationCoverage += 1;
|
|
156
|
+
}
|
|
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
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await input.port.recordCursor({
|
|
173
|
+
status: result.ok ? "succeeded" : "failed",
|
|
174
|
+
lastStockSyncedAt: result.ok ? now.toISOString() : null,
|
|
175
|
+
lastMovementOccurredAt: result.lastMovementOccurredAt,
|
|
176
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
177
|
+
coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : [],
|
|
178
|
+
refusal: result.ok ? null : { reason: "io_failure", phase: "sync", retryable: true },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Recovery: a healthy run (succeeded, not itself stale) closes any open stale_sync row.
|
|
182
|
+
if (result.ok && !syncIsStale) {
|
|
183
|
+
await input.port.resolveLowStockEvidence({
|
|
184
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
185
|
+
activeThresholdKinds: [],
|
|
186
|
+
syncRunId,
|
|
187
|
+
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function lowStockSignalsFor(
|
|
195
|
+
stock: StockSnapshot,
|
|
196
|
+
local: LocalStock,
|
|
197
|
+
activeReserved: number,
|
|
198
|
+
confirmedCoverageGap: boolean,
|
|
199
|
+
): Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> {
|
|
200
|
+
const signals: Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> = [];
|
|
201
|
+
const remainingAfterReservations = Math.max(0, stock.forSaleQuantity - activeReserved);
|
|
202
|
+
if (local.safetyStock > 0 && remainingAfterReservations <= local.safetyStock) {
|
|
203
|
+
signals.push({ thresholdKind: "safety_stock", severity: remainingAfterReservations <= 0 ? "critical" : "warning" });
|
|
204
|
+
}
|
|
205
|
+
if (confirmedCoverageGap) signals.push({ thresholdKind: "reservation_coverage", severity: "critical" });
|
|
206
|
+
return signals;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readStringSet(value: unknown): Set<string> {
|
|
210
|
+
return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : []);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function availableQuantity(balance: LocalStock): number {
|
|
214
|
+
return Math.max(0, balance.onHand - balance.reserved - balance.unavailable - balance.safetyStock);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function latestOccurredAt(movements: StockMovement[]): string | null {
|
|
218
|
+
return movements
|
|
219
|
+
.map((movement) => movement.occurredAt)
|
|
220
|
+
.filter((value): value is string => Boolean(value))
|
|
221
|
+
.sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
|
|
222
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FulfillmentTrackingReference } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
export function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean {
|
|
4
|
+
return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
|
|
5
|
+
}
|
|
6
|
+
/** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
|
|
7
|
+
export function enrichTrackingReference(
|
|
8
|
+
ref: FulfillmentTrackingReference,
|
|
9
|
+
deliveryCarrier: { carrierRef: string | null; serviceRef: string | null } | null,
|
|
10
|
+
codecs: {
|
|
11
|
+
normalizeCarrier(input: { carrierRef: string | null; serviceRef: string | null }): string | null;
|
|
12
|
+
publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
|
|
13
|
+
},
|
|
14
|
+
): FulfillmentTrackingReference {
|
|
15
|
+
const providerCarrier = ref.carrierRef ?? codecs.normalizeCarrier(ref);
|
|
16
|
+
const carrierRef = providerCarrier ?? codecs.normalizeCarrier({ carrierRef: deliveryCarrier?.carrierRef ?? null, serviceRef: deliveryCarrier?.serviceRef ?? null });
|
|
17
|
+
return { trackingNumber: ref.trackingNumber, publicUrl: ref.publicUrl ?? codecs.publicUrl(carrierRef, ref.trackingNumber),
|
|
18
|
+
carrierRef, serviceRef: ref.serviceRef ?? (providerCarrier ? null : deliveryCarrier?.serviceRef ?? null) };
|
|
19
|
+
}
|
|
20
|
+
/** First occurrence wins, including its selection, after trimming the number. */
|
|
21
|
+
export function uniqueTrackingReferences(refs: readonly FulfillmentTrackingReference[]): FulfillmentTrackingReference[] {
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
const result: FulfillmentTrackingReference[] = [];
|
|
24
|
+
for (const ref of refs) {
|
|
25
|
+
const trackingNumber = ref.trackingNumber.trim();
|
|
26
|
+
if (!trackingNumber || seen.has(trackingNumber)) continue;
|
|
27
|
+
seen.add(trackingNumber);
|
|
28
|
+
result.push({ trackingNumber, publicUrl: ref.publicUrl ?? null, carrierRef: ref.carrierRef ?? null, serviceRef: ref.serviceRef ?? null });
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS = [
|
|
2
|
+
"invalid_input",
|
|
3
|
+
"idempotency_conflict",
|
|
4
|
+
"order_not_found",
|
|
5
|
+
"order_not_replaceable",
|
|
6
|
+
"missing_client",
|
|
7
|
+
"missing_shipping_address",
|
|
8
|
+
"shipping_address_not_found",
|
|
9
|
+
"payment_not_succeeded",
|
|
10
|
+
"subscription_cycle_not_paid",
|
|
11
|
+
"no_parcel_to_replace",
|
|
12
|
+
"undispatched_replacement_exists",
|
|
13
|
+
"missing_order_items",
|
|
14
|
+
"missing_catalog_sku",
|
|
15
|
+
"missing_inventory_reservation",
|
|
16
|
+
"blocked_by_payment_not_succeeded_hold",
|
|
17
|
+
"blocked_by_inventory_review_hold",
|
|
18
|
+
"blocked_by_risk_review_hold",
|
|
19
|
+
"blocked_by_address_review_hold",
|
|
20
|
+
"blocked_by_manual_support_hold",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export const FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS = [
|
|
24
|
+
"inventory_external_provider_insufficient_available_stock",
|
|
25
|
+
"inventory_external_provider_stock_stale",
|
|
26
|
+
"inventory_external_provider_stock_missing",
|
|
27
|
+
"inventory_external_provider_sku_not_found",
|
|
28
|
+
"inventory_external_provider_location_missing",
|
|
29
|
+
"inventory_reservation_insufficient_available_stock",
|
|
30
|
+
"inventory_reservation_allocation_incomplete",
|
|
31
|
+
"inventory_reservation_payment_status_not_reservable",
|
|
32
|
+
"inventory_reservation_invalid_quantity",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export const UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL = "unspecified_refusal";
|
|
36
|
+
|
|
37
|
+
export const FULFILLMENT_REPLACEMENT_REFUSALS = [
|
|
38
|
+
...FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS,
|
|
39
|
+
...FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS,
|
|
40
|
+
UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL,
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
export type FulfillmentReplacementRefusal = (typeof FULFILLMENT_REPLACEMENT_REFUSALS)[number];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { adminCommerceOrderRequestReplacementShipmentRequestSchema, adminCommerceOrderRequestReplacementShipmentResponseSchema } from './handlerSchemas.js';
|
|
2
|
+
import { authorize, CommerceFulfillmentConflictError, isNativeUnsupported, type BaseDeps, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
|
|
3
|
+
import type { FulfillmentReplacementPort } from './contracts/replacement.js';
|
|
4
|
+
export function createAdminCommerceOrderRequestReplacementShipmentHandler<Response>({
|
|
5
|
+
authorizeAdmin,
|
|
6
|
+
respond,
|
|
7
|
+
omsPort,
|
|
8
|
+
}: BaseDeps<Response> & {
|
|
9
|
+
omsPort: FulfillmentReplacementPort;
|
|
10
|
+
}) {
|
|
11
|
+
const { sendMethodNotAllowed } = respond;
|
|
12
|
+
return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
|
|
13
|
+
if (req.method !== "POST") return sendMethodNotAllowed(res, ["POST"]);
|
|
14
|
+
const auth = await authorize(req, res, authorizeAdmin, respond);
|
|
15
|
+
if (!auth) return;
|
|
16
|
+
|
|
17
|
+
const request = parseOmsRequest(res, respond, adminCommerceOrderRequestReplacementShipmentRequestSchema, req.body ?? {}, "Invalid admin commerce replacement shipment request");
|
|
18
|
+
if (request === undefined) return;
|
|
19
|
+
|
|
20
|
+
await sendOmsMutationResponse(
|
|
21
|
+
res,
|
|
22
|
+
respond,
|
|
23
|
+
() => omsPort.requestReplacementShipment({ ...request, actorUserId: auth.userId }),
|
|
24
|
+
adminCommerceOrderRequestReplacementShipmentResponseSchema,
|
|
25
|
+
"Admin commerce replacement shipment returned invalid response",
|
|
26
|
+
"Admin commerce replacement shipment failed",
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseOmsRequest<Response, T>(res: Response, respond: FulfillmentHandlerResponder<Response>, schema: { safeParse(input: unknown): { success: true; data: T } | { success: false; error: { flatten(): unknown } } }, input: unknown, message: string): T | undefined {
|
|
32
|
+
const request = schema.safeParse(input);
|
|
33
|
+
if (!request.success) { respond.sendBffError(res, 'BAD_REQUEST', message, { details: request.error.flatten() }); return undefined; }
|
|
34
|
+
return request.data;
|
|
35
|
+
}
|
|
36
|
+
async function sendOmsMutationResponse<Response>(res: Response, respond: FulfillmentHandlerResponder<Response>, action: () => Promise<unknown>, schema: typeof adminCommerceOrderRequestReplacementShipmentResponseSchema, invalidMessage: string, failureMessage: string) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await action();
|
|
39
|
+
if (isNativeUnsupported(result)) { respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', 'Native fulfillment operation unsupported', { details: result }); return; }
|
|
40
|
+
const response = schema.safeParse(result);
|
|
41
|
+
if (!response.success) { respond.sendBffError(res, 'INVALID_RESPONSE', invalidMessage); return; }
|
|
42
|
+
respond.sendBffSuccess(res, response.data);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof CommerceFulfillmentConflictError) { respond.sendBffError(res, 'CONFLICT', error.message, { details: error.details }); return; }
|
|
45
|
+
respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', failureMessage);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Sparse projections require the full canonical graph and a trusted native binding. */
|
|
2
|
+
export const FULFILLMENT_NATIVE_CAPABILITIES = {
|
|
3
|
+
shipment: 'supported', commandFence: 'supported', deliveryAlignment: 'supported',
|
|
4
|
+
stockEvidence: 'supported', observationPersistence: 'supported',
|
|
5
|
+
localInventoryBalances: 'host_required', dispatchContactPreparation: 'host_required',
|
|
6
|
+
productPacks: 'unsupported', providerAccess: 'unsupported', automaticReplacementConfirmation: 'unsupported',
|
|
7
|
+
} as const;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isNativeUnsupported, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { FulfillmentShipmentsOverviewPort, FulfillmentShipmentsOverviewResponse } from './contracts/read.js';
|
|
4
|
+
|
|
5
|
+
const adminShipmentsOverviewRequestSchema = z.object({
|
|
6
|
+
shippedFilter: z.enum(['today', '7d', '30d', 'all']).default('7d'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export interface FulfillmentShipmentsOverviewHandlerDeps<Response> {
|
|
10
|
+
respond: FulfillmentHandlerResponder<Response>;
|
|
11
|
+
overviewPort: FulfillmentShipmentsOverviewPort;
|
|
12
|
+
responseSchema: { safeParse(input: unknown): { success: true; data: FulfillmentShipmentsOverviewResponse } | { success: false } };
|
|
13
|
+
authorizeAdmin: (req: FulfillmentHandlerRequest) => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFulfillmentShipmentsOverviewHandler<Response>({
|
|
17
|
+
respond,
|
|
18
|
+
overviewPort,
|
|
19
|
+
responseSchema: adminShipmentsOverviewResponseSchema,
|
|
20
|
+
authorizeAdmin,
|
|
21
|
+
}: FulfillmentShipmentsOverviewHandlerDeps<Response>) {
|
|
22
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
|
|
23
|
+
return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
|
|
24
|
+
if (req.method !== "GET") {
|
|
25
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let authorized = false;
|
|
30
|
+
try {
|
|
31
|
+
authorized = await authorizeAdmin(req);
|
|
32
|
+
} catch {
|
|
33
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!authorized) {
|
|
38
|
+
sendBffError(res, "UNAUTHORIZED", "Admin session required");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const request = adminShipmentsOverviewRequestSchema.safeParse({
|
|
43
|
+
shippedFilter: scalarQuery(req.query.shippedFilter),
|
|
44
|
+
});
|
|
45
|
+
if (!request.success) {
|
|
46
|
+
sendBffError(res, "BAD_REQUEST", "Invalid shipments overview request", {
|
|
47
|
+
details: request.error.flatten(),
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await overviewPort.getShipmentsOverview(request.data);
|
|
54
|
+
if (isNativeUnsupported(result)) {
|
|
55
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Native fulfillment operation unsupported", { details: result });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const response = adminShipmentsOverviewResponseSchema.safeParse(result);
|
|
59
|
+
if (!response.success) {
|
|
60
|
+
sendBffError(res, "INVALID_RESPONSE", "Shipments overview returned invalid response");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
sendBffSuccess(res, response.data);
|
|
65
|
+
} catch {
|
|
66
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Shipments overview read failed");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scalarQuery(value: string | string[] | undefined): string | undefined {
|
|
72
|
+
return Array.isArray(value) ? value[0] : value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "@openlup/core/fulfillment";
|
|
2
|
+
export type { FulfillmentStatusStage, FulfillmentStatusMap } from "@openlup/core/fulfillment";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fulfillmentProgressPresentation } from "@openlup/core/fulfillment";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FulfillmentObservation, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
|
|
2
|
+
import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
3
|
+
import { observationStatusPolicy, projectObservation } from "./observationProjection.js";
|
|
4
|
+
import { uniqueTrackingReferences } from "./providerTrackingReferences.js";
|
|
5
|
+
|
|
6
|
+
type EffectInput = Parameters<typeof applyFulfillmentObservationEffects<FulfillmentObservation["fact"]["canonicalStatus"], "in_transit" | "delivered">>[0];
|
|
7
|
+
type DispatchRef = { orderRef: string; fulfillmentRef: string; dispatchRef: string; externalRef: string | null };
|
|
8
|
+
/** Adapter verification owns auth-before-parse; the host owns durable replay. */
|
|
9
|
+
export async function verifyAndNormalizeFulfillmentObservation(input: {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
provider: FulfillmentObservationPort;
|
|
12
|
+
verification: Parameters<FulfillmentObservationPort["verifyAndNormalize"]>[0];
|
|
13
|
+
localPersistence: {
|
|
14
|
+
/** G17: atomic insert by eventKey; retries keep the first stored firstReceivedAt. */
|
|
15
|
+
recordObservation(observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
|
|
16
|
+
/** Idempotent; only called after completion or a classified superseding conflict. */
|
|
17
|
+
markProcessed(eventKey: string, outcome: { status: "processed" | "ignored"; refusal?: FulfillmentRefusal }): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
lifecycle: {
|
|
20
|
+
findDispatchRef(observation: FulfillmentObservation): Promise<DispatchRef | null>;
|
|
21
|
+
acknowledgeDispatchAcceptance(ref: DispatchRef, observation: FulfillmentObservation): Promise<void>;
|
|
22
|
+
recordStatusEvidence(ref: DispatchRef, observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
|
|
23
|
+
effects: EffectInput["port"];
|
|
24
|
+
keys: { stock(ref: string): string; handoff(ref: string): string; tracking(eventKey: string, number: string): string };
|
|
25
|
+
issueAccountingInvoice?(ref: string): Promise<void>;
|
|
26
|
+
classifyFailure(error: unknown): "stock_contention" | "state_conflict" | "other";
|
|
27
|
+
annotateStockContention?(eventKey: string, fulfillmentRef: string): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
}): Promise<{ kind: "refused"; refusal: FulfillmentRefusal } | { kind: "processed" | "ignored" | "superseded"; observation: FulfillmentObservation; replayed: boolean; trackingRefs: number; trackingReadBacks: number }> {
|
|
30
|
+
if (!input.enabled) return { kind: "refused", refusal: { reason: "disabled", phase: "verify", retryable: false } };
|
|
31
|
+
const verified = await input.provider.verifyAndNormalize(input.verification);
|
|
32
|
+
if (verified.kind === "refused") return verified;
|
|
33
|
+
const observation = projectObservation(verified.observation);
|
|
34
|
+
const inbound = await input.localPersistence.recordObservation(observation);
|
|
35
|
+
const ref = await input.lifecycle.findDispatchRef(observation);
|
|
36
|
+
if (!ref) {
|
|
37
|
+
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 };
|
|
39
|
+
}
|
|
40
|
+
const externalRef = observation.correlation.externalRef?.trim() || ref.externalRef?.trim();
|
|
41
|
+
if (!externalRef) throw new TypeError("fulfillment_dispatch_acceptance_external_ref_missing");
|
|
42
|
+
await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
|
|
43
|
+
const statusEvidence = await input.lifecycle.recordStatusEvidence(ref, observation);
|
|
44
|
+
let trackingRefs = 0;
|
|
45
|
+
let trackingReadBacks = 0;
|
|
46
|
+
const effectPort: EffectInput["port"] = {
|
|
47
|
+
...input.lifecycle.effects,
|
|
48
|
+
async markProviderStockConsumed(request) {
|
|
49
|
+
try { return await input.lifecycle.effects.markProviderStockConsumed(request); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (input.lifecycle.classifyFailure(error) === "stock_contention") {
|
|
52
|
+
try {
|
|
53
|
+
await input.lifecycle.annotateStockContention?.(observation.eventKey, ref.fulfillmentRef);
|
|
54
|
+
} catch { /* Annotation is best-effort; preserve the original stock failure. */ }
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const localStatus = observation.fact.canonicalStatus;
|
|
61
|
+
const effects: EffectInput = {
|
|
62
|
+
port: effectPort, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
|
|
63
|
+
trackingRefs: uniqueTrackingReferences(observation.trackingRefs).map(tracking => ({ trackingNumber: tracking.trackingNumber, trackingUrl: tracking.publicUrl, carrierKind: tracking.carrierRef, service: tracking.serviceRef })),
|
|
64
|
+
stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
|
|
65
|
+
trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation.eventKey, number),
|
|
66
|
+
issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => input.lifecycle.issueAccountingInvoice!(ref.fulfillmentRef) : undefined,
|
|
67
|
+
onTrackingRecorded: result => { trackingRefs += result.replayed ? 0 : 1; trackingReadBacks += result.readBack ? 1 : 0; },
|
|
68
|
+
};
|
|
69
|
+
if (!observationStatusPolicy.handsOver(localStatus)) await applyFulfillmentObservationEffects({ ...effects, trackingRefs: [] }, observationStatusPolicy);
|
|
70
|
+
else {
|
|
71
|
+
try { await applyFulfillmentObservationEffects(effects, observationStatusPolicy); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (input.lifecycle.classifyFailure(error) !== "state_conflict") throw error;
|
|
74
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
|
|
75
|
+
return { kind: "superseded", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
|
|
79
|
+
return { kind: "processed", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
80
|
+
}
|
|
@@ -233,3 +233,33 @@ function mapVerifiedStripeEvent(raw: Record<string, unknown>): VerificationResul
|
|
|
233
233
|
return { ...result, fingerprint: createHash("sha256").update(JSON.stringify(result)).digest("hex"),
|
|
234
234
|
providerAuditPayload: normalized.rawPayload };
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @experimental Reference-application support surface; may change.
|
|
239
|
+
* Reference consumer: `server/adapters/stripe/stripeAdapterFactory.ts`.
|
|
240
|
+
*/
|
|
241
|
+
export { createStripeSandboxPaymentExecutionAdapter } from "./stripeSandboxPaymentExecutionAdapter.js";
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @experimental Reference-application support surface; may change.
|
|
245
|
+
* Reference consumer: `server/runtime/paymentProviderReadbackRegistry.ts`.
|
|
246
|
+
*/
|
|
247
|
+
export { createStripePaymentReconciliationProvider } from "./stripePaymentReconciliationProvider.js";
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @experimental Reference-application support surface; may change.
|
|
251
|
+
* Reference consumer: `server/infra/stripe/stripeApiClient.ts`.
|
|
252
|
+
*/
|
|
253
|
+
export { mapStripeIntent } from "./intentMapper.js";
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @experimental Reference-application support surface; may change.
|
|
257
|
+
* Reference consumer: `server/infra/stripe/stripeApiClient.ts`.
|
|
258
|
+
*/
|
|
259
|
+
export { mapSetupIntent } from "./intentMapper.js";
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @experimental Reference-application support surface; may change.
|
|
263
|
+
* Reference consumer: `server/adapters/paymentRecoveryGuidance.ts`.
|
|
264
|
+
*/
|
|
265
|
+
export { normalizeStripeFailureEvidence } from "./stripeFailureEvidence.js";
|
|
@@ -255,3 +255,39 @@ function mapVerifiedTpayEvent(rawBody: string, occurred: string | null, mode: "l
|
|
|
255
255
|
return { ...evidence, fingerprint: createHash("sha256").update(JSON.stringify(evidence)).digest("hex"),
|
|
256
256
|
providerAuditPayload: payload };
|
|
257
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @experimental Reference-application support surface; may change.
|
|
261
|
+
* Reference consumer: `server/adapters/tpay/tpayAdapterFactory.ts`.
|
|
262
|
+
*/
|
|
263
|
+
export { createTpayPaymentExecutionAdapter } from "./tpayPaymentExecutionAdapter.js";
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @experimental Reference-application support surface; may change.
|
|
267
|
+
* Reference consumer: `server/runtime/paymentProviderReadbackRegistry.ts`.
|
|
268
|
+
*/
|
|
269
|
+
export { createTpayPaymentReconciliationProvider } from "./tpayPaymentReconciliationProvider.js";
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @experimental Reference-application support surface; may change.
|
|
273
|
+
* Reference consumer: `server/bff/commerce/tpay-channels.ts`.
|
|
274
|
+
*/
|
|
275
|
+
export { createTpayHttpClient } from "./tpayHttpClient.js";
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @experimental Reference-application support surface; may change.
|
|
279
|
+
* Reference consumer: `server/adapters/paymentRecoveryGuidance.ts`.
|
|
280
|
+
*/
|
|
281
|
+
export { normalizeTpayFailureEvidence } from "./tpayFailureEvidence.js";
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @experimental Reference-application support surface; may change.
|
|
285
|
+
* Reference consumer: `server/adapters/supabase/payment/tpaySubscriptionActivation.ts`.
|
|
286
|
+
*/
|
|
287
|
+
export { aliasValue } from "./tpayCreateTransactionInput.js";
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @experimental Reference-application support surface; may change.
|
|
291
|
+
* Reference consumer: `server/shared/preparedProviderAttempt.ts`.
|
|
292
|
+
*/
|
|
293
|
+
export { ProviderAttemptPreDispatchError } from "./preparedProviderAttempt.js";
|