@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
@@ -24,11 +24,11 @@ export function createOmnipackClient(config, fetchImpl) {
24
24
  const body = await requestJson(fetchImpl, config, "createProductPack", path, post(payload));
25
25
  return mapOmnipackProductPackCreatedResponse(body, sku, ean);
26
26
  },
27
- async getStock() {
28
- return mapOmnipackStockResponse(await requestJson(fetchImpl, config, "getStock", "/stock", { method: "GET" }));
27
+ async getStock(options) {
28
+ return mapOmnipackStockResponse(await requestJson(fetchImpl, config, "getStock", "/stock", { method: "GET" }, options?.refuseRedirectRetry));
29
29
  },
30
30
  async getStockMovements(params) {
31
- return mapOmnipackStockMovementsResponse(await requestJson(fetchImpl, config, "getStockMovements", withQuery("/stock-movements", params), { method: "GET" }));
31
+ return mapOmnipackStockMovementsResponse(await requestJson(fetchImpl, config, "getStockMovements", withQuery("/stock-movements", params), { method: "GET" }, true));
32
32
  },
33
33
  async getFulfilmentsPage(params) {
34
34
  const body = await requestJson(fetchImpl, config, "getFulfilments", withQuery("/fulfilments", params), { method: "GET" });
@@ -49,9 +49,12 @@ export function createOmnipackClient(config, fetchImpl) {
49
49
  },
50
50
  };
51
51
  }
52
- async function requestJson(fetchImpl, config, operation, path, init) {
52
+ async function requestJson(fetchImpl, config, operation, path, init, refuseRedirectRetry = false) {
53
53
  const method = (init.method ?? "GET").toUpperCase();
54
54
  const attempts = method === "GET" ? config.safeReadRetries + 1 : 1;
55
+ // Dispatch/readback diagnostics must not change product-sync or stock retry policy.
56
+ const diagnoseRedirectFailure = refuseRedirectRetry || operation === "createOrder"
57
+ || operation === "getFulfilments" || operation === "getFulfilmentRequests";
55
58
  let lastError = null;
56
59
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
57
60
  let submitted = false;
@@ -69,7 +72,7 @@ async function requestJson(fetchImpl, config, operation, path, init) {
69
72
  lastError = new OmnipackProviderError({
70
73
  operation,
71
74
  status: response.status,
72
- code: readProviderErrorCode(body) ?? `http_${response.status}`,
75
+ code: refuseRedirectRetry && response.status >= 300 && response.status < 400 ? "redirect_refused" : readProviderErrorCode(body) ?? `http_${response.status}`,
73
76
  retryable,
74
77
  mayHaveSucceeded: mayPostHaveSucceeded(method, response.status),
75
78
  });
@@ -83,11 +86,11 @@ async function requestJson(fetchImpl, config, operation, path, init) {
83
86
  : new OmnipackProviderError({
84
87
  operation,
85
88
  status: null,
86
- code: "network_error",
89
+ code: diagnoseRedirectFailure && isRedirectRefusal(error) ? "redirect_refused" : "network_error",
87
90
  retryable,
88
91
  mayHaveSucceeded: submitted && mayPostHaveSucceeded(method, null),
89
92
  });
90
- if (!retryable)
93
+ if (!retryable || (refuseRedirectRetry && lastError.code === "redirect_refused"))
91
94
  throw lastError;
92
95
  }
93
96
  }
@@ -160,3 +163,10 @@ function readProviderErrorCode(body) {
160
163
  function isRetryableStatus(status) {
161
164
  return status === 408 || status === 429 || status >= 500;
162
165
  }
166
+ // Fetch implementations expose redirect refusal as an exception, sometimes through cause.
167
+ // Inspect it only to select a closed diagnostic; no exception text leaves this adapter.
168
+ function isRedirectRefusal(error) {
169
+ if (!(error instanceof Error))
170
+ return false;
171
+ return /redirect/i.test(error.message) || (error.cause instanceof Error && /redirect/i.test(error.cause.message));
172
+ }
@@ -1,5 +1,6 @@
1
- import type { FulfillmentCapabilityDescriptor, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentLabelPort, FulfillmentObservationPort, FulfillmentReadbackPort, FulfillmentStockSyncProvider, FulfillmentProductSyncProvider } from "../contracts/index.js";
1
+ import type { FulfillmentCapabilityDescriptor, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentLabelPort, FulfillmentObservationPort, FulfillmentReadbackPort, FulfillmentObservationPagePort, FulfillmentStockSyncProvider, FulfillmentProductSyncProvider } from "../contracts/index.js";
2
2
  import { type OmnipackWebhookRouteEvent } from "./omnipackWebhookPayload.js";
3
+ import { type OmnipackObservationCodecs } from "./observation.js";
3
4
  export { omnipackFulfillmentCapabilities } from "./capabilities.js";
4
5
  /** Node-only, explicit construction. Secrets are supplied per call, never retained as values. */
5
6
  export interface OmnipackFulfillmentAdapterConfig {
@@ -35,6 +36,7 @@ export interface OmnipackFulfillmentAdapterConfig {
35
36
  mintEvidenceReference: () => `evidence:${string}`;
36
37
  mintSyncRunId: () => string;
37
38
  approveTrackingUrl: (url: string) => boolean;
39
+ observationCodecs?: OmnipackObservationCodecs;
38
40
  /** Writes occur only through deliberate calls; scheduled composition must remain pull-only. */
39
41
  product?: {
40
42
  resolvePushMetadata: (sku: string) => Promise<Record<string, unknown> | null>;
@@ -47,6 +49,7 @@ export declare function createOmnipackFulfillmentAdapter(config: OmnipackFulfill
47
49
  label: FulfillmentLabelPort;
48
50
  observation: FulfillmentObservationPort;
49
51
  readback: FulfillmentReadbackPort;
52
+ reconciliation: FulfillmentObservationPagePort;
50
53
  stockSync: FulfillmentStockSyncProvider;
51
54
  productSync: FulfillmentProductSyncProvider;
52
55
  };
@@ -1,11 +1,47 @@
1
+ import { z } from "zod";
2
+ import { publicTrackingUrl } from "../observationProjection.js";
1
3
  import { createOmnipackClient, OmnipackProviderError } from "./client.js";
2
4
  import { omnipackFulfillmentCapabilities } from "./capabilities.js";
3
5
  import { buildOmnipackDispatchCommand, serializeOmnipackDispatchPayload, stableDispatchHash } from "./omnipackDispatchPayload.js";
4
6
  import { mapOmnipackWebhookPayload } from "./mappers.js";
5
7
  import { verifyOmnipackWebhookToken } from "./omnipackWebhookPayload.js";
6
- import { localStatusFor, normalizeStatus } from "./omnipackStatusVocabulary.js";
7
- import { digest, eventKind, observation, reference, refusal, timestamp } from "./observation.js";
8
+ import { createReconciliationPagePort, reconciliationObservation } from "./reconciliation.js";
9
+ import { dispatchEvidence, operationError } from "../dispatchProjection.js";
10
+ import { correlationReference, digest, eventKind, observation, reference, refusal, timestamp } from "./observation.js";
8
11
  export { omnipackFulfillmentCapabilities } from "./capabilities.js";
12
+ // Internal error projection: retain diagnostics without exposing a provider payload or cause.
13
+ class OmnipackSyncError extends Error {
14
+ reason;
15
+ phase = "sync";
16
+ retryable;
17
+ providerFailure;
18
+ status;
19
+ code;
20
+ constructor(reason, operation, error) {
21
+ super(reason === "io_failure" ? `OmniPack request failed: ${operation}` : `OmniPack sync refused: ${reason}`);
22
+ this.reason = reason;
23
+ this.name = "OmnipackSyncError";
24
+ this.providerFailure = reason === "io_failure";
25
+ this.retryable = error instanceof OmnipackProviderError ? error.retryable : false;
26
+ this.status = error instanceof OmnipackProviderError ? error.status : null;
27
+ this.code = error instanceof OmnipackProviderError ? error.code : null;
28
+ }
29
+ }
30
+ function syncFailure(operation, error) {
31
+ return error instanceof OmnipackSyncError ? error : new OmnipackSyncError("io_failure", operation, error);
32
+ }
33
+ function requireProductSku(sku) {
34
+ // URL resolution collapses dot segments; encodeURIComponent rejects lone surrogates.
35
+ // Keep these SKUs in the listing so the worker counts a visible per-SKU failure.
36
+ try {
37
+ if (!sku.trim() || sku === "." || sku === "..")
38
+ throw Error();
39
+ encodeURIComponent(sku);
40
+ }
41
+ catch {
42
+ throw new OmnipackSyncError("malformed", "getProduct");
43
+ }
44
+ }
9
45
  /** No route, schedule, environment reader or default binding is installed. */
10
46
  export function createOmnipackFulfillmentAdapter(config) {
11
47
  const maxRetainedSubmissions = config.maxRetainedSubmissions === undefined ? 10_000 : config.maxRetainedSubmissions;
@@ -48,13 +84,13 @@ export function createOmnipackFulfillmentAdapter(config) {
48
84
  };
49
85
  const requireSync = () => {
50
86
  if (!capabilities.selected || !capabilities.readiness.ready)
51
- throw refusal(capabilities.selected ? "not_ready" : "disabled", "sync");
87
+ throw new OmnipackSyncError(capabilities.selected ? "not_ready" : "disabled", "selection");
52
88
  };
53
89
  const syncIdentity = () => {
54
90
  const syncRunId = reference(config.mintSyncRunId());
55
91
  const collectedAt = timestamp(config.clock());
56
92
  if (!syncRunId || !collectedAt)
57
- throw refusal("malformed", "sync");
93
+ throw new OmnipackSyncError("malformed", "sync");
58
94
  return { providerRef: capabilities.providerRef, syncRunId, collectedAt, sourceRevision: null, revisionSource: "collection" };
59
95
  };
60
96
  // Shared computation: host metadata and serialized bytes live only for this call.
@@ -81,9 +117,12 @@ export function createOmnipackFulfillmentAdapter(config) {
81
117
  selectedDelivery: { providerKind: capabilities.providerRef, carrierKind: request.selection.carrierRef,
82
118
  serviceCode: request.selection.serviceRef, deliveryKind: request.selection.deliveryKind, pickupPoint: request.selection.pickupPoint } }, lines,
83
119
  });
84
- const evidence = { commandFingerprint: command.requestFingerprint,
120
+ const evidence = dispatchEvidence({ commandFingerprint: command.requestFingerprint,
85
121
  contactRevision: { schemaVersion: request.contactRevision.schemaVersion, source: request.contactRevision.source, revision: request.contactRevision.revision },
86
- providerRef: capabilities.providerRef, carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef };
122
+ providerRef: capabilities.providerRef, carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef,
123
+ transcript: { itemCount: command.providerPayload.items.length,
124
+ items: command.providerPayload.items.map(item => ({ skuRef: item.sku, quantity: item.quantity, lotRef: item.lotNumber ?? null, expirationDate: item.expirationDate ?? null })),
125
+ deliveryKind: request.selection.deliveryKind, pickupPointRef: request.selection.pickupPoint?.id ?? null } });
87
126
  return { body: command.body, evidence };
88
127
  };
89
128
  return {
@@ -107,7 +146,7 @@ export function createOmnipackFulfillmentAdapter(config) {
107
146
  let requestCommandDigest;
108
147
  try {
109
148
  // Validate the closed evidence and its request-bound values without consulting preparation state.
110
- if (Object.keys(evidence).length !== 5 || Object.keys(evidence.contactRevision).length !== 3
149
+ if (Object.keys(evidence).length !== 6 || Object.keys(evidence.contactRevision).length !== 3
111
150
  || typeof evidence.commandFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(evidence.commandFingerprint)
112
151
  || !reference(evidence.providerRef)
113
152
  || !reference(evidence.carrierRef) || !reference(evidence.serviceRef)
@@ -121,6 +160,8 @@ export function createOmnipackFulfillmentAdapter(config) {
121
160
  || evidence.carrierRef !== request.selection.carrierRef || evidence.serviceRef !== request.selection.serviceRef)
122
161
  throw Error();
123
162
  evidenceKey = serializeOmnipackDispatchPayload(evidence).body;
163
+ if (serializeOmnipackDispatchPayload(dispatchEvidence(evidence)).body !== evidenceKey)
164
+ throw Error();
124
165
  requestCommandDigest = commandDigest(request);
125
166
  const submitted = submissions.get(evidence.commandFingerprint);
126
167
  if (submitted) {
@@ -142,7 +183,7 @@ export function createOmnipackFulfillmentAdapter(config) {
142
183
  let command;
143
184
  try {
144
185
  command = await buildCommand(commandRequest);
145
- if (command.evidence.commandFingerprint !== fingerprint)
186
+ if (command.evidence.commandFingerprint !== fingerprint || serializeOmnipackDispatchPayload(command.evidence).body !== evidenceKey)
146
187
  throw Error();
147
188
  }
148
189
  catch {
@@ -161,9 +202,10 @@ export function createOmnipackFulfillmentAdapter(config) {
161
202
  providerAttemptKey: `attempt:${proof}`, labelKey: `label:${proof}` };
162
203
  }
163
204
  catch (error) {
164
- return error instanceof OmnipackProviderError && !error.mayHaveSucceeded
165
- ? { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: refusal("io_failure", "dispatch") }
166
- : { kind: "unknown", effectMayHaveSucceeded: true, refusal: refusal("io_failure", "dispatch") };
205
+ const detail = operationError(error instanceof OmnipackProviderError ? error : { code: "provider_dispatch_failed", status: null, retryable: true, mayHaveSucceeded: true });
206
+ return !detail.mayHaveSucceeded
207
+ ? { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail }
208
+ : { kind: "unknown", effectMayHaveSucceeded: true, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail };
167
209
  }
168
210
  finally {
169
211
  command.body = "";
@@ -198,14 +240,28 @@ export function createOmnipackFulfillmentAdapter(config) {
198
240
  return { kind: "refused", refusal: refusal("malformed", "normalize") };
199
241
  if ("event" in payload && (typeof payload.event !== "string" || !eventKind(payload.event)))
200
242
  return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
201
- const evidence = mapOmnipackWebhookPayload(payload, config.notification.expectedEvent);
243
+ const original = payload;
244
+ // Validate calendar fields here only; keep compact offsets and local
245
+ // timestamps accepted by the base parser, without rewriting key bytes.
246
+ if (original.occurredAt !== undefined && original.occurredAt !== null
247
+ && (typeof original.occurredAt !== "string" || !timestamp(original.occurredAt)
248
+ || !z.iso.datetime({ offset: true, local: true }).safeParse(original.occurredAt.replace(/([+-]\d{2})(\d{2})$/u, "$1:$2")).success))
249
+ return { kind: "refused", refusal: refusal("malformed", "normalize"), diagnostics: { invalidTimestamp: true } };
250
+ const rejectedCorrelationRefs = [original.orderId, original.providerOrderId, original.orderNumber, original.fulfilmentNumber]
251
+ .filter(value => value !== undefined && value !== null && (typeof value !== "string" || !correlationReference(value, config.observationCodecs))).length;
252
+ if (rejectedCorrelationRefs)
253
+ return { kind: "refused", refusal: refusal("missing_correlation", "normalize"), diagnostics: { rejectedCorrelationRefs } };
254
+ const evidence = mapOmnipackWebhookPayload(payload, config.notification.expectedEvent, value => correlationReference(value, config.observationCodecs) !== null, {
255
+ service: value => correlationReference(value, config.observationCodecs) !== null,
256
+ url: value => publicTrackingUrl(value, config.approveTrackingUrl, config.observationCodecs?.trackingUrl) !== null,
257
+ });
202
258
  const event = eventKind(evidence.event);
203
259
  if (!event || event !== config.notification.expectedEvent)
204
260
  return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
205
- if (![evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => reference(value)))
261
+ if (![evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => correlationReference(value, config.observationCodecs)))
206
262
  return { kind: "refused", refusal: refusal("missing_correlation", "normalize") };
207
263
  return { kind: "verified", observation: observation({ evidence, event, receivedAt: input.receivedAt,
208
- rawDigest: digest(rawBody), evidenceRef: config.mintEvidenceReference(), source: "provider_webhook", approveTrackingUrl: config.approveTrackingUrl }) };
264
+ rawDigest: digest(rawBody), evidenceRef: config.mintEvidenceReference(), source: "provider_webhook", approveTrackingUrl: config.approveTrackingUrl, observationCodecs: config.observationCodecs }) };
209
265
  }
210
266
  catch {
211
267
  return { kind: "refused", refusal: refusal("malformed", "normalize") };
@@ -228,55 +284,47 @@ export function createOmnipackFulfillmentAdapter(config) {
228
284
  ? row.fulfilmentNumber === input.externalRef : row.providerOrderId === input.externalRef);
229
285
  if (matches.length !== 1)
230
286
  return { kind: "unknown", refusal: refusal("local_missing", "readback") };
231
- const row = matches[0];
232
- const normalized = normalizeStatus(row.status);
233
- if (!normalized)
234
- return { kind: "unknown", refusal: refusal("unsupported_event", "readback") };
235
- const status = localStatusFor(normalized);
236
- const event = { provider_received: "shipment.accepted", picking: "order.processing_started", packed: "order.picked", in_transit: "order.shipped", delivered: "order.delivered", exception: "parcel.exception" }[status];
237
- const evidence = { provider: "omnipack", event, providerOrderId: row.providerOrderId, orderNumber: row.orderNumber,
238
- fulfilmentNumber: row.fulfilmentNumber, occurredAt: row.updatedAt, trackingNumbers: row.trackingNumbers,
239
- trackingReferences: row.trackingReferences, shippingMethods: [] };
240
- return { kind: "found", externalRef: input.externalRef, createdAt: timestamp(row.createdAt), updatedAt: timestamp(row.updatedAt),
241
- observation: observation({ evidence, event, status, fulfillmentRef: input.fulfillmentRef, receivedAt: config.clock(),
242
- rawDigest: digest(JSON.stringify(evidence)), evidenceRef: config.mintEvidenceReference(), source: "provider_reconciliation", approveTrackingUrl: config.approveTrackingUrl }) };
287
+ const mapped = reconciliationObservation(matches[0], config, input.fulfillmentRef);
288
+ return mapped.kind === "unmapped" ? { kind: "unknown", refusal: refusal("unsupported_event", "readback") } : { ...mapped, externalRef: input.externalRef };
243
289
  }
244
290
  catch {
245
291
  return { kind: "unknown", refusal: refusal("io_failure", "readback") };
246
292
  }
247
293
  },
248
294
  },
295
+ reconciliation: createReconciliationPagePort(client, config, () => !capabilities.selected || !capabilities.readiness.ready
296
+ ? refusal(capabilities.selected ? "not_ready" : "disabled", "readback") : null),
249
297
  stockSync: {
250
298
  async getStock() {
251
299
  requireSync();
252
300
  try {
253
301
  const identity = syncIdentity();
254
- return (await client.getStock()).map(row => {
302
+ return (await client.getStock({ refuseRedirectRetry: true })).map((row, itemIndex) => {
255
303
  const skuRef = reference(row.sku);
256
304
  if (!skuRef)
257
- throw refusal("malformed", "sync");
305
+ return { kind: "rejected", itemIndex, reason: "invalid_sku_ref" };
258
306
  return { ...identity, skuRef, quantityKind: "snapshot", totalQuantity: row.totalQuantity,
259
307
  forSaleQuantity: row.forSaleQuantity, reservedOrUnavailableQuantity: row.reservedOrUnavailableQuantity };
260
308
  });
261
309
  }
262
- catch {
263
- throw refusal("io_failure", "sync");
310
+ catch (error) {
311
+ throw stockFailure(error, "get_stock");
264
312
  }
265
313
  },
266
314
  async getStockMovements(input) {
267
315
  requireSync();
268
316
  try {
269
317
  const identity = syncIdentity();
270
- return (await client.getStockMovements(input)).map(row => {
318
+ return (await client.getStockMovements(input)).map((row, itemIndex) => {
271
319
  const skuRef = reference(row.sku);
272
320
  if (!skuRef)
273
- throw refusal("malformed", "sync");
321
+ return { kind: "rejected", itemIndex, reason: "invalid_sku_ref" };
274
322
  return { ...identity, skuRef, quantityKind: "movement", quantity: row.quantity, occurredAt: timestamp(row.occurredAt),
275
323
  operationType: reference(row.operationType), documentRef: reference(row.warehouseDocumentNumber), lotRef: reference(row.lotNumber), expirationDate: /^\d{4}-\d\d-\d\d$/u.test(row.expirationDate ?? "") ? row.expirationDate : null };
276
324
  });
277
325
  }
278
- catch {
279
- throw refusal("io_failure", "sync");
326
+ catch (error) {
327
+ throw stockFailure(error, "get_stock_movements");
280
328
  }
281
329
  },
282
330
  },
@@ -290,37 +338,38 @@ export function createOmnipackFulfillmentAdapter(config) {
290
338
  await client.createProduct(metadata, sku);
291
339
  return { replayed: false };
292
340
  }
293
- catch {
294
- throw refusal("io_failure", "sync");
341
+ catch (error) {
342
+ throw syncFailure("createProduct", error);
295
343
  }
296
344
  },
297
345
  async getProductPacks(sku) {
298
346
  requireSync();
347
+ requireProductSku(sku);
299
348
  try {
300
- return (await client.getProduct(sku)).packs.filter(pack => /^\d{8,14}$/u.test(pack.ean) && Number.isSafeInteger(pack.quantity) && pack.quantity > 0).map(pack => ({ ean: pack.ean, quantity: pack.quantity }));
349
+ return (await client.getProduct(sku)).packs.map(pack => ({ ean: pack.ean, quantity: pack.quantity }));
301
350
  }
302
- catch {
303
- throw refusal("io_failure", "sync");
351
+ catch (error) {
352
+ throw syncFailure("getProduct", error);
304
353
  }
305
354
  },
306
355
  async addProductPack(sku, pack) {
307
356
  requireSync();
308
357
  if (!/^\d{8,14}$/u.test(pack.ean) || !Number.isSafeInteger(pack.quantity) || pack.quantity <= 0)
309
- throw refusal("malformed", "sync");
358
+ throw new OmnipackSyncError("malformed", "sync");
310
359
  try {
311
360
  await client.createProductPack(sku, { ean: pack.ean, quantity: pack.quantity }, pack.ean);
312
361
  }
313
- catch {
314
- throw refusal("io_failure", "sync");
362
+ catch (error) {
363
+ throw syncFailure("createProductPack", error);
315
364
  }
316
365
  },
317
366
  async listStockedSkus() {
318
367
  requireSync();
319
368
  try {
320
- return [...new Set((await client.getStock()).flatMap(row => reference(row.sku) ? [row.sku] : []))];
369
+ return [...new Set((await client.getStock()).flatMap(row => row.sku.trim() ? [row.sku] : []))];
321
370
  }
322
- catch {
323
- throw refusal("io_failure", "sync");
371
+ catch (error) {
372
+ throw syncFailure("getStock", error);
324
373
  }
325
374
  },
326
375
  },
@@ -347,3 +396,13 @@ function commandDigest(request) {
347
396
  pickupPointId: request.selection.pickupPoint?.id ?? null },
348
397
  });
349
398
  }
399
+ /** Translate only the contributed client's typed error; never copy a message or payload. */
400
+ function stockFailure(error, operation) {
401
+ const failure = syncFailure(operation === "get_stock" ? "getStock" : "getStockMovements", error);
402
+ if (!(error instanceof OmnipackProviderError))
403
+ return failure;
404
+ const retryable = error.code !== "redirect_refused" && (error.retryable || error.code === "network_error");
405
+ return Object.assign(failure, { retryable, providerError: {
406
+ operation, status: error.status, code: error.code, retryable,
407
+ } });
408
+ }
@@ -53,6 +53,9 @@ export interface OmnipackFulfilmentRequestEvidence {
53
53
  state: string | null;
54
54
  }
55
55
  export interface OmnipackWebhookEvidence {
56
+ rejectedTrackingRefs?: number;
57
+ rejectedServices?: number;
58
+ rejectedTrackingUrls?: number;
56
59
  provider: "omnipack";
57
60
  event: string;
58
61
  providerOrderId: string | null;
@@ -69,5 +72,8 @@ export declare function mapOmnipackStockResponse(body: unknown): OmnipackStockEv
69
72
  export declare function mapOmnipackStockMovementsResponse(body: unknown): OmnipackStockMovementEvidence[];
70
73
  export declare function mapOmnipackFulfilmentsResponse(body: unknown): OmnipackFulfilmentEvidence[];
71
74
  export declare function mapOmnipackFulfilmentRequestsResponse(body: unknown): OmnipackFulfilmentRequestEvidence[];
72
- export declare function mapOmnipackWebhookPayload(body: unknown, fallbackEvent?: string): OmnipackWebhookEvidence;
75
+ export declare function mapOmnipackWebhookPayload(body: unknown, fallbackEvent?: string, admitTrackingNumber?: (value: string) => boolean, admitMetadata?: {
76
+ service(value: string): boolean;
77
+ url(value: string): boolean;
78
+ }): OmnipackWebhookEvidence;
73
79
  export declare function sanitizeOmnipackPayload(input: Record<string, unknown>): Record<string, unknown>;
@@ -102,13 +102,35 @@ export function mapOmnipackFulfilmentRequestsResponse(body) {
102
102
  };
103
103
  });
104
104
  }
105
- export function mapOmnipackWebhookPayload(body, fallbackEvent = "") {
105
+ export function mapOmnipackWebhookPayload(body, fallbackEvent = "", admitTrackingNumber, admitMetadata) {
106
106
  const payload = asRecord(body);
107
107
  const shipments = readArray(payload.shipments).map(asRecord);
108
- const trackingReferences = trackingReferencesFrom({ shipments });
108
+ let rejectedTrackingRefs = 0, rejectedServices = 0, rejectedTrackingUrls = 0;
109
+ // Count every supplied spelling before extraction, tracking refusal or first-wins deduplication.
110
+ if (admitMetadata)
111
+ for (const row of shipments) {
112
+ for (const value of [row.shippingMethod, row.service]) {
113
+ if (value != null && (typeof value !== "string" || !admitMetadata.service(value)))
114
+ rejectedServices++;
115
+ }
116
+ for (const value of [row.trackingUrl, row.trackingURL, row.trackingLink, row.publicTrackingUrl]) {
117
+ if (value != null && (typeof value !== "string" || !admitMetadata.url(value)))
118
+ rejectedTrackingUrls++;
119
+ }
120
+ }
121
+ // Admit and count original entries before the contributed extractor trims/deduplicates.
122
+ const admitted = admitTrackingNumber ? shipments.filter(row => {
123
+ if (admitTrackingNumber(readString(row.trackingNo) || readString(row.trackingNumber)))
124
+ return true;
125
+ rejectedTrackingRefs++;
126
+ return false;
127
+ }) : shipments;
128
+ const trackingReferences = trackingReferencesFrom({ shipments: admitted });
109
129
  return {
110
130
  provider: "omnipack",
111
131
  event: readString(payload.event) || fallbackEvent,
132
+ ...(rejectedTrackingRefs ? { rejectedTrackingRefs } : {}),
133
+ ...(admitMetadata ? { rejectedServices, rejectedTrackingUrls } : {}),
112
134
  providerOrderId: readNullableString(payload.orderId) ?? readNullableString(payload.providerOrderId),
113
135
  orderNumber: readNullableString(payload.orderNumber),
114
136
  fulfilmentNumber: readNullableString(payload.fulfilmentNumber),
@@ -1,13 +1,19 @@
1
- import type { CanonicalLocalFulfillmentStatus, FulfillmentObservation, FulfillmentRefusal } from "../contracts/index.js";
1
+ export { reference } from "../dispatchProjection.js";
2
+ import type { CanonicalLocalFulfillmentStatus, FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentRefusal } from "../contracts/index.js";
2
3
  import type { OmnipackWebhookEvidence } from "./mappers.js";
3
4
  import { type OmnipackWebhookRouteEvent } from "./omnipackWebhookPayload.js";
4
5
  export declare const refusal: (reason: FulfillmentRefusal["reason"], phase: FulfillmentRefusal["phase"], retryable?: boolean) => FulfillmentRefusal;
5
6
  export declare const digest: (bytes: string | Uint8Array) => string;
6
- /** References are opaque identifiers, never arbitrary provider prose or URLs. */
7
- export declare function reference(value: string | null | undefined): string | null;
7
+ /** Share admission with the durable sink, including explicit deny allowlists. */
8
+ export declare function correlationReference(value: string | null | undefined, codecs?: OmnipackObservationCodecs): string | null;
9
+ export interface OmnipackObservationCodecs extends FulfillmentObservationCodecs {
10
+ normalizeCarrier?(input: {
11
+ carrierRef: string | null;
12
+ serviceRef: string | null;
13
+ }): string | null;
14
+ }
8
15
  export declare function timestamp(value: string | null): string | null;
9
16
  export declare function eventKind(value: string): OmnipackWebhookRouteEvent | null;
10
- export declare function publicUrl(value: string | null, approve: (url: string) => boolean): string | null;
11
17
  export declare function observation(input: {
12
18
  evidence: OmnipackWebhookEvidence;
13
19
  event: FulfillmentObservation["eventKind"];
@@ -18,4 +24,5 @@ export declare function observation(input: {
18
24
  source: "provider_webhook" | "provider_reconciliation";
19
25
  fulfillmentRef?: string;
20
26
  approveTrackingUrl: (url: string) => boolean;
27
+ observationCodecs?: OmnipackObservationCodecs;
21
28
  }): FulfillmentObservation;
@@ -1,12 +1,15 @@
1
+ import { admittedReference, reference } from "../dispatchProjection.js";
2
+ import { projectObservation, publicTrackingUrl } from "../observationProjection.js";
3
+ export { reference } from "../dispatchProjection.js";
1
4
  import { createHash } from "node:crypto";
2
5
  import { parseNormalizedFulfillmentFact } from "@openlup/core/fulfillment";
3
- import { EVENT_TO_STATUS } from "./omnipackWebhookPayload.js";
6
+ import { buildProviderEventId, EVENT_TO_STATUS } from "./omnipackWebhookPayload.js";
4
7
  import { localStatusFor } from "./omnipackStatusVocabulary.js";
5
8
  export const refusal = (reason, phase, retryable = false) => ({ reason, phase, retryable });
6
9
  export const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
7
- /** References are opaque identifiers, never arbitrary provider prose or URLs. */
8
- export function reference(value) {
9
- return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) ? value : null;
10
+ /** Share admission with the durable sink, including explicit deny allowlists. */
11
+ export function correlationReference(value, codecs = {}) {
12
+ return admittedReference(value, codecs.approveReference);
10
13
  }
11
14
  export function timestamp(value) {
12
15
  return value && /^\d{4}-\d\d-\d\dT/u.test(value) && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
@@ -14,44 +17,56 @@ export function timestamp(value) {
14
17
  export function eventKind(value) {
15
18
  return Object.hasOwn(EVENT_TO_STATUS, value) ? value : null;
16
19
  }
17
- export function publicUrl(value, approve) {
18
- if (!value)
19
- return null;
20
- try {
21
- const url = new URL(value);
22
- // Query-free URLs only: signing tokens and credentials never enter durable tracking.
23
- return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash && approve(url.href) ? url.href : null;
20
+ function tracking(refs, codecs, approve) {
21
+ let rejectedTrackingRefs = 0, rejectedServices = 0, rejectedTrackingUrls = 0;
22
+ const trackingRefs = [];
23
+ for (const item of refs) {
24
+ if (item.service !== null && !correlationReference(item.service, codecs))
25
+ rejectedServices++;
26
+ if (item.trackingUrl !== null && !publicTrackingUrl(item.trackingUrl, approve, codecs.trackingUrl))
27
+ rejectedTrackingUrls++;
28
+ const trackingNumber = correlationReference(item.trackingNumber, codecs);
29
+ if (!trackingNumber) {
30
+ rejectedTrackingRefs++;
31
+ continue;
32
+ }
33
+ // Service admission precedes any derivation. A refused service cannot leak a prefix.
34
+ const serviceRef = correlationReference(item.service, codecs);
35
+ const carrierRef = item.service !== null && !serviceRef ? null : reference(codecs.normalizeCarrier
36
+ ? codecs.normalizeCarrier({ carrierRef: item.carrier, serviceRef })
37
+ : serviceRef?.split("_")[0]?.toLowerCase());
38
+ trackingRefs.push({ trackingNumber, carrierRef, serviceRef: item.service, publicUrl: item.trackingUrl });
24
39
  }
25
- catch {
26
- return null;
27
- }
28
- }
29
- function tracking(refs, approve) {
30
- return refs.flatMap(item => {
31
- const trackingNumber = reference(item.trackingNumber);
32
- return trackingNumber ? [{ trackingNumber, carrierRef: reference(item.carrier), serviceRef: reference(item.service), publicUrl: publicUrl(item.trackingUrl, approve) }] : [];
33
- });
40
+ return { trackingRefs, rejectedTrackingRefs, rejectedServices, rejectedTrackingUrls };
34
41
  }
35
42
  export function observation(input) {
36
43
  const evidence = input.evidence;
37
- const externalRef = reference(evidence.providerOrderId);
38
- const providerFulfillmentRef = reference(evidence.fulfilmentNumber);
39
- const orderNumber = reference(evidence.orderNumber);
44
+ const codecs = input.observationCodecs ?? {};
45
+ const externalRef = correlationReference(evidence.providerOrderId, codecs);
46
+ const providerFulfillmentRef = correlationReference(evidence.fulfilmentNumber, codecs);
47
+ const orderNumber = correlationReference(evidence.orderNumber, codecs);
40
48
  const fulfillmentRef = reference(input.fulfillmentRef) ?? providerFulfillmentRef ?? externalRef ?? orderNumber;
41
- if (!fulfillmentRef)
49
+ if (!fulfillmentRef || input.source === "provider_webhook" && [evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => value !== null && !correlationReference(value, codecs)))
42
50
  throw refusal("missing_correlation", "normalize");
43
51
  const occurredAt = timestamp(evidence.occurredAt);
44
52
  if (evidence.occurredAt && !occurredAt)
45
53
  throw refusal("malformed", "normalize");
46
- const key = digest(JSON.stringify([input.event, externalRef ?? orderNumber, providerFulfillmentRef, occurredAt]));
54
+ // Compose the key with the provider's
55
+ // original bytes, including minute precision and offsets, rather than the UTC fact.
56
+ const key = buildProviderEventId(evidence);
57
+ const normalizedTracking = tracking(evidence.trackingReferences, codecs, input.approveTrackingUrl);
58
+ const rejectedTrackingRefs = normalizedTracking.rejectedTrackingRefs + (evidence.rejectedTrackingRefs ?? 0)
59
+ + (evidence.rejectedServices ?? normalizedTracking.rejectedServices);
47
60
  const status = input.status ?? localStatusFor(EVENT_TO_STATUS[input.event]);
48
61
  const fact = parseNormalizedFulfillmentFact({
49
- schemaVersion: 1, mapperVersion: "1", factId: key, providerKind: "omnipack", fulfillmentReference: fulfillmentRef,
62
+ schemaVersion: 1, mapperVersion: "1", factId: digest(key), providerKind: "omnipack", fulfillmentReference: reference(fulfillmentRef) ?? digest(fulfillmentRef),
50
63
  canonicalStatus: status, provenance: { source: input.source, authority: "provider_asserted" },
51
64
  occurredAt, receivedAt: input.receivedAt,
52
65
  rawEvent: { reference: input.evidenceRef, digestAlgorithm: "sha256", digest: input.rawDigest },
53
66
  }, { isCanonicalStatus: (value) => ["provider_received", "picking", "packed", "in_transit", "delivered", "exception", "cancelled"].includes(value) });
54
- return { fact, eventKind: input.event, eventKey: key, firstReceivedAt: input.receivedAt,
55
- correlation: { orderRef: null, fulfillmentRef: reference(input.fulfillmentRef), externalRef, orderNumber, providerFulfillmentRef, parcelRef: null },
56
- trackingRefs: tracking(evidence.trackingReferences, input.approveTrackingUrl) };
67
+ return projectObservation({ fact, eventKind: input.event, eventKey: key, firstReceivedAt: input.receivedAt,
68
+ correlation: { orderRef: null, fulfillmentRef: reference(input.fulfillmentRef), externalRef: evidence.providerOrderId, orderNumber: evidence.orderNumber, providerFulfillmentRef: evidence.fulfilmentNumber, parcelRef: null },
69
+ trackingRefs: normalizedTracking.trackingRefs, diagnostics: { admission: {
70
+ rejectedTrackingRefs, rejectedTrackingUrls: evidence.rejectedTrackingUrls ?? normalizedTracking.rejectedTrackingUrls,
71
+ } } }, input.approveTrackingUrl, codecs);
57
72
  }
@@ -0,0 +1,15 @@
1
+ import type { FulfillmentObservationPagePort, FulfillmentReconciliationObservation } from "../contracts/index.js";
2
+ import type { OmnipackClient } from "./client.js";
3
+ import type { OmnipackFulfilmentEvidence } from "./mappers.js";
4
+ import { refusal, type OmnipackObservationCodecs } from "./observation.js";
5
+ type Config = {
6
+ observationCodecs?: OmnipackObservationCodecs;
7
+ clock(): string;
8
+ mintEvidenceReference(): `evidence:${string}`;
9
+ approveTrackingUrl(url: string): boolean;
10
+ };
11
+ export declare function reconciliationObservation(row: OmnipackFulfilmentEvidence, config: Config, fulfillmentRef?: string): Extract<FulfillmentReconciliationObservation, {
12
+ kind: "found" | "unmapped";
13
+ }>;
14
+ export declare function createReconciliationPagePort(client: OmnipackClient, config: Config, ready: () => ReturnType<typeof refusal> | null): FulfillmentObservationPagePort;
15
+ export {};