@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.
Files changed (241) hide show
  1. package/CHANGELOG.md +440 -158
  2. package/README.md +141 -589
  3. package/dist/fulfillment/adapterConformance.js +1 -1
  4. package/dist/fulfillment/bindings.js +1 -1
  5. package/dist/fulfillment/contracts/conformance.d.ts +3 -1
  6. package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
  7. package/dist/fulfillment/contracts/index.d.ts +3 -3
  8. package/dist/fulfillment/contracts/observation.d.ts +21 -1
  9. package/dist/fulfillment/contracts/readback.d.ts +21 -0
  10. package/dist/fulfillment/contracts/store.d.ts +3 -0
  11. package/dist/fulfillment/contracts/sync.d.ts +58 -2
  12. package/dist/fulfillment/dispatchProjection.d.ts +20 -1
  13. package/dist/fulfillment/dispatchProjection.js +97 -6
  14. package/dist/fulfillment/index.d.ts +3 -2
  15. package/dist/fulfillment/index.js +2 -2
  16. package/dist/fulfillment/migrations.d.ts +5 -0
  17. package/dist/fulfillment/migrations.js +1 -0
  18. package/dist/fulfillment/observationProjection.d.ts +24 -3
  19. package/dist/fulfillment/observationProjection.js +65 -12
  20. package/dist/fulfillment/omnipack/client.d.ts +3 -1
  21. package/dist/fulfillment/omnipack/client.js +31 -13
  22. package/dist/fulfillment/omnipack/index.d.ts +4 -1
  23. package/dist/fulfillment/omnipack/index.js +107 -45
  24. package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
  25. package/dist/fulfillment/omnipack/mappers.js +24 -2
  26. package/dist/fulfillment/omnipack/observation.d.ts +11 -4
  27. package/dist/fulfillment/omnipack/observation.js +44 -29
  28. package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
  29. package/dist/fulfillment/omnipack/reconciliation.js +55 -0
  30. package/dist/fulfillment/postgresDispatchStore.js +1 -1
  31. package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
  32. package/dist/fulfillment/postgresShipmentSpine.js +17 -3
  33. package/dist/fulfillment/postgresStockSyncPort.js +6 -3
  34. package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
  35. package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
  36. package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
  37. package/dist/fulfillment/providerDispatchWorker.js +65 -20
  38. package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
  39. package/dist/fulfillment/providerReconciliationWorker.js +75 -19
  40. package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
  41. package/dist/fulfillment/providerStockSyncWorker.js +229 -120
  42. package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
  43. package/dist/fulfillment/verifiedIngress.d.ts +38 -11
  44. package/dist/fulfillment/verifiedIngress.js +36 -8
  45. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  46. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  47. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  48. package/dist/invoicing/accountingLineAllocation.js +69 -0
  49. package/dist/invoicing/adapterConformance.d.ts +42 -0
  50. package/dist/invoicing/adapterConformance.js +296 -0
  51. package/dist/invoicing/artifacts.d.ts +7 -0
  52. package/dist/invoicing/artifacts.js +20 -0
  53. package/dist/invoicing/bindings.d.ts +46 -0
  54. package/dist/invoicing/bindings.js +69 -0
  55. package/dist/invoicing/capabilities.d.ts +11 -0
  56. package/dist/invoicing/capabilities.js +10 -0
  57. package/dist/invoicing/channelPolicy.d.ts +28 -0
  58. package/dist/invoicing/channelPolicy.js +27 -0
  59. package/dist/invoicing/continuity.d.ts +12 -0
  60. package/dist/invoicing/continuity.js +18 -0
  61. package/dist/invoicing/contracts/capabilities.d.ts +40 -0
  62. package/dist/invoicing/contracts/capabilities.js +1 -0
  63. package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
  64. package/dist/invoicing/contracts/companyLookup.js +1 -0
  65. package/dist/invoicing/contracts/delivery.d.ts +74 -0
  66. package/dist/invoicing/contracts/delivery.js +1 -0
  67. package/dist/invoicing/contracts/facts.d.ts +110 -0
  68. package/dist/invoicing/contracts/facts.js +1 -0
  69. package/dist/invoicing/contracts/host.d.ts +245 -0
  70. package/dist/invoicing/contracts/host.js +1 -0
  71. package/dist/invoicing/contracts/index.d.ts +16 -0
  72. package/dist/invoicing/contracts/index.js +1 -0
  73. package/dist/invoicing/contracts/provider.d.ts +126 -0
  74. package/dist/invoicing/contracts/provider.js +1 -0
  75. package/dist/invoicing/contracts/settlement.d.ts +66 -0
  76. package/dist/invoicing/contracts/settlement.js +1 -0
  77. package/dist/invoicing/contracts/store.d.ts +277 -0
  78. package/dist/invoicing/contracts/store.js +1 -0
  79. package/dist/invoicing/contribution.d.ts +21 -0
  80. package/dist/invoicing/contribution.js +19 -0
  81. package/dist/invoicing/correction.d.ts +42 -0
  82. package/dist/invoicing/correction.js +44 -0
  83. package/dist/invoicing/delivery.d.ts +85 -0
  84. package/dist/invoicing/delivery.js +79 -0
  85. package/dist/invoicing/documentHistory.d.ts +50 -0
  86. package/dist/invoicing/documentHistory.js +213 -0
  87. package/dist/invoicing/documentSync.d.ts +24 -0
  88. package/dist/invoicing/documentSync.js +26 -0
  89. package/dist/invoicing/dutyCollector.d.ts +15 -0
  90. package/dist/invoicing/dutyCollector.js +35 -0
  91. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  92. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  93. package/dist/invoicing/handoff.d.ts +37 -0
  94. package/dist/invoicing/handoff.js +62 -0
  95. package/dist/invoicing/index.d.ts +39 -0
  96. package/dist/invoicing/index.js +37 -0
  97. package/dist/invoicing/issue.d.ts +39 -0
  98. package/dist/invoicing/issue.js +55 -0
  99. package/dist/invoicing/migrations.d.ts +48 -0
  100. package/dist/invoicing/migrations.js +25 -0
  101. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  102. package/dist/invoicing/nativeUnsupported.js +22 -0
  103. package/dist/invoicing/paymentReadback.d.ts +20 -0
  104. package/dist/invoicing/paymentReadback.js +47 -0
  105. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  106. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  107. package/dist/invoicing/readModel.d.ts +74 -0
  108. package/dist/invoicing/readModel.js +90 -0
  109. package/dist/invoicing/readback.d.ts +4 -0
  110. package/dist/invoicing/readback.js +30 -0
  111. package/dist/invoicing/reversal.d.ts +37 -0
  112. package/dist/invoicing/reversal.js +28 -0
  113. package/dist/invoicing/sellerRevision.d.ts +19 -0
  114. package/dist/invoicing/sellerRevision.js +14 -0
  115. package/dist/invoicing/settlementSync.d.ts +18 -0
  116. package/dist/invoicing/settlementSync.js +32 -0
  117. package/dist/invoicing/snapshot.d.ts +52 -0
  118. package/dist/invoicing/snapshot.js +141 -0
  119. package/dist/subscriptions/automaticRenewal.d.ts +44 -0
  120. package/dist/subscriptions/automaticRenewal.js +81 -0
  121. package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
  122. package/dist/subscriptions/automaticRenewalExecution.js +44 -0
  123. package/dist/subscriptions/contracts/commands.d.ts +115 -0
  124. package/dist/subscriptions/contracts/commands.js +1 -0
  125. package/dist/subscriptions/contracts/dunning.d.ts +133 -0
  126. package/dist/subscriptions/contracts/dunning.js +1 -0
  127. package/dist/subscriptions/contracts/index.d.ts +6 -0
  128. package/dist/subscriptions/contracts/index.js +1 -0
  129. package/dist/subscriptions/contracts/messages.d.ts +43 -0
  130. package/dist/subscriptions/contracts/messages.js +1 -0
  131. package/dist/subscriptions/contracts/renewal.d.ts +533 -0
  132. package/dist/subscriptions/contracts/renewal.js +1 -0
  133. package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
  134. package/dist/subscriptions/contracts/stockWait.js +1 -0
  135. package/dist/subscriptions/index.d.ts +39 -0
  136. package/dist/subscriptions/index.js +17 -0
  137. package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
  138. package/dist/subscriptions/paymentOutcomes.js +27 -0
  139. package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
  140. package/dist/subscriptions/postgresRenewalStore.js +61 -0
  141. package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
  142. package/dist/subscriptions/postgresRetryStore.js +142 -0
  143. package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
  144. package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
  145. package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
  146. package/dist/subscriptions/renewalCycleMechanism.js +157 -0
  147. package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
  148. package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
  149. package/dist/subscriptions/renewalIdentity.d.ts +3 -0
  150. package/dist/subscriptions/renewalIdentity.js +4 -0
  151. package/package.json +23 -3
  152. package/release-gates.json +74 -7
  153. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  154. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  155. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  156. package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
  157. package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
  158. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  159. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  160. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  161. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  162. package/src/fulfillment/adapterConformance.ts +1 -1
  163. package/src/fulfillment/bindings.ts +1 -1
  164. package/src/fulfillment/contracts/conformance.ts +3 -1
  165. package/src/fulfillment/contracts/dispatch.ts +24 -5
  166. package/src/fulfillment/contracts/index.ts +3 -3
  167. package/src/fulfillment/contracts/observation.ts +11 -3
  168. package/src/fulfillment/contracts/readback.ts +13 -0
  169. package/src/fulfillment/contracts/store.ts +1 -0
  170. package/src/fulfillment/contracts/sync.ts +13 -2
  171. package/src/fulfillment/dispatchProjection.ts +94 -7
  172. package/src/fulfillment/index.ts +3 -2
  173. package/src/fulfillment/migrations.ts +1 -0
  174. package/src/fulfillment/observationProjection.ts +64 -13
  175. package/src/fulfillment/omnipack/client.ts +32 -13
  176. package/src/fulfillment/omnipack/index.ts +99 -39
  177. package/src/fulfillment/omnipack/mappers.ts +23 -2
  178. package/src/fulfillment/omnipack/observation.ts +45 -27
  179. package/src/fulfillment/omnipack/reconciliation.ts +56 -0
  180. package/src/fulfillment/postgresDispatchStore.ts +1 -1
  181. package/src/fulfillment/postgresShipmentSpine.ts +21 -4
  182. package/src/fulfillment/postgresStockSyncPort.ts +6 -3
  183. package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
  184. package/src/fulfillment/providerDispatchWorker.ts +61 -20
  185. package/src/fulfillment/providerReconciliationWorker.ts +81 -25
  186. package/src/fulfillment/providerStockSyncWorker.ts +222 -126
  187. package/src/fulfillment/providerTrackingReferences.ts +3 -2
  188. package/src/fulfillment/verifiedIngress.ts +44 -11
  189. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  190. package/src/invoicing/accountingLineAllocation.ts +69 -0
  191. package/src/invoicing/adapterConformance.ts +249 -0
  192. package/src/invoicing/artifacts.ts +19 -0
  193. package/src/invoicing/bindings.ts +80 -0
  194. package/src/invoicing/capabilities.ts +9 -0
  195. package/src/invoicing/channelPolicy.ts +50 -0
  196. package/src/invoicing/continuity.ts +15 -0
  197. package/src/invoicing/contracts/capabilities.ts +15 -0
  198. package/src/invoicing/contracts/companyLookup.ts +13 -0
  199. package/src/invoicing/contracts/delivery.ts +24 -0
  200. package/src/invoicing/contracts/facts.ts +71 -0
  201. package/src/invoicing/contracts/host.ts +84 -0
  202. package/src/invoicing/contracts/index.ts +16 -0
  203. package/src/invoicing/contracts/provider.ts +78 -0
  204. package/src/invoicing/contracts/settlement.ts +39 -0
  205. package/src/invoicing/contracts/store.ts +102 -0
  206. package/src/invoicing/contribution.ts +28 -0
  207. package/src/invoicing/correction.ts +56 -0
  208. package/src/invoicing/delivery.ts +122 -0
  209. package/src/invoicing/documentHistory.ts +301 -0
  210. package/src/invoicing/documentSync.ts +51 -0
  211. package/src/invoicing/dutyCollector.ts +31 -0
  212. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  213. package/src/invoicing/handoff.ts +71 -0
  214. package/src/invoicing/index.ts +39 -0
  215. package/src/invoicing/issue.ts +64 -0
  216. package/src/invoicing/migrations.ts +25 -0
  217. package/src/invoicing/nativeUnsupported.ts +24 -0
  218. package/src/invoicing/paymentReadback.ts +70 -0
  219. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  220. package/src/invoicing/readModel.ts +80 -0
  221. package/src/invoicing/readback.ts +26 -0
  222. package/src/invoicing/reversal.ts +34 -0
  223. package/src/invoicing/sellerRevision.ts +20 -0
  224. package/src/invoicing/settlementSync.ts +37 -0
  225. package/src/invoicing/snapshot.ts +192 -0
  226. package/src/subscriptions/automaticRenewal.ts +87 -0
  227. package/src/subscriptions/automaticRenewalExecution.ts +39 -0
  228. package/src/subscriptions/contracts/commands.ts +46 -0
  229. package/src/subscriptions/contracts/dunning.ts +48 -0
  230. package/src/subscriptions/contracts/index.ts +6 -0
  231. package/src/subscriptions/contracts/messages.ts +18 -0
  232. package/src/subscriptions/contracts/renewal.ts +325 -0
  233. package/src/subscriptions/contracts/stockWait.ts +34 -0
  234. package/src/subscriptions/index.ts +24 -0
  235. package/src/subscriptions/paymentOutcomes.ts +23 -0
  236. package/src/subscriptions/postgresRenewalStore.ts +76 -0
  237. package/src/subscriptions/postgresRetryStore.ts +174 -0
  238. package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
  239. package/src/subscriptions/renewalCycleMechanism.ts +225 -0
  240. package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
  241. 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('SELECT public.fulfillment_dispatch_read(ref) AS response FROM public.fulfillment_dispatch_refs ref WHERE request_key=$1', [key]);
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, fingerprint(v), v.skuRef, v.thresholdKind, v.severity, v.providerForSaleQuantity, v.localAvailableQuantity]),
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
  }
@@ -20,6 +20,7 @@ export declare function recoverDispatchAcceptanceWrite(input: {
20
20
  acceptance: Extract<FulfillmentDispatchResult, {
21
21
  kind: "accepted";
22
22
  }>;
23
+ error: unknown;
23
24
  }): Promise<{
24
25
  committed: boolean;
25
26
  readBacks: number;
@@ -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
- const prepared = await input.provider.prepare(request);
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
- evidence = dispatchEvidence(prepared.evidence);
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 }, evidence));
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 ??= "dispatch_local_operation_failed";
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, FulfillmentReadbackResult } from "./contracts/index.js";
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
- correlation: FulfillmentObservation["correlation"];
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 (FulfillmentReadbackResult | {
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
- readLocalFulfillmentState(ref: string): Promise<{
52
- status: string | null;
53
- latestStatusOccurredAt: string | null;
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 {};