@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,232 @@
|
|
|
1
|
+
import { nativeCall } from './postgresDispatchStore.js';
|
|
2
|
+
import { projectObservation } from './observationProjection.js';
|
|
3
|
+
class CommerceFulfillmentConflictError extends Error {
|
|
4
|
+
detail;
|
|
5
|
+
constructor(message, detail) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.detail = detail;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class CommerceFulfillmentPersistenceError extends Error {
|
|
11
|
+
detail;
|
|
12
|
+
constructor(message, detail) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.detail = detail;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const CONTRACT_VERSION = "platform.fulfillment.shipment.v1";
|
|
18
|
+
const CREATE_SHIPMENT_SQL = `
|
|
19
|
+
WITH existing_operation AS (
|
|
20
|
+
SELECT 1 AS found
|
|
21
|
+
FROM public.fulfillment_shipment_operations
|
|
22
|
+
WHERE idempotency_key = $1::text || ':operation'
|
|
23
|
+
), configured_port AS (
|
|
24
|
+
SELECT port_key
|
|
25
|
+
FROM public.fulfillment_ports
|
|
26
|
+
WHERE port_key = $3::text
|
|
27
|
+
AND routable IS TRUE
|
|
28
|
+
AND ($6::text IS NULL OR provider_type = $6::text)
|
|
29
|
+
)
|
|
30
|
+
SELECT
|
|
31
|
+
CASE WHEN configured_port.port_key IS NULL AND existing_operation.found IS NULL THEN NULL
|
|
32
|
+
ELSE public.fulfillment_create_shipment(
|
|
33
|
+
$1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
|
|
34
|
+
)
|
|
35
|
+
END AS response,
|
|
36
|
+
configured_port.port_key IS NOT NULL OR existing_operation.found IS NOT NULL AS port_ready
|
|
37
|
+
FROM (SELECT (SELECT port_key FROM configured_port LIMIT 1) AS port_key) configured_port
|
|
38
|
+
CROSS JOIN (SELECT (SELECT found FROM existing_operation LIMIT 1) AS found) existing_operation`;
|
|
39
|
+
const ADVANCE_SHIPMENT_SQL = `
|
|
40
|
+
SELECT public.fulfillment_advance_shipment(
|
|
41
|
+
$1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
|
|
42
|
+
) AS response`;
|
|
43
|
+
const RECORD_EVIDENCE_SQL = `
|
|
44
|
+
SELECT public.fulfillment_record_evidence(
|
|
45
|
+
$1::text, $2::uuid, $3::text, $4::text, $5::text, $6::jsonb,
|
|
46
|
+
COALESCE($7::timestamptz, now())
|
|
47
|
+
) AS response`;
|
|
48
|
+
const CANCEL_SHIPMENT_SQL = `
|
|
49
|
+
SELECT public.fulfillment_cancel_shipment(
|
|
50
|
+
$1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
|
|
51
|
+
) AS response`;
|
|
52
|
+
const RAISE_EXCEPTION_SQL = `
|
|
53
|
+
SELECT public.fulfillment_raise_shipment_exception(
|
|
54
|
+
$1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
|
|
55
|
+
) AS response`;
|
|
56
|
+
/** Direct node-postgres adapter for the provider-neutral public shipment rail. */
|
|
57
|
+
export function createPostgresFulfillmentShipmentSpinePort(executor, options) {
|
|
58
|
+
const portKey = options.portKey.trim();
|
|
59
|
+
if (!portKey)
|
|
60
|
+
throw new Error("fulfillment_shipment_spine_port_key_required");
|
|
61
|
+
const evidenceChannel = options.evidenceChannel;
|
|
62
|
+
if (evidenceChannel !== undefined && evidenceChannel !== "poll" && evidenceChannel !== "push") {
|
|
63
|
+
throw new Error("fulfillment_shipment_spine_evidence_channel_invalid");
|
|
64
|
+
}
|
|
65
|
+
const advance = (idempotencyKey, shipmentId, status, metadata, requestedAt) => call(executor, ADVANCE_SHIPMENT_SQL, [
|
|
66
|
+
idempotencyKey,
|
|
67
|
+
shipmentId,
|
|
68
|
+
status,
|
|
69
|
+
metadata ?? {},
|
|
70
|
+
requestedAt ?? null,
|
|
71
|
+
]);
|
|
72
|
+
const evidence = (idempotencyKey, shipmentId, role, externalRef, metadata, requestedAt) => {
|
|
73
|
+
if (!evidenceChannel || !externalRef)
|
|
74
|
+
return null;
|
|
75
|
+
return call(executor, RECORD_EVIDENCE_SQL, [
|
|
76
|
+
idempotencyKey,
|
|
77
|
+
shipmentId,
|
|
78
|
+
evidenceChannel,
|
|
79
|
+
role,
|
|
80
|
+
externalRef,
|
|
81
|
+
metadata ?? {},
|
|
82
|
+
requestedAt ?? null,
|
|
83
|
+
]);
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
localPersistence: localPersistence(executor),
|
|
87
|
+
createShipment(request) {
|
|
88
|
+
return callCreate(executor, portKey, [
|
|
89
|
+
request.idempotencyKey,
|
|
90
|
+
request.orderId,
|
|
91
|
+
portKey,
|
|
92
|
+
request.metadata ?? {},
|
|
93
|
+
request.requestedAt ?? null,
|
|
94
|
+
options.requiredProviderType ?? null,
|
|
95
|
+
]);
|
|
96
|
+
},
|
|
97
|
+
async recordLabel(request) {
|
|
98
|
+
await advance(`${request.idempotencyKey}:packed`, request.shipmentId, "packed", request.metadata, request.requestedAt);
|
|
99
|
+
await advance(`${request.idempotencyKey}:label-pending`, request.shipmentId, "label_pending", request.metadata, request.requestedAt);
|
|
100
|
+
const labelled = await advance(`${request.idempotencyKey}:label-created`, request.shipmentId, "label_created", request.metadata, request.requestedAt);
|
|
101
|
+
return await evidence(`${request.idempotencyKey}:label-evidence`, request.shipmentId, "label", request.externalRef, request.metadata, request.requestedAt) ?? labelled;
|
|
102
|
+
},
|
|
103
|
+
handOff(request) {
|
|
104
|
+
return advance(request.idempotencyKey, request.shipmentId, "handed_over", request.metadata, request.requestedAt);
|
|
105
|
+
},
|
|
106
|
+
async recordTracking(request) {
|
|
107
|
+
// Evidence precedes the transition because the public rail correctly
|
|
108
|
+
// closes evidence ingestion once a shipment reaches `delivered`.
|
|
109
|
+
await evidence(`${request.idempotencyKey}:tracking-evidence`, request.shipmentId, "tracking", request.externalRef, request.metadata, request.requestedAt);
|
|
110
|
+
return advance(request.idempotencyKey, request.shipmentId, request.status, request.metadata, request.requestedAt);
|
|
111
|
+
},
|
|
112
|
+
cancel(request) {
|
|
113
|
+
return call(executor, CANCEL_SHIPMENT_SQL, [
|
|
114
|
+
request.idempotencyKey,
|
|
115
|
+
request.shipmentId,
|
|
116
|
+
request.reason,
|
|
117
|
+
request.metadata ?? {},
|
|
118
|
+
request.requestedAt ?? null,
|
|
119
|
+
]);
|
|
120
|
+
},
|
|
121
|
+
raiseException(request) {
|
|
122
|
+
return call(executor, RAISE_EXCEPTION_SQL, [
|
|
123
|
+
request.idempotencyKey,
|
|
124
|
+
request.shipmentId,
|
|
125
|
+
request.reason,
|
|
126
|
+
request.metadata ?? {},
|
|
127
|
+
request.requestedAt ?? null,
|
|
128
|
+
]);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function callCreate(executor, portKey, values) {
|
|
133
|
+
try {
|
|
134
|
+
const result = await executor.query(CREATE_SHIPMENT_SQL, values);
|
|
135
|
+
const row = asRecord(result.rows[0]);
|
|
136
|
+
if (row?.port_ready !== true) {
|
|
137
|
+
throw new CommerceFulfillmentConflictError("fulfillment_port_not_ready", {
|
|
138
|
+
reason: "fulfillment_port_not_ready",
|
|
139
|
+
portKey,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return parseResponse(row.response);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof CommerceFulfillmentConflictError
|
|
146
|
+
|| error instanceof CommerceFulfillmentPersistenceError)
|
|
147
|
+
throw error;
|
|
148
|
+
throw mapPostgresError(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function call(executor, sql, values) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await executor.query(sql, values);
|
|
154
|
+
return parseResponse(asRecord(result.rows[0])?.response);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error instanceof CommerceFulfillmentPersistenceError)
|
|
158
|
+
throw error;
|
|
159
|
+
throw mapPostgresError(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function parseResponse(input) {
|
|
163
|
+
const response = asRecord(input);
|
|
164
|
+
const shipment = asRecord(response?.shipment);
|
|
165
|
+
if (response?.contractVersion !== CONTRACT_VERSION
|
|
166
|
+
|| typeof shipment?.id !== "string"
|
|
167
|
+
|| typeof shipment.orderId !== "string"
|
|
168
|
+
|| typeof shipment.status !== "string"
|
|
169
|
+
|| typeof response.replayed !== "boolean") {
|
|
170
|
+
throw new CommerceFulfillmentPersistenceError("Fulfillment shipment spine response invalid");
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
shipmentId: shipment.id,
|
|
174
|
+
orderId: shipment.orderId,
|
|
175
|
+
status: shipment.status,
|
|
176
|
+
replayed: response.replayed,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function mapPostgresError(error) {
|
|
180
|
+
const value = asRecord(error);
|
|
181
|
+
const code = typeof value?.code === "string" ? value.code : undefined;
|
|
182
|
+
const message = typeof value?.message === "string" ? value.message : "";
|
|
183
|
+
if (code === "22023" || code === "23505") {
|
|
184
|
+
return new CommerceFulfillmentConflictError("Fulfillment shipment spine conflict", {
|
|
185
|
+
code,
|
|
186
|
+
reason: classifyConflictReason(message),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return new CommerceFulfillmentPersistenceError("Fulfillment shipment spine persistence failed", { code });
|
|
190
|
+
}
|
|
191
|
+
function classifyConflictReason(message) {
|
|
192
|
+
if (message.includes("fulfillment_create_shipment_order_not_fulfillable"))
|
|
193
|
+
return "order_not_fulfillable";
|
|
194
|
+
if (message.includes("fulfillment_advance_shipment_archetype_forbids_handover"))
|
|
195
|
+
return "handover_without_label";
|
|
196
|
+
if (message.includes("fulfillment_advance_shipment_invalid_transition"))
|
|
197
|
+
return "invalid_transition";
|
|
198
|
+
if (message.includes("fulfillment_cancel_shipment_invalid_status"))
|
|
199
|
+
return "cancel_after_label";
|
|
200
|
+
if (message.includes("idempotency_conflict"))
|
|
201
|
+
return "idempotency_conflict";
|
|
202
|
+
return "fulfillment_conflict";
|
|
203
|
+
}
|
|
204
|
+
function asRecord(value) {
|
|
205
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
206
|
+
? value
|
|
207
|
+
: null;
|
|
208
|
+
}
|
|
209
|
+
/** G17 native local persistence, composed on the same injected executor. */
|
|
210
|
+
function localPersistence(executor) {
|
|
211
|
+
return {
|
|
212
|
+
async readLocalFulfillmentState(ref) {
|
|
213
|
+
const result = await executor.query(`SELECT shipment.status,shipment.handed_over_at,
|
|
214
|
+
(SELECT max((observation#>>'{fact,occurredAt}')::timestamptz) FROM public.fulfillment_observations WHERE shipment_id=shipment.id AND status_recorded) AS latest_status_occurred_at
|
|
215
|
+
FROM public.fulfillment_shipments shipment WHERE shipment.id=$1::uuid`, [ref]);
|
|
216
|
+
const row = result.rows[0];
|
|
217
|
+
return { status: typeof row?.status === 'string' ? row.status : null, latestStatusOccurredAt: date(row?.latest_status_occurred_at), handedOverAt: date(row?.handed_over_at) };
|
|
218
|
+
},
|
|
219
|
+
async recordObservation(observation) {
|
|
220
|
+
const safe = projectObservation(observation);
|
|
221
|
+
return nativeCall(executor, 'fulfillment_record_observation', [safe.eventKey, safe.correlation.fulfillmentRef, JSON.stringify(safe), safe.firstReceivedAt]);
|
|
222
|
+
},
|
|
223
|
+
async recordStatusEvidence(ref, observation) {
|
|
224
|
+
return nativeCall(executor, 'fulfillment_record_status_evidence', [observation.eventKey, ref.fulfillmentRef]);
|
|
225
|
+
},
|
|
226
|
+
async markProcessed(eventKey, outcome) {
|
|
227
|
+
await nativeCall(executor, 'fulfillment_mark_observation_processed', [eventKey, JSON.stringify({ status: outcome.status,
|
|
228
|
+
...(outcome.refusal ? { refusal: { reason: outcome.refusal.reason, phase: outcome.refusal.phase, retryable: outcome.refusal.retryable } } : {}) })]);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function date(value) { return value instanceof Date ? value.toISOString() : typeof value === 'string' ? value : null; }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FulfillmentStockSyncPort } from './contracts/index.js';
|
|
2
|
+
import { type PgExecutor } from './postgresDispatchStore.js';
|
|
3
|
+
/** The native catalogue has observed stock, not adopter inventory balances. */
|
|
4
|
+
export declare function createPostgresFulfillmentStockSyncPort(db: PgExecutor, options: {
|
|
5
|
+
sourceKey: string;
|
|
6
|
+
inventoryClasses?: ReadonlyMap<string, 'sellable' | 'packaging' | null>;
|
|
7
|
+
readLocalInventoryStock: FulfillmentStockSyncPort['readLocalInventoryStock'];
|
|
8
|
+
}): FulfillmentStockSyncPort;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { nativeCall } from './postgresDispatchStore.js';
|
|
3
|
+
/** The native catalogue has observed stock, not adopter inventory balances. */
|
|
4
|
+
export function createPostgresFulfillmentStockSyncPort(db, options) {
|
|
5
|
+
const source = options.sourceKey.trim();
|
|
6
|
+
if (!source)
|
|
7
|
+
throw new Error('fulfillment_stock_source_key_required');
|
|
8
|
+
return {
|
|
9
|
+
readLocalInventoryStock: at => options.readLocalInventoryStock(at),
|
|
10
|
+
async readCursor() {
|
|
11
|
+
const result = await db.query('SELECT status,last_stock_synced_at,last_movement_occurred_at,cursor FROM public.fulfillment_stock_sync_cursors WHERE source_key=$1', [source]);
|
|
12
|
+
const row = result.rows[0];
|
|
13
|
+
if (!row)
|
|
14
|
+
return null;
|
|
15
|
+
const cursor = row.cursor;
|
|
16
|
+
return { status: row.status, lastStockSyncedAt: timestamp(row.last_stock_synced_at), lastMovementOccurredAt: timestamp(row.last_movement_occurred_at),
|
|
17
|
+
continuation: cursor.continuation ?? { nextRef: null, startLocalDate: null, endLocalDate: null }, coverageGapSkus: cursor.coverageGapSkus ?? [], refusal: cursor.refusal ?? null };
|
|
18
|
+
},
|
|
19
|
+
async recordCursor(v) {
|
|
20
|
+
await db.query(`INSERT INTO public.fulfillment_stock_sync_cursors(source_key,status,last_stock_synced_at,last_movement_occurred_at,cursor,error_code,updated_at)
|
|
21
|
+
VALUES($1,$2,$3,$4,$5::jsonb,$6,now()) ON CONFLICT(source_key) DO UPDATE SET status=EXCLUDED.status,
|
|
22
|
+
last_stock_synced_at=EXCLUDED.last_stock_synced_at,last_movement_occurred_at=EXCLUDED.last_movement_occurred_at,cursor=EXCLUDED.cursor,error_code=EXCLUDED.error_code,updated_at=now()`, [source, v.status, v.lastStockSyncedAt, v.lastMovementOccurredAt, JSON.stringify({ continuation: { nextRef: v.continuation.nextRef, startLocalDate: v.continuation.startLocalDate, endLocalDate: v.continuation.endLocalDate }, coverageGapSkus: [...(v.coverageGapSkus ?? [])], refusal: v.refusal }), v.refusal?.reason ?? null]);
|
|
23
|
+
},
|
|
24
|
+
async readSkuInventoryClasses(skus) {
|
|
25
|
+
if (!skus.length)
|
|
26
|
+
return new Map();
|
|
27
|
+
const rows = (await db.query('SELECT sku FROM public.catalog_skus WHERE sku=ANY($1::text[])', [skus])).rows;
|
|
28
|
+
const known = new Set(rows.map(row => row.sku));
|
|
29
|
+
return new Map(skus.map(sku => [sku, options.inventoryClasses?.has(sku) ? options.inventoryClasses.get(sku) : known.has(sku) ? 'sellable' : null]));
|
|
30
|
+
},
|
|
31
|
+
async readActiveReservations(skus, at) {
|
|
32
|
+
const result = await db.query(`SELECT line.sku,sum(line.quantity)::integer AS quantity FROM public.fulfillment_inventory_reservation_lines line
|
|
33
|
+
JOIN public.fulfillment_inventory_reservations reservation ON reservation.id=line.reservation_id
|
|
34
|
+
WHERE reservation.source_key=$1 AND line.sku=ANY($2::text[]) AND reservation.status IN ('held','committed')
|
|
35
|
+
AND (reservation.status='committed' OR reservation.expires_at>$3::timestamptz) GROUP BY line.sku`, [source, skus, at]);
|
|
36
|
+
return new Map(result.rows.map(row => [String(row.sku), Number(row.quantity)]));
|
|
37
|
+
},
|
|
38
|
+
recordStockSnapshot(v) {
|
|
39
|
+
const s = v.snapshot, l = v.local;
|
|
40
|
+
return nativeCall(db, 'fulfillment_record_stock_snapshot', [source, v.requestKey, fingerprint(v), s.skuRef, s.totalQuantity, s.forSaleQuantity, s.reservedOrUnavailableQuantity, l.onHand, l.reserved, l.unavailable, l.safetyStock, v.inventoryClass, s.syncRunId]);
|
|
41
|
+
},
|
|
42
|
+
recordProviderStockCurrent(v) {
|
|
43
|
+
const s = v.snapshot;
|
|
44
|
+
return nativeCall(db, 'fulfillment_record_stock_current', [source, v.requestKey, fingerprint(v), s.skuRef, s.totalQuantity, s.forSaleQuantity, s.reservedOrUnavailableQuantity, v.inventoryClass, v.lastSyncedAt, v.staleAfter, s.syncRunId]);
|
|
45
|
+
},
|
|
46
|
+
recordMovement(v) {
|
|
47
|
+
const { providerRef, syncRunId, collectedAt, sourceRevision, revisionSource, skuRef, quantityKind, quantity, occurredAt, operationType, documentRef, lotRef, expirationDate } = v.movement;
|
|
48
|
+
const movement = { providerRef, syncRunId, collectedAt, sourceRevision, revisionSource, skuRef, quantityKind, quantity, occurredAt, operationType, documentRef, lotRef, expirationDate };
|
|
49
|
+
return nativeCall(db, 'fulfillment_record_stock_movement', [source, v.requestKey, fingerprint({ requestKey: v.requestKey, movement }), JSON.stringify(movement)]);
|
|
50
|
+
},
|
|
51
|
+
recordLowStockEvidence: v => nativeCall(db, 'fulfillment_record_stock_evidence', [source, v.requestKey, fingerprint(v), v.skuRef, v.thresholdKind, v.severity, v.providerForSaleQuantity, v.localAvailableQuantity]),
|
|
52
|
+
resolveLowStockEvidence: v => nativeCall(db, 'fulfillment_resolve_stock_evidence', [source, v.skuRef, v.activeThresholdKinds, v.syncRunId]),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function timestamp(value) { return value instanceof Date ? value.toISOString() : typeof value === 'string' ? value : null; }
|
|
56
|
+
function fingerprint(value) { return createHash('sha256').update(stable(value)).digest('hex'); }
|
|
57
|
+
function stable(value) {
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return `[${value.map(stable).join(',')}]`;
|
|
60
|
+
if (value && typeof value === 'object')
|
|
61
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
|
|
62
|
+
return JSON.stringify(value) ?? 'null';
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Read-only evidence boundary; parsing remains with the owning host schema. */
|
|
2
|
+
export declare function createProductReconciliationEvidenceHandler<Request extends {
|
|
3
|
+
method?: string;
|
|
4
|
+
query: Record<string, string | string[] | undefined>;
|
|
5
|
+
}, Response, Query, Result>({ evidencePort, authorizeAdmin, requestSchema, responseSchema, respond }: {
|
|
6
|
+
evidencePort: {
|
|
7
|
+
getProductReconciliationEvidence(query: Query): Promise<unknown>;
|
|
8
|
+
};
|
|
9
|
+
authorizeAdmin(request: Request): Promise<boolean>;
|
|
10
|
+
requestSchema: {
|
|
11
|
+
safeParse(input: unknown): {
|
|
12
|
+
success: true;
|
|
13
|
+
data: Query;
|
|
14
|
+
} | {
|
|
15
|
+
success: false;
|
|
16
|
+
error: {
|
|
17
|
+
flatten(): unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
responseSchema: {
|
|
22
|
+
safeParse(input: unknown): {
|
|
23
|
+
success: true;
|
|
24
|
+
data: Result;
|
|
25
|
+
} | {
|
|
26
|
+
success: false;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
respond: {
|
|
30
|
+
sendBffError(res: Response, code: "UPSTREAM_UNAVAILABLE" | "UNAUTHORIZED" | "BAD_REQUEST" | "INVALID_RESPONSE", message: string, options?: {
|
|
31
|
+
details: unknown;
|
|
32
|
+
}): void;
|
|
33
|
+
sendBffSuccess(res: Response, data: Result): void;
|
|
34
|
+
sendMethodNotAllowed(res: Response, methods: readonly string[]): void;
|
|
35
|
+
};
|
|
36
|
+
}): (req: Request, res: Response) => Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Read-only evidence boundary; parsing remains with the owning host schema. */
|
|
2
|
+
export function createProductReconciliationEvidenceHandler({ evidencePort, authorizeAdmin, requestSchema, responseSchema, respond }) {
|
|
3
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
|
|
4
|
+
return async function handler(req, res) {
|
|
5
|
+
if (req.method !== "GET") {
|
|
6
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
let authorized = false;
|
|
10
|
+
try {
|
|
11
|
+
authorized = await authorizeAdmin(req);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed", {
|
|
15
|
+
details: { reason: "product_reconciliation_authorization_unavailable" },
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!authorized) {
|
|
20
|
+
sendBffError(res, "UNAUTHORIZED", "Admin session required");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const request = requestSchema.safeParse({
|
|
24
|
+
statusFilter: scalarQuery(req.query.statusFilter),
|
|
25
|
+
});
|
|
26
|
+
if (!request.success) {
|
|
27
|
+
sendBffError(res, "BAD_REQUEST", "Invalid product reconciliation request", {
|
|
28
|
+
details: request.error.flatten(),
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const result = await evidencePort.getProductReconciliationEvidence(request.data);
|
|
34
|
+
const response = responseSchema.safeParse(result);
|
|
35
|
+
if (!response.success) {
|
|
36
|
+
sendBffError(res, "INVALID_RESPONSE", "Product reconciliation returned invalid response");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
sendBffSuccess(res, response.data);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Product reconciliation read failed", {
|
|
43
|
+
details: { reason: "product_reconciliation_read_unavailable" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function scalarQuery(value) {
|
|
49
|
+
return Array.isArray(value) ? value[0] : value;
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FulfillmentCapabilityDescriptor, FulfillmentRefusal, FulfillmentSelection } from "./contracts/index.js";
|
|
2
|
+
/** Injected profiles replace the private registry. Support never activates a provider. */
|
|
3
|
+
export declare function readFulfillmentCapabilities(profiles: readonly FulfillmentCapabilityDescriptor[], selection: FulfillmentSelection | null | undefined): {
|
|
4
|
+
kind: "selected";
|
|
5
|
+
descriptor: FulfillmentCapabilityDescriptor;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "refused";
|
|
8
|
+
refusal: FulfillmentRefusal;
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Injected profiles replace the private registry. Support never activates a provider. */
|
|
2
|
+
export function readFulfillmentCapabilities(profiles, selection) {
|
|
3
|
+
const refuse = (reason) => ({ kind: "refused", refusal: { reason, phase: "selection", retryable: false } });
|
|
4
|
+
const provider = selection?.providerRef?.trim().toLowerCase() || null;
|
|
5
|
+
if (!provider)
|
|
6
|
+
return refuse("selection_required");
|
|
7
|
+
const descriptor = profiles.find(profile => profile.providerRef === provider);
|
|
8
|
+
if (!descriptor)
|
|
9
|
+
return refuse("provider_mismatch");
|
|
10
|
+
if (descriptor.role === "warehouse" && (!selection?.carrierRef || !selection.serviceRef))
|
|
11
|
+
return refuse("carrier_service_required");
|
|
12
|
+
if (!descriptor.selected)
|
|
13
|
+
return refuse("disabled");
|
|
14
|
+
if (!descriptor.readiness.ready)
|
|
15
|
+
return { kind: "refused", refusal: descriptor.readiness.refusal };
|
|
16
|
+
return { kind: "selected", descriptor };
|
|
17
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Structural protocol for the contributed durable ledger. Each operation must
|
|
2
|
+
* be one atomic store transaction, including its transition evidence. P4 owns SQL.
|
|
3
|
+
* This factory holds no leases, retry counters, commands or persistence in memory.
|
|
4
|
+
*/
|
|
5
|
+
type CommandStatus = "queued" | "claimed" | "submitting" | "accepted" | "confirmed" | "retry_wait" | "uncertain" | "manual_review" | "terminal_rejected" | "cancelled";
|
|
6
|
+
type CommandResult = {
|
|
7
|
+
commandId: string;
|
|
8
|
+
status: CommandStatus;
|
|
9
|
+
version: number;
|
|
10
|
+
applied: boolean;
|
|
11
|
+
};
|
|
12
|
+
type Fence = {
|
|
13
|
+
commandId: string;
|
|
14
|
+
claimToken: string;
|
|
15
|
+
claimGeneration: number;
|
|
16
|
+
expectedVersion: number;
|
|
17
|
+
};
|
|
18
|
+
type Proof = {
|
|
19
|
+
kind: "provider_reference" | "correlation_key" | "provider_idempotency_key";
|
|
20
|
+
fingerprint: string;
|
|
21
|
+
observedAt: string;
|
|
22
|
+
source: "provider_lookup" | "provider_webhook" | "operator_verified";
|
|
23
|
+
};
|
|
24
|
+
type ClaimedCommand = {
|
|
25
|
+
commandId: string;
|
|
26
|
+
fulfillmentRef: string;
|
|
27
|
+
orderRef: string;
|
|
28
|
+
providerRef: string;
|
|
29
|
+
commandKind: "dispatch_create";
|
|
30
|
+
contractVersion: "1";
|
|
31
|
+
status: "claimed";
|
|
32
|
+
version: number;
|
|
33
|
+
commandKeyFingerprint: string;
|
|
34
|
+
commandPayloadFingerprint: string;
|
|
35
|
+
providerIdempotencyKeyFingerprint: string | null;
|
|
36
|
+
routingFingerprint: string;
|
|
37
|
+
configurationFingerprint: string;
|
|
38
|
+
correlationKeyFingerprint: string;
|
|
39
|
+
retryBudget: number;
|
|
40
|
+
retryCount: number;
|
|
41
|
+
claimToken: string;
|
|
42
|
+
claimGeneration: number;
|
|
43
|
+
leaseExpiresAt: string;
|
|
44
|
+
};
|
|
45
|
+
type LedgerStore = {
|
|
46
|
+
enqueue(input: {
|
|
47
|
+
fulfillmentRef: string;
|
|
48
|
+
providerRef: string;
|
|
49
|
+
commandKind: "dispatch_create";
|
|
50
|
+
contractVersion: "1";
|
|
51
|
+
idempotencyKey: string;
|
|
52
|
+
commandKeyFingerprint: string;
|
|
53
|
+
commandPayloadFingerprint: string;
|
|
54
|
+
providerIdempotencyKeyFingerprint: string | null;
|
|
55
|
+
routingFingerprint: string;
|
|
56
|
+
configurationFingerprint: string;
|
|
57
|
+
correlationKeyFingerprint: string;
|
|
58
|
+
retryBudget: number;
|
|
59
|
+
}): Promise<CommandResult>;
|
|
60
|
+
claim(input: {
|
|
61
|
+
providerRef: string;
|
|
62
|
+
commandKind: "dispatch_create";
|
|
63
|
+
limit: number;
|
|
64
|
+
claimOwnerFingerprint: string;
|
|
65
|
+
leaseSeconds: number;
|
|
66
|
+
}): Promise<readonly ClaimedCommand[]>;
|
|
67
|
+
markSubmitting(input: Fence & {
|
|
68
|
+
providerAttemptRef: string | null;
|
|
69
|
+
}): Promise<CommandResult>;
|
|
70
|
+
applyWriteOutcome(input: Fence & {
|
|
71
|
+
identityFingerprint: string;
|
|
72
|
+
providerAttemptRef: string | null;
|
|
73
|
+
} & ({
|
|
74
|
+
kind: "accepted";
|
|
75
|
+
reason: "provider_accepted";
|
|
76
|
+
providerReferenceFingerprint: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "uncertain";
|
|
79
|
+
reason: "write_uncertain";
|
|
80
|
+
} | {
|
|
81
|
+
kind: "rejected";
|
|
82
|
+
reason: "safe_rejection" | "terminal_rejection";
|
|
83
|
+
retryNotBefore: string | null;
|
|
84
|
+
proof: {
|
|
85
|
+
kind: "definitive_provider_rejection";
|
|
86
|
+
fingerprint: string;
|
|
87
|
+
observedAt: string;
|
|
88
|
+
source: "provider_response";
|
|
89
|
+
};
|
|
90
|
+
})): Promise<CommandResult>;
|
|
91
|
+
confirm(input: Fence & {
|
|
92
|
+
identityFingerprint: string;
|
|
93
|
+
providerAttemptRef: string | null;
|
|
94
|
+
proof: Proof;
|
|
95
|
+
}): Promise<CommandResult>;
|
|
96
|
+
reconcile(input: {
|
|
97
|
+
commandId: string;
|
|
98
|
+
expectedVersion: number;
|
|
99
|
+
identityFingerprint: string;
|
|
100
|
+
proof: Proof;
|
|
101
|
+
}): Promise<CommandResult & {
|
|
102
|
+
proofRecorded: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
resolve(input: {
|
|
105
|
+
commandId: string;
|
|
106
|
+
expectedVersion: number;
|
|
107
|
+
identityFingerprint: string;
|
|
108
|
+
} & ({
|
|
109
|
+
decision: "manual_review";
|
|
110
|
+
reason: "effect_unknown" | "identity_mismatch" | "invalid_contract" | "unsafe_retry";
|
|
111
|
+
proof: Proof;
|
|
112
|
+
} | {
|
|
113
|
+
decision: "retry_wait";
|
|
114
|
+
reason: "authoritative_absence";
|
|
115
|
+
retryNotBefore: string;
|
|
116
|
+
proof: {
|
|
117
|
+
kind: "authoritative_absence";
|
|
118
|
+
fingerprint: string;
|
|
119
|
+
observedAt: string;
|
|
120
|
+
source: "provider_lookup";
|
|
121
|
+
};
|
|
122
|
+
})): Promise<CommandResult & {
|
|
123
|
+
proofRecorded: boolean;
|
|
124
|
+
}>;
|
|
125
|
+
release(input: Fence & ({
|
|
126
|
+
action: "release";
|
|
127
|
+
reason: "command_enqueued";
|
|
128
|
+
} | {
|
|
129
|
+
action: "cancel";
|
|
130
|
+
reason: "operator_cancelled";
|
|
131
|
+
})): Promise<CommandResult>;
|
|
132
|
+
recoverExpired(input: {
|
|
133
|
+
limit: number;
|
|
134
|
+
}): Promise<readonly CommandResult[]>;
|
|
135
|
+
};
|
|
136
|
+
/** Bind the durable protocol without substituting a local state machine.
|
|
137
|
+
* Store fences compare token, generation, version and unexpired lease together.
|
|
138
|
+
* Expired claimed work becomes queued; expired submitting/accepted work becomes
|
|
139
|
+
* uncertain. Only authoritative scoped absence may admit uncertain work to retry.
|
|
140
|
+
* Claim respects availability/retry budget; finalization cannot invent absence.
|
|
141
|
+
*/
|
|
142
|
+
export declare function createFulfillmentCommandLedger(store: LedgerStore): LedgerStore;
|
|
143
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Bind the durable protocol without substituting a local state machine.
|
|
2
|
+
* Store fences compare token, generation, version and unexpired lease together.
|
|
3
|
+
* Expired claimed work becomes queued; expired submitting/accepted work becomes
|
|
4
|
+
* uncertain. Only authoritative scoped absence may admit uncertain work to retry.
|
|
5
|
+
* Claim respects availability/retry budget; finalization cannot invent absence.
|
|
6
|
+
*/
|
|
7
|
+
export function createFulfillmentCommandLedger(store) {
|
|
8
|
+
return {
|
|
9
|
+
enqueue: input => store.enqueue(input), claim: input => store.claim(input),
|
|
10
|
+
markSubmitting: input => store.markSubmitting(input), applyWriteOutcome: input => store.applyWriteOutcome(input),
|
|
11
|
+
confirm: input => store.confirm(input), reconcile: input => store.reconcile(input),
|
|
12
|
+
resolve: input => store.resolve(input), release: input => store.release(input), recoverExpired: input => store.recoverExpired(input),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DispatchRecord, FulfillmentDispatchResult, FulfillmentStorePort } from "./contracts/index.js";
|
|
2
|
+
export declare function acknowledgeAcceptedDispatch(input: {
|
|
3
|
+
store: FulfillmentStorePort;
|
|
4
|
+
record: DispatchRecord & {
|
|
5
|
+
externalRef: string;
|
|
6
|
+
};
|
|
7
|
+
keys: {
|
|
8
|
+
providerAttemptKey: string;
|
|
9
|
+
labelKey: string;
|
|
10
|
+
};
|
|
11
|
+
}): Promise<{
|
|
12
|
+
acknowledged: boolean;
|
|
13
|
+
replayed: boolean;
|
|
14
|
+
record: DispatchRecord;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function recoverDispatchAcceptanceWrite(input: {
|
|
17
|
+
store: FulfillmentStorePort;
|
|
18
|
+
requestKey: string;
|
|
19
|
+
dispatchRef: string;
|
|
20
|
+
acceptance: Extract<FulfillmentDispatchResult, {
|
|
21
|
+
kind: "accepted";
|
|
22
|
+
}>;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
committed: boolean;
|
|
25
|
+
readBacks: number;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { dispatchEvidence } from "./dispatchProjection.js";
|
|
2
|
+
export function acknowledgeAcceptedDispatch(input) {
|
|
3
|
+
return input.store.acknowledge({ dispatchRef: input.record.dispatchRef, acceptance: {
|
|
4
|
+
kind: "accepted", externalRef: input.record.externalRef, evidence: dispatchEvidence(input.record.evidence), ...input.keys,
|
|
5
|
+
} });
|
|
6
|
+
}
|
|
7
|
+
export async function recoverDispatchAcceptanceWrite(input) {
|
|
8
|
+
let record = await input.store.read(input.requestKey).catch(() => null);
|
|
9
|
+
let readBacks = record ? 1 : 0;
|
|
10
|
+
const committed = () => record?.status === "created" && record.externalRef === input.acceptance.externalRef;
|
|
11
|
+
if (committed())
|
|
12
|
+
return { committed: true, readBacks };
|
|
13
|
+
try {
|
|
14
|
+
// Accepted proof survives a failed atomic ACK; finalization records uncertainty,
|
|
15
|
+
// and the next worker repairs the ACK locally without another provider call.
|
|
16
|
+
record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: input.acceptance });
|
|
17
|
+
readBacks += 1;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Submitting remains fenced; the stale sweep moves it to uncertain.
|
|
21
|
+
}
|
|
22
|
+
return { committed: committed(), readBacks };
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FulfillmentDispatchResult } from "./contracts/index.js";
|
|
2
|
+
/** Adapters translate structured provider failures into the port result before returning.
|
|
3
|
+
* An unclassified throw after invoking dispatch can follow a successful POST. */
|
|
4
|
+
export declare function unknownDispatchFailure(): Extract<FulfillmentDispatchResult, {
|
|
5
|
+
kind: "unknown";
|
|
6
|
+
}>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Adapters translate structured provider failures into the port result before returning.
|
|
2
|
+
* An unclassified throw after invoking dispatch can follow a successful POST. */
|
|
3
|
+
export function unknownDispatchFailure() {
|
|
4
|
+
return { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: "io_failure", phase: "dispatch", retryable: true } };
|
|
5
|
+
}
|