@openlup/platform 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -0
- package/dist/invoicing/contracts/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +39 -0
- package/dist/invoicing/index.js +37 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -0
- package/dist/subscriptions/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +23 -3
- package/release-gates.json +74 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +71 -0
- package/src/invoicing/contracts/host.ts +84 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +102 -0
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +39 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -0
- package/src/subscriptions/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { operationError } from "../dispatchProjection.js";
|
|
2
|
+
import { projectPollEvidence, projectObservation } from "../observationProjection.js";
|
|
3
|
+
import { normalizeReconciliationTimestamp } from "../providerReconciliationTimestamp.js";
|
|
4
|
+
import { OmnipackProviderError } from "./client.js";
|
|
5
|
+
import { digest, observation, reference, refusal, timestamp } from "./observation.js";
|
|
6
|
+
import { localStatusFor, normalizeStatus } from "./omnipackStatusVocabulary.js";
|
|
7
|
+
export function reconciliationObservation(row, config, fulfillmentRef) {
|
|
8
|
+
const providerStatus = { primary: row.status, subStatus: row.subStatus };
|
|
9
|
+
// Original status and occurrence bytes remain part of identity, even for aliases
|
|
10
|
+
// mapping to the same canonical event. Received time never changes that identity.
|
|
11
|
+
const eventKey = digest(JSON.stringify([row.providerOrderId, row.fulfilmentNumber, row.orderNumber, row.status, row.subStatus, row.updatedAt]));
|
|
12
|
+
const correlation = { orderRef: null, fulfillmentRef: fulfillmentRef ?? null, externalRef: reference(row.providerOrderId),
|
|
13
|
+
orderNumber: reference(row.orderNumber), providerFulfillmentRef: reference(row.fulfilmentNumber), parcelRef: null };
|
|
14
|
+
const normalized = normalizeStatus(row.status);
|
|
15
|
+
if (!normalized)
|
|
16
|
+
return { kind: "unmapped", externalRef: correlation.externalRef, evidence: projectPollEvidence({ eventKey, correlation: { ...correlation, externalRef: row.providerOrderId, orderNumber: row.orderNumber, providerFulfillmentRef: row.fulfilmentNumber }, providerStatus }) };
|
|
17
|
+
const status = localStatusFor(normalized);
|
|
18
|
+
const event = { provider_received: "shipment.accepted", picking: "order.processing_started", packed: "order.picked", in_transit: "order.shipped", delivered: "order.delivered", exception: "parcel.exception" }[status];
|
|
19
|
+
const occurredAt = normalizeReconciliationTimestamp(row.updatedAt);
|
|
20
|
+
const evidence = { provider: "omnipack", event, providerOrderId: row.providerOrderId, orderNumber: row.orderNumber,
|
|
21
|
+
fulfilmentNumber: row.fulfilmentNumber, occurredAt, trackingNumbers: row.trackingNumbers,
|
|
22
|
+
trackingReferences: row.trackingReferences, shippingMethods: [] };
|
|
23
|
+
const mapped = observation({ evidence, event, status, fulfillmentRef, receivedAt: config.clock(),
|
|
24
|
+
rawDigest: digest(JSON.stringify({ ...evidence, providerStatus })), evidenceRef: config.mintEvidenceReference(), source: "provider_reconciliation", approveTrackingUrl: config.approveTrackingUrl, observationCodecs: config.observationCodecs });
|
|
25
|
+
return { kind: "found", externalRef: row.providerOrderId ?? "", createdAt: timestamp(row.createdAt), updatedAt: timestamp(occurredAt),
|
|
26
|
+
observation: projectObservation({ ...mapped, eventKey, fact: { ...mapped.fact, factId: eventKey }, providerStatus }, config.approveTrackingUrl, config.observationCodecs) };
|
|
27
|
+
}
|
|
28
|
+
export function createReconciliationPagePort(client, config, ready) {
|
|
29
|
+
return {
|
|
30
|
+
async readPage(input) {
|
|
31
|
+
const inactive = ready();
|
|
32
|
+
if (inactive)
|
|
33
|
+
return { kind: "failed", refusal: inactive };
|
|
34
|
+
if (!Number.isSafeInteger(input.page) || input.page < 0 || !Number.isSafeInteger(input.size) || input.size <= 0)
|
|
35
|
+
return { kind: "failed", refusal: refusal("malformed", "readback") };
|
|
36
|
+
try {
|
|
37
|
+
const rows = await client.getFulfilments(input);
|
|
38
|
+
return { kind: "page", rows: rows.map(row => {
|
|
39
|
+
try {
|
|
40
|
+
return reconciliationObservation(row, config);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { kind: "unknown", refusal: refusal("malformed", "normalize") };
|
|
44
|
+
}
|
|
45
|
+
}) };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof OmnipackProviderError && error.status === 400 && error.code === "ERR_PAGE_MUST_BE_LESS_THAN_TOTAL_PAGES")
|
|
49
|
+
return { kind: "page_exhausted" };
|
|
50
|
+
return { kind: "failed", refusal: refusal("io_failure", "readback", error instanceof OmnipackProviderError && error.retryable),
|
|
51
|
+
error: operationError(error instanceof OmnipackProviderError ? error : { code: "provider_read_failed", status: null, retryable: false, mayHaveSucceeded: false }) };
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -10,7 +10,7 @@ export function createPostgresFulfillmentDispatchStore(db, host) {
|
|
|
10
10
|
readCandidate: ref => host.readCandidate(ref),
|
|
11
11
|
prepare: ref => host.prepare(ref),
|
|
12
12
|
async read(key) {
|
|
13
|
-
const result = await db.query(
|
|
13
|
+
const result = await db.query("SELECT public.fulfillment_dispatch_read(ref) || jsonb_build_object('acceptance',ref.acceptance) AS response FROM public.fulfillment_dispatch_refs ref WHERE request_key=$1", [key]);
|
|
14
14
|
return result.rows[0]?.response ?? null;
|
|
15
15
|
},
|
|
16
16
|
claim: input => nativeCall(db, 'fulfillment_dispatch_claim', [input.requestKey, input.fulfillmentRef, input.orderRef, input.parcelRef, input.mode, JSON.stringify(dispatchEvidence(input.evidence))]),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FulfillmentObservationCodecs } from "./contracts/index.js";
|
|
1
2
|
import type { PgExecutor as PgQueryExecutor } from './postgresDispatchStore.js';
|
|
2
3
|
interface ShipmentSpineResult {
|
|
3
4
|
shipmentId: string;
|
|
@@ -33,6 +34,8 @@ interface ShipmentSpineMutationPort {
|
|
|
33
34
|
}
|
|
34
35
|
export interface PostgresFulfillmentShipmentSpineOptions {
|
|
35
36
|
portKey: string;
|
|
37
|
+
approveTrackingUrl?(url: string): boolean;
|
|
38
|
+
observationCodecs?: FulfillmentObservationCodecs;
|
|
36
39
|
evidenceChannel?: "poll" | "push";
|
|
37
40
|
requiredProviderType?: "simulator";
|
|
38
41
|
}
|
|
@@ -41,12 +44,15 @@ export declare function createPostgresFulfillmentShipmentSpinePort(executor: PgQ
|
|
|
41
44
|
localPersistence: ReturnType<typeof localPersistence>;
|
|
42
45
|
};
|
|
43
46
|
/** G17 native local persistence, composed on the same injected executor. */
|
|
44
|
-
declare function localPersistence(executor: PgQueryExecutor): {
|
|
47
|
+
declare function localPersistence(executor: PgQueryExecutor, approveTrackingUrl?: (url: string) => boolean, observationCodecs?: FulfillmentObservationCodecs): {
|
|
45
48
|
readLocalFulfillmentState(ref: string): Promise<{
|
|
46
49
|
status: string | null;
|
|
47
50
|
latestStatusOccurredAt: string | null;
|
|
48
51
|
handedOverAt: string | null;
|
|
49
52
|
}>;
|
|
53
|
+
readLocalFulfillmentStatus(ref: string): Promise<string | null>;
|
|
54
|
+
latestStatusOccurredAt(ref: string): Promise<string | null>;
|
|
55
|
+
readHandedOverAt(ref: string): Promise<string | null>;
|
|
50
56
|
recordObservation(observation: import("./contracts/index.js").FulfillmentObservation): Promise<{
|
|
51
57
|
replayed: boolean;
|
|
52
58
|
}>;
|
|
@@ -58,6 +64,7 @@ declare function localPersistence(executor: PgQueryExecutor): {
|
|
|
58
64
|
markProcessed(eventKey: string, outcome: {
|
|
59
65
|
status: "processed" | "ignored";
|
|
60
66
|
refusal?: import("./contracts/index.js").FulfillmentRefusal;
|
|
67
|
+
supersededCause?: string;
|
|
61
68
|
}): Promise<void>;
|
|
62
69
|
};
|
|
63
70
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeDiagnosticCode } from "./dispatchProjection.js";
|
|
1
2
|
import { nativeCall } from './postgresDispatchStore.js';
|
|
2
3
|
import { projectObservation } from './observationProjection.js';
|
|
3
4
|
class CommerceFulfillmentConflictError extends Error {
|
|
@@ -83,7 +84,7 @@ export function createPostgresFulfillmentShipmentSpinePort(executor, options) {
|
|
|
83
84
|
]);
|
|
84
85
|
};
|
|
85
86
|
return {
|
|
86
|
-
localPersistence: localPersistence(executor),
|
|
87
|
+
localPersistence: localPersistence(executor, options.approveTrackingUrl, options.observationCodecs),
|
|
87
88
|
createShipment(request) {
|
|
88
89
|
return callCreate(executor, portKey, [
|
|
89
90
|
request.idempotencyKey,
|
|
@@ -207,7 +208,7 @@ function asRecord(value) {
|
|
|
207
208
|
: null;
|
|
208
209
|
}
|
|
209
210
|
/** G17 native local persistence, composed on the same injected executor. */
|
|
210
|
-
function localPersistence(executor) {
|
|
211
|
+
function localPersistence(executor, approveTrackingUrl, observationCodecs) {
|
|
211
212
|
return {
|
|
212
213
|
async readLocalFulfillmentState(ref) {
|
|
213
214
|
const result = await executor.query(`SELECT shipment.status,shipment.handed_over_at,
|
|
@@ -216,8 +217,20 @@ function localPersistence(executor) {
|
|
|
216
217
|
const row = result.rows[0];
|
|
217
218
|
return { status: typeof row?.status === 'string' ? row.status : null, latestStatusOccurredAt: date(row?.latest_status_occurred_at), handedOverAt: date(row?.handed_over_at) };
|
|
218
219
|
},
|
|
220
|
+
async readLocalFulfillmentStatus(ref) {
|
|
221
|
+
const result = await executor.query('SELECT status FROM public.fulfillment_shipments WHERE id=$1::uuid', [ref]);
|
|
222
|
+
return typeof result.rows[0]?.status === 'string' ? result.rows[0].status : null;
|
|
223
|
+
},
|
|
224
|
+
async latestStatusOccurredAt(ref) {
|
|
225
|
+
const result = await executor.query("SELECT max((observation#>>'{fact,occurredAt}')::timestamptz) AS latest_status_occurred_at FROM public.fulfillment_observations WHERE shipment_id=$1::uuid AND status_recorded", [ref]);
|
|
226
|
+
return date(result.rows[0]?.latest_status_occurred_at);
|
|
227
|
+
},
|
|
228
|
+
async readHandedOverAt(ref) {
|
|
229
|
+
const result = await executor.query('SELECT handed_over_at FROM public.fulfillment_shipments WHERE id=$1::uuid', [ref]);
|
|
230
|
+
return date(result.rows[0]?.handed_over_at);
|
|
231
|
+
},
|
|
219
232
|
async recordObservation(observation) {
|
|
220
|
-
const safe = projectObservation(observation);
|
|
233
|
+
const safe = projectObservation(observation, approveTrackingUrl, observationCodecs);
|
|
221
234
|
return nativeCall(executor, 'fulfillment_record_observation', [safe.eventKey, safe.correlation.fulfillmentRef, JSON.stringify(safe), safe.firstReceivedAt]);
|
|
222
235
|
},
|
|
223
236
|
async recordStatusEvidence(ref, observation) {
|
|
@@ -225,6 +238,7 @@ function localPersistence(executor) {
|
|
|
225
238
|
},
|
|
226
239
|
async markProcessed(eventKey, outcome) {
|
|
227
240
|
await nativeCall(executor, 'fulfillment_mark_observation_processed', [eventKey, JSON.stringify({ status: outcome.status,
|
|
241
|
+
...(outcome.supersededCause ? { supersededCause: safeDiagnosticCode(outcome.supersededCause) ?? 'fulfillment_state_conflict' } : {}),
|
|
228
242
|
...(outcome.refusal ? { refusal: { reason: outcome.refusal.reason, phase: outcome.refusal.phase, retryable: outcome.refusal.retryable } } : {}) })]);
|
|
229
243
|
},
|
|
230
244
|
};
|
|
@@ -13,13 +13,13 @@ export function createPostgresFulfillmentStockSyncPort(db, options) {
|
|
|
13
13
|
if (!row)
|
|
14
14
|
return null;
|
|
15
15
|
const cursor = row.cursor;
|
|
16
|
-
return { status: row.status, lastStockSyncedAt: timestamp(row.last_stock_synced_at), lastMovementOccurredAt: timestamp(row.last_movement_occurred_at),
|
|
16
|
+
return { ...cursor, status: row.status, lastStockSyncedAt: timestamp(row.last_stock_synced_at), lastMovementOccurredAt: timestamp(row.last_movement_occurred_at),
|
|
17
17
|
continuation: cursor.continuation ?? { nextRef: null, startLocalDate: null, endLocalDate: null }, coverageGapSkus: cursor.coverageGapSkus ?? [], refusal: cursor.refusal ?? null };
|
|
18
18
|
},
|
|
19
19
|
async recordCursor(v) {
|
|
20
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
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]);
|
|
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({ syncRunId: v.syncRunId, counts: v.counts, items: v.items, alerts: v.alerts, staleSync: v.staleSync, error: v.error, continuation: { nextRef: v.continuation.nextRef, startLocalDate: v.continuation.startLocalDate, endLocalDate: v.continuation.endLocalDate }, coverageGapSkus: [...(v.coverageGapSkus ?? [])], refusal: v.refusal }), v.error?.reason ?? v.refusal?.reason ?? null]);
|
|
23
23
|
},
|
|
24
24
|
async readSkuInventoryClasses(skus) {
|
|
25
25
|
if (!skus.length)
|
|
@@ -48,7 +48,10 @@ export function createPostgresFulfillmentStockSyncPort(db, options) {
|
|
|
48
48
|
const movement = { providerRef, syncRunId, collectedAt, sourceRevision, revisionSource, skuRef, quantityKind, quantity, occurredAt, operationType, documentRef, lotRef, expirationDate };
|
|
49
49
|
return nativeCall(db, 'fulfillment_record_stock_movement', [source, v.requestKey, fingerprint({ requestKey: v.requestKey, movement }), JSON.stringify(movement)]);
|
|
50
50
|
},
|
|
51
|
-
recordLowStockEvidence: v => nativeCall(db, 'fulfillment_record_stock_evidence', [source, v.requestKey,
|
|
51
|
+
recordLowStockEvidence: v => nativeCall(db, 'fulfillment_record_stock_evidence', [source, v.requestKey,
|
|
52
|
+
// Quantities, severity and run evidence are observations of the same alert identity.
|
|
53
|
+
fingerprint({ requestKey: v.requestKey, skuRef: v.skuRef, thresholdKind: v.thresholdKind }),
|
|
54
|
+
v.skuRef, v.thresholdKind, v.severity, v.providerForSaleQuantity, v.localAvailableQuantity]),
|
|
52
55
|
resolveLowStockEvidence: v => nativeCall(db, 'fulfillment_resolve_stock_evidence', [source, v.skuRef, v.activeThresholdKinds, v.syncRunId]),
|
|
53
56
|
};
|
|
54
57
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { dispatchEvidence } from "./dispatchProjection.js";
|
|
1
|
+
import { durableStrings, dispatchEvidence, dispatchResult, localFailureReason } from "./dispatchProjection.js";
|
|
2
2
|
export function acknowledgeAcceptedDispatch(input) {
|
|
3
3
|
return input.store.acknowledge({ dispatchRef: input.record.dispatchRef, acceptance: {
|
|
4
|
+
...(input.record.acceptance ? dispatchResult(input.record.acceptance) : {}),
|
|
5
|
+
...(input.record.status === "uncertain" ? { recovery: { source: "persisted_acceptance", proof: "persisted_external_ref", noProviderPost: true } } : {}),
|
|
4
6
|
kind: "accepted", externalRef: input.record.externalRef, evidence: dispatchEvidence(input.record.evidence), ...input.keys,
|
|
5
7
|
} });
|
|
6
8
|
}
|
|
@@ -10,10 +12,14 @@ export async function recoverDispatchAcceptanceWrite(input) {
|
|
|
10
12
|
const committed = () => record?.status === "created" && record.externalRef === input.acceptance.externalRef;
|
|
11
13
|
if (committed())
|
|
12
14
|
return { committed: true, readBacks };
|
|
15
|
+
const privacy = durableStrings();
|
|
16
|
+
const cause = localFailureReason("ack", input.error, privacy);
|
|
13
17
|
try {
|
|
14
18
|
// Accepted proof survives a failed atomic ACK; finalization records uncertainty,
|
|
15
19
|
// and the next worker repairs the ACK locally without another provider call.
|
|
16
|
-
record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: input.acceptance
|
|
20
|
+
record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: { ...input.acceptance, error: {
|
|
21
|
+
code: "dispatch_acceptance_write_failed", cause, ...(privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}), status: null, retryable: false, mayHaveSucceeded: true,
|
|
22
|
+
} } });
|
|
17
23
|
readBacks += 1;
|
|
18
24
|
}
|
|
19
25
|
catch {
|
|
@@ -16,8 +16,59 @@ export declare function runFulfillmentDispatchWorker(input: {
|
|
|
16
16
|
mode: DispatchRecord["mode"];
|
|
17
17
|
batchLimit: number;
|
|
18
18
|
};
|
|
19
|
+
/** Pure local shadow fingerprint: a lowercase 64-hex SHA-256 digest of the adopter's canonical command bytes. Invalid output or a throw refuses before evidence writes. Never return raw command/PII. Used across shadow claim/readback/attempt; shadow never begins, and live preparation must produce the same identity. */
|
|
20
|
+
shadowCommandFingerprint?: (request: FulfillmentDispatchRequest) => string;
|
|
19
21
|
now?: () => Date;
|
|
20
22
|
}): Promise<{
|
|
23
|
+
redactedFieldCount: number;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
failures: number;
|
|
26
|
+
reason: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
|
|
27
|
+
refusal: {
|
|
28
|
+
reason: "malformed";
|
|
29
|
+
phase: "prepare";
|
|
30
|
+
retryable: false;
|
|
31
|
+
cause: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
|
|
32
|
+
};
|
|
33
|
+
skipped: boolean;
|
|
34
|
+
checked: number;
|
|
35
|
+
updated: number;
|
|
36
|
+
mode: "shadow" | "stage" | "live";
|
|
37
|
+
replayed: number;
|
|
38
|
+
providerCalls: number;
|
|
39
|
+
readBacks: number;
|
|
40
|
+
} | {
|
|
41
|
+
redactedFieldCount?: undefined;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
failures: number;
|
|
44
|
+
reason: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
|
|
45
|
+
refusal: {
|
|
46
|
+
reason: "malformed";
|
|
47
|
+
phase: "prepare";
|
|
48
|
+
retryable: false;
|
|
49
|
+
cause: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
|
|
50
|
+
};
|
|
51
|
+
skipped: boolean;
|
|
52
|
+
checked: number;
|
|
53
|
+
updated: number;
|
|
54
|
+
mode: "shadow" | "stage" | "live";
|
|
55
|
+
replayed: number;
|
|
56
|
+
providerCalls: number;
|
|
57
|
+
readBacks: number;
|
|
58
|
+
} | {
|
|
59
|
+
redactedFieldCount: number;
|
|
60
|
+
ok: boolean;
|
|
61
|
+
skipped: boolean;
|
|
62
|
+
checked: number;
|
|
63
|
+
updated: number;
|
|
64
|
+
failures: number;
|
|
65
|
+
mode: "shadow" | "stage" | "live";
|
|
66
|
+
replayed: number;
|
|
67
|
+
providerCalls: number;
|
|
68
|
+
readBacks: number;
|
|
69
|
+
reason: string | undefined;
|
|
70
|
+
} | {
|
|
71
|
+
redactedFieldCount?: undefined;
|
|
21
72
|
ok: boolean;
|
|
22
73
|
skipped: boolean;
|
|
23
74
|
checked: number;
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import { acceptedProviderProof, dispatchEvidence, dispatchResult } from "./dispatchProjection.js";
|
|
1
|
+
import { durableStrings, acceptedProviderProof, dispatchEvidence, dispatchResult, localFailureReason, shadowDispatchEvidence } from "./dispatchProjection.js";
|
|
2
2
|
import { acknowledgeAcceptedDispatch, recoverDispatchAcceptanceWrite } from "./providerDispatchAcceptance.js";
|
|
3
3
|
import { unknownDispatchFailure } from "./providerDispatchError.js";
|
|
4
4
|
export async function runFulfillmentDispatchWorker(input) {
|
|
5
|
+
const privacy = durableStrings();
|
|
6
|
+
const failure = (stage, error) => localFailureReason(stage, error, privacy);
|
|
7
|
+
const diagnostics = () => privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {};
|
|
5
8
|
const result = { ok: true, skipped: false, checked: 0, updated: 0, failures: 0,
|
|
6
9
|
mode: input.config.mode, replayed: 0, providerCalls: 0, readBacks: 0, reason: undefined };
|
|
7
10
|
if (input.config.mode !== "shadow") {
|
|
8
11
|
try {
|
|
9
12
|
await input.store.markStaleSubmissionsUncertain(new Date((input.now?.() ?? new Date()).getTime() - 5 * 60_000).toISOString());
|
|
10
13
|
}
|
|
11
|
-
catch {
|
|
12
|
-
return { ...result, ok: false, failures: 1, reason: "dispatch_stale_submission_sweep_failed" };
|
|
14
|
+
catch (error) {
|
|
15
|
+
return { ...result, ok: false, failures: 1, reason: failure("dispatch_stale_submission_sweep_failed", error), ...diagnostics() };
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
const candidates = await input.listCandidates(input.config.batchLimit);
|
|
16
19
|
result.checked = candidates.length;
|
|
17
20
|
candidateLoop: for (const candidate of candidates) {
|
|
18
21
|
const requestKey = input.keys.request(candidate);
|
|
22
|
+
let stage = "dispatch_read_failed";
|
|
19
23
|
try {
|
|
20
24
|
let readBack = await input.store.read(requestKey);
|
|
21
25
|
if (readBack)
|
|
@@ -27,9 +31,11 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
27
31
|
result.updated += ack.replayed ? 0 : 1;
|
|
28
32
|
continue;
|
|
29
33
|
}
|
|
34
|
+
stage = "dispatch_prepare_failed";
|
|
30
35
|
const preparation = await input.store.prepare(candidate.fulfillmentRef);
|
|
31
36
|
if (preparation.disposition === "withheld")
|
|
32
37
|
continue;
|
|
38
|
+
stage = "dispatch_candidate_read_failed";
|
|
33
39
|
const preparedCandidate = await input.store.readCandidate(candidate.fulfillmentRef);
|
|
34
40
|
if (!preparedCandidate)
|
|
35
41
|
throw new Error("dispatch_prepared_candidate_missing");
|
|
@@ -39,23 +45,48 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
39
45
|
result.reason ??= "provider_not_configured";
|
|
40
46
|
continue;
|
|
41
47
|
}
|
|
42
|
-
// Shadow needs command preparation too, but never calls dispatch.
|
|
43
|
-
if (!input.provider)
|
|
44
|
-
throw new Error("provider_command_preparation_not_configured");
|
|
45
48
|
let mappedCandidate = preparedCandidate;
|
|
46
49
|
let dispatchRef;
|
|
47
50
|
let evidence;
|
|
51
|
+
let providerEvidence;
|
|
48
52
|
let mappedAttemptCount = 0;
|
|
49
53
|
for (let remapCount = 0;; remapCount += 1) {
|
|
50
54
|
const request = { ...mappedCandidate, requestKey };
|
|
51
|
-
|
|
55
|
+
stage = "dispatch_command_prepare_failed";
|
|
56
|
+
let shadowFingerprint;
|
|
57
|
+
if (input.config.mode === "shadow" && input.shadowCommandFingerprint) {
|
|
58
|
+
let value;
|
|
59
|
+
let cause;
|
|
60
|
+
try {
|
|
61
|
+
value = input.shadowCommandFingerprint(request);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
cause = "adopter_fingerprint_failed";
|
|
65
|
+
}
|
|
66
|
+
if (!cause && typeof value === "string" && /^[a-f0-9]{64}$/.test(value))
|
|
67
|
+
shadowFingerprint = value;
|
|
68
|
+
else
|
|
69
|
+
cause ??= "adopter_fingerprint_invalid";
|
|
70
|
+
if (cause)
|
|
71
|
+
return { ...result, ok: false, failures: result.failures + 1, reason: cause,
|
|
72
|
+
refusal: { reason: "malformed", phase: "prepare", retryable: false, cause }, ...diagnostics() };
|
|
73
|
+
}
|
|
74
|
+
const prepared = input.config.mode === "shadow"
|
|
75
|
+
? { kind: "prepared", evidence: shadowDispatchEvidence(request, shadowFingerprint) }
|
|
76
|
+
: await input.provider.prepare(request);
|
|
52
77
|
if (prepared.kind === "refused") {
|
|
53
78
|
result.ok = false;
|
|
54
79
|
result.failures += 1;
|
|
55
|
-
result.reason ??= prepared.refusal.reason;
|
|
80
|
+
result.reason ??= privacy.token(prepared.refusal.reason) ?? "redacted";
|
|
56
81
|
continue candidateLoop;
|
|
57
82
|
}
|
|
58
|
-
|
|
83
|
+
providerEvidence = dispatchEvidence(prepared.evidence);
|
|
84
|
+
evidence = { ...providerEvidence, mapping: { source: "dispatch_worker", mode: input.config.mode, shadowMode: input.config.mode === "shadow" } };
|
|
85
|
+
// Native claims count any JSON change as a remap; attempts compare
|
|
86
|
+
// the whole JSON. Enrichment must not rewrite an existing identity.
|
|
87
|
+
if (readBack && sameCommandIdentity(readBack.evidence, evidence))
|
|
88
|
+
evidence = dispatchEvidence(readBack.evidence);
|
|
89
|
+
stage = "dispatch_claim_failed";
|
|
59
90
|
const claim = await input.store.claim({ requestKey, fulfillmentRef: mappedCandidate.fulfillmentRef,
|
|
60
91
|
orderRef: mappedCandidate.orderRef, parcelRef: mappedCandidate.parcelRef, mode: input.config.mode, evidence });
|
|
61
92
|
if (claim.kind === "refused") {
|
|
@@ -68,18 +99,20 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
68
99
|
}
|
|
69
100
|
result.ok = false;
|
|
70
101
|
result.failures += 1;
|
|
71
|
-
result.reason ??= claim.refusal.reason;
|
|
102
|
+
result.reason ??= privacy.token(claim.refusal.reason) ?? "redacted";
|
|
72
103
|
continue candidateLoop;
|
|
73
104
|
}
|
|
74
105
|
dispatchRef = claim.record;
|
|
75
106
|
if (dispatchRef.replayed)
|
|
76
107
|
result.replayed += 1;
|
|
77
108
|
// Re-claim may refresh a correctable draft atomically; readback is authoritative.
|
|
109
|
+
stage = "dispatch_readback_failed";
|
|
78
110
|
readBack = await input.store.read(requestKey);
|
|
79
111
|
if (readBack)
|
|
80
112
|
result.readBacks += 1;
|
|
81
113
|
if (!readBack || readBack.fulfillmentRef !== mappedCandidate.fulfillmentRef)
|
|
82
114
|
throw new Error("dispatch_readback_failed");
|
|
115
|
+
stage = "dispatch_attempt_write_failed";
|
|
83
116
|
await input.store.recordProviderAttempt(input.keys.mappedAttempt(requestKey, mappedAttemptCount), mappedCandidate.fulfillmentRef, evidence, input.config.mode, "recorded");
|
|
84
117
|
mappedAttemptCount += 1;
|
|
85
118
|
if (input.config.mode === "shadow") {
|
|
@@ -92,6 +125,7 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
92
125
|
result.reason ??= `dispatch_not_safe_to_submit:${readBack.status}`;
|
|
93
126
|
continue candidateLoop;
|
|
94
127
|
}
|
|
128
|
+
stage = "dispatch_begin_failed";
|
|
95
129
|
const submission = await input.store.begin({ dispatchRef: dispatchRef.dispatchRef, commandFingerprint: evidence.commandFingerprint });
|
|
96
130
|
if (submission.kind === "refused") {
|
|
97
131
|
if (remapCount === 0 && submission.refusal.reason === "contact_stale") {
|
|
@@ -103,10 +137,11 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
103
137
|
}
|
|
104
138
|
result.ok = false;
|
|
105
139
|
result.failures += 1;
|
|
106
|
-
result.reason ??= submission.refusal.reason;
|
|
140
|
+
result.reason ??= privacy.token(submission.refusal.reason) ?? "redacted";
|
|
107
141
|
continue candidateLoop;
|
|
108
142
|
}
|
|
109
143
|
if (!submission.begun) {
|
|
144
|
+
stage = "dispatch_concurrent_read_failed";
|
|
110
145
|
const concurrent = await input.store.read(requestKey);
|
|
111
146
|
if (concurrent)
|
|
112
147
|
result.readBacks += 1;
|
|
@@ -128,27 +163,32 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
128
163
|
result.providerCalls += 1;
|
|
129
164
|
let response;
|
|
130
165
|
try {
|
|
131
|
-
response = dispatchResult(await input.provider.dispatch({ ...mappedCandidate, requestKey },
|
|
166
|
+
response = dispatchResult(await input.provider.dispatch({ ...mappedCandidate, requestKey }, providerEvidence));
|
|
132
167
|
}
|
|
133
168
|
catch {
|
|
134
169
|
response = unknownDispatchFailure();
|
|
135
170
|
}
|
|
171
|
+
if (response.kind === "accepted" && JSON.stringify(response.evidence) !== JSON.stringify(providerEvidence)) {
|
|
172
|
+
response = { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: "malformed", phase: "acknowledge", retryable: false },
|
|
173
|
+
error: { code: "dispatch_acceptance_evidence_mismatch", status: null, retryable: false, mayHaveSucceeded: true } };
|
|
174
|
+
}
|
|
136
175
|
if (response.kind !== "accepted") {
|
|
176
|
+
stage = "dispatch_finalize_failed";
|
|
137
177
|
await input.store.finalize({ dispatchRef: dispatchRef.dispatchRef, result: response });
|
|
138
178
|
result.ok = false;
|
|
139
179
|
result.failures += 1;
|
|
140
|
-
result.reason ??= response.refusal.reason;
|
|
180
|
+
result.reason ??= response.error?.code ?? response.refusal.reason;
|
|
141
181
|
continue;
|
|
142
182
|
}
|
|
143
183
|
// All ACK paths use the original host keys, ignoring adapter key fields.
|
|
144
|
-
const acceptance = { ...response, ...input.keys.acceptance(mappedCandidate.fulfillmentRef) };
|
|
184
|
+
const acceptance = { ...response, evidence, ...input.keys.acceptance(mappedCandidate.fulfillmentRef) };
|
|
145
185
|
try {
|
|
146
186
|
const ack = await input.store.acknowledge({ dispatchRef: dispatchRef.dispatchRef, acceptance });
|
|
147
187
|
if (ack.replayed)
|
|
148
188
|
result.replayed += 1;
|
|
149
189
|
}
|
|
150
|
-
catch {
|
|
151
|
-
const recovery = await recoverDispatchAcceptanceWrite({ store: input.store, requestKey, dispatchRef: dispatchRef.dispatchRef, acceptance });
|
|
190
|
+
catch (error) {
|
|
191
|
+
const recovery = await recoverDispatchAcceptanceWrite({ store: input.store, requestKey, dispatchRef: dispatchRef.dispatchRef, acceptance, error });
|
|
152
192
|
result.readBacks += recovery.readBacks;
|
|
153
193
|
if (recovery.committed) {
|
|
154
194
|
result.updated += 1;
|
|
@@ -161,12 +201,17 @@ export async function runFulfillmentDispatchWorker(input) {
|
|
|
161
201
|
}
|
|
162
202
|
result.updated += 1;
|
|
163
203
|
}
|
|
164
|
-
catch {
|
|
165
|
-
// Exception messages may contain raw provider bodies or credentials.
|
|
204
|
+
catch (error) {
|
|
166
205
|
result.ok = false;
|
|
167
206
|
result.failures += 1;
|
|
168
|
-
result.reason ??=
|
|
207
|
+
result.reason ??= failure(stage, error);
|
|
169
208
|
}
|
|
170
209
|
}
|
|
171
|
-
return result;
|
|
210
|
+
return { ...result, ...diagnostics() };
|
|
211
|
+
}
|
|
212
|
+
function sameCommandIdentity(a, b) {
|
|
213
|
+
return a.commandFingerprint === b.commandFingerprint && a.providerRef === b.providerRef
|
|
214
|
+
&& a.carrierRef === b.carrierRef && a.serviceRef === b.serviceRef
|
|
215
|
+
&& a.contactRevision.schemaVersion === b.contactRevision.schemaVersion
|
|
216
|
+
&& a.contactRevision.source === b.contactRevision.source && a.contactRevision.revision === b.contactRevision.revision;
|
|
172
217
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import type { FulfillmentObservation,
|
|
1
|
+
import type { FulfillmentObservation, FulfillmentObservationPagePort, FulfillmentReconciliationObservation, FulfillmentOperationError } from "./contracts/index.js";
|
|
2
2
|
import { enrichTrackingReference } from "./providerTrackingReferences.js";
|
|
3
3
|
type Ingress = Parameters<typeof import("./verifiedIngress.js").verifyAndNormalizeFulfillmentObservation>[0];
|
|
4
4
|
type DispatchRef = NonNullable<Awaited<ReturnType<Ingress["lifecycle"]["findDispatchRef"]>>>;
|
|
5
|
-
type PollEvidence = FulfillmentObservation |
|
|
6
|
-
|
|
7
|
-
eventKey: string;
|
|
8
|
-
};
|
|
9
|
-
interface ReconciliationResult {
|
|
5
|
+
type PollEvidence = Pick<FulfillmentObservation, "correlation" | "eventKey" | "providerStatus" | "diagnostics"> | FulfillmentObservation;
|
|
6
|
+
export interface ReconciliationResult {
|
|
10
7
|
ok: boolean;
|
|
11
8
|
checked: number;
|
|
12
9
|
updated: number;
|
|
@@ -17,6 +14,7 @@ interface ReconciliationResult {
|
|
|
17
14
|
stale: number;
|
|
18
15
|
offTrackSkips: number;
|
|
19
16
|
replayed: number;
|
|
17
|
+
redactedFieldCount?: number;
|
|
20
18
|
failures: number;
|
|
21
19
|
invoiceIssueFailures: number;
|
|
22
20
|
invoiceIssueRefused: number;
|
|
@@ -30,29 +28,21 @@ interface ReconciliationResult {
|
|
|
30
28
|
readBacks: number;
|
|
31
29
|
skipped: boolean;
|
|
32
30
|
reason?: string;
|
|
31
|
+
providerError?: FulfillmentOperationError;
|
|
33
32
|
}
|
|
34
33
|
export declare function reconcileFulfillmentObservations(input: {
|
|
35
34
|
/** Adapter/host maps the private page into typed readback, preserving unknowns. */
|
|
36
35
|
readPage(input: {
|
|
37
36
|
page: number;
|
|
38
37
|
size: number;
|
|
39
|
-
}): Promise<readonly
|
|
40
|
-
kind: "unmapped";
|
|
41
|
-
evidence: {
|
|
42
|
-
correlation: FulfillmentObservation["correlation"];
|
|
43
|
-
eventKey: string;
|
|
44
|
-
};
|
|
45
|
-
externalRef: string | null;
|
|
46
|
-
})[]>;
|
|
38
|
+
}): Promise<readonly FulfillmentReconciliationObservation[]>;
|
|
47
39
|
batchSize: number;
|
|
48
40
|
page?: number;
|
|
49
41
|
localPersistence: {
|
|
50
42
|
/** G17: local persisted chronology, never inferred from dispatch or provider state. */
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
handedOverAt: string | null;
|
|
55
|
-
}>;
|
|
43
|
+
readLocalFulfillmentStatus(ref: string): Promise<string | null>;
|
|
44
|
+
latestStatusOccurredAt(ref: string): Promise<string | null>;
|
|
45
|
+
readHandedOverAt(ref: string): Promise<string | null>;
|
|
56
46
|
};
|
|
57
47
|
lifecycle: Pick<Ingress["lifecycle"], "recordStatusEvidence" | "effects" | "keys"> & {
|
|
58
48
|
findDispatchRef(evidence: PollEvidence): Promise<DispatchRef | null>;
|
|
@@ -79,4 +69,22 @@ export declare function reconcileFulfillmentObservations(input: {
|
|
|
79
69
|
classifyStateConflict(error: unknown): "fulfillment_not_found" | "stock_consumed_requires_label" | null;
|
|
80
70
|
};
|
|
81
71
|
}): Promise<ReconciliationResult>;
|
|
72
|
+
/** Host owns scheduling, HTTP and durable job-ledger writes. This operation only projects them. */
|
|
73
|
+
export declare function runFulfillmentReconciliationPage(input: Omit<Parameters<typeof reconcileFulfillmentObservations>[0], "readPage" | "batchSize" | "page"> & {
|
|
74
|
+
provider: FulfillmentObservationPagePort;
|
|
75
|
+
pageSize: number;
|
|
76
|
+
checkpoint: unknown;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
status: 200 | 502;
|
|
79
|
+
ledgerStatus: "failed" | "succeeded";
|
|
80
|
+
result: ReconciliationResult;
|
|
81
|
+
metadata: {
|
|
82
|
+
reconciliationPagination: {
|
|
83
|
+
version: number;
|
|
84
|
+
page: number;
|
|
85
|
+
pageSize: number;
|
|
86
|
+
nextPage: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
82
90
|
export {};
|