@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,61 @@
|
|
|
1
|
+
import type { FulfillmentRefusal } from "./dispatch.js";
|
|
2
|
+
/** Collection identity never masquerades as a monotonic provider revision. */
|
|
3
|
+
interface SyncIdentity {
|
|
4
|
+
providerRef: string; syncRunId: string; collectedAt: string;
|
|
5
|
+
sourceRevision: string | null; revisionSource: "provider" | "collection" | "unknown";
|
|
6
|
+
}
|
|
7
|
+
export interface StockSnapshot extends SyncIdentity {
|
|
8
|
+
skuRef: string; quantityKind: "snapshot"; totalQuantity: number; forSaleQuantity: number; reservedOrUnavailableQuantity: number;
|
|
9
|
+
}
|
|
10
|
+
export interface StockMovement extends SyncIdentity {
|
|
11
|
+
skuRef: string; quantityKind: "movement"; quantity: number; occurredAt: string | null;
|
|
12
|
+
operationType: string | null; documentRef: string | null; lotRef: string | null; expirationDate: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface SyncCursor {
|
|
15
|
+
status: "idle" | "running" | "succeeded" | "failed";
|
|
16
|
+
lastStockSyncedAt: string | null; lastMovementOccurredAt: string | null;
|
|
17
|
+
/** Sanitized continuation only; no credentials, URLs or arbitrary provider payload. */
|
|
18
|
+
continuation: { nextRef: string | null; startLocalDate: string | null; endLocalDate: string | null };
|
|
19
|
+
/** G15: successful sellable coverage gaps; legacy absent values read as []. */
|
|
20
|
+
coverageGapSkus?: readonly string[];
|
|
21
|
+
refusal: FulfillmentRefusal | null;
|
|
22
|
+
}
|
|
23
|
+
export interface LocalStock { skuRef: string; onHand: number; reserved: number; unavailable: number; safetyStock: number }
|
|
24
|
+
type Threshold = "safety_stock" | "reservation_coverage" | "forecast" | "stale_sync";
|
|
25
|
+
type Conflict = "unknown_sku" | "ean_conflict" | "kind_quantity_conflict";
|
|
26
|
+
export interface FulfillmentStockSyncProvider {
|
|
27
|
+
getStock(): Promise<StockSnapshot[]>;
|
|
28
|
+
getStockMovements(input?: { startLocalDate?: string; endLocalDate?: string; size?: number }): Promise<StockMovement[]>;
|
|
29
|
+
}
|
|
30
|
+
export interface FulfillmentStockSyncPort {
|
|
31
|
+
readCursor(): Promise<SyncCursor | null>;
|
|
32
|
+
/** G16: unaggregated lines at the worker clock; quantity retains the four local balances. */
|
|
33
|
+
readLocalInventoryStock(requestedAt: string): Promise<readonly {
|
|
34
|
+
skuRef: string; quantity: Pick<LocalStock, "onHand" | "reserved" | "unavailable" | "safetyStock">;
|
|
35
|
+
fulfillable: boolean; locationStatus: "active" | "inactive";
|
|
36
|
+
lotStatus: "available" | "quarantined" | "expired" | "recalled" | null; expiresAt: string | null;
|
|
37
|
+
}[]>;
|
|
38
|
+
readSkuInventoryClasses(skus: string[]): Promise<Map<string, "sellable" | "packaging" | null>>;
|
|
39
|
+
readActiveReservations(skus: string[], requestedAt: string): Promise<Map<string, number>>;
|
|
40
|
+
recordCursor(input: SyncCursor): Promise<void>;
|
|
41
|
+
recordStockSnapshot(input: { requestKey: string; snapshot: StockSnapshot; local: LocalStock; mismatchKind: "none"; inventoryClass: "sellable" | "packaging" | null }): Promise<{ replayed: boolean; readBack: boolean }>;
|
|
42
|
+
recordProviderStockCurrent(input: { requestKey: string; snapshot: StockSnapshot; inventoryClass: "sellable" | "packaging" | null; lastSyncedAt: string; staleAfter: string }): Promise<{ replayed: boolean; readBack: boolean }>;
|
|
43
|
+
recordMovement(input: { requestKey: string; movement: StockMovement }): Promise<{ replayed: boolean; readBack: boolean }>;
|
|
44
|
+
recordLowStockEvidence(input: { requestKey: string; skuRef: string; thresholdKind: Exclude<Threshold, "forecast">; severity: "info" | "warning" | "critical"; providerForSaleQuantity: number; localAvailableQuantity: number; syncRunId: string }): Promise<{ replayed: boolean }>;
|
|
45
|
+
resolveLowStockEvidence(input: { skuRef: string; activeThresholdKinds: Threshold[]; syncRunId: string }): Promise<{ resolved: number }>;
|
|
46
|
+
}
|
|
47
|
+
interface ProductPack { ean: string; quantity: number }
|
|
48
|
+
export interface FulfillmentProductSyncProvider {
|
|
49
|
+
/** null means skipped (no private push metadata), never a newly created local SKU. */
|
|
50
|
+
ensureProduct(sku: string): Promise<{ replayed: boolean } | null>;
|
|
51
|
+
getProductPacks(sku: string): Promise<ProductPack[]>;
|
|
52
|
+
addProductPack(sku: string, pack: ProductPack): Promise<void>;
|
|
53
|
+
listStockedSkus(): Promise<string[]>;
|
|
54
|
+
}
|
|
55
|
+
export interface FulfillmentProductSyncPort {
|
|
56
|
+
listLocalProductPacks(): Promise<{ sku: string; packs: (ProductPack & { kind: "unit" | "collective" })[] }[]>;
|
|
57
|
+
markPackSynced(input: { sku: string; ean: string }): Promise<void>;
|
|
58
|
+
upsertPulledPack(input: { sku: string; ean: string; quantity: number; kind: "unit" | "collective" }): Promise<{ conflict: "none" | "local_locked_mismatch" | "ean_owned_by_other_sku" | "sku_not_found"; replayed: boolean }>;
|
|
59
|
+
recordReconciliationEvidence(input: { requestKey: string; sku: string; ean: string | null; conflictKind: Conflict; providerQuantity: number; identity: SyncIdentity; refusal: FulfillmentRefusal | null }): Promise<{ replayed: boolean }>;
|
|
60
|
+
resolveReconciliationEvidence(input: { sku: string; activeConflictKinds: Conflict[]; syncRunId: string }): Promise<{ resolved: number }>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { parcelSequenceNo, compareParcelPrecedence, currentParcel, currentParcelFor, currentParcelPerOrder, currentParcelFirst, belongsToParcel, rowsForParcel } from "@openlup/core/fulfillment";
|
|
2
|
+
export type { FulfillmentParcelRow, ParcelAttributedRow } from "@openlup/core/fulfillment";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CustomerDeliveryAlignment, FulfillmentCustomerAlignmentRowsReader } from './contracts/customerAlignment.js';
|
|
2
|
+
import type { FulfillmentNativeUnsupported } from './contracts/nativeUnsupported.js';
|
|
3
|
+
import { isNativeUnsupported } from './handlerSupport.js';
|
|
4
|
+
|
|
5
|
+
export type DeliveryAlignmentRow = Record<string, unknown>;
|
|
6
|
+
export type DeliveryAlignmentRowsReader = FulfillmentCustomerAlignmentRowsReader;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads only customer-safe current cases. An aligned case remains current while
|
|
10
|
+
* its aligned schedule is still the subscription's next schedule; once a paid
|
|
11
|
+
* cycle advances `next_cycle_at`, the historical ledger row disappears from UI.
|
|
12
|
+
*/
|
|
13
|
+
export async function readFulfillmentDelay(
|
|
14
|
+
readRows: DeliveryAlignmentRowsReader | undefined,
|
|
15
|
+
nextCycleAtBySubscription: ReadonlyMap<string, string | null>,
|
|
16
|
+
): Promise<Map<string, CustomerDeliveryAlignment> | FulfillmentNativeUnsupported> {
|
|
17
|
+
const subscriptionIds = [...nextCycleAtBySubscription.keys()].filter(Boolean);
|
|
18
|
+
if (!readRows || subscriptionIds.length === 0) return new Map();
|
|
19
|
+
const rows = await readRows(subscriptionIds);
|
|
20
|
+
if (isNativeUnsupported(rows)) return rows;
|
|
21
|
+
return mapCurrentDeliveryAlignments(rows, nextCycleAtBySubscription);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mapCurrentDeliveryAlignments(
|
|
25
|
+
rows: DeliveryAlignmentRow[],
|
|
26
|
+
nextCycleAtBySubscription: ReadonlyMap<string, string | null>,
|
|
27
|
+
): Map<string, CustomerDeliveryAlignment> {
|
|
28
|
+
const result = new Map<string, CustomerDeliveryAlignment>();
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const subscriptionId = text(row.subscription_id);
|
|
31
|
+
if (!subscriptionId || result.has(subscriptionId)) continue;
|
|
32
|
+
const state = text(row.state);
|
|
33
|
+
if (state === "protected") {
|
|
34
|
+
result.set(subscriptionId, { state });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (state === "aligned" && sameInstant(
|
|
38
|
+
nullableText(row.aligned_next_cycle_at),
|
|
39
|
+
nextCycleAtBySubscription.get(subscriptionId) ?? null,
|
|
40
|
+
)) {
|
|
41
|
+
result.set(subscriptionId, { state });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameInstant(left: string | null, right: string | null): boolean {
|
|
48
|
+
if (!left || !right) return false;
|
|
49
|
+
const leftTime = new Date(left).getTime();
|
|
50
|
+
const rightTime = new Date(right).getTime();
|
|
51
|
+
return Number.isFinite(leftTime) && leftTime === rightTime;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function text(value: unknown): string {
|
|
55
|
+
return typeof value === "string" ? value : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nullableText(value: unknown): string | null {
|
|
59
|
+
return typeof value === "string" && value ? value : null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DeliveryAlignmentPort } from './contracts/index.js';
|
|
2
|
+
import type { PgExecutor } from './postgresDispatchStore.js';
|
|
3
|
+
|
|
4
|
+
/** Server-authorized projection; database browser roles have no direct access. */
|
|
5
|
+
export async function readDeliveryAlignment(db: PgExecutor, fulfillmentRef: string): ReturnType<DeliveryAlignmentPort['read']> {
|
|
6
|
+
const result = await db.query(`SELECT alignment.state, alignment.aligned_next_cycle_at
|
|
7
|
+
FROM public.fulfillment_shipments shipment
|
|
8
|
+
LEFT JOIN public.subscription_delivery_alignment_cases alignment ON alignment.predecessor_order_id=shipment.order_id
|
|
9
|
+
WHERE shipment.id=$1::uuid
|
|
10
|
+
ORDER BY (alignment.state IN ('protected','manual_review')) DESC, alignment.created_at ASC LIMIT 1`, [fulfillmentRef]);
|
|
11
|
+
const row = result.rows[0];
|
|
12
|
+
const date = row?.aligned_next_cycle_at;
|
|
13
|
+
return { disposition: row?.state === 'aligned' ? 'aligned' : row?.state === 'protected' ? 'outstanding' : 'manual_review',
|
|
14
|
+
nextCycleAt: date instanceof Date ? date.toISOString() : typeof date === 'string' ? date : null };
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DispatchEvidence, DispatchRecord, FulfillmentDispatchResult } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
/** Copy only the durable allowlist, including when an adapter object has extra runtime properties. */
|
|
4
|
+
export function dispatchEvidence(evidence: DispatchEvidence): DispatchEvidence {
|
|
5
|
+
return {
|
|
6
|
+
commandFingerprint: evidence.commandFingerprint,
|
|
7
|
+
contactRevision: { schemaVersion: evidence.contactRevision.schemaVersion, source: evidence.contactRevision.source, revision: evidence.contactRevision.revision },
|
|
8
|
+
providerRef: evidence.providerRef, carrierRef: evidence.carrierRef, serviceRef: evidence.serviceRef,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function dispatchResult(result: FulfillmentDispatchResult): FulfillmentDispatchResult {
|
|
12
|
+
switch (result.kind) {
|
|
13
|
+
case "accepted": return { kind: "accepted", externalRef: result.externalRef, providerAttemptKey: result.providerAttemptKey, labelKey: result.labelKey, evidence: dispatchEvidence(result.evidence) };
|
|
14
|
+
case "not_dispatched": return { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: { reason: result.refusal.reason, phase: result.refusal.phase, retryable: result.refusal.retryable } };
|
|
15
|
+
case "unknown": return { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: result.refusal.reason, phase: result.refusal.phase, retryable: result.refusal.retryable } };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function acceptedProviderProof(record: DispatchRecord | null): record is DispatchRecord & { externalRef: string } {
|
|
19
|
+
return Boolean(record?.externalRef && (record.status === "created" || record.status === "uncertain"));
|
|
20
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { FulfillmentObservation } from "./contracts/index.js";
|
|
2
|
+
/** Ledger effects for an already accepted observation; acceptance and retry belong to the caller. */
|
|
3
|
+
interface FulfillmentObservationPort<TrackingStatus> {
|
|
4
|
+
markProviderStockConsumed(input: { idempotencyKey: string; fulfillmentOrderId: string }): Promise<{ replayed: boolean }>;
|
|
5
|
+
markHandedOver(input: { idempotencyKey: string; fulfillmentOrderId: string; suppressDispatched: boolean }): Promise<{ replayed: boolean }>;
|
|
6
|
+
recordTrackingReference(input: {
|
|
7
|
+
idempotencyKey: string;
|
|
8
|
+
orderId: string;
|
|
9
|
+
fulfillmentOrderId: string;
|
|
10
|
+
trackingNumber: string;
|
|
11
|
+
trackingUrl: string | null;
|
|
12
|
+
carrierKind: string | null;
|
|
13
|
+
service: string | null;
|
|
14
|
+
status: TrackingStatus;
|
|
15
|
+
observation: FulfillmentObservation;
|
|
16
|
+
}): Promise<{ replayed: boolean; readBack: boolean }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FulfillmentObservationTrackingReference {
|
|
20
|
+
trackingNumber: string;
|
|
21
|
+
trackingUrl?: string | null;
|
|
22
|
+
carrierKind?: string | null;
|
|
23
|
+
service?: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
interface FulfillmentObservationInput<LocalStatus, TrackingStatus> {
|
|
28
|
+
port: FulfillmentObservationPort<TrackingStatus>;
|
|
29
|
+
localStatus: LocalStatus;
|
|
30
|
+
ids: { orderId: string; fulfillmentOrderId: string };
|
|
31
|
+
observation: FulfillmentObservation;
|
|
32
|
+
trackingRefs: readonly FulfillmentObservationTrackingReference[];
|
|
33
|
+
stockIdempotencyKey: string;
|
|
34
|
+
handoffIdempotencyKey: string;
|
|
35
|
+
trackingIdempotencyKey: (trackingNumber: string) => string;
|
|
36
|
+
issueAccountingInvoice?: () => Promise<void>;
|
|
37
|
+
onReplayed?: () => void;
|
|
38
|
+
onTrackingRecorded?: (result: { replayed: boolean; readBack: boolean }) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pure status decisions/projection; the mechanism owns every effect and await. */
|
|
42
|
+
interface FulfillmentObservationStatusPolicy<LocalStatus, TrackingStatus> {
|
|
43
|
+
consumesStock(status: LocalStatus): boolean;
|
|
44
|
+
handsOver(status: LocalStatus): boolean;
|
|
45
|
+
suppressDispatched(status: LocalStatus): boolean;
|
|
46
|
+
trackingStatus(status: LocalStatus): TrackingStatus;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Stock → durable handover → every tracking reference → optional invoice. No catches or retries. */
|
|
50
|
+
export async function applyFulfillmentObservationEffects<LocalStatus, TrackingStatus>(
|
|
51
|
+
input: FulfillmentObservationInput<LocalStatus, TrackingStatus>,
|
|
52
|
+
policy: FulfillmentObservationStatusPolicy<LocalStatus, TrackingStatus>,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const { fulfillmentOrderId, orderId } = input.ids;
|
|
55
|
+
if (policy.consumesStock(input.localStatus)) {
|
|
56
|
+
const consumed = await input.port.markProviderStockConsumed({
|
|
57
|
+
idempotencyKey: input.stockIdempotencyKey,
|
|
58
|
+
fulfillmentOrderId,
|
|
59
|
+
});
|
|
60
|
+
if (consumed.replayed) input.onReplayed?.();
|
|
61
|
+
}
|
|
62
|
+
if (!policy.handsOver(input.localStatus)) return;
|
|
63
|
+
|
|
64
|
+
const handoff = await input.port.markHandedOver({
|
|
65
|
+
idempotencyKey: input.handoffIdempotencyKey,
|
|
66
|
+
fulfillmentOrderId,
|
|
67
|
+
suppressDispatched: policy.suppressDispatched(input.localStatus),
|
|
68
|
+
});
|
|
69
|
+
if (handoff.replayed) input.onReplayed?.();
|
|
70
|
+
|
|
71
|
+
for (const trackingRef of input.trackingRefs) {
|
|
72
|
+
const tracking = await input.port.recordTrackingReference({
|
|
73
|
+
idempotencyKey: input.trackingIdempotencyKey(trackingRef.trackingNumber),
|
|
74
|
+
orderId,
|
|
75
|
+
fulfillmentOrderId,
|
|
76
|
+
trackingNumber: trackingRef.trackingNumber,
|
|
77
|
+
trackingUrl: trackingRef.trackingUrl ?? null,
|
|
78
|
+
carrierKind: trackingRef.carrierKind ?? null,
|
|
79
|
+
service: trackingRef.service ?? null,
|
|
80
|
+
status: policy.trackingStatus(input.localStatus),
|
|
81
|
+
observation: input.observation,
|
|
82
|
+
});
|
|
83
|
+
input.onTrackingRecorded?.(tracking);
|
|
84
|
+
}
|
|
85
|
+
await input.issueAccountingInvoice?.();
|
|
86
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PAYMENT_INTENT_STATUSES } from '@openlup/core/payment';
|
|
3
|
+
const OMS_FULFILLMENT_BLOCK_REASONS = ['order_not_paid','payment_not_succeeded','subscription_cycle_not_paid','active_hold','missing_shipping_address','inventory_review','risk_review'] as const;
|
|
4
|
+
const OMS_INVENTORY_STATUSES = ['not_checked','reserved','missing','released','expired','consumed','review_required'] as const;
|
|
5
|
+
export const COMMERCE_FULFILLMENT_CONTRACT_VERSION = "commerce.fulfillment.v0";
|
|
6
|
+
|
|
7
|
+
export const COMMERCE_FULFILLMENT_STATUSES = [
|
|
8
|
+
"created",
|
|
9
|
+
"packed",
|
|
10
|
+
"label_pending",
|
|
11
|
+
"label_created",
|
|
12
|
+
"handed_over",
|
|
13
|
+
"in_transit",
|
|
14
|
+
"delivered",
|
|
15
|
+
"exception",
|
|
16
|
+
"cancelled",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export const COMMERCE_FULFILLMENT_OPERATION_TYPES = [
|
|
20
|
+
"created",
|
|
21
|
+
"packed",
|
|
22
|
+
"provider_attempt_recorded",
|
|
23
|
+
"label_created",
|
|
24
|
+
"handed_over",
|
|
25
|
+
"tracking_event_recorded",
|
|
26
|
+
"cancelled",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export const COMMERCE_FULFILLMENT_PROVIDER_ATTEMPT_STATUSES = [
|
|
30
|
+
"recorded",
|
|
31
|
+
"succeeded",
|
|
32
|
+
"failed",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
const uuidSchema = z.guid();
|
|
36
|
+
const datetimeSchema = z.string().datetime({ offset: true });
|
|
37
|
+
|
|
38
|
+
export const commerceFulfillmentStatusSchema = z.enum(COMMERCE_FULFILLMENT_STATUSES);
|
|
39
|
+
export const commerceFulfillmentOperationTypeSchema = z.enum(COMMERCE_FULFILLMENT_OPERATION_TYPES);
|
|
40
|
+
export const commerceFulfillmentProviderAttemptStatusSchema = z.enum(
|
|
41
|
+
COMMERCE_FULFILLMENT_PROVIDER_ATTEMPT_STATUSES,
|
|
42
|
+
);
|
|
43
|
+
export const commerceFulfillmentPaymentStatusSchema = z.enum(["not_started", ...PAYMENT_INTENT_STATUSES]);
|
|
44
|
+
export const commerceFulfillmentInventoryStatusSchema = z.enum(OMS_INVENTORY_STATUSES);
|
|
45
|
+
export const commerceFulfillmentBlockReasonSchema = z.enum(OMS_FULFILLMENT_BLOCK_REASONS);
|
|
46
|
+
|
|
47
|
+
export const commerceFulfillmentShippingAddressSnapshotSchema = z.object({
|
|
48
|
+
addressId: uuidSchema,
|
|
49
|
+
clientId: uuidSchema,
|
|
50
|
+
label: z.string().nullable(),
|
|
51
|
+
line1: z.string().trim().min(1),
|
|
52
|
+
line2: z.string().nullable(),
|
|
53
|
+
city: z.string().trim().min(1),
|
|
54
|
+
postalCode: z.string().trim().min(1),
|
|
55
|
+
country: z.string().trim().min(2).max(3),
|
|
56
|
+
}).strict();
|
|
57
|
+
|
|
58
|
+
export const commerceFulfillmentLineSchema = z.object({
|
|
59
|
+
id: uuidSchema.nullable(),
|
|
60
|
+
orderItemId: uuidSchema,
|
|
61
|
+
skuId: uuidSchema,
|
|
62
|
+
sku: z.string().trim().min(1),
|
|
63
|
+
title: z.string().trim().min(1).nullable(),
|
|
64
|
+
quantity: z.number().int().positive(),
|
|
65
|
+
inventoryReservationIds: z.array(uuidSchema),
|
|
66
|
+
productSnapshot: z.record(z.string(), z.unknown()),
|
|
67
|
+
}).strict();
|
|
68
|
+
|
|
69
|
+
export const commerceFulfillmentPaymentSummarySchema = z.object({
|
|
70
|
+
paymentIntentId: uuidSchema,
|
|
71
|
+
paymentStatus: commerceFulfillmentPaymentStatusSchema,
|
|
72
|
+
providerPaymentId: z.string().nullable(),
|
|
73
|
+
}).strict();
|
|
74
|
+
|
|
75
|
+
export const commerceFulfillmentInventorySummarySchema = z.object({
|
|
76
|
+
status: commerceFulfillmentInventoryStatusSchema,
|
|
77
|
+
reservationId: uuidSchema.nullable(),
|
|
78
|
+
reservationStatus: z.enum(["reserved", "released", "consumed", "expired"]).nullable(),
|
|
79
|
+
expiresAt: datetimeSchema.nullable(),
|
|
80
|
+
locationId: uuidSchema.nullable(),
|
|
81
|
+
locationCode: z.string().nullable(),
|
|
82
|
+
}).strict();
|
|
83
|
+
|
|
84
|
+
export const commerceFulfillmentOmsEligibilitySchema = z.object({
|
|
85
|
+
allowed: z.boolean(),
|
|
86
|
+
reason: commerceFulfillmentBlockReasonSchema.nullable(),
|
|
87
|
+
}).strict();
|
|
88
|
+
|
|
89
|
+
export const commerceFulfillmentOperationSchema = z.object({
|
|
90
|
+
id: uuidSchema,
|
|
91
|
+
type: commerceFulfillmentOperationTypeSchema,
|
|
92
|
+
occurredAt: datetimeSchema,
|
|
93
|
+
actorUserId: uuidSchema.nullable(),
|
|
94
|
+
payload: z.record(z.string(), z.unknown()),
|
|
95
|
+
}).strict();
|
|
96
|
+
|
|
97
|
+
export const commerceFulfillmentOrderSchema = z.object({
|
|
98
|
+
id: uuidSchema,
|
|
99
|
+
orderId: uuidSchema,
|
|
100
|
+
clientId: uuidSchema.nullable(),
|
|
101
|
+
status: commerceFulfillmentStatusSchema,
|
|
102
|
+
providerKind: z.string().trim().min(1).nullable(),
|
|
103
|
+
providerTrackingId: z.string().trim().min(1).nullable(),
|
|
104
|
+
shippingAddress: commerceFulfillmentShippingAddressSnapshotSchema,
|
|
105
|
+
lines: z.array(commerceFulfillmentLineSchema).min(1),
|
|
106
|
+
payment: commerceFulfillmentPaymentSummarySchema,
|
|
107
|
+
inventory: commerceFulfillmentInventorySummarySchema,
|
|
108
|
+
omsEligibility: commerceFulfillmentOmsEligibilitySchema,
|
|
109
|
+
latestOperation: commerceFulfillmentOperationSchema.nullable(),
|
|
110
|
+
createdAt: datetimeSchema,
|
|
111
|
+
updatedAt: datetimeSchema,
|
|
112
|
+
}).strict();
|
|
113
|
+
|
|
114
|
+
export const adminCommerceFulfillmentOrdersListRequestSchema = z.object({
|
|
115
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
116
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(25),
|
|
117
|
+
status: commerceFulfillmentStatusSchema.optional(),
|
|
118
|
+
}).strict();
|
|
119
|
+
|
|
120
|
+
export const adminCommerceFulfillmentOrderDetailRequestSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
fulfillmentOrderId: uuidSchema.optional(),
|
|
123
|
+
orderId: uuidSchema.optional(),
|
|
124
|
+
})
|
|
125
|
+
.strict()
|
|
126
|
+
.refine((request) => request.fulfillmentOrderId || request.orderId, {
|
|
127
|
+
message: "fulfillmentOrderId or orderId is required",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const adminCommerceFulfillmentCreateRequestSchema = z.object({
|
|
131
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
132
|
+
orderId: uuidSchema,
|
|
133
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
134
|
+
}).strict();
|
|
135
|
+
|
|
136
|
+
export const adminCommerceFulfillmentRecordProviderAttemptRequestSchema = z.object({
|
|
137
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
138
|
+
fulfillmentOrderId: uuidSchema,
|
|
139
|
+
providerKind: z.string().trim().min(1),
|
|
140
|
+
status: commerceFulfillmentProviderAttemptStatusSchema,
|
|
141
|
+
requestPayload: z.record(z.string(), z.unknown()).default({}),
|
|
142
|
+
responsePayload: z.record(z.string(), z.unknown()).default({}),
|
|
143
|
+
error: z.string().trim().min(1).nullable().optional(),
|
|
144
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
145
|
+
}).strict();
|
|
146
|
+
|
|
147
|
+
export const adminCommerceFulfillmentRecordLabelRequestSchema = z.object({
|
|
148
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
149
|
+
fulfillmentOrderId: uuidSchema,
|
|
150
|
+
providerKind: z.string().trim().min(1),
|
|
151
|
+
providerTrackingId: z.string().trim().min(1),
|
|
152
|
+
labelUrl: z.string().trim().min(1).nullable().optional(),
|
|
153
|
+
deliveryEstimateDays: z.number().int().nonnegative().nullable().optional(),
|
|
154
|
+
rawProviderPayload: z.record(z.string(), z.unknown()).default({}),
|
|
155
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
156
|
+
}).strict();
|
|
157
|
+
|
|
158
|
+
export const adminCommerceFulfillmentHandOffRequestSchema = z.object({
|
|
159
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
160
|
+
fulfillmentOrderId: uuidSchema,
|
|
161
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
162
|
+
}).strict();
|
|
163
|
+
|
|
164
|
+
export const adminCommerceFulfillmentTrackingEventRequestSchema = z.object({
|
|
165
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
166
|
+
fulfillmentOrderId: uuidSchema,
|
|
167
|
+
status: z.enum(["in_transit", "delivered", "exception"]),
|
|
168
|
+
providerTrackingId: z.string().trim().min(1).optional(),
|
|
169
|
+
rawEvent: z.record(z.string(), z.unknown()).default({}),
|
|
170
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
171
|
+
}).strict();
|
|
172
|
+
|
|
173
|
+
export const adminCommerceFulfillmentCancelRequestSchema = z.object({
|
|
174
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
175
|
+
fulfillmentOrderId: uuidSchema,
|
|
176
|
+
reason: z.string().trim().min(1).max(240),
|
|
177
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
178
|
+
}).strict();
|
|
179
|
+
|
|
180
|
+
export const adminCommerceFulfillmentOrdersListResponseSchema = z.object({
|
|
181
|
+
contractVersion: z.literal(COMMERCE_FULFILLMENT_CONTRACT_VERSION),
|
|
182
|
+
orders: z.array(commerceFulfillmentOrderSchema),
|
|
183
|
+
totalCount: z.number().int().min(0),
|
|
184
|
+
page: z.number().int().min(1),
|
|
185
|
+
pageSize: z.number().int().min(1),
|
|
186
|
+
}).strict();
|
|
187
|
+
|
|
188
|
+
export const adminCommerceFulfillmentOrderDetailResponseSchema = z.object({
|
|
189
|
+
contractVersion: z.literal(COMMERCE_FULFILLMENT_CONTRACT_VERSION),
|
|
190
|
+
order: commerceFulfillmentOrderSchema,
|
|
191
|
+
}).strict();
|
|
192
|
+
|
|
193
|
+
export const adminCommerceFulfillmentMutationResponseSchema = z.object({
|
|
194
|
+
contractVersion: z.literal(COMMERCE_FULFILLMENT_CONTRACT_VERSION),
|
|
195
|
+
fulfillmentOrderId: uuidSchema,
|
|
196
|
+
orderId: uuidSchema,
|
|
197
|
+
status: commerceFulfillmentStatusSchema,
|
|
198
|
+
replayed: z.boolean(),
|
|
199
|
+
}).strict();
|
|
200
|
+
|
|
201
|
+
export type AdminCommerceFulfillmentOrdersListRequest = z.infer<typeof adminCommerceFulfillmentOrdersListRequestSchema>;
|
|
202
|
+
export type AdminCommerceFulfillmentOrderDetailRequest = z.infer<typeof adminCommerceFulfillmentOrderDetailRequestSchema>;
|
|
203
|
+
export type AdminCommerceFulfillmentCreateRequest = z.infer<typeof adminCommerceFulfillmentCreateRequestSchema>;
|
|
204
|
+
export type AdminCommerceFulfillmentRecordProviderAttemptRequest = z.input<typeof adminCommerceFulfillmentRecordProviderAttemptRequestSchema>;
|
|
205
|
+
export type AdminCommerceFulfillmentRecordLabelRequest = z.input<typeof adminCommerceFulfillmentRecordLabelRequestSchema>;
|
|
206
|
+
export type AdminCommerceFulfillmentHandOffRequest = z.infer<typeof adminCommerceFulfillmentHandOffRequestSchema>;
|
|
207
|
+
export type AdminCommerceFulfillmentTrackingEventRequest = z.input<typeof adminCommerceFulfillmentTrackingEventRequestSchema>;
|
|
208
|
+
export type AdminCommerceFulfillmentCancelRequest = z.infer<typeof adminCommerceFulfillmentCancelRequestSchema>;
|
|
209
|
+
export type AdminCommerceFulfillmentOrdersListResponse = z.infer<typeof adminCommerceFulfillmentOrdersListResponseSchema>;
|
|
210
|
+
export type AdminCommerceFulfillmentOrderDetailResponse = z.infer<typeof adminCommerceFulfillmentOrderDetailResponseSchema>;
|
|
211
|
+
export type AdminCommerceFulfillmentMutationResponse = z.infer<typeof adminCommerceFulfillmentMutationResponseSchema>;
|
|
212
|
+
export type CommerceFulfillmentOrder = z.infer<typeof commerceFulfillmentOrderSchema>;
|
|
213
|
+
export type CommerceFulfillmentStatus = z.infer<typeof commerceFulfillmentStatusSchema>;
|
|
214
|
+
export type CommerceFulfillmentOperationType = z.infer<typeof commerceFulfillmentOperationTypeSchema>;
|
|
215
|
+
|
|
216
|
+
export const adminCommerceOrderRequestReplacementShipmentRequestSchema = z.object({
|
|
217
|
+
idempotencyKey: z.string().trim().min(8).max(120),
|
|
218
|
+
orderId: uuidSchema,
|
|
219
|
+
reason: z.enum(['damaged', 'lost', 'returned_undelivered', 'other']),
|
|
220
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
221
|
+
}).strict();
|
|
222
|
+
export const omsOperationTypeSchema = z.enum([
|
|
223
|
+
"hold_created",
|
|
224
|
+
"hold_released",
|
|
225
|
+
"fulfillment_reviewed",
|
|
226
|
+
"support_note",
|
|
227
|
+
"shipping_address_updated",
|
|
228
|
+
"order_marked_refunded",
|
|
229
|
+
"order_cancelled_manual",
|
|
230
|
+
"replacement_shipment_requested",
|
|
231
|
+
]);
|
|
232
|
+
export const omsOrderOperationSchema = z.object({
|
|
233
|
+
id: uuidSchema,
|
|
234
|
+
orderId: uuidSchema,
|
|
235
|
+
type: omsOperationTypeSchema,
|
|
236
|
+
holdId: uuidSchema.nullable(),
|
|
237
|
+
actorUserId: uuidSchema.nullable(),
|
|
238
|
+
occurredAt: datetimeSchema,
|
|
239
|
+
payload: z.record(z.string(), z.unknown()),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export const adminCommerceOrderRequestReplacementShipmentResponseSchema = z.object({
|
|
243
|
+
contractVersion: z.literal('commerce.v0'),
|
|
244
|
+
operation: omsOrderOperationSchema,
|
|
245
|
+
replacement: z.object({
|
|
246
|
+
fulfillmentOrderId: z.string().uuid(),
|
|
247
|
+
orderId: z.string().uuid(),
|
|
248
|
+
sequenceNo: z.number().int().min(1),
|
|
249
|
+
replacesFulfillmentOrderId: z.string().uuid(),
|
|
250
|
+
reason: z.enum(["damaged", "lost", "returned_undelivered", "other"]),
|
|
251
|
+
status: z.string(),
|
|
252
|
+
}),
|
|
253
|
+
releasedHoldIds: z.array(z.string().uuid()).default([]),
|
|
254
|
+
replayed: z.boolean(),
|
|
255
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FulfillmentNativeUnsupported } from './contracts/nativeUnsupported.js';
|
|
2
|
+
export interface FulfillmentHandlerRequest { method?: string; query: Record<string, string | string[] | undefined>; body?: unknown }
|
|
3
|
+
export interface FulfillmentHandlerResponder<Response> {
|
|
4
|
+
sendMethodNotAllowed(res: Response, methods: readonly string[]): void;
|
|
5
|
+
sendBffSuccess(res: Response, data: unknown): void;
|
|
6
|
+
sendBffError(res: Response, code: 'UNAUTHORIZED' | 'FORBIDDEN' | 'BAD_REQUEST' | 'INVALID_RESPONSE' | 'NOT_FOUND' | 'CONFLICT' | 'UPSTREAM_UNAVAILABLE', message: string, options?: { details: unknown }): void;
|
|
7
|
+
}
|
|
8
|
+
export type AdminAuthResult = { ok: false; code: 'UNAUTHORIZED' | 'FORBIDDEN'; message: string } | { ok: true; userId: string };
|
|
9
|
+
export interface BaseDeps<Response> {
|
|
10
|
+
authorizeAdmin(req: FulfillmentHandlerRequest): Promise<AdminAuthResult>;
|
|
11
|
+
respond: FulfillmentHandlerResponder<Response>;
|
|
12
|
+
}
|
|
13
|
+
export function isNativeUnsupported(value: unknown): value is FulfillmentNativeUnsupported {
|
|
14
|
+
return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'native_unsupported';
|
|
15
|
+
}
|
|
16
|
+
export class CommerceFulfillmentConflictError extends Error {
|
|
17
|
+
constructor(message = 'Commerce fulfillment conflict', readonly details: Record<string, unknown> = {}) { super(message); this.name = 'CommerceFulfillmentConflictError'; }
|
|
18
|
+
}
|
|
19
|
+
export async function authorize<Response>(req: FulfillmentHandlerRequest, res: Response, authorizeAdmin: BaseDeps<Response>['authorizeAdmin'], respond: FulfillmentHandlerResponder<Response>) {
|
|
20
|
+
try {
|
|
21
|
+
const authorization = await authorizeAdmin(req);
|
|
22
|
+
if (authorization.ok === false) { respond.sendBffError(res, authorization.code, authorization.message); return null; }
|
|
23
|
+
return { userId: authorization.userId };
|
|
24
|
+
} catch { respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', 'Admin authorization failed'); return null; }
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "./statusMap.js";
|
|
2
|
+
export { parseFulfillmentFact, parseFulfillmentTransitionConflict } from "./normalizedFulfillmentFacts.js";
|
|
3
|
+
export { readFulfillmentCapabilities } from "./providerCapabilities.js";
|
|
4
|
+
export { createOutboxOrderPaidFulfillmentHandler } from "./paidHandoff.js";
|
|
5
|
+
export { collectPaidFulfillmentRecoveryCandidates } from "./paidFulfillmentRecovery.js";
|
|
6
|
+
export { runFulfillmentDispatchWorker } from "./providerDispatchWorker.js";
|
|
7
|
+
export { createFulfillmentCommandLedger } from "./providerCommandLedger.js";
|
|
8
|
+
export { verifyAndNormalizeFulfillmentObservation } from "./verifiedIngress.js";
|
|
9
|
+
export { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
10
|
+
export { reconcileFulfillmentObservations } from "./providerReconciliationWorker.js";
|
|
11
|
+
export { runFulfillmentStockSyncWorker } from "./providerStockSyncWorker.js";
|
|
12
|
+
export { runFulfillmentProductSyncWorker } from "./providerProductSyncWorker.js";
|
|
13
|
+
export { createFulfillmentLowStockEvidenceHandler } from "./lowStockEvidenceHandler.js";
|
|
14
|
+
export { createProductReconciliationEvidenceHandler } from "./productReconciliationEvidenceHandler.js";
|
|
15
|
+
export { runFulfillmentAdapterConformance } from "./adapterConformance.js";
|
|
16
|
+
export { createPostgresFulfillmentShipmentSpinePort } from './postgresShipmentSpine.js';
|
|
17
|
+
export { createPostgresFulfillmentLowStockEvidencePort } from './postgresEvidenceReadPort.js';
|
|
18
|
+
export { createPostgresFulfillmentStockSyncPort } from './postgresStockSyncPort.js';
|
|
19
|
+
export { createPostgresPaidFulfillmentRecoveryOpsPort } from './postgresPaidFulfillmentRecovery.js';
|
|
20
|
+
export { createPostgresFulfillmentDispatchStore } from './postgresDispatchStore.js';
|
|
21
|
+
export { createPostgresPaidFulfillmentHandoffStore } from './postgresPaidHandoffStore.js';
|
|
22
|
+
export { createPostgresDeliveryAlignmentPort } from './postgresDeliveryAlignment.js';
|
|
23
|
+
export { readDeliveryAlignment } from './deliveryAlignmentReadModel.js';
|
|
24
|
+
export { FULFILLMENT_NATIVE_CAPABILITIES } from './schemaCapabilities.js';
|
|
25
|
+
export { FULFILLMENT_MIGRATIONS } from './migrations.js';
|
|
26
|
+
|
|
27
|
+
export { createAdminCommerceFulfillmentOrdersListHandler, createAdminCommerceFulfillmentOrderDetailHandler, createAdminCommerceFulfillmentCreateOrderHandler, createAdminCommerceFulfillmentRecordProviderAttemptHandler, createAdminCommerceFulfillmentRecordLabelHandler, createAdminCommerceFulfillmentHandOffHandler, createAdminCommerceFulfillmentTrackingEventHandler, createAdminCommerceFulfillmentCancelOrderHandler } from './commerceFulfillmentHandlers.js';
|
|
28
|
+
export { createAdminCommerceOrderRequestReplacementShipmentHandler } from './replacementShipmentHandler.js';
|
|
29
|
+
export { CommerceFulfillmentConflictError } from './handlerSupport.js';
|
|
30
|
+
export { readFulfillmentDelay } from './delayReadModel.js';
|
|
31
|
+
export { readFulfillmentLabel, cleanupFulfillmentLabel } from './labelLifecycle.js';
|
|
32
|
+
export { currentParcel, currentParcelFor, currentParcelPerOrder, currentParcelFirst, belongsToParcel, rowsForParcel, parcelSequenceNo, compareParcelPrecedence } from './currentFulfillmentParcel.js';
|
|
33
|
+
export { FULFILLMENT_NATIVE_UNSUPPORTED, createNativeUnsupportedFulfillmentPorts } from './nativeUnsupported.js';
|
|
34
|
+
export { FULFILLMENT_REPLACEMENT_REFUSALS } from './replacementRefusals.js';
|
|
35
|
+
export { buildCommerceFulfillmentOrdersListResponse, buildCommerceFulfillmentOrderDetailResponse } from './commerceFulfillmentReadModel.js';
|
|
36
|
+
export { createFulfillmentShipmentsOverviewHandler } from './shipmentsOverviewHandler.js';
|
|
37
|
+
export { fulfillmentProgressPresentation } from './trackingProgress.js';
|
|
38
|
+
export { createFulfillmentServices } from './bindings.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FulfillmentLabelPort, LabelDocument } from './contracts/label.js';
|
|
2
|
+
/** Storage-only read: a missing cached label never proves the parcel absent. Signed URLs are transient. */
|
|
3
|
+
export async function readFulfillmentLabel(port: Pick<FulfillmentLabelPort, 'read'>, document: LabelDocument | null) {
|
|
4
|
+
if (!document) return { kind: 'refused' as const, refusal: { reason: 'local_missing' as const, phase: 'readback' as const, retryable: false } };
|
|
5
|
+
return port.read(document);
|
|
6
|
+
}
|
|
7
|
+
/** Provider and storage deletion remain host-owned; canProceed never asserts either deletion. */
|
|
8
|
+
type LabelCleanupInput = Parameters<FulfillmentLabelPort['void']>[0] & { actorUserId: string; idempotencyKey: string };
|
|
9
|
+
export async function cleanupFulfillmentLabel(port: { void(input: LabelCleanupInput): ReturnType<FulfillmentLabelPort['void']> }, input: LabelCleanupInput) {
|
|
10
|
+
return port.void(input);
|
|
11
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Read-only evidence boundary; parsing remains with the owning host schema. */
|
|
2
|
+
export function createFulfillmentLowStockEvidenceHandler<Request extends { method?: string; query: Record<string, string | string[] | undefined> }, Response, Query, Result>({ evidencePort, authorizeAdmin, requestSchema, responseSchema, respond }: {
|
|
3
|
+
evidencePort: { getLowStockEvidence(query: Query): Promise<unknown> };
|
|
4
|
+
authorizeAdmin(request: Request): Promise<boolean>;
|
|
5
|
+
requestSchema: { safeParse(input: unknown): { success: true; data: Query } | { success: false; error: { flatten(): unknown } } };
|
|
6
|
+
responseSchema: { safeParse(input: unknown): { success: true; data: Result } | { success: false } };
|
|
7
|
+
respond: {
|
|
8
|
+
sendBffError(res: Response, code: "UPSTREAM_UNAVAILABLE" | "UNAUTHORIZED" | "BAD_REQUEST" | "INVALID_RESPONSE", message: string, options?: { details: unknown }): void;
|
|
9
|
+
sendBffSuccess(res: Response, data: Result): void;
|
|
10
|
+
sendMethodNotAllowed(res: Response, methods: readonly string[]): void;
|
|
11
|
+
};
|
|
12
|
+
}) {
|
|
13
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
|
|
14
|
+
return async function handler(req: Request, res: Response): Promise<void> {
|
|
15
|
+
if (req.method !== "GET") {
|
|
16
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let authorized = false;
|
|
21
|
+
try {
|
|
22
|
+
authorized = await authorizeAdmin(req);
|
|
23
|
+
} catch {
|
|
24
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!authorized) {
|
|
29
|
+
sendBffError(res, "UNAUTHORIZED", "Admin session required");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const request = requestSchema.safeParse({
|
|
34
|
+
statusFilter: scalarQuery(req.query.statusFilter),
|
|
35
|
+
});
|
|
36
|
+
if (!request.success) {
|
|
37
|
+
sendBffError(res, "BAD_REQUEST", "Invalid low-stock evidence request", {
|
|
38
|
+
details: request.error.flatten(),
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const result = await evidencePort.getLowStockEvidence(request.data);
|
|
45
|
+
const response = responseSchema.safeParse(result);
|
|
46
|
+
if (!response.success) {
|
|
47
|
+
sendBffError(res, "INVALID_RESPONSE", "Low-stock evidence returned invalid response");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sendBffSuccess(res, response.data);
|
|
52
|
+
} catch {
|
|
53
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Low-stock evidence read failed");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scalarQuery(value: string | string[] | undefined): string | undefined {
|
|
59
|
+
return Array.isArray(value) ? value[0] : value;
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Byte-identical sparse projections; install the complete canonical catalogue. */
|
|
2
|
+
export const FULFILLMENT_MIGRATIONS = [
|
|
3
|
+
{ position: 5, catalogPath: "db/platform/migrations/20260808210000_fulfillment_shipment_rail.sql", sha256: "eb5824551903717f3868690909fcc2cb6a7f845abe5cb19bd4cff13103b1383d", url: new URL("../../sql/20260808210000_fulfillment_shipment_rail.sql", import.meta.url) },
|
|
4
|
+
{ position: 27, catalogPath: "db/platform/migrations/20260814000000_fulfillment_stock_evidence.sql", sha256: "df20c2491cb0196b4e28d19e7578c95bfa5c87b8f89fb0b6284c0e0695a3cc1d", url: new URL("../../sql/20260814000000_fulfillment_stock_evidence.sql", import.meta.url) },
|
|
5
|
+
{ position: 51, catalogPath: "db/platform/migrations/20260819150000_fulfillment_shipment_sequence.sql", sha256: "906706cf66a6a555aa1219b12eb674d937eaa099abde6da18766310af43aaa9f", url: new URL("../../sql/20260819150000_fulfillment_shipment_sequence.sql", import.meta.url) },
|
|
6
|
+
{ position: 73, catalogPath: "db/platform/migrations/20261022080000_fulfillment_command_ledger.sql", sha256: "c6806906cdb0b6931b29f7a5c7c800016e98d04e6ea956f3c1e219a6cdb7e9d1", url: new URL("../../sql/20261022080000_fulfillment_command_ledger.sql", import.meta.url) },
|
|
7
|
+
{ position: 74, catalogPath: "db/platform/migrations/20261022090000_fulfillment_delivery_alignment.sql", sha256: "d9966ae79fe77a0feae8317dc5b46ef2d8196dd0790b1cd0ee1b06ab58061499", url: new URL("../../sql/20261022090000_fulfillment_delivery_alignment.sql", import.meta.url) },
|
|
8
|
+
] as const;
|