@openlup/platform 0.22.0 → 0.23.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 (156) hide show
  1. package/CHANGELOG.md +222 -99
  2. package/README.md +59 -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 +41 -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 +95 -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 +17 -7
  22. package/dist/fulfillment/omnipack/index.d.ts +4 -1
  23. package/dist/fulfillment/omnipack/index.js +104 -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 +13 -0
  37. package/dist/fulfillment/providerDispatchWorker.js +47 -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/capabilities.d.ts +11 -0
  46. package/dist/invoicing/capabilities.js +10 -0
  47. package/dist/invoicing/contracts/capabilities.d.ts +40 -0
  48. package/dist/invoicing/contracts/capabilities.js +1 -0
  49. package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
  50. package/dist/invoicing/contracts/companyLookup.js +1 -0
  51. package/dist/invoicing/contracts/delivery.d.ts +74 -0
  52. package/dist/invoicing/contracts/delivery.js +1 -0
  53. package/dist/invoicing/contracts/facts.d.ts +100 -0
  54. package/dist/invoicing/contracts/facts.js +1 -0
  55. package/dist/invoicing/contracts/host.d.ts +246 -0
  56. package/dist/invoicing/contracts/host.js +1 -0
  57. package/dist/invoicing/contracts/index.d.ts +16 -0
  58. package/dist/invoicing/contracts/index.js +1 -0
  59. package/dist/invoicing/contracts/provider.d.ts +126 -0
  60. package/dist/invoicing/contracts/provider.js +1 -0
  61. package/dist/invoicing/contracts/settlement.d.ts +66 -0
  62. package/dist/invoicing/contracts/settlement.js +1 -0
  63. package/dist/invoicing/contracts/store.d.ts +267 -0
  64. package/dist/invoicing/contracts/store.js +1 -0
  65. package/dist/invoicing/index.d.ts +1 -0
  66. package/dist/invoicing/index.js +1 -0
  67. package/dist/subscriptions/automaticRenewal.d.ts +44 -0
  68. package/dist/subscriptions/automaticRenewal.js +81 -0
  69. package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
  70. package/dist/subscriptions/automaticRenewalExecution.js +44 -0
  71. package/dist/subscriptions/contracts/commands.d.ts +115 -0
  72. package/dist/subscriptions/contracts/commands.js +1 -0
  73. package/dist/subscriptions/contracts/dunning.d.ts +133 -0
  74. package/dist/subscriptions/contracts/dunning.js +1 -0
  75. package/dist/subscriptions/contracts/index.d.ts +6 -0
  76. package/dist/subscriptions/contracts/index.js +1 -0
  77. package/dist/subscriptions/contracts/messages.d.ts +43 -0
  78. package/dist/subscriptions/contracts/messages.js +1 -0
  79. package/dist/subscriptions/contracts/renewal.d.ts +533 -0
  80. package/dist/subscriptions/contracts/renewal.js +1 -0
  81. package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
  82. package/dist/subscriptions/contracts/stockWait.js +1 -0
  83. package/dist/subscriptions/index.d.ts +39 -0
  84. package/dist/subscriptions/index.js +17 -0
  85. package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
  86. package/dist/subscriptions/paymentOutcomes.js +27 -0
  87. package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
  88. package/dist/subscriptions/postgresRenewalStore.js +61 -0
  89. package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
  90. package/dist/subscriptions/postgresRetryStore.js +142 -0
  91. package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
  92. package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
  93. package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
  94. package/dist/subscriptions/renewalCycleMechanism.js +157 -0
  95. package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
  96. package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
  97. package/dist/subscriptions/renewalIdentity.d.ts +3 -0
  98. package/dist/subscriptions/renewalIdentity.js +4 -0
  99. package/package.json +22 -2
  100. package/release-gates.json +56 -3
  101. package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
  102. package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
  103. package/src/fulfillment/adapterConformance.ts +1 -1
  104. package/src/fulfillment/bindings.ts +1 -1
  105. package/src/fulfillment/contracts/conformance.ts +3 -1
  106. package/src/fulfillment/contracts/dispatch.ts +20 -5
  107. package/src/fulfillment/contracts/index.ts +3 -3
  108. package/src/fulfillment/contracts/observation.ts +11 -3
  109. package/src/fulfillment/contracts/readback.ts +13 -0
  110. package/src/fulfillment/contracts/store.ts +1 -0
  111. package/src/fulfillment/contracts/sync.ts +13 -2
  112. package/src/fulfillment/dispatchProjection.ts +92 -7
  113. package/src/fulfillment/index.ts +3 -2
  114. package/src/fulfillment/migrations.ts +1 -0
  115. package/src/fulfillment/observationProjection.ts +64 -13
  116. package/src/fulfillment/omnipack/client.ts +19 -7
  117. package/src/fulfillment/omnipack/index.ts +96 -39
  118. package/src/fulfillment/omnipack/mappers.ts +23 -2
  119. package/src/fulfillment/omnipack/observation.ts +45 -27
  120. package/src/fulfillment/omnipack/reconciliation.ts +56 -0
  121. package/src/fulfillment/postgresDispatchStore.ts +1 -1
  122. package/src/fulfillment/postgresShipmentSpine.ts +21 -4
  123. package/src/fulfillment/postgresStockSyncPort.ts +6 -3
  124. package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
  125. package/src/fulfillment/providerDispatchWorker.ts +48 -20
  126. package/src/fulfillment/providerReconciliationWorker.ts +81 -25
  127. package/src/fulfillment/providerStockSyncWorker.ts +222 -126
  128. package/src/fulfillment/providerTrackingReferences.ts +3 -2
  129. package/src/fulfillment/verifiedIngress.ts +44 -11
  130. package/src/invoicing/capabilities.ts +9 -0
  131. package/src/invoicing/contracts/capabilities.ts +15 -0
  132. package/src/invoicing/contracts/companyLookup.ts +13 -0
  133. package/src/invoicing/contracts/delivery.ts +24 -0
  134. package/src/invoicing/contracts/facts.ts +60 -0
  135. package/src/invoicing/contracts/host.ts +85 -0
  136. package/src/invoicing/contracts/index.ts +16 -0
  137. package/src/invoicing/contracts/provider.ts +78 -0
  138. package/src/invoicing/contracts/settlement.ts +39 -0
  139. package/src/invoicing/contracts/store.ts +98 -0
  140. package/src/invoicing/index.ts +1 -0
  141. package/src/subscriptions/automaticRenewal.ts +87 -0
  142. package/src/subscriptions/automaticRenewalExecution.ts +39 -0
  143. package/src/subscriptions/contracts/commands.ts +46 -0
  144. package/src/subscriptions/contracts/dunning.ts +48 -0
  145. package/src/subscriptions/contracts/index.ts +6 -0
  146. package/src/subscriptions/contracts/messages.ts +18 -0
  147. package/src/subscriptions/contracts/renewal.ts +325 -0
  148. package/src/subscriptions/contracts/stockWait.ts +34 -0
  149. package/src/subscriptions/index.ts +24 -0
  150. package/src/subscriptions/paymentOutcomes.ts +23 -0
  151. package/src/subscriptions/postgresRenewalStore.ts +76 -0
  152. package/src/subscriptions/postgresRetryStore.ts +174 -0
  153. package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
  154. package/src/subscriptions/renewalCycleMechanism.ts +225 -0
  155. package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
  156. package/src/subscriptions/renewalIdentity.ts +5 -0
@@ -88,7 +88,7 @@ export async function runFulfillmentAdapterConformance(subjects) {
88
88
  if (!caps.operations.verifyObservation.supported)
89
89
  requireFact(result.kind === "refused" && result.refusal.reason === "unsupported", "unsupported observation");
90
90
  if (result.kind === "verified") {
91
- requireSame(result.observation, projectObservation(result.observation), "observation allowlist");
91
+ requireSame(result.observation, projectObservation(result.observation, subject.approveTrackingUrl, subject.observationCodecs), "observation allowlist");
92
92
  const observation = result.observation;
93
93
  const identity = stable({ provider: observation.fact.providerKind, reference: observation.fact.fulfillmentReference,
94
94
  kind: observation.eventKind, outcome: observation.fact.canonicalStatus, occurredAt: observation.fact.occurredAt });
@@ -13,7 +13,7 @@ export function createFulfillmentServices(options) {
13
13
  return { ready: false, refusal: selected.refusal };
14
14
  if (!selected.descriptor.operations.dispatch.supported)
15
15
  return { ready: false, refusal: { reason: 'unsupported', phase: 'selection', retryable: false } };
16
- if (!store || !provider)
16
+ if (!store || (config.mode !== "shadow" && !provider))
17
17
  return { ready: false, refusal: FULFILLMENT_NATIVE_UNSUPPORTED };
18
18
  return observe();
19
19
  };
@@ -1,7 +1,7 @@
1
1
  import type { FulfillmentCapabilityDescriptor } from "./capabilities.js";
2
2
  import type { FulfillmentDispatchPort, FulfillmentDispatchRequest } from "./dispatch.js";
3
3
  import type { FulfillmentLabelPort } from "./label.js";
4
- import type { FulfillmentObservationPort } from "./observation.js";
4
+ import type { FulfillmentObservationPort, FulfillmentObservationCodecs } from "./observation.js";
5
5
  import type { FulfillmentReadbackPort } from "./readback.js";
6
6
  import type { FulfillmentStockSyncProvider, FulfillmentProductSyncProvider } from "./sync.js";
7
7
  import type { FulfillmentStorePort } from "./store.js";
@@ -25,6 +25,8 @@ type SyncFixture<Port> = {
25
25
  }[keyof Port];
26
26
  /** Offline adapter-owned inputs and exact expected outputs; never real customer data. */
27
27
  export interface FulfillmentAdapterConformanceSubject {
28
+ approveTrackingUrl?(url: string): boolean;
29
+ observationCodecs?: FulfillmentObservationCodecs;
28
30
  subjectId: string;
29
31
  adapter: {
30
32
  capabilities: FulfillmentCapabilityDescriptor;
@@ -22,7 +22,7 @@ export interface FulfillmentRefusal {
22
22
  phase: "selection" | "prepare" | "claim" | "begin" | "dispatch" | "acknowledge" | "verify" | "normalize" | "apply" | "readback" | "sync";
23
23
  retryable: boolean;
24
24
  }
25
- /** Frozen domain input, never copied into command evidence. Credentials stay in adapter closures. */
25
+ /** Frozen domain input. Contact PII stays transient; only allowlisted mapped items enter evidence. Credentials stay in adapter closures. */
26
26
  export interface FulfillmentDispatchRequest {
27
27
  requestKey: string;
28
28
  fulfillmentRef: string;
@@ -52,23 +52,61 @@ export interface FulfillmentDispatchRequest {
52
52
  quantity: number;
53
53
  productRef: string | null;
54
54
  packRef: string | null;
55
+ lotRef?: string | null;
56
+ expirationDate?: string | null;
55
57
  }[];
56
58
  selection: FulfillmentSelection;
57
59
  }
58
60
  /** Allowlisted durable evidence; fingerprint commits to actual adapter command bytes. */
59
61
  export interface DispatchEvidence {
62
+ redactedFieldCount?: number;
60
63
  commandFingerprint: string;
61
64
  contactRevision: FulfillmentDispatchRequest["contactRevision"];
62
65
  providerRef: string;
63
66
  carrierRef: string | null;
64
67
  serviceRef: string | null;
68
+ transcript?: {
69
+ itemCount: number;
70
+ items: readonly {
71
+ skuRef: string;
72
+ quantity: number;
73
+ lotRef: string | null;
74
+ expirationDate: string | null;
75
+ }[];
76
+ deliveryKind: DeliveryKind;
77
+ pickupPointRef: string | null;
78
+ /** Count of populated reference/date fields redacted by the durable projection. */
79
+ redactedFieldCount?: number;
80
+ };
81
+ mapping?: {
82
+ source: "dispatch_worker";
83
+ mode: "shadow" | "stage" | "live";
84
+ shadowMode: boolean;
85
+ };
86
+ }
87
+ export interface FulfillmentOperationError {
88
+ redactedFieldCount?: number;
89
+ code: string;
90
+ status: number | null;
91
+ retryable: boolean;
92
+ mayHaveSucceeded: boolean;
93
+ /** Machine cause only; never an arbitrary exception message. */
94
+ cause?: string;
65
95
  }
66
- export type FulfillmentDispatchResult = {
96
+ export type FulfillmentDispatchResult = ({
97
+ error?: FulfillmentOperationError;
98
+ redactedFieldCount?: number;
99
+ } & ({
67
100
  kind: "accepted";
68
101
  externalRef: string;
69
102
  evidence: DispatchEvidence;
70
103
  providerAttemptKey: string;
71
104
  labelKey: string;
105
+ recovery?: {
106
+ source: "persisted_acceptance";
107
+ proof: "persisted_external_ref";
108
+ noProviderPost: true;
109
+ };
72
110
  } | {
73
111
  kind: "not_dispatched";
74
112
  effectMayHaveSucceeded: false;
@@ -77,7 +115,7 @@ export type FulfillmentDispatchResult = {
77
115
  kind: "unknown";
78
116
  effectMayHaveSucceeded: true;
79
117
  refusal: FulfillmentRefusal;
80
- };
118
+ }));
81
119
  export type FulfillmentDispatchPrepareResult = {
82
120
  kind: "prepared";
83
121
  evidence: DispatchEvidence;
@@ -1,8 +1,8 @@
1
- export type { DispatchEvidence, FulfillmentDispatchPrepareResult, FulfillmentSelection, FulfillmentDispatchRequest, FulfillmentDispatchResult, FulfillmentDispatchPort, FulfillmentRefusal } from "./dispatch.js";
1
+ export type { FulfillmentOperationError, DispatchEvidence, FulfillmentDispatchPrepareResult, FulfillmentSelection, FulfillmentDispatchRequest, FulfillmentDispatchResult, FulfillmentDispatchPort, FulfillmentRefusal } from "./dispatch.js";
2
2
  export type { LabelDocument, FulfillmentLabelPort } from "./label.js";
3
3
  /** G17 local reads/ingress persistence are inline runtime worker inputs; this provider envelope is unchanged. */
4
- export type { FulfillmentObservation, FulfillmentObservationPort, FulfillmentTrackingReference } from "./observation.js";
5
- export type { FulfillmentReadbackResult, FulfillmentReadbackPort } from "./readback.js";
4
+ export type { FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentObservationPort, FulfillmentTrackingReference } from "./observation.js";
5
+ export type { FulfillmentReadbackResult, FulfillmentReadbackPort, FulfillmentReconciliationObservation, FulfillmentObservationPagePort } from "./readback.js";
6
6
  export type { SyncCursor, StockSnapshot, StockMovement, LocalStock, FulfillmentStockSyncProvider, FulfillmentStockSyncPort, FulfillmentProductSyncProvider, FulfillmentProductSyncPort } from "./sync.js";
7
7
  export type { FulfillmentCapabilityDescriptor } from "./capabilities.js";
8
8
  export type { DispatchRecord, FulfillmentStorePort, FulfillmentPaidHandoffPort } from "./store.js";
@@ -1,8 +1,14 @@
1
1
  import type { NormalizedFulfillmentFact } from "@openlup/core/fulfillment";
2
2
  import type { CanonicalLocalFulfillmentStatus } from "./index.js";
3
3
  import type { FulfillmentRefusal } from "./dispatch.js";
4
+ /** Explicit adopter policy, forwarded through each durable observation projection. */
5
+ export interface FulfillmentObservationCodecs {
6
+ approveReference?(value: string): boolean;
7
+ /** Overrides the query/fragment default; HTTPS, no credentials and URL approval still apply. */
8
+ trackingUrl?(url: string): string | null;
9
+ }
4
10
  export interface FulfillmentTrackingReference {
5
- trackingNumber: string;
11
+ trackingNumber: string; /** Normalized carrier kind, never a raw provider code. */
6
12
  carrierRef: string | null;
7
13
  serviceRef: string | null;
8
14
  publicUrl: string | null;
@@ -22,7 +28,17 @@ export interface FulfillmentObservation {
22
28
  parcelRef: string | null;
23
29
  };
24
30
  trackingRefs: readonly FulfillmentTrackingReference[];
31
+ /** Original provider status tokens; canonical status does not identify an occurrence. */
32
+ providerStatus?: {
33
+ primary: string | null;
34
+ subStatus: string | null;
35
+ };
25
36
  diagnostics?: {
37
+ redactedFieldCount?: number;
38
+ admission?: {
39
+ rejectedTrackingRefs: number;
40
+ rejectedTrackingUrls: number;
41
+ };
26
42
  refusal?: FulfillmentRefusal;
27
43
  stockContention?: {
28
44
  sqlState: "55P03";
@@ -48,5 +64,9 @@ export interface FulfillmentObservationPort {
48
64
  } | {
49
65
  kind: "refused";
50
66
  refusal: FulfillmentRefusal;
67
+ diagnostics?: {
68
+ invalidTimestamp?: true;
69
+ rejectedCorrelationRefs?: number;
70
+ };
51
71
  }>;
52
72
  }
@@ -36,3 +36,24 @@ export interface FulfillmentReadbackPort {
36
36
  mode: "command" | "parcel";
37
37
  }): Promise<FulfillmentReadbackResult>;
38
38
  }
39
+ /** Collection observation is separate from authoritative exact lookup. */
40
+ export type FulfillmentReconciliationObservation = FulfillmentReadbackResult | {
41
+ kind: "unmapped";
42
+ externalRef: string | null;
43
+ evidence: Pick<FulfillmentObservation, "correlation" | "eventKey" | "providerStatus" | "diagnostics">;
44
+ };
45
+ export interface FulfillmentObservationPagePort {
46
+ readPage(input: {
47
+ page: number;
48
+ size: number;
49
+ }): Promise<{
50
+ kind: "page";
51
+ rows: readonly FulfillmentReconciliationObservation[];
52
+ } | {
53
+ kind: "page_exhausted";
54
+ } | {
55
+ kind: "failed";
56
+ refusal: FulfillmentRefusal;
57
+ error?: import("./dispatch.js").FulfillmentOperationError;
58
+ }>;
59
+ }
@@ -10,6 +10,9 @@ export interface DispatchRecord {
10
10
  status: "draft" | "submitting" | "created" | "uncertain" | "failed" | "cancel_requested" | "cancelled";
11
11
  evidence: DispatchEvidence;
12
12
  replayed: boolean;
13
+ acceptance?: Extract<FulfillmentDispatchResult, {
14
+ kind: "accepted";
15
+ }> | null;
13
16
  }
14
17
  export interface FulfillmentStorePort {
15
18
  read(requestKey: string): Promise<DispatchRecord | null>;
@@ -37,6 +37,53 @@ export interface SyncCursor {
37
37
  /** G15: successful sellable coverage gaps; legacy absent values read as []. */
38
38
  coverageGapSkus?: readonly string[];
39
39
  refusal: FulfillmentRefusal | null;
40
+ /** Per-entry bounded diagnostics and run evidence in the existing cursor JSON, never raw errors. */
41
+ syncRunId?: string;
42
+ counts?: {
43
+ checked: number;
44
+ updated: number;
45
+ movements: number;
46
+ rejected: number;
47
+ failures: number;
48
+ providerCalls: number;
49
+ readBacks: number;
50
+ };
51
+ items?: readonly {
52
+ kind?: "stock" | "movement";
53
+ itemIndex: number;
54
+ skuRef: string | null;
55
+ status: "recorded" | "failed" | "rejected";
56
+ reason?: string;
57
+ activeReserved?: number;
58
+ sellableNow?: number | null;
59
+ coverageGapQuantity?: number;
60
+ }[];
61
+ alerts?: readonly {
62
+ skuRef: string;
63
+ thresholdKind: Exclude<Threshold, "forecast">;
64
+ severity: "info" | "warning" | "critical";
65
+ providerForSaleQuantity: number;
66
+ localAvailableQuantity: number;
67
+ detectedAt: string;
68
+ activeReserved?: number;
69
+ coverageGapQuantity?: number;
70
+ gapHours?: number;
71
+ thresholdHours?: number;
72
+ }[];
73
+ staleSync?: {
74
+ gapHours: number;
75
+ thresholdHours: number;
76
+ detectedAt: string;
77
+ };
78
+ error?: {
79
+ reason: string;
80
+ providerError?: {
81
+ operation: "get_stock" | "get_stock_movements";
82
+ status: number | null;
83
+ code: string;
84
+ retryable: boolean;
85
+ };
86
+ } | null;
40
87
  }
41
88
  export interface LocalStock {
42
89
  skuRef: string;
@@ -48,12 +95,21 @@ export interface LocalStock {
48
95
  type Threshold = "safety_stock" | "reservation_coverage" | "forecast" | "stale_sync";
49
96
  type Conflict = "unknown_sku" | "ean_conflict" | "kind_quantity_conflict";
50
97
  export interface FulfillmentStockSyncProvider {
51
- getStock(): Promise<StockSnapshot[]>;
98
+ /** Rejected rows carry only their input index and a closed reason, never an unsafe reference. */
99
+ getStock(): Promise<(StockSnapshot | {
100
+ kind: "rejected";
101
+ itemIndex: number;
102
+ reason: "invalid_sku_ref";
103
+ })[]>;
52
104
  getStockMovements(input?: {
53
105
  startLocalDate?: string;
54
106
  endLocalDate?: string;
55
107
  size?: number;
56
- }): Promise<StockMovement[]>;
108
+ }): Promise<(StockMovement | {
109
+ kind: "rejected";
110
+ itemIndex: number;
111
+ reason: "invalid_sku_ref";
112
+ })[]>;
57
113
  }
58
114
  export interface FulfillmentStockSyncPort {
59
115
  readCursor(): Promise<SyncCursor | null>;
@@ -1,6 +1,25 @@
1
- import type { DispatchEvidence, DispatchRecord, FulfillmentDispatchResult } from "./contracts/index.js";
1
+ import type { DispatchEvidence, DispatchRecord, FulfillmentDispatchRequest, FulfillmentDispatchResult, FulfillmentOperationError } from "./contracts/index.js";
2
+ /** Machine identities only: arbitrary exception prose, URLs and credentials are omitted. */
3
+ export declare function safeDiagnosticCode(value: unknown): string | null;
4
+ export declare function localFailureReason(stage: string, error: unknown, privacy?: {
5
+ token(value: string | null | undefined, codec?: (value: string) => string | null): string | null;
6
+ readonly redactedFieldCount: number;
7
+ }): string;
8
+ export declare function operationError(error: Omit<FulfillmentOperationError, "cause"> & {
9
+ cause?: unknown;
10
+ }): FulfillmentOperationError;
11
+ export declare const reference: (value: unknown) => string | null;
12
+ /** Opaque by default; an explicit allowlist can admit wider incumbent references. */
13
+ export declare function admittedReference(value: string | null | undefined, approve?: (value: string) => boolean): string | null;
14
+ /** One counted sanitizer for durable projections; codecs are explicit host policy. */
15
+ export declare function durableStrings(priorCount?: number): {
16
+ token(value: string | null | undefined, codec?: (value: string) => string | null): string | null;
17
+ readonly redactedFieldCount: number;
18
+ };
2
19
  /** Copy only the durable allowlist, including when an adapter object has extra runtime properties. */
3
20
  export declare function dispatchEvidence(evidence: DispatchEvidence): DispatchEvidence;
21
+ /** Shadow has a local command identity; it never invokes a provider or resolves credentials. */
22
+ export declare function shadowDispatchEvidence(request: FulfillmentDispatchRequest): DispatchEvidence;
4
23
  export declare function dispatchResult(result: FulfillmentDispatchResult): FulfillmentDispatchResult;
5
24
  export declare function acceptedProviderProof(record: DispatchRecord | null): record is DispatchRecord & {
6
25
  externalRef: string;
@@ -1,16 +1,105 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Machine identities only: arbitrary exception prose, URLs and credentials are omitted. */
3
+ export function safeDiagnosticCode(value) {
4
+ return reference(value) && typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_]{0,95}$/u.test(value) ? value : null;
5
+ }
6
+ export function localFailureReason(stage, error, privacy = durableStrings()) {
7
+ if (!error || typeof error !== "object")
8
+ return stage;
9
+ const fields = error;
10
+ const diagnostic = (value) => typeof value === "string" ? privacy.token(value, safeDiagnosticCode) : null;
11
+ const ownCode = diagnostic(fields.code), causeCode = diagnostic(fields.causeCode), causeMessage = diagnostic(fields.causeMessage);
12
+ const messagePattern = /^([A-Za-z][A-Za-z0-9_]*)(?::([A-Z0-9]{5}))?$/u;
13
+ const safeMessage = typeof fields.message === "string" ? privacy.token(fields.message, value => messagePattern.test(value) ? value : null) : null;
14
+ const message = safeMessage?.match(messagePattern);
15
+ const cause = causeMessage ?? (message && (message[2] || /_(?:failed|down|missing|invalid|conflict|not_found)$/u.test(message[1])) ? safeDiagnosticCode(message[1]) : null);
16
+ const code = causeCode ?? ownCode ?? message?.[2];
17
+ return [stage, cause, code].filter(Boolean).join(":");
18
+ }
19
+ export function operationError(error) {
20
+ const privacy = durableStrings(error.redactedFieldCount);
21
+ const code = privacy.token(error.code, safeDiagnosticCode) ?? "operation_failed";
22
+ const cause = typeof error.cause === "string" ? privacy.token(error.cause, value => /^ack(?::[A-Za-z0-9_]{1,96}){0,2}$/u.test(value) ? value.slice(0, 180) : null) : null;
23
+ return { code, status: Number.isInteger(error.status) && error.status >= 100 && error.status <= 599 ? error.status : null,
24
+ retryable: error.retryable === true, mayHaveSucceeded: error.mayHaveSucceeded === true,
25
+ ...(cause ? { cause } : {}), ...(privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}) };
26
+ }
27
+ // The fulfillment opaque-ID rule; never accept free-form metadata as a reference.
28
+ export const reference = (value) => typeof value === "string"
29
+ && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) ? value : null;
30
+ /** Opaque by default; an explicit allowlist can admit wider incumbent references. */
31
+ export function admittedReference(value, approve) {
32
+ if (typeof value !== "string" || /[\p{Cc}\p{Cf}]/u.test(value))
33
+ return null;
34
+ const admitted = approve ? approve(value) : reference(value) !== null;
35
+ const text = value.trim();
36
+ return admitted && text && text.length <= 256 && !/[@]|[a-z][a-z0-9+.-]*:\/\//iu.test(text) ? text : null;
37
+ }
38
+ const isoDate = (value) => typeof value === "string"
39
+ && /^\d{4}-\d\d-\d\d$/u.test(value) && Number.isFinite(Date.parse(value))
40
+ && new Date(value).toISOString().slice(0, 10) === value ? value : null;
41
+ /** One counted sanitizer for durable projections; codecs are explicit host policy. */
42
+ export function durableStrings(priorCount = 0) {
43
+ let count = Number.isSafeInteger(priorCount) && priorCount > 0 ? priorCount : 0;
44
+ return {
45
+ token(value, codec = reference) {
46
+ if (value == null)
47
+ return null;
48
+ const safe = codec(value);
49
+ if (safe === null)
50
+ count += 1;
51
+ return safe;
52
+ },
53
+ get redactedFieldCount() { return count; },
54
+ };
55
+ }
1
56
  /** Copy only the durable allowlist, including when an adapter object has extra runtime properties. */
2
57
  export function dispatchEvidence(evidence) {
58
+ const privacy = durableStrings(evidence.redactedFieldCount ?? evidence.transcript?.redactedFieldCount);
59
+ const sanitize = privacy.token;
60
+ const commandFingerprint = evidence.commandFingerprint;
61
+ const source = evidence.contactRevision.source;
62
+ const providerRef = sanitize(evidence.providerRef) ?? "redacted", carrierRef = sanitize(evidence.carrierRef), serviceRef = sanitize(evidence.serviceRef);
63
+ const mode = evidence.mapping ? (sanitize(evidence.mapping.mode) ?? "shadow") : null;
64
+ const transcript = evidence.transcript ? {
65
+ itemCount: evidence.transcript.items.length,
66
+ items: evidence.transcript.items.map(item => ({ skuRef: sanitize(item.skuRef) ?? "redacted", quantity: Number.isSafeInteger(item.quantity) && item.quantity > 0 ? item.quantity : 0,
67
+ lotRef: sanitize(item.lotRef), expirationDate: sanitize(item.expirationDate, isoDate) })),
68
+ deliveryKind: evidence.transcript.deliveryKind === "parcel-locker" ? "parcel-locker" : "courier",
69
+ pickupPointRef: sanitize(evidence.transcript.pickupPointRef),
70
+ ...(privacy.redactedFieldCount > 0 ? { redactedFieldCount: privacy.redactedFieldCount } : {}),
71
+ } : null;
3
72
  return {
4
- commandFingerprint: evidence.commandFingerprint,
5
- contactRevision: { schemaVersion: evidence.contactRevision.schemaVersion, source: evidence.contactRevision.source, revision: evidence.contactRevision.revision },
6
- providerRef: evidence.providerRef, carrierRef: evidence.carrierRef, serviceRef: evidence.serviceRef,
73
+ commandFingerprint,
74
+ contactRevision: { schemaVersion: evidence.contactRevision.schemaVersion, source, revision: evidence.contactRevision.revision },
75
+ providerRef, carrierRef, serviceRef,
76
+ ...(transcript ? { transcript } : privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}),
77
+ ...(evidence.mapping ? { mapping: { source: "dispatch_worker", mode: mode, shadowMode: evidence.mapping.mode === "shadow" } } : {}),
7
78
  };
8
79
  }
80
+ /** Shadow has a local command identity; it never invokes a provider or resolves credentials. */
81
+ export function shadowDispatchEvidence(request) {
82
+ if (!request.selection.providerRef || !request.lines.length || request.lines.some(line => !Number.isSafeInteger(line.quantity) || line.quantity <= 0))
83
+ throw new Error("dispatch_shadow_command_invalid");
84
+ return dispatchEvidence({ commandFingerprint: createHash("sha256").update(JSON.stringify(request)).digest("hex"),
85
+ contactRevision: request.contactRevision, providerRef: request.selection.providerRef,
86
+ carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef,
87
+ transcript: { itemCount: request.lines.length, items: request.lines.map(line => ({ skuRef: line.skuRef, quantity: line.quantity,
88
+ lotRef: line.lotRef ?? null, expirationDate: line.expirationDate ?? null })),
89
+ deliveryKind: request.selection.deliveryKind, pickupPointRef: request.selection.pickupPoint?.id ?? null } });
90
+ }
9
91
  export function dispatchResult(result) {
92
+ const privacy = durableStrings(result.redactedFieldCount);
93
+ const externalRef = result.kind === "accepted" ? privacy.token(result.externalRef) ?? "redacted" : null;
94
+ const refusal = result.kind !== "accepted" ? { reason: (privacy.token(result.refusal.reason) ?? "malformed"),
95
+ phase: (privacy.token(result.refusal.phase) ?? "normalize"), retryable: result.refusal.retryable } : null;
96
+ const redactions = privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {};
97
+ const error = result.error ? { error: operationError({ ...result.error, mayHaveSucceeded: result.kind !== "not_dispatched" }) } : {};
10
98
  switch (result.kind) {
11
- case "accepted": return { kind: "accepted", externalRef: result.externalRef, providerAttemptKey: result.providerAttemptKey, labelKey: result.labelKey, evidence: dispatchEvidence(result.evidence) };
12
- case "not_dispatched": return { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: { reason: result.refusal.reason, phase: result.refusal.phase, retryable: result.refusal.retryable } };
13
- case "unknown": return { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: result.refusal.reason, phase: result.refusal.phase, retryable: result.refusal.retryable } };
99
+ case "accepted": return { kind: "accepted", externalRef: externalRef, providerAttemptKey: result.providerAttemptKey, labelKey: result.labelKey, ...redactions, evidence: dispatchEvidence(result.evidence), ...error,
100
+ ...(result.recovery ? { recovery: { source: "persisted_acceptance", proof: "persisted_external_ref", noProviderPost: true } } : {}) };
101
+ case "not_dispatched": return { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: refusal, ...redactions, ...error };
102
+ case "unknown": return { kind: "unknown", effectMayHaveSucceeded: true, refusal: refusal, ...redactions, ...error };
14
103
  }
15
104
  }
16
105
  export function acceptedProviderProof(record) {
@@ -5,9 +5,10 @@ export { createOutboxOrderPaidFulfillmentHandler } from "./paidHandoff.js";
5
5
  export { collectPaidFulfillmentRecoveryCandidates } from "./paidFulfillmentRecovery.js";
6
6
  export { runFulfillmentDispatchWorker } from "./providerDispatchWorker.js";
7
7
  export { createFulfillmentCommandLedger } from "./providerCommandLedger.js";
8
- export { verifyAndNormalizeFulfillmentObservation } from "./verifiedIngress.js";
8
+ export type { FulfillmentIngressCounts, FulfillmentIngressResult } from "./verifiedIngress.js";
9
+ export { verifyAndNormalizeFulfillmentObservation, fulfillmentIngressHttpResult } from "./verifiedIngress.js";
9
10
  export { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
10
- export { reconcileFulfillmentObservations } from "./providerReconciliationWorker.js";
11
+ export { reconcileFulfillmentObservations, runFulfillmentReconciliationPage } from "./providerReconciliationWorker.js";
11
12
  export { runFulfillmentStockSyncWorker } from "./providerStockSyncWorker.js";
12
13
  export { runFulfillmentProductSyncWorker } from "./providerProductSyncWorker.js";
13
14
  export { createFulfillmentLowStockEvidenceHandler } from "./lowStockEvidenceHandler.js";
@@ -5,9 +5,9 @@ export { createOutboxOrderPaidFulfillmentHandler } from "./paidHandoff.js";
5
5
  export { collectPaidFulfillmentRecoveryCandidates } from "./paidFulfillmentRecovery.js";
6
6
  export { runFulfillmentDispatchWorker } from "./providerDispatchWorker.js";
7
7
  export { createFulfillmentCommandLedger } from "./providerCommandLedger.js";
8
- export { verifyAndNormalizeFulfillmentObservation } from "./verifiedIngress.js";
8
+ export { verifyAndNormalizeFulfillmentObservation, fulfillmentIngressHttpResult } from "./verifiedIngress.js";
9
9
  export { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
10
- export { reconcileFulfillmentObservations } from "./providerReconciliationWorker.js";
10
+ export { reconcileFulfillmentObservations, runFulfillmentReconciliationPage } from "./providerReconciliationWorker.js";
11
11
  export { runFulfillmentStockSyncWorker } from "./providerStockSyncWorker.js";
12
12
  export { runFulfillmentProductSyncWorker } from "./providerProductSyncWorker.js";
13
13
  export { createFulfillmentLowStockEvidenceHandler } from "./lowStockEvidenceHandler.js";
@@ -24,4 +24,9 @@ export declare const FULFILLMENT_MIGRATIONS: readonly [{
24
24
  readonly catalogPath: "db/platform/migrations/20261022090000_fulfillment_delivery_alignment.sql";
25
25
  readonly sha256: "d9966ae79fe77a0feae8317dc5b46ef2d8196dd0790b1cd0ee1b06ab58061499";
26
26
  readonly url: URL;
27
+ }, {
28
+ readonly position: 76;
29
+ readonly catalogPath: "db/platform/migrations/20261022165000_fulfillment_stock_evidence_replay_reopen.sql";
30
+ readonly sha256: "6d132dec2820047f4e337bcf752b3d884600ccc12de03887fe6d6aa5575bc1db";
31
+ readonly url: URL;
27
32
  }];
@@ -5,4 +5,5 @@ export const FULFILLMENT_MIGRATIONS = [
5
5
  { position: 51, catalogPath: "db/platform/migrations/20260819150000_fulfillment_shipment_sequence.sql", sha256: "906706cf66a6a555aa1219b12eb674d937eaa099abde6da18766310af43aaa9f", url: new URL("../../sql/20260819150000_fulfillment_shipment_sequence.sql", import.meta.url) },
6
6
  { position: 73, catalogPath: "db/platform/migrations/20261022080000_fulfillment_command_ledger.sql", sha256: "c6806906cdb0b6931b29f7a5c7c800016e98d04e6ea956f3c1e219a6cdb7e9d1", url: new URL("../../sql/20261022080000_fulfillment_command_ledger.sql", import.meta.url) },
7
7
  { position: 74, catalogPath: "db/platform/migrations/20261022090000_fulfillment_delivery_alignment.sql", sha256: "d9966ae79fe77a0feae8317dc5b46ef2d8196dd0790b1cd0ee1b06ab58061499", url: new URL("../../sql/20261022090000_fulfillment_delivery_alignment.sql", import.meta.url) },
8
+ { position: 76, catalogPath: "db/platform/migrations/20261022165000_fulfillment_stock_evidence_replay_reopen.sql", sha256: "6d132dec2820047f4e337bcf752b3d884600ccc12de03887fe6d6aa5575bc1db", url: new URL("../../sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql", import.meta.url) },
8
9
  ];
@@ -1,7 +1,28 @@
1
- import type { CanonicalLocalFulfillmentStatus, FulfillmentObservation } from "./contracts/index.js";
2
- export declare function projectCorrelation(value: FulfillmentObservation["correlation"]): FulfillmentObservation["correlation"];
1
+ import type { CanonicalLocalFulfillmentStatus, FulfillmentObservation, FulfillmentObservationCodecs } from "./contracts/index.js";
2
+ export declare function projectCorrelation(value: FulfillmentObservation["correlation"], privacy?: {
3
+ token(value: string | null | undefined, codec?: (value: string) => string | null): string | null;
4
+ readonly redactedFieldCount: number;
5
+ }, codecs?: FulfillmentObservationCodecs): FulfillmentObservation["correlation"];
6
+ export declare function projectProviderStatus(value: NonNullable<FulfillmentObservation["providerStatus"]>, privacy?: {
7
+ token(value: string | null | undefined, codec?: (value: string) => string | null): string | null;
8
+ readonly redactedFieldCount: number;
9
+ }): NonNullable<FulfillmentObservation["providerStatus"]>;
10
+ export type PollEvidence = Pick<FulfillmentObservation, "correlation" | "eventKey" | "providerStatus" | "diagnostics">;
11
+ /** Unknown-status quarantine/ACK evidence has the same privacy boundary as observations. */
12
+ export declare function projectPollEvidence(value: PollEvidence): PollEvidence;
13
+ /** One URL policy: strict default, optional adopter codec, and approval at every sink. */
14
+ export declare function publicTrackingUrl(value: string, approve?: (url: string) => boolean, codec?: (url: string) => string | null): string | null;
15
+ export declare function projectTrackingReference(value: FulfillmentObservation["trackingRefs"][number], privacy?: {
16
+ token(value: string | null | undefined, codec?: (value: string) => string | null): string | null;
17
+ readonly redactedFieldCount: number;
18
+ }, approveTrackingUrl?: (url: string) => boolean, codecs?: FulfillmentObservationCodecs): {
19
+ trackingNumber: string;
20
+ carrierRef: string | null;
21
+ serviceRef: string | null;
22
+ publicUrl: string | null;
23
+ } | null;
3
24
  /** New boundary projection: arbitrary adapter properties are never durable. */
4
- export declare function projectObservation(value: FulfillmentObservation): FulfillmentObservation;
25
+ export declare function projectObservation(value: FulfillmentObservation, approveTrackingUrl?: (url: string) => boolean, codecs?: FulfillmentObservationCodecs): FulfillmentObservation;
5
26
  /** Donor finished-picking boundary is narrower than the coarse packing stage. */
6
27
  export declare const observationStatusPolicy: {
7
28
  consumesStock: (status: CanonicalLocalFulfillmentStatus) => boolean;
@@ -1,31 +1,84 @@
1
+ import { z } from "zod";
1
2
  import { parseFulfillmentFact } from "./normalizedFulfillmentFacts.js";
3
+ import { admittedReference, durableStrings, reference } from "./dispatchProjection.js";
2
4
  import { FULFILLMENT_STATUS_MAP } from "./statusMap.js";
3
- export function projectCorrelation(value) {
4
- const { orderRef, fulfillmentRef, externalRef, orderNumber, providerFulfillmentRef, parcelRef } = value;
5
- return { orderRef, fulfillmentRef, externalRef, orderNumber, providerFulfillmentRef, parcelRef };
5
+ const timestamp = z.iso.datetime({ offset: true });
6
+ export function projectCorrelation(value, privacy = durableStrings(), codecs = {}) {
7
+ const token = (value) => privacy.token(value, text => admittedReference(text, codecs.approveReference));
8
+ return { orderRef: value.orderRef, fulfillmentRef: value.fulfillmentRef, externalRef: token(value.externalRef),
9
+ orderNumber: token(value.orderNumber), providerFulfillmentRef: token(value.providerFulfillmentRef), parcelRef: value.parcelRef };
10
+ }
11
+ export function projectProviderStatus(value, privacy = durableStrings()) {
12
+ return { primary: privacy.token(value.primary), subStatus: privacy.token(value.subStatus) };
13
+ }
14
+ function pollEvidence(value, privacy, codecs = {}) {
15
+ // Host identities and keys composed from admitted parts are already authoritative.
16
+ return { eventKey: value.eventKey, correlation: projectCorrelation(value.correlation, privacy, codecs),
17
+ ...(value.providerStatus ? { providerStatus: projectProviderStatus(value.providerStatus, privacy) } : {}) };
18
+ }
19
+ /** Unknown-status quarantine/ACK evidence has the same privacy boundary as observations. */
20
+ export function projectPollEvidence(value) {
21
+ const privacy = durableStrings(value.diagnostics?.redactedFieldCount);
22
+ const safe = pollEvidence(value, privacy);
23
+ return { ...safe, ...(privacy.redactedFieldCount ? { diagnostics: { redactedFieldCount: privacy.redactedFieldCount } } : {}) };
24
+ }
25
+ /** One URL policy: strict default, optional adopter codec, and approval at every sink. */
26
+ export function publicTrackingUrl(value, approve, codec) {
27
+ try {
28
+ const candidate = codec ? codec(value) : value;
29
+ if (!candidate)
30
+ return null;
31
+ const url = new URL(candidate);
32
+ return url.protocol === "https:" && !url.username && !url.password && (codec || (!url.search && !url.hash)) && approve?.(url.href) ? url.href : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export function projectTrackingReference(value, privacy = durableStrings(), approveTrackingUrl, codecs = {}) {
39
+ const token = (value) => privacy.token(value, text => admittedReference(text, codecs.approveReference));
40
+ const trackingNumber = token(value.trackingNumber);
41
+ if (trackingNumber === null)
42
+ return null;
43
+ const serviceRef = token(value.serviceRef);
44
+ // Even an opaque-looking derived carrier is unsafe when its service was refused.
45
+ const carrierRef = privacy.token(value.carrierRef, text => value.serviceRef !== null && serviceRef === null ? null : reference(text));
46
+ return { trackingNumber, carrierRef, serviceRef,
47
+ publicUrl: privacy.token(value.publicUrl, url => publicTrackingUrl(url, approveTrackingUrl, codecs.trackingUrl)) };
6
48
  }
7
49
  /** New boundary projection: arbitrary adapter properties are never durable. */
8
- export function projectObservation(value) {
50
+ export function projectObservation(value, approveTrackingUrl, codecs = {}) {
51
+ const privacy = durableStrings(value.diagnostics?.redactedFieldCount), token = privacy.token;
9
52
  const diagnostics = {};
53
+ if (value.diagnostics?.admission) {
54
+ const count = (value) => Number.isSafeInteger(value) && value > 0 ? value : 0;
55
+ diagnostics.admission = { rejectedTrackingRefs: count(value.diagnostics.admission.rejectedTrackingRefs), rejectedTrackingUrls: count(value.diagnostics.admission.rejectedTrackingUrls) };
56
+ }
10
57
  if (value.diagnostics?.refusal) {
11
58
  const { reason, phase, retryable } = value.diagnostics.refusal;
12
- diagnostics.refusal = { reason, phase, retryable };
59
+ diagnostics.refusal = { reason: token(reason), phase: token(phase), retryable };
13
60
  }
14
61
  if (value.diagnostics?.stockContention)
15
62
  diagnostics.stockContention = { sqlState: "55P03", phase: "provider_stock_consumed", retryable: true };
16
63
  if (value.diagnostics?.annotation)
17
- diagnostics.annotation = value.diagnostics.annotation;
64
+ diagnostics.annotation = token(value.diagnostics.annotation);
18
65
  if (value.diagnostics?.recoveredPickedReceipt) {
19
66
  const { receiptRef, closed } = value.diagnostics.recoveredPickedReceipt;
20
67
  diagnostics.recoveredPickedReceipt = { receiptRef, closed };
21
68
  }
22
- return {
23
- fact: parseFulfillmentFact(value.fact), eventKind: value.eventKind, eventKey: value.eventKey,
24
- firstReceivedAt: value.firstReceivedAt,
25
- correlation: projectCorrelation(value.correlation),
26
- trackingRefs: value.trackingRefs.map(({ trackingNumber, carrierRef, serviceRef, publicUrl }) => ({ trackingNumber, carrierRef, serviceRef, publicUrl })),
27
- ...(Object.keys(diagnostics).length ? { diagnostics } : {}),
69
+ const fact = parseFulfillmentFact(value.fact);
70
+ const safe = {
71
+ ...pollEvidence(value, privacy, codecs),
72
+ fact: { ...fact, mapperVersion: token(fact.mapperVersion) ?? "redacted" }, eventKind: (token(value.eventKind) ?? "redacted"),
73
+ firstReceivedAt: timestamp.parse(value.firstReceivedAt),
74
+ trackingRefs: value.trackingRefs.flatMap(ref => {
75
+ const projected = projectTrackingReference(ref, privacy, approveTrackingUrl, codecs);
76
+ return projected ? [projected] : [];
77
+ }),
28
78
  };
79
+ if (privacy.redactedFieldCount)
80
+ diagnostics.redactedFieldCount = privacy.redactedFieldCount;
81
+ return { ...safe, ...(Object.keys(diagnostics).length ? { diagnostics } : {}) };
29
82
  }
30
83
  const HANDOVER_STATUSES = new Set(FULFILLMENT_STATUS_MAP.stages.filter(stage => stage.effects.handover).flatMap(stage => stage.localStatus));
31
84
  /** Donor finished-picking boundary is narrower than the coarse packing stage. */
@@ -19,7 +19,9 @@ export interface OmnipackClient {
19
19
  createProduct(payload: Record<string, unknown>, sku: string): Promise<OmnipackProductCreatedEvidence>;
20
20
  getProduct(sku: string): Promise<OmnipackProductEvidence>;
21
21
  createProductPack(sku: string, payload: Record<string, unknown>, ean: string): Promise<OmnipackProductPackCreatedEvidence>;
22
- getStock(): Promise<OmnipackStockEvidence[]>;
22
+ getStock(options?: {
23
+ refuseRedirectRetry: boolean;
24
+ }): Promise<OmnipackStockEvidence[]>;
23
25
  getStockMovements(params?: OmnipackQueryParams): Promise<OmnipackStockMovementEvidence[]>;
24
26
  getFulfilmentsPage(params?: OmnipackQueryParams): Promise<{
25
27
  rows: OmnipackFulfilmentEvidence[];