@openlup/platform 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -0
- package/dist/invoicing/contracts/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +39 -0
- package/dist/invoicing/index.js +37 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -0
- package/dist/subscriptions/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +23 -3
- package/release-gates.json +74 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +71 -0
- package/src/invoicing/contracts/host.ts +84 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +102 -0
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +39 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -0
- package/src/subscriptions/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DispatchRecord, FulfillmentDispatchResult, FulfillmentStorePort } from "./contracts/index.js";
|
|
2
|
-
import { dispatchEvidence } from "./dispatchProjection.js";
|
|
2
|
+
import { durableStrings, dispatchEvidence, dispatchResult, localFailureReason } from "./dispatchProjection.js";
|
|
3
3
|
|
|
4
4
|
export function acknowledgeAcceptedDispatch(input: {
|
|
5
5
|
store: FulfillmentStorePort;
|
|
@@ -7,22 +7,28 @@ export function acknowledgeAcceptedDispatch(input: {
|
|
|
7
7
|
keys: { providerAttemptKey: string; labelKey: string };
|
|
8
8
|
}) {
|
|
9
9
|
return input.store.acknowledge({ dispatchRef: input.record.dispatchRef, acceptance: {
|
|
10
|
+
...(input.record.acceptance ? dispatchResult(input.record.acceptance) : {}),
|
|
11
|
+
...(input.record.status === "uncertain" ? { recovery: { source: "persisted_acceptance" as const, proof: "persisted_external_ref" as const, noProviderPost: true as const } } : {}),
|
|
10
12
|
kind: "accepted", externalRef: input.record.externalRef, evidence: dispatchEvidence(input.record.evidence), ...input.keys,
|
|
11
13
|
} });
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export async function recoverDispatchAcceptanceWrite(input: {
|
|
15
17
|
store: FulfillmentStorePort; requestKey: string; dispatchRef: string;
|
|
16
|
-
acceptance: Extract<FulfillmentDispatchResult, { kind: "accepted" }>;
|
|
18
|
+
acceptance: Extract<FulfillmentDispatchResult, { kind: "accepted" }>; error: unknown;
|
|
17
19
|
}): Promise<{ committed: boolean; readBacks: number }> {
|
|
18
20
|
let record = await input.store.read(input.requestKey).catch(() => null);
|
|
19
21
|
let readBacks = record ? 1 : 0;
|
|
20
22
|
const committed = () => record?.status === "created" && record.externalRef === input.acceptance.externalRef;
|
|
21
23
|
if (committed()) return { committed: true, readBacks };
|
|
24
|
+
const privacy = durableStrings();
|
|
25
|
+
const cause = localFailureReason("ack", input.error, privacy);
|
|
22
26
|
try {
|
|
23
27
|
// Accepted proof survives a failed atomic ACK; finalization records uncertainty,
|
|
24
28
|
// and the next worker repairs the ACK locally without another provider call.
|
|
25
|
-
record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: input.acceptance
|
|
29
|
+
record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: { ...input.acceptance, error: {
|
|
30
|
+
code: "dispatch_acceptance_write_failed", cause, ...(privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}), status: null, retryable: false, mayHaveSucceeded: true,
|
|
31
|
+
} } });
|
|
26
32
|
readBacks += 1;
|
|
27
33
|
} catch {
|
|
28
34
|
// Submitting remains fenced; the stale sweep moves it to uncertain.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { DispatchRecord, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentStorePort } from "./contracts/index.js";
|
|
2
|
-
import { acceptedProviderProof, dispatchEvidence, dispatchResult } from "./dispatchProjection.js";
|
|
1
|
+
import type { DispatchRecord, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentDispatchResult, FulfillmentStorePort } from "./contracts/index.js";
|
|
2
|
+
import { durableStrings, acceptedProviderProof, dispatchEvidence, dispatchResult, localFailureReason, shadowDispatchEvidence } from "./dispatchProjection.js";
|
|
3
3
|
import { acknowledgeAcceptedDispatch, recoverDispatchAcceptanceWrite } from "./providerDispatchAcceptance.js";
|
|
4
4
|
import { unknownDispatchFailure } from "./providerDispatchError.js";
|
|
5
5
|
|
|
@@ -14,18 +14,24 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
14
14
|
acceptance(fulfillmentRef: string): { providerAttemptKey: string; labelKey: string };
|
|
15
15
|
};
|
|
16
16
|
config: { mode: DispatchRecord["mode"]; batchLimit: number };
|
|
17
|
+
/** 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. */
|
|
18
|
+
shadowCommandFingerprint?: (request: FulfillmentDispatchRequest) => string;
|
|
17
19
|
now?: () => Date;
|
|
18
20
|
}) {
|
|
21
|
+
const privacy = durableStrings();
|
|
22
|
+
const failure = (stage: string, error: unknown) => localFailureReason(stage, error, privacy);
|
|
23
|
+
const diagnostics = () => privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {};
|
|
19
24
|
const result = { ok: true, skipped: false, checked: 0, updated: 0, failures: 0,
|
|
20
25
|
mode: input.config.mode, replayed: 0, providerCalls: 0, readBacks: 0, reason: undefined as string | undefined };
|
|
21
26
|
if (input.config.mode !== "shadow") {
|
|
22
27
|
try { await input.store.markStaleSubmissionsUncertain(new Date((input.now?.() ?? new Date()).getTime() - 5 * 60_000).toISOString()); }
|
|
23
|
-
catch { return { ...result, ok: false, failures: 1, reason: "dispatch_stale_submission_sweep_failed" }; }
|
|
28
|
+
catch (error) { return { ...result, ok: false, failures: 1, reason: failure("dispatch_stale_submission_sweep_failed", error), ...diagnostics() }; }
|
|
24
29
|
}
|
|
25
30
|
const candidates = await input.listCandidates(input.config.batchLimit);
|
|
26
31
|
result.checked = candidates.length;
|
|
27
32
|
candidateLoop: for (const candidate of candidates) {
|
|
28
33
|
const requestKey = input.keys.request(candidate);
|
|
34
|
+
let stage = "dispatch_read_failed";
|
|
29
35
|
try {
|
|
30
36
|
let readBack = await input.store.read(requestKey);
|
|
31
37
|
if (readBack) result.readBacks += 1;
|
|
@@ -35,26 +41,46 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
35
41
|
result.updated += ack.replayed ? 0 : 1;
|
|
36
42
|
continue;
|
|
37
43
|
}
|
|
44
|
+
stage = "dispatch_prepare_failed";
|
|
38
45
|
const preparation = await input.store.prepare(candidate.fulfillmentRef);
|
|
39
46
|
if (preparation.disposition === "withheld") continue;
|
|
47
|
+
stage = "dispatch_candidate_read_failed";
|
|
40
48
|
const preparedCandidate = await input.store.readCandidate(candidate.fulfillmentRef);
|
|
41
49
|
if (!preparedCandidate) throw new Error("dispatch_prepared_candidate_missing");
|
|
42
50
|
if (input.config.mode !== "shadow" && !input.provider) {
|
|
43
51
|
result.ok = false; result.failures += 1; result.reason ??= "provider_not_configured"; continue;
|
|
44
52
|
}
|
|
45
|
-
// Shadow needs command preparation too, but never calls dispatch.
|
|
46
|
-
if (!input.provider) throw new Error("provider_command_preparation_not_configured");
|
|
47
53
|
let mappedCandidate = preparedCandidate;
|
|
48
54
|
let dispatchRef: DispatchRecord;
|
|
49
55
|
let evidence: DispatchRecord["evidence"];
|
|
56
|
+
let providerEvidence: DispatchRecord["evidence"];
|
|
50
57
|
let mappedAttemptCount = 0;
|
|
51
58
|
for (let remapCount = 0; ; remapCount += 1) {
|
|
52
59
|
const request = { ...mappedCandidate, requestKey };
|
|
53
|
-
|
|
60
|
+
stage = "dispatch_command_prepare_failed";
|
|
61
|
+
let shadowFingerprint: string | undefined;
|
|
62
|
+
if (input.config.mode === "shadow" && input.shadowCommandFingerprint) {
|
|
63
|
+
let value: unknown;
|
|
64
|
+
let cause: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed" | undefined;
|
|
65
|
+
try { value = input.shadowCommandFingerprint(request); }
|
|
66
|
+
catch { cause = "adopter_fingerprint_failed"; }
|
|
67
|
+
if (!cause && typeof value === "string" && /^[a-f0-9]{64}$/.test(value)) shadowFingerprint = value;
|
|
68
|
+
else cause ??= "adopter_fingerprint_invalid";
|
|
69
|
+
if (cause) return { ...result, ok: false, failures: result.failures + 1, reason: cause,
|
|
70
|
+
refusal: { reason: "malformed" as const, phase: "prepare" as const, retryable: false as const, cause }, ...diagnostics() };
|
|
71
|
+
}
|
|
72
|
+
const prepared = input.config.mode === "shadow"
|
|
73
|
+
? { kind: "prepared" as const, evidence: shadowDispatchEvidence(request, shadowFingerprint) }
|
|
74
|
+
: await input.provider!.prepare(request);
|
|
54
75
|
if (prepared.kind === "refused") {
|
|
55
|
-
result.ok = false; result.failures += 1; result.reason ??= prepared.refusal.reason; continue candidateLoop;
|
|
76
|
+
result.ok = false; result.failures += 1; result.reason ??= privacy.token(prepared.refusal.reason) ?? "redacted"; continue candidateLoop;
|
|
56
77
|
}
|
|
57
|
-
|
|
78
|
+
providerEvidence = dispatchEvidence(prepared.evidence);
|
|
79
|
+
evidence = { ...providerEvidence, mapping: { source: "dispatch_worker", mode: input.config.mode, shadowMode: input.config.mode === "shadow" } };
|
|
80
|
+
// Native claims count any JSON change as a remap; attempts compare
|
|
81
|
+
// the whole JSON. Enrichment must not rewrite an existing identity.
|
|
82
|
+
if (readBack && sameCommandIdentity(readBack.evidence, evidence)) evidence = dispatchEvidence(readBack.evidence);
|
|
83
|
+
stage = "dispatch_claim_failed";
|
|
58
84
|
const claim = await input.store.claim({ requestKey, fulfillmentRef: mappedCandidate.fulfillmentRef,
|
|
59
85
|
orderRef: mappedCandidate.orderRef, parcelRef: mappedCandidate.parcelRef, mode: input.config.mode, evidence });
|
|
60
86
|
if (claim.kind === "refused") {
|
|
@@ -63,14 +89,16 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
63
89
|
if (!refreshed) throw new Error("dispatch_contact_remap_candidate_missing");
|
|
64
90
|
mappedCandidate = refreshed; continue;
|
|
65
91
|
}
|
|
66
|
-
result.ok = false; result.failures += 1; result.reason ??= claim.refusal.reason; continue candidateLoop;
|
|
92
|
+
result.ok = false; result.failures += 1; result.reason ??= privacy.token(claim.refusal.reason) ?? "redacted"; continue candidateLoop;
|
|
67
93
|
}
|
|
68
94
|
dispatchRef = claim.record;
|
|
69
95
|
if (dispatchRef.replayed) result.replayed += 1;
|
|
70
96
|
// Re-claim may refresh a correctable draft atomically; readback is authoritative.
|
|
97
|
+
stage = "dispatch_readback_failed";
|
|
71
98
|
readBack = await input.store.read(requestKey);
|
|
72
99
|
if (readBack) result.readBacks += 1;
|
|
73
100
|
if (!readBack || readBack.fulfillmentRef !== mappedCandidate.fulfillmentRef) throw new Error("dispatch_readback_failed");
|
|
101
|
+
stage = "dispatch_attempt_write_failed";
|
|
74
102
|
await input.store.recordProviderAttempt(input.keys.mappedAttempt(requestKey, mappedAttemptCount),
|
|
75
103
|
mappedCandidate.fulfillmentRef, evidence, input.config.mode, "recorded");
|
|
76
104
|
mappedAttemptCount += 1;
|
|
@@ -80,6 +108,7 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
80
108
|
if (readBack.status !== "draft") {
|
|
81
109
|
result.ok = false; result.failures += 1; result.reason ??= `dispatch_not_safe_to_submit:${readBack.status}`; continue candidateLoop;
|
|
82
110
|
}
|
|
111
|
+
stage = "dispatch_begin_failed";
|
|
83
112
|
const submission = await input.store.begin({ dispatchRef: dispatchRef.dispatchRef, commandFingerprint: evidence.commandFingerprint });
|
|
84
113
|
if (submission.kind === "refused") {
|
|
85
114
|
if (remapCount === 0 && submission.refusal.reason === "contact_stale") {
|
|
@@ -87,9 +116,10 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
87
116
|
if (!refreshed) throw new Error("dispatch_contact_remap_candidate_missing");
|
|
88
117
|
mappedCandidate = refreshed; continue;
|
|
89
118
|
}
|
|
90
|
-
result.ok = false; result.failures += 1; result.reason ??= submission.refusal.reason; continue candidateLoop;
|
|
119
|
+
result.ok = false; result.failures += 1; result.reason ??= privacy.token(submission.refusal.reason) ?? "redacted"; continue candidateLoop;
|
|
91
120
|
}
|
|
92
121
|
if (!submission.begun) {
|
|
122
|
+
stage = "dispatch_concurrent_read_failed";
|
|
93
123
|
const concurrent = await input.store.read(requestKey);
|
|
94
124
|
if (concurrent) result.readBacks += 1;
|
|
95
125
|
if (acceptedProviderProof(concurrent)) {
|
|
@@ -103,29 +133,40 @@ export async function runFulfillmentDispatchWorker(input: {
|
|
|
103
133
|
break;
|
|
104
134
|
}
|
|
105
135
|
result.providerCalls += 1;
|
|
106
|
-
let response;
|
|
107
|
-
try { response = dispatchResult(await input.provider
|
|
136
|
+
let response: FulfillmentDispatchResult;
|
|
137
|
+
try { response = dispatchResult(await input.provider!.dispatch({ ...mappedCandidate, requestKey }, providerEvidence)); }
|
|
108
138
|
catch { response = unknownDispatchFailure(); }
|
|
139
|
+
if (response.kind === "accepted" && JSON.stringify(response.evidence) !== JSON.stringify(providerEvidence)) {
|
|
140
|
+
response = { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: "malformed", phase: "acknowledge", retryable: false },
|
|
141
|
+
error: { code: "dispatch_acceptance_evidence_mismatch", status: null, retryable: false, mayHaveSucceeded: true } };
|
|
142
|
+
}
|
|
109
143
|
if (response.kind !== "accepted") {
|
|
144
|
+
stage = "dispatch_finalize_failed";
|
|
110
145
|
await input.store.finalize({ dispatchRef: dispatchRef.dispatchRef, result: response });
|
|
111
|
-
result.ok = false; result.failures += 1; result.reason ??= response.refusal.reason; continue;
|
|
146
|
+
result.ok = false; result.failures += 1; result.reason ??= response.error?.code ?? response.refusal.reason; continue;
|
|
112
147
|
}
|
|
113
148
|
// All ACK paths use the original host keys, ignoring adapter key fields.
|
|
114
|
-
const acceptance = { ...response, ...input.keys.acceptance(mappedCandidate.fulfillmentRef) };
|
|
149
|
+
const acceptance = { ...response, evidence, ...input.keys.acceptance(mappedCandidate.fulfillmentRef) };
|
|
115
150
|
try {
|
|
116
151
|
const ack = await input.store.acknowledge({ dispatchRef: dispatchRef.dispatchRef, acceptance });
|
|
117
152
|
if (ack.replayed) result.replayed += 1;
|
|
118
|
-
} catch {
|
|
119
|
-
const recovery = await recoverDispatchAcceptanceWrite({ store: input.store, requestKey, dispatchRef: dispatchRef.dispatchRef, acceptance });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const recovery = await recoverDispatchAcceptanceWrite({ store: input.store, requestKey, dispatchRef: dispatchRef.dispatchRef, acceptance, error });
|
|
120
155
|
result.readBacks += recovery.readBacks;
|
|
121
156
|
if (recovery.committed) { result.updated += 1; continue; }
|
|
122
157
|
result.ok = false; result.failures += 1; result.reason ??= "dispatch_proof_write_failed_after_provider_acceptance"; break;
|
|
123
158
|
}
|
|
124
159
|
result.updated += 1;
|
|
125
|
-
} catch {
|
|
126
|
-
|
|
127
|
-
result.ok = false; result.failures += 1; result.reason ??= "dispatch_local_operation_failed";
|
|
160
|
+
} catch (error) {
|
|
161
|
+
result.ok = false; result.failures += 1; result.reason ??= failure(stage, error);
|
|
128
162
|
}
|
|
129
163
|
}
|
|
130
|
-
return result;
|
|
164
|
+
return { ...result, ...diagnostics() };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sameCommandIdentity(a: DispatchRecord["evidence"], b: DispatchRecord["evidence"]): boolean {
|
|
168
|
+
return a.commandFingerprint === b.commandFingerprint && a.providerRef === b.providerRef
|
|
169
|
+
&& a.carrierRef === b.carrierRef && a.serviceRef === b.serviceRef
|
|
170
|
+
&& a.contactRevision.schemaVersion === b.contactRevision.schemaVersion
|
|
171
|
+
&& a.contactRevision.source === b.contactRevision.source && a.contactRevision.revision === b.contactRevision.revision;
|
|
131
172
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { FulfillmentObservation,
|
|
1
|
+
import type { FulfillmentObservation, FulfillmentObservationPagePort, FulfillmentReconciliationObservation, FulfillmentOperationError } from "./contracts/index.js";
|
|
2
|
+
import { durableStrings, localFailureReason, safeDiagnosticCode, operationError } from "./dispatchProjection.js";
|
|
2
3
|
import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
3
|
-
import { observationStatusPolicy,
|
|
4
|
+
import { observationStatusPolicy, projectObservation, projectPollEvidence, projectTrackingReference } from "./observationProjection.js";
|
|
4
5
|
import { quarantineReconciliationStateConflict } from "./providerReconciliationError.js";
|
|
5
6
|
import { isOffTrackFulfillmentStatus } from "./providerReconciliationTerminalSkip.js";
|
|
6
7
|
import { normalizeReconciliationTimestamp } from "./providerReconciliationTimestamp.js";
|
|
@@ -8,22 +9,24 @@ import { enrichTrackingReference, trackingReferenceComplete, uniqueTrackingRefer
|
|
|
8
9
|
|
|
9
10
|
type Ingress = Parameters<typeof import("./verifiedIngress.js").verifyAndNormalizeFulfillmentObservation>[0];
|
|
10
11
|
type DispatchRef = NonNullable<Awaited<ReturnType<Ingress["lifecycle"]["findDispatchRef"]>>>;
|
|
11
|
-
type PollEvidence = FulfillmentObservation |
|
|
12
|
-
interface ReconciliationResult {
|
|
12
|
+
type PollEvidence = Pick<FulfillmentObservation, "correlation" | "eventKey" | "providerStatus" | "diagnostics"> | FulfillmentObservation;
|
|
13
|
+
export interface ReconciliationResult {
|
|
13
14
|
ok: boolean; checked: number; updated: number; trackingRefs: number; quarantined: number;
|
|
14
15
|
stateConflicts: number; exceptions: number; stale: number; offTrackSkips: number; replayed: number;
|
|
15
|
-
failures: number; invoiceIssueFailures: number; invoiceIssueRefused: number;
|
|
16
|
+
redactedFieldCount?: number; failures: number; invoiceIssueFailures: number; invoiceIssueRefused: number;
|
|
16
17
|
invoiceIssueRefusals: { fulfillmentRef: string; identifier: string | null; sqlstate: string; benign: boolean }[];
|
|
17
|
-
providerCalls: number; readBacks: number; skipped: boolean; reason?: string;
|
|
18
|
+
providerCalls: number; readBacks: number; skipped: boolean; reason?: string; providerError?: FulfillmentOperationError;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function reconcileFulfillmentObservations(input: {
|
|
21
22
|
/** Adapter/host maps the private page into typed readback, preserving unknowns. */
|
|
22
|
-
readPage(input: { page: number; size: number }): Promise<readonly
|
|
23
|
+
readPage(input: { page: number; size: number }): Promise<readonly FulfillmentReconciliationObservation[]>;
|
|
23
24
|
batchSize: number; page?: number;
|
|
24
25
|
localPersistence: {
|
|
25
26
|
/** G17: local persisted chronology, never inferred from dispatch or provider state. */
|
|
26
|
-
|
|
27
|
+
readLocalFulfillmentStatus(ref: string): Promise<string | null>;
|
|
28
|
+
latestStatusOccurredAt(ref: string): Promise<string | null>;
|
|
29
|
+
readHandedOverAt(ref: string): Promise<string | null>;
|
|
27
30
|
};
|
|
28
31
|
lifecycle: Pick<Ingress["lifecycle"], "recordStatusEvidence" | "effects" | "keys"> & {
|
|
29
32
|
findDispatchRef(evidence: PollEvidence): Promise<DispatchRef | null>;
|
|
@@ -38,6 +41,8 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
38
41
|
classifyStateConflict(error: unknown): "fulfillment_not_found" | "stock_consumed_requires_label" | null;
|
|
39
42
|
};
|
|
40
43
|
}): Promise<ReconciliationResult> {
|
|
44
|
+
const privacy = durableStrings();
|
|
45
|
+
const failure = (stage: string, error: unknown) => localFailureReason(stage, error, privacy);
|
|
41
46
|
const result: ReconciliationResult = {
|
|
42
47
|
ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
|
|
43
48
|
stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0,
|
|
@@ -45,8 +50,6 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
45
50
|
};
|
|
46
51
|
const page = await input.readPage({ page: input.page ?? 0, size: input.batchSize });
|
|
47
52
|
result.providerCalls += 1;
|
|
48
|
-
// A failed provider read cannot become a successful unknown-status quarantine.
|
|
49
|
-
if (page.some(readback => readback.kind === "unknown")) throw new Error("fulfillment_reconciliation_provider_read_failed");
|
|
50
53
|
result.checked = page.length;
|
|
51
54
|
const quarantine = async (observation: PollEvidence | null, reason: Parameters<typeof input.lifecycle.recordQuarantine>[1]) => {
|
|
52
55
|
const evidence = await input.lifecycle.recordQuarantine(observation, reason);
|
|
@@ -59,26 +62,31 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
59
62
|
const refusal = input.lifecycle.invoiceFailure(error);
|
|
60
63
|
if (refusal.permanent) {
|
|
61
64
|
result.invoiceIssueRefused += 1;
|
|
65
|
+
const identifier = privacy.token(refusal.identifier, safeDiagnosticCode), sqlstate = privacy.token(refusal.sqlstate, safeDiagnosticCode);
|
|
62
66
|
if (result.invoiceIssueRefusals.length < 10) result.invoiceIssueRefusals.push({ fulfillmentRef: ref,
|
|
63
|
-
identifier
|
|
64
|
-
result.reason ??= "reconciliation_invoice_issue_refused";
|
|
67
|
+
identifier, sqlstate: sqlstate ?? "22023", benign: refusal.benign });
|
|
68
|
+
result.reason ??= failure("reconciliation_invoice_issue_refused", { causeMessage: identifier, causeCode: sqlstate, message: error instanceof Error ? error.message : undefined });
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
result.ok = false; result.failures += 1; result.invoiceIssueFailures += 1;
|
|
68
|
-
result.reason ??= "reconciliation_invoice_issue_failed";
|
|
72
|
+
result.reason ??= failure("reconciliation_invoice_issue_failed", error);
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
for (const readback of page) {
|
|
72
76
|
let observation: FulfillmentObservation | null = null;
|
|
73
77
|
try {
|
|
78
|
+
if (readback.kind === "unknown") {
|
|
79
|
+
result.ok = false; result.failures += 1; result.reason ??= "fulfillment_reconciliation_provider_read_failed";
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
74
82
|
if (readback.kind === "unmapped") {
|
|
75
83
|
// An unknown primary status still correlates/ACKs, but an off-track row
|
|
76
84
|
// must never trade a benign skip for a quarantine alarm.
|
|
77
|
-
const evidence =
|
|
85
|
+
const evidence = projectPollEvidence(readback.evidence);
|
|
78
86
|
const ref = await input.lifecycle.findDispatchRef(evidence);
|
|
79
87
|
if (!ref) { await quarantine(evidence, "missing_correlation"); continue; }
|
|
80
|
-
const
|
|
81
|
-
if (isOffTrackFulfillmentStatus(
|
|
88
|
+
const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
|
|
89
|
+
if (isOffTrackFulfillmentStatus(localState)) { result.offTrackSkips += 1; continue; }
|
|
82
90
|
if (!(readback.externalRef?.trim() || ref.externalRef?.trim())) { await quarantine(evidence, "missing_external_ref"); continue; }
|
|
83
91
|
await input.lifecycle.acknowledgeDispatchAcceptance(ref, evidence);
|
|
84
92
|
await quarantine(evidence, "unknown_status");
|
|
@@ -88,11 +96,14 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
88
96
|
// Neither unsupported nor a provider absence is a local transition.
|
|
89
97
|
continue;
|
|
90
98
|
}
|
|
91
|
-
observation = projectObservation(readback.observation);
|
|
99
|
+
observation = projectObservation(readback.observation, input.lifecycle.trackingCodecs.approveTrackingUrl, input.lifecycle.trackingCodecs);
|
|
100
|
+
if (observation.diagnostics?.redactedFieldCount) {
|
|
101
|
+
result.redactedFieldCount = (result.redactedFieldCount ?? 0) + observation.diagnostics.redactedFieldCount;
|
|
102
|
+
}
|
|
92
103
|
const ref = await input.lifecycle.findDispatchRef(observation);
|
|
93
104
|
if (!ref) { await quarantine(observation, "missing_correlation"); continue; }
|
|
94
|
-
const
|
|
95
|
-
const offTrack = isOffTrackFulfillmentStatus(
|
|
105
|
+
const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
|
|
106
|
+
const offTrack = isOffTrackFulfillmentStatus(localState);
|
|
96
107
|
if (offTrack) result.offTrackSkips += 1;
|
|
97
108
|
if (!offTrack) {
|
|
98
109
|
if (!(readback.externalRef.trim() || ref.externalRef?.trim())) { await quarantine(observation, "missing_external_ref"); continue; }
|
|
@@ -100,9 +111,10 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
100
111
|
}
|
|
101
112
|
const occurredAt = normalizeReconciliationTimestamp(readback.updatedAt);
|
|
102
113
|
const localStatus = observation.fact.canonicalStatus;
|
|
103
|
-
|
|
114
|
+
const latest = await input.localPersistence.latestStatusOccurredAt(ref.fulfillmentRef);
|
|
115
|
+
if (latest && occurredAt && Date.parse(occurredAt) < Date.parse(latest)) {
|
|
104
116
|
result.stale += 1;
|
|
105
|
-
if (!offTrack && input.lifecycle.issueAccountingInvoice &&
|
|
117
|
+
if (!offTrack && input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef)) await invoice(ref.fulfillmentRef);
|
|
106
118
|
continue;
|
|
107
119
|
}
|
|
108
120
|
// The normalized fact carries the same nullable occurrence used by dedupe.
|
|
@@ -115,7 +127,7 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
115
127
|
const exception = await input.lifecycle.recordProviderException(ref, observation);
|
|
116
128
|
if (exception.replayed) result.replayed += 1;
|
|
117
129
|
result.exceptions += exception.replayed ? 0 : 1;
|
|
118
|
-
if (input.lifecycle.issueAccountingInvoice &&
|
|
130
|
+
if (input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef)) await invoice(ref.fulfillmentRef);
|
|
119
131
|
continue;
|
|
120
132
|
}
|
|
121
133
|
const trackingRefs = [];
|
|
@@ -123,7 +135,12 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
123
135
|
let carrier: Awaited<ReturnType<typeof input.lifecycle.readDeliveryCarrier>> | undefined;
|
|
124
136
|
for (const tracking of uniqueTrackingReferences(observation.trackingRefs)) {
|
|
125
137
|
if (!trackingReferenceComplete(tracking) && carrier === undefined) carrier = await input.lifecycle.readDeliveryCarrier(ref.fulfillmentRef);
|
|
126
|
-
const
|
|
138
|
+
const codecs = input.lifecycle.trackingCodecs;
|
|
139
|
+
const candidate = enrichTrackingReference(tracking, carrier ?? null, { ...codecs, publicUrl: () => null });
|
|
140
|
+
const enriched = projectTrackingReference(candidate, privacy, codecs.approveTrackingUrl, codecs);
|
|
141
|
+
if (!enriched) continue;
|
|
142
|
+
// Host-built links use admitted parts and pass through unchanged, including queries.
|
|
143
|
+
enriched.publicUrl ??= codecs.publicUrl(enriched.carrierRef, enriched.trackingNumber);
|
|
127
144
|
trackingRefs.push({ trackingNumber: enriched.trackingNumber, trackingUrl: enriched.publicUrl, carrierKind: enriched.carrierRef, service: enriched.serviceRef });
|
|
128
145
|
}
|
|
129
146
|
}
|
|
@@ -143,8 +160,47 @@ export async function reconcileFulfillmentObservations(input: {
|
|
|
143
160
|
const error = observation ? await quarantineReconciliationStateConflict(caught, observation,
|
|
144
161
|
input.lifecycle.classifyStateConflict, input.lifecycle.recordQuarantine, result) : caught;
|
|
145
162
|
if (error === null) continue;
|
|
146
|
-
result.ok = false; result.failures += 1; result.reason ??= "reconciliation_local_operation_failed";
|
|
163
|
+
result.ok = false; result.failures += 1; result.reason ??= failure("reconciliation_local_operation_failed", error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { ...result, ...(privacy.redactedFieldCount ? { redactedFieldCount: (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount } : {}) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/** Host owns scheduling, HTTP and durable job-ledger writes. This operation only projects them. */
|
|
171
|
+
export async function runFulfillmentReconciliationPage(input: Omit<Parameters<typeof reconcileFulfillmentObservations>[0], "readPage" | "batchSize" | "page"> & {
|
|
172
|
+
provider: FulfillmentObservationPagePort; pageSize: number; checkpoint: unknown;
|
|
173
|
+
}) {
|
|
174
|
+
if (!Number.isSafeInteger(input.pageSize) || input.pageSize <= 0) throw new Error("reconciliation_page_size_invalid");
|
|
175
|
+
const isPage = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0;
|
|
176
|
+
const checkpoint = input.checkpoint as { reconciliationPagination?: { version?: unknown; pageSize?: unknown; page?: unknown; nextPage?: unknown } } | null;
|
|
177
|
+
const saved = checkpoint?.reconciliationPagination;
|
|
178
|
+
const page = saved?.version === 1 && saved.pageSize === input.pageSize && isPage(saved.page) && isPage(saved.nextPage)
|
|
179
|
+
&& (saved.nextPage === 0 || saved.nextPage === saved.page || saved.page < Number.MAX_SAFE_INTEGER && saved.nextPage === saved.page + 1) ? saved.nextPage : 0;
|
|
180
|
+
const privacy = durableStrings();
|
|
181
|
+
let result: ReconciliationResult;
|
|
182
|
+
let pageRead = false;
|
|
183
|
+
const empty = (): ReconciliationResult => ({ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
|
|
184
|
+
stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0, invoiceIssueRefusals: [], providerCalls: 1, readBacks: 0, skipped: false });
|
|
185
|
+
try {
|
|
186
|
+
const response = await input.provider.readPage({ page, size: input.pageSize });
|
|
187
|
+
if (response.kind === "page_exhausted") result = { ...empty(), reason: "page_exhausted" };
|
|
188
|
+
else if (response.kind === "failed") {
|
|
189
|
+
const providerError = response.error ? operationError(response.error) : undefined;
|
|
190
|
+
result = { ...empty(), ok: false, failures: 1, ...(providerError ? { providerError } : {}),
|
|
191
|
+
reason: providerError?.code ?? privacy.token(response.refusal.reason) ?? "redacted" };
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
pageRead = true;
|
|
195
|
+
result = await reconcileFulfillmentObservations({ ...input, batchSize: input.pageSize, page, readPage: async () => response.rows });
|
|
147
196
|
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
result = { ...empty(), ok: false, failures: 1, reason: localFailureReason(pageRead ? "reconciliation_page_failed" : "reconciliation_provider_read_failed", error, privacy) };
|
|
148
199
|
}
|
|
149
|
-
|
|
200
|
+
if (privacy.redactedFieldCount) result.redactedFieldCount = (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount;
|
|
201
|
+
// A pre-traversal abort must not skip a page. Item failures return normally.
|
|
202
|
+
const nextPage = pageRead && result.checked === 0 && !result.ok ? page
|
|
203
|
+
: result.checked === input.pageSize && page < Number.MAX_SAFE_INTEGER ? page + 1 : 0;
|
|
204
|
+
return { status: result.ok ? 200 as const : 502 as const, ledgerStatus: result.ok ? "succeeded" as const : "failed" as const, result,
|
|
205
|
+
metadata: { reconciliationPagination: { version: 1, page, pageSize: input.pageSize, nextPage } } };
|
|
150
206
|
}
|