@openlup/platform 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +218 -59
  2. package/README.md +93 -11
  3. package/dist/fulfillment/contracts/dispatch.d.ts +4 -0
  4. package/dist/fulfillment/dispatchProjection.d.ts +1 -1
  5. package/dist/fulfillment/dispatchProjection.js +4 -2
  6. package/dist/fulfillment/omnipack/client.js +14 -6
  7. package/dist/fulfillment/omnipack/index.js +5 -2
  8. package/dist/fulfillment/providerDispatchWorker.d.ts +38 -0
  9. package/dist/fulfillment/providerDispatchWorker.js +19 -1
  10. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  11. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  12. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  13. package/dist/invoicing/accountingLineAllocation.js +69 -0
  14. package/dist/invoicing/adapterConformance.d.ts +42 -0
  15. package/dist/invoicing/adapterConformance.js +296 -0
  16. package/dist/invoicing/artifacts.d.ts +7 -0
  17. package/dist/invoicing/artifacts.js +20 -0
  18. package/dist/invoicing/bindings.d.ts +46 -0
  19. package/dist/invoicing/bindings.js +69 -0
  20. package/dist/invoicing/channelPolicy.d.ts +28 -0
  21. package/dist/invoicing/channelPolicy.js +27 -0
  22. package/dist/invoicing/continuity.d.ts +12 -0
  23. package/dist/invoicing/continuity.js +18 -0
  24. package/dist/invoicing/contracts/facts.d.ts +11 -1
  25. package/dist/invoicing/contracts/host.d.ts +3 -4
  26. package/dist/invoicing/contracts/index.d.ts +1 -1
  27. package/dist/invoicing/contracts/store.d.ts +15 -5
  28. package/dist/invoicing/contribution.d.ts +21 -0
  29. package/dist/invoicing/contribution.js +19 -0
  30. package/dist/invoicing/correction.d.ts +42 -0
  31. package/dist/invoicing/correction.js +44 -0
  32. package/dist/invoicing/delivery.d.ts +85 -0
  33. package/dist/invoicing/delivery.js +79 -0
  34. package/dist/invoicing/documentHistory.d.ts +50 -0
  35. package/dist/invoicing/documentHistory.js +213 -0
  36. package/dist/invoicing/documentSync.d.ts +24 -0
  37. package/dist/invoicing/documentSync.js +26 -0
  38. package/dist/invoicing/dutyCollector.d.ts +15 -0
  39. package/dist/invoicing/dutyCollector.js +35 -0
  40. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  41. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  42. package/dist/invoicing/handoff.d.ts +37 -0
  43. package/dist/invoicing/handoff.js +62 -0
  44. package/dist/invoicing/index.d.ts +38 -0
  45. package/dist/invoicing/index.js +36 -0
  46. package/dist/invoicing/issue.d.ts +39 -0
  47. package/dist/invoicing/issue.js +55 -0
  48. package/dist/invoicing/migrations.d.ts +48 -0
  49. package/dist/invoicing/migrations.js +25 -0
  50. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  51. package/dist/invoicing/nativeUnsupported.js +22 -0
  52. package/dist/invoicing/paymentReadback.d.ts +20 -0
  53. package/dist/invoicing/paymentReadback.js +47 -0
  54. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  55. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  56. package/dist/invoicing/readModel.d.ts +74 -0
  57. package/dist/invoicing/readModel.js +90 -0
  58. package/dist/invoicing/readback.d.ts +4 -0
  59. package/dist/invoicing/readback.js +30 -0
  60. package/dist/invoicing/reversal.d.ts +37 -0
  61. package/dist/invoicing/reversal.js +28 -0
  62. package/dist/invoicing/sellerRevision.d.ts +19 -0
  63. package/dist/invoicing/sellerRevision.js +14 -0
  64. package/dist/invoicing/settlementSync.d.ts +18 -0
  65. package/dist/invoicing/settlementSync.js +32 -0
  66. package/dist/invoicing/snapshot.d.ts +52 -0
  67. package/dist/invoicing/snapshot.js +141 -0
  68. package/package.json +3 -3
  69. package/release-gates.json +21 -7
  70. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  71. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  72. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  73. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  74. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  75. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  76. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  77. package/src/fulfillment/contracts/dispatch.ts +4 -0
  78. package/src/fulfillment/dispatchProjection.ts +4 -2
  79. package/src/fulfillment/omnipack/client.ts +13 -6
  80. package/src/fulfillment/omnipack/index.ts +5 -2
  81. package/src/fulfillment/providerDispatchWorker.ts +14 -1
  82. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  83. package/src/invoicing/accountingLineAllocation.ts +69 -0
  84. package/src/invoicing/adapterConformance.ts +249 -0
  85. package/src/invoicing/artifacts.ts +19 -0
  86. package/src/invoicing/bindings.ts +80 -0
  87. package/src/invoicing/channelPolicy.ts +50 -0
  88. package/src/invoicing/continuity.ts +15 -0
  89. package/src/invoicing/contracts/facts.ts +12 -1
  90. package/src/invoicing/contracts/host.ts +3 -4
  91. package/src/invoicing/contracts/index.ts +1 -1
  92. package/src/invoicing/contracts/store.ts +11 -7
  93. package/src/invoicing/contribution.ts +28 -0
  94. package/src/invoicing/correction.ts +56 -0
  95. package/src/invoicing/delivery.ts +122 -0
  96. package/src/invoicing/documentHistory.ts +301 -0
  97. package/src/invoicing/documentSync.ts +51 -0
  98. package/src/invoicing/dutyCollector.ts +31 -0
  99. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  100. package/src/invoicing/handoff.ts +71 -0
  101. package/src/invoicing/index.ts +38 -0
  102. package/src/invoicing/issue.ts +64 -0
  103. package/src/invoicing/migrations.ts +25 -0
  104. package/src/invoicing/nativeUnsupported.ts +24 -0
  105. package/src/invoicing/paymentReadback.ts +70 -0
  106. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  107. package/src/invoicing/readModel.ts +80 -0
  108. package/src/invoicing/readback.ts +26 -0
  109. package/src/invoicing/reversal.ts +34 -0
  110. package/src/invoicing/sellerRevision.ts +20 -0
  111. package/src/invoicing/settlementSync.ts +37 -0
  112. package/src/invoicing/snapshot.ts +192 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,224 @@ in the source repository.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.24.0]
14
+
15
+ - Publishable on the npm `latest` dist-tag as `0.24.0`, from tag
16
+ `openlup-platform-v0.24.0`.
17
+
18
+ - Require Zod `^4.6.6` to fix GHSA-mrhx-c7xc-m3w5; oversized constrained
19
+ request collections fail before element validation accumulates errors.
20
+ - Preserve the observation adapter's accepted minute-precision occurrence timestamps and raw
21
+ idempotency-key bytes with the updated validator; impossible dates still fail.
22
+
23
+ - Add the optional `runFulfillmentDispatchWorker.shadowCommandFingerprint` callback for a pure, local adopter command hash in shadow mode. Without it, shadow evidence still hashes the same `JSON.stringify(request)` bytes with SHA-256. The callback must return a lowercase 64-hex SHA-256 digest of the adopter's canonical command bytes, never command contents, credentials or contact data. The package accepts only strings matching `/^[a-f0-9]{64}$/`; invalid output or a throw returns a typed refusal with closed cause `adopter_fingerprint_invalid` or `adopter_fingerprint_failed`, without raw output, exception text or claim/attempt/evidence writes. Stage/live never invoke the callback. The fingerprint is used across shadow claim/readback/attempt, including mapped-attempt replay; shadow never begins, and live preparation must produce the same identity; shadow still performs no provider preparation, credentials lookup or POST.
24
+ - Preserve optional boolean `submissionAttempted` and `responseBodyRead` facts on typed dispatch errors and finalize evidence. `submissionAttempted: false` proves no provider submission was attempted; absence is unknown. `responseBodyRead: true` means the response body was read, including empty or malformed JSON; `false` means reading failed, and absence means there is no body-read evidence. Credential/request-construction failures include explicit non-submission proof; command validation/rebuild retains its closed `not_dispatchable` refusal. No raw body, exception prose or credentials enter these facts; durable projections admit only actual booleans.
25
+
26
+ Migration: existing callers need no changes. `shadowDispatchEvidence(request)` becomes `shadowDispatchEvidence(request, commandFingerprint?)`; omitting the new optional argument keeps the same evidence bytes and hash. These facts allow adopter classification without inferring uncertainty from retryability. Package classification remains unchanged: definitive rejections (including an unreadable HTTP 400 body) are `not_dispatched`; ambiguous submission and unreadable successful acknowledgements are `unknown`. No release, default binding, route, schedule or activation is added.
27
+
28
+ - Add inert native unsupported invoicing ports and runner-independent offline adapter conformance for exact recovery, parent identity, scoped absence, declared host capabilities and safe evidence.
29
+ - Document the invoicing design and host boundaries without installing workers, stores, fiscal providers, routes or activation.
30
+
31
+ - Extract invoice issue sequencing, snapshot/canonical line verification, unchanged allocation, payment evidence and handoff helpers into the existing invoicing entry; retain host payloads, keys, seller/tax policy and routing.
32
+ - Migration: compatibility callbacks preserve existing RPC/provider request bytes, issue fences and handoff descriptors. No native store, D7/O2 behavior, default provider, executor registration or activation is added.
33
+
34
+ - Add correction and reversal workers to the experimental invoicing entry, preserving durable correction identity, recovery, host event/key semantics and fenced outcomes. The reference host requires an explicit conforming correction bridge; its legacy vendor copy stays unchanged and unselected.
35
+ - Migration: introduce the two pinned managed v2/compatibility forwards with versioned adapter and pgTAP callers. Unkeyed attempted legacy corrections remain visibly unresolved for provider reconciliation. Preview correction references use correctionRef. Native forwards and runtime activation remain separate.
36
+ - See the [M09 forward owner](../../docs/platform/DATA_AND_MIGRATIONS.md#m09-invoicing-forward-design-and-control-approval) for sync-first settlement and permanent-failure parking.
37
+ - Registered limit: multi-reason / late-issue compensation can yield more than one full correction per invoice, as the incumbent does today.
38
+
39
+ - Add an experimental structural-executor native invoice store, a create/readback issue binding, separate native/managed migration descriptors, and six byte-identical sparse SQL assets. Read-only bounded discovery exposes duty/order IDs before facts are installed. Missing host fiscal facts refuse before writes; local-only document construction retains its previous behavior. Native fresh non-ready preflights settle before dispatch; stale preflight/finalizer results write no failure.
40
+ - Migration: append the three reviewed native duty/effect/payment forwards at positions 77–79 after the unchanged 160000/165000 prefix. Install the complete catalogue. Native child-document and managed base-row corrections have distinct descriptors. Seller revision and activation remain separate.
41
+
42
+ ### Independent native invoicing duties
43
+
44
+ Experimental invoicing adds `collectInvoiceDuties` and
45
+ `createIndependentInvoiceDutyBinding` for explicitly invoked, separately committed
46
+ duty collection. Native fulfillment no longer waits for accounting readiness or
47
+ invoice requests. Managed dispatch behavior stays unchanged.
48
+
49
+ Install the full native catalogue, including 190000/191000 before the exact
50
+ reviewed 192000 payment-decoupling forward. Captured settlement and dunning
51
+ recovery survive invoice capture failure; retained paid/handoff facts support
52
+ later repair. New unlinked payment events retain `accountingRequested=false`,
53
+ including after independent issue; historical linked events retain their readback.
54
+ Use owed-duty readback for invoice progress. No payment
55
+ export, provider activation, scheduler registration or fiscal default is added.
56
+
57
+ - Extract invoice delivery, PDF validation, accepted-document synchronization, payout item recording and document-history/read helpers into the existing invoicing entry; bind the reference host facades through explicit callbacks.
58
+ - Migration: F8 retains A2 payout transport and exact settlement keys; the legacy batch/payment importer preserves donor calls and aggregate results, while package receipt reuse across item scopes is an identity conflict with continued processing and no extra write; distinct recorded receipts require an item-capable host recorder/resolver. F11 retains M04 content, recipient, locale, transport and uncertainty. F9 bounds reference consumers; F12 leaves runbooks and customer correction intent host-owned. Native delivery uses an optional host envelope projection over P4’s store; job store mode binds fenced terminal/backoff outcomes (whole seconds clamp 60..86400, absent/invalid 900, retryable:false parks); callback mode requires terminalFailure before claim; native history requires a safe owned projection. PDF downloads now refuse invalid bytes; the reference panel displays closed diagnostic codes without free-text provider errors. No SQL, activation, executor or fiscal-policy change.
59
+
60
+ Explicit invoicing composition and first-claim seller revision
61
+
62
+ PART A: `createInvoiceServices`, `InvoiceServices` and `createInvoiceContribution`
63
+ use the existing `/invoicing` entry. Host selection is default-off; readiness
64
+ precedes factories and claim. Native helpers compose the real P4/P5 store,
65
+ collection, delivery, sync and read-model implementations. Host-specific factories
66
+ retain auth, configuration, transport/query projection and the existing executor,
67
+ registry and lease. Targeted module runs refuse before leasing.
68
+
69
+ PART B — Migration: native 195000 and managed 195100 are forward-only,
70
+ byte-identical introductions of the approved CTL-M09-4 fixtures. Eligible
71
+ obligations freeze the seller at their first-ever durable claim, independently
72
+ for corrections; retries retain it. Previously claimed legacy obligations keep
73
+ execution-time fallback. Issued documents and installed buyer/line facts are
74
+ unchanged. Native reversals use the correction request branch; managed order
75
+ reversals preserve incumbent results through the seller request wrapper.
76
+ Unknown/unsupported seller reads and order-scoped managed corrections refuse
77
+ before claim. SQL replay conflicts surface as typed identity conflicts.
78
+
79
+ ### Selection and installation boundary
80
+
81
+ Invoicing remains EXPERIMENTAL and DEFAULT-OFF. The issue/correction/reversal,
82
+ durable duty collection, native store, delivery/document/payout sync, safe history
83
+ and first-claim seller revision helpers are available for explicit host composition.
84
+ Select a provider and capability, bind host ports and establish readiness before
85
+ factories or claim; the structural contribution registers nothing itself. Fiscal
86
+ policy, ownership/authentication, M04 transport, artifact/history projections,
87
+ execution leases and route/job registration remain host responsibilities. No live
88
+ provider, fiscal acceptance or inbox-delivery evidence is established. Adopters
89
+ must not activate unselected or unready invoicing, or infer activation from SQL.
90
+
91
+ M10 subscription renewal/retry stages remain EXPERIMENTAL, INCOMPLETE and
92
+ DEFAULT-OFF, not for adopter activation: contracts, native stores/executor and
93
+ N1 SQL are present, but runner activation and the G23/N1 host composition,
94
+ correlation, authorization and replay obligations remain pending. Native delivery
95
+ alignment must remain off or shadow until the host composes the renewal runner
96
+ with its G23 admission decorator.
97
+
98
+ Migration: upgrade core, outbox and platform together from `0.23.0` to `0.24.0`
99
+ for exact internal peers. Install the complete canonical native migration chain
100
+ through position 80 (`20261022195000_invoicing_seller_revision.sql`), including
101
+ invoicing positions 77/78/79 (`190000`/`191000`/`192000`); P5 adds no SQL and P7
102
+ adds position 80. Sparse package projections are not an installation chain.
103
+ Managed installations use their own ordered forwards, including correction
104
+ `193000`/`193100` and seller `195100`; managed descriptor paths are catalogue
105
+ references, not files installed by the npm tarball. Preserve first-claim seller
106
+ snapshots and legacy fallback; there is no retroactive backfill or reversal of
107
+ forward SQL on application rollback. Retain the `0.23.0` shadow/observation retry
108
+ and legacy stock-fingerprint migration precautions when upgrading older rows.
109
+
110
+ ### Limits closed by this set
111
+
112
+ - The earlier invoicing contract-only/no-SQL/no-worker surface limit is closed:
113
+ P1b–P7 introduce explicit services, native store and helpers, with CTL-M09-1–4
114
+ reviewed forwards at native positions 77–80 and the managed correction/seller
115
+ forwards; see the [design boundary](../../docs/platform/plans/oss-m09-invoicing.md#p7-composition-and-seller-revision).
116
+ Default activation and live fiscal/provider acceptance remain unclaimed.
117
+ - M08 stock-evidence replay/reopen was already closed in `0.23.0` by position 76;
118
+ this set does not close its legacy fingerprint conflict or other M08 gaps.
119
+
120
+ ### Known limitations
121
+
122
+ - Managed dunning exhaustion still pauses active subscriptions; retaining active status needs a managed forward. Native N1 lands the bounded retry/failure repair, with the host obligations below still pending.
123
+ - D8-1: native advisory v2 promo-code candidates/rejection details unavailable.
124
+ - D8-2: native mounted v2 issue/honor/fallback unavailable.
125
+ - D8-3: native acquisition-only mounted issuance/honor unavailable.
126
+ - D8-4: full native/managed eligibility equivalence unavailable.
127
+ - Bundle-1: independent DB human-only actor-kind enforcement absent natively; first in the native-backend parity round.
128
+ - Bundle-2: managed-equivalent actor-kind/email history absent natively.
129
+ - Bundle-3 closes injected-reader M05 mounted admin/feed readback only. Native new-purchase stock read absent despite P5a installed-store proof; deferred second after bundle-1 (owner 2026-10-21); readiness refuses.
130
+ - Bundle-4: incumbent real-PostgreSQL contention only; no exactly-once guarantee.
131
+ - Managed schema/pgTAP retain `44.404`; removal needs owner decision/forward; reference composition supplies it to app code.
132
+ - A1 offline only; account/hosted-time/owner TEST evidence unclaimed; `communications/klaviyo` verification-pending.
133
+ - M05 omitted opt-ins preserve defaults/outputs/read order/schedules/refusals/acceptance-token bytes/SQL.
134
+ - Native inventory adjustment unavailable; address writes refuse readiness. Flat-rate save needs selected port/Core readiness/applied-seed observation.
135
+ - Native shipping audit unavailable; handler swallows best-effort audit failure.
136
+ - Native flat-rate integer bounds (0 through host maximum; reference 100,000 minor units) use request schema, not DB CHECK.
137
+ - Managed ATP/forecast/dated-supply/pagination readback unproved/pending M06.
138
+ - Managed two-session locks/concurrency/uncovered compensation unproved/pending M06.
139
+ - Managed exact-prefix upgrade/ledger unproved/pending M06.
140
+ - Managed address idempotency/conflict/readback unproved/pending M06.
141
+ - Managed successful flat-rate read/save unproved/pending host authority.
142
+ - M06 stock-wait lifecycle/durable timing/staging controls retain C5/C6; busy/try-gate, continuity/flag/TTL transfers are dispositions, not PASS.
143
+ - Shipping national-registry fields/locale-specific `canonicalAddressKey` fold await M16 host market pack/key migration; reference shipping copy stays locale-specific until M16.
144
+ - Native durable prepared storage, method setup, absence reopen and cumulative refunds unavailable; no production installation parity claimed.
145
+ - Typed refusal-envelope forwarding unavailable (`failureReason` only); reusable registration needs host ownership resolution.
146
+ - Incomplete chargebacks stay refunds/unapplied: reclassification bypasses evidence validation into dispute apply; limited operator recovery.
147
+ - Audit compatibility still detects `chargeId`; refund operation identity/original money supply no typed charge identity.
148
+ - M07 follow-up: repair poll can clear host redirect; post-timeout verify never restarts polling; unlocalized poller; fixture builds/never calls handler.
149
+ - M07 follow-up: reference status route wrong-rail witness.
150
+ - M07 follow-up: explicit reference-route refusal for unsupported method setup.
151
+ - M07 follow-up: deployment-neutral disabled-verify reason.
152
+ - M07 follow-up: reference card normalizer missing-amount path; package settling evidence already requires positive money.
153
+
154
+ - Native delivery alignment stays off or shadow until a host composes the native automatic-renewal runner with the G23 renewal admission decorator. The decorator alone neither composes nor activates the runner.
155
+ - The core fulfillment status map carries localized adopter labels; replace them with host-injected labels or internationalization keys at the host presentation boundary.
156
+ - The warehouse adapter example is opt-in and has offline conformance only; no provider-account proof or activation is established. Exact absence, label create/void and cancellation remain unsupported.
157
+ - After an alignment extension, a deferred renewal operation at the old schedule remains claimed with no reader or reservation effect. Changed terms snapshots conflict under the existing snapshot-conflict rules.
158
+
159
+ Native parity remains host-owned at these seams. Missing native bindings use the shared `FULFILLMENT_NATIVE_UNSUPPORTED` refusal; no partial write or parity claim is implied.
160
+
161
+ | Amendment | Host-owned seam and native boundary |
162
+ | --- | --- |
163
+ | G18 | Replacement creation and atomic eligibility/replay receipts; native replacement callback refuses. |
164
+ | G19 | Operator mutations, actor attribution and replay receipts; native mutation callbacks refuse. |
165
+ | G20 | Customer alignment rows; native reader refuses, while the pure delay projection remains callable. |
166
+ | G21 | Commerce list/detail and shipments overview; native read callbacks refuse. |
167
+ | G22 | Authoritative preparation edits, authorization and replay receipts; missing preparation binding refuses before dispatch, after disabled/selection/readiness checks. |
168
+
169
+ - M08 parity D2 KEEP-GAP: native dispatch finish still loses rejected/unknown error detail.
170
+ - M08 parity KEEP-GAP: immutable stock snapshot/alert evidence storage is absent; cursor JSON is overwritten. Hosts retain referenced evidence.
171
+ - Stock-evidence replay/reopen is CLOSED by native position 76; legacy `0.22.0` alert fingerprints still conflict with `23505`, including after resolution.
172
+
173
+ ### Invoicing registered limitations
174
+
175
+ These are notes carried from the landed design, not fixes in this preparation.
176
+
177
+ - M07 `payment_truth_readback` still joins `accounting_documents`: operator
178
+ ACCESS EXCLUSIVE DDL or denied ACL can block/refuse the call. New unlinked
179
+ payment events remain `accountingRequested=false` after independent issue;
180
+ use owed-duty readback for invoice progress (PR #334).
181
+ - Managed multi-reason and late-issue compensation can create more than one full
182
+ correction per invoice. A later finalizer can be fenced `claim_inactive`, leaving
183
+ its row `processing`; failed/pending rows on already-corrected parents may never
184
+ be reclaimed. Reconcile provider identity/evidence, including unresolved attempted
185
+ legacy corrections. Broader reversal/manual arbitration needs a separately
186
+ designed replacement forward (PR #329, #330, #348).
187
+ - An in-flight issue can still compensate outside the seller wrapper with
188
+ `legacy_fallback`; latest-parent reselection retains incumbent behavior.
189
+ Managed keys are scoped to invoice/provider/reason, and delegated void stores
190
+ no key; native cross-order reuse refuses. Pre-wrapper native reversal-key
191
+ retries can conflict after wrapper evidence is added (PR #346, #348).
192
+ - Managed paid/handoff contention (`55P03`) is a visible retryable no-effect
193
+ refusal with no automatic adapter retry. Fail-soft dispatch can finish without
194
+ an invoice request; durable recreation can require operator recovery and delayed
195
+ shipped-without-invoice monitoring (PR #348).
196
+ - A bad host fiscal revision can refuse an entire scoped/provider batch. A null
197
+ payment `providerRef` falls back to the fiscal provider: hosts must supply explicit
198
+ payment identity. A failing `store.fail` aborts remaining leased work; retain
199
+ lease recovery and host retry handling (PR #334).
200
+ - Reversed-duty fact installation can still report owed: read authoritative
201
+ reversal/duty evidence, never treat that answer as permission to issue.
202
+ Unsupported reversal and parked document/request residues remain visible and
203
+ unclaimable; later fact installation/reversal retry can change their diagnostics
204
+ (PR #332, #334; design phase obligations).
205
+ - Native corrections ride the issue batch; a separate correction operation without
206
+ a host factory refuses `host_binding_required`. Unknown native seller selection
207
+ currently maps to non-retryable unsupported; hosts must handle recovery explicitly
208
+ (PR #348).
209
+ - Delivery visibility must exceed transport timeout. Fenced settlement, database
210
+ failure or lease expiry can abort a batch and leave unsent leased rows uncertain.
211
+ Retryable/unsupported transport diagnostics may project as `retryable`; counters
212
+ omit projection failures/parked mismatches, so inspect durable status/error
213
+ evidence. Malformed projections and permanently invalid PDFs can retry without
214
+ an attempt cap (PR #332, #344).
215
+ - Reused settlement-receipt detection continues within one call only; it does not
216
+ detect another item's reuse in a later run. Legacy host aggregates expose item
217
+ conflicts through count differences; distinct receipts require an item-capable
218
+ recorder/resolver (PR #344).
219
+ - Hosts retain accepted-document observation polling and safe history composition.
220
+ `resolveDownload` must bind the document to the authorized order; the package
221
+ ticket check compares `documentRef`. Opaque-reference filtering covers its listed
222
+ schemes only (PR #344).
223
+ - Closed diagnostic policy reduces managed message-only/non-allowlisted errors to
224
+ `unknown`; an empty/non-string earlier `code` can hide a later closed code. The
225
+ separate OMS projection retains redacted free text. Missing-order cleanup can
226
+ log error rather than warn with the same refused/discarded outcome (PR #344, #348).
227
+ - PDF content-type normalization retains OpenLup behavior; signature checks are
228
+ stricter than donor ASCII decoding. `invoicePdfFilename` falls back to English
229
+ `invoice`; supply host captions (PR #344).
230
+
13
231
  ## [0.23.0]
14
232
 
15
233
  - Publishable on the npm `latest` dist-tag as `0.23.0`, from tag
@@ -441,62 +659,3 @@ Native parity remains host-owned at these seams. Missing native bindings use the
441
659
  | G20 | Customer alignment rows; native reader refuses, while the pure delay projection remains callable. |
442
660
  | G21 | Commerce list/detail and shipments overview; native read callbacks refuse. |
443
661
  | G22 | Authoritative preparation edits, authorization and replay receipts; missing preparation binding refuses before dispatch, after disabled/selection/readiness checks. |
444
-
445
- ## [0.21.0]
446
-
447
- - Publishable on the npm `latest` dist-tag as `0.21.0`, from tag
448
- `openlup-platform-v0.21.0`.
449
- - Preview set: core/outbox unchanged, UI private `0.0.0`; published `0.20.0` size baselines, unchanged caps.
450
-
451
- ### Cumulative M07 status: payment truth and repair surfaces complete for this wave
452
-
453
- - Experimental payments/contracts/runtime: unchanged prepared ordering; neutral execution/evidence/readback/conformance, verified truth, first-receipt ingress, method lifecycle/reconciliation; host-owned persistence, translation/failure policy.
454
- - Native PostgreSQL truth/settlement/later-only alignment; three unchanged sparse SQL projections, not an install chain. Refunds never cancel subscriptions.
455
- - Repair/recovery/buyer verify inject rail capabilities; unknown rails refuse. Reference BFF retains mapping/schemas/ownership/auth order/transient callbacks. Browser/verify outcomes never prove payment success.
456
- - Reference-application source (no new export), opt-in repair view: `commerce/paymentStatusClient`, `commerce/paymentVerifyClient`, `payment/components/PaymentStatusPoller`; stored paid alone confirms, redirects require HTTPS, collection/confirmation stay injected, unsupported setup is visible. Exact file/specifier guards refuse broad commerce/operator clients; source-import proof keeps default view unmounted.
457
- - Truth services/one optional named binder use the host executor, no new schedule/executor. Selectors default-off: nothing activates by default. First-insert adapter fingerprint excludes receipt time/audit.
458
- - Two experimental example adapter subpaths under `./payments/`, named in the README. Card-processor example: three frozen exports, injected host client/neutral ports; refund creation, split authorization/capture and exact absence unavailable; cancellation needs host support.
459
- - Regional-processor example: host I/O, Model O aliases, verified observations, purpose-sensitive readback; check certificate validity/notification freshness. Both examples have no import I/O/default binding and await account TEST evidence.
460
- - Example adapter advisory: mismatched nonzero refusal money stays unknown/manual; trusted document refusals mean customer data correction; configuration mismatches throw before I/O. Unpaid/transient no-dispatch classifications unchanged.
461
- - Settling money missing/invalid/non-positive or blank currency refuses as amount mismatch; conformance requires positive money. Empty optional transients stop false leak refusals; JSON-escaped transients in diagnostics/readbacks/stored payloads and payer contact in execution results now refuse.
462
- - Private types reuse canonical money/attempts; unused imports removed, runtime/API unchanged.
463
-
464
- Migration: upgrade all `@openlup/*` `0.20.0` → `0.21.0` for exact pins. No schema migration; install the canonical catalogue, not sparse projections. Attempts add optional method selection/transient payer contact; contact is dispatch-only, never persisted/audited/logged/diagnosed. Missing adapter-required input requires exact before-dispatch `notDispatched`. Registration refs/kind/status require host `resolveMethodOwner` client/subscription ownership or are omitted. Lifecycle/normalized nested audit copies typed fields only; conformance scans all for secrets/contact. Adapter-filtered redacted audit never stores raw bodies; typed evidence overrides reader facts, audit bag fills untyped facts; identity/fingerprint exclude bag/receipt time. Other changes need no migration, and nothing rewires a host automatically.
465
-
466
- Migration: F2 replaces assumed full refund with `require-evidence`: missing/null kind/amount/operation identity is durably recorded/acknowledged, not applied. Supply original money, stable operation identity and cumulative snapshot, event delta or explicit full reversal (`refund`/`chargeback`); full evidence stays valid. Former default needs `refundEvidencePolicy: "legacy-assume-full"`, also in the reference handler; no native parity implied.
467
-
468
- Migration: D11 changes every caller of the reference payment control port (the selected verified ingress, the existing provider webhook handler and the captured reference checkout): apply/post-process redelivery using the first stored receipt instant, not redelivery time, so it cannot shift the cycle again. Replay reads winning receipt evidence and refuses (503) when that instant is unavailable. A legacy pre-fingerprint row takes the later clock once. Retain receipt evidence on upgrade; provider routes are not switched to opt-in ingress.
469
-
470
- ### Known limitations
471
-
472
- - Portable dunning repair has no managed twin: exhaustion still pauses active subscriptions there; retaining active status needs a managed forward.
473
- - D8-1: native advisory v2 promo-code candidates/rejection details unavailable.
474
- - D8-2: native mounted v2 issue/honor/fallback unavailable.
475
- - D8-3: native acquisition-only mounted issuance/honor unavailable.
476
- - D8-4: full native/managed eligibility equivalence unavailable.
477
- - Bundle-1: independent DB human-only actor-kind enforcement absent natively; first in the native-backend parity round.
478
- - Bundle-2: managed-equivalent actor-kind/email history absent natively.
479
- - Bundle-3 closes injected-reader M05 mounted admin/feed readback only. Native new-purchase stock read absent despite P5a installed-store proof; deferred second after bundle-1 (owner 2026-10-21); readiness refuses.
480
- - Bundle-4: incumbent real-PostgreSQL contention only; no exactly-once guarantee.
481
- - Managed schema/pgTAP retain `44.404`; removal needs owner decision/forward; reference composition supplies it to app code.
482
- - A1 offline only; account/hosted-time/owner TEST evidence unclaimed; `communications/klaviyo` verification-pending.
483
- - M05 omitted opt-ins preserve defaults/outputs/read order/schedules/refusals/acceptance-token bytes/SQL.
484
- - Native inventory adjustment unavailable; address writes refuse readiness. Flat-rate save needs selected port/Core readiness/applied-seed observation.
485
- - Native shipping audit unavailable; handler swallows best-effort audit failure.
486
- - Native flat-rate integer bounds (0 through host maximum; reference 100,000 minor units) use request schema, not DB CHECK.
487
- - Managed ATP/forecast/dated-supply/pagination readback unproved/pending M06.
488
- - Managed two-session locks/concurrency/uncovered compensation unproved/pending M06.
489
- - Managed exact-prefix upgrade/ledger unproved/pending M06.
490
- - Managed address idempotency/conflict/readback unproved/pending M06.
491
- - Managed successful flat-rate read/save unproved/pending host authority.
492
- - M06 stock-wait lifecycle/durable timing/staging controls retain C5/C6; busy/try-gate, continuity/flag/TTL transfers are dispositions, not PASS.
493
- - Shipping national-registry fields/locale-specific `canonicalAddressKey` fold await M16 host market pack/key migration; reference shipping copy stays locale-specific until M16.
494
- - Native durable prepared storage, method setup, absence reopen and cumulative refunds unavailable; no production installation parity claimed.
495
- - Typed refusal-envelope forwarding unavailable (`failureReason` only); reusable registration needs host ownership resolution.
496
- - Incomplete chargebacks stay refunds/unapplied: reclassification bypasses evidence validation into dispute apply; limited operator recovery.
497
- - Audit compatibility still detects `chargeId`; refund operation identity/original money supply no typed charge identity.
498
- - M07 follow-up: repair poll can clear host redirect; post-timeout verify never restarts polling; unlocalized poller; fixture builds/never calls handler.
499
- - M07 follow-up: reference status route wrong-rail witness.
500
- - M07 follow-up: explicit reference-route refusal for unsupported method setup.
501
- - M07 follow-up: deployment-neutral disabled-verify reason.
502
- - M07 follow-up: reference card normalizer missing-amount path; package settling evidence already requires positive money.
package/README.md CHANGED
@@ -1,25 +1,46 @@
1
1
  # @openlup/platform
2
2
 
3
+ Invoicing exports explicit `createInvoiceServices`, `InvoiceServices` and inert
4
+ `createInvoiceContribution` through `/invoicing`, with no new composition entry.
5
+ `claimInvoiceSellerRevision` reads an injected selection before claim; the pinned
6
+ SQL wrappers persist it for eligible obligations. `InvoiceSellerRevision` is also
7
+ available from the type-only contracts entry. Legacy obligations keep fallback.
8
+
3
9
  Development preview. This package is part of the publishable set with core and outbox; preparation does not establish a registry release or a stable API. Its repository-directory publish command refuses publication; the authorized package-release workflow owns publication.
4
10
 
5
11
  It holds the runnable server substrate an OpenLup application composes: the Node HTTP runtime, BFF dispatch, scheduling and job leases, blob storage, data gateways, the node-postgres migration runner and bundle composition. Ports and their contracts stay in `@openlup/core/platform-runtime`.
6
12
 
7
- For the prepared `0.23.0` set, read the [adjacent-release migration and known
8
- limitations](CHANGELOG.md#0230) before selecting the opt-in parity seams.
9
- Install the complete native chain through position 76; follow the `0.22.0`
10
- native-row migration notes. Native delivery alignment stays off or shadow until
11
- a host composes its automatic-renewal runner with G23 admission.
13
+ For the prepared `0.24.0` set, read the [adjacent-release migration and known
14
+ limitations](CHANGELOG.md#0240) before selecting optional services.
15
+ Install the complete native chain through position 80, including invoicing
16
+ positions 77–79 and the first-claim seller revision forward; sparse SQL projections
17
+ are not an install chain. Keep the older native-row migration precautions in the
18
+ release notes when upgrading `0.22.0` data. Native delivery alignment stays off or
19
+ shadow until a host composes its automatic-renewal runner with G23 admission.
12
20
  M10 subscription renewal/retry (P1 exports, forward controls, P2 stores/executor,
13
- native N1 SQL landed) and M09 invoicing selection/type-only contracts (no SQL)
14
- are **EXPERIMENTAL, INCOMPLETE and DEFAULT-OFF**, not for adopter activation.
15
- No runner is activated; G23/N1 host obligations remain pending.
21
+ native N1 SQL) remains **EXPERIMENTAL, INCOMPLETE and DEFAULT-OFF**, not for
22
+ adopter activation; G23/N1 host obligations remain pending.
23
+ M09 invoicing remains **EXPERIMENTAL and DEFAULT-OFF**, available for explicit
24
+ provider/capability selection and ready host composition, including issue,
25
+ correction, independent duties, delivery/read-side and seller revision helpers.
26
+ Adopters must not activate unselected or unready invoicing; installation and
27
+ inert contributions register no runner, worker, route or schedule.
16
28
  M06 managed-host proof classes remain pending in the
17
29
  [M06 parity ledger](../../docs/platform/plans/oss-m06-stock-reservations-delivery.md#parity-pending-rows).
18
- Size comparison uses the published `0.22.0` tarball. Preparation and offline
30
+ Size comparison uses the published `0.23.0` tarball. Preparation and offline
19
31
  adapter evidence grant no publication or provider-account certification.
20
32
 
21
33
  Longer module recipes live in the [platform package reference](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md).
22
34
 
35
+ The prepared set requires Zod `^4.6.6` for GHSA-mrhx-c7xc-m3w5.
36
+ Public quote requests retain their collection limits and valid-input behavior;
37
+ oversized collections now fail with the size error before accumulating invalid
38
+ element errors. Package API and installed-consumer checks remain required.
39
+ The observation adapter explicitly retains minute-precision timestamps
40
+ alongside seconds/fractions, compact offsets and local timestamps. Calendar
41
+ validation still rejects impossible dates; validation never rewrites the raw
42
+ timestamp used in the webhook's idempotency key.
43
+
23
44
  ## Package surface
24
45
 
25
46
  `release-gates.json` declares kind `rail`: this foundation provides runtime infrastructure over core ports, matching the architecture's rail responsibility. Its experimental entries each bind a declaration snapshot and package-owned smoke tests. The API checker reads this same inventory.
@@ -45,7 +66,7 @@ Longer module recipes live in the [platform package reference](../../docs/platfo
45
66
  | `./payments` | Experimental prepared-attempt ordering with injected persistence, execution and failure policy; contracts at `./payments/contracts`. |
46
67
  | `./fulfillment`, `./fulfillment/contracts` | [Experimental fulfillment services, contracts and status canon](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#fulfillment-contracts). |
47
68
  | `./subscriptions`, `./subscriptions/contracts` | Default-off, incomplete; [experimental subscription contracts and renewal port](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#subscription-contracts). |
48
- | `./invoicing`, `./invoicing/contracts` | Default-off, incomplete invoicing selection and type-only contracts; no SQL or activation. |
69
+ | `./invoicing`, `./invoicing/contracts` | Default-off invoicing contracts and independent duty collection; no provider activation. |
49
70
  | `./inventory` | Experimental schemas/ports, handlers, native stores, projection, injected forecast and SQL descriptor; `./inventory/contracts` DTOs. |
50
71
  | `./catalog` | Experimental neutral catalog schemas, drafts, reads, media source packs and publication ports |
51
72
  | `./catalog/contracts` | Dependency-free catalog DTOs, media resolver, authority and canonicalization helpers |
@@ -710,4 +731,65 @@ Experimental `@openlup/platform/invoicing` exposes `readInvoiceCapabilities`;
710
731
  Frozen facts, correction recovery, exact absence, owed duties and host-only fiscal,
711
732
  artifact, delivery, payout and owned-history seams are described in the
712
733
  [invoicing reference](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#invoicing-contracts).
713
- No store, worker, adapter, route or activation is supplied by this contract step.
734
+ The contract entry supplies no executor or activation.
735
+ `createNativeUnsupportedInvoicePorts` supplies inert typed refusals for every
736
+ host, store and provider seam. `runInvoiceAdapterConformance` checks adapter-owned
737
+ offline fixtures; its sanitized evidence does not establish live fiscal readiness.
738
+
739
+ Issue helpers in `invoicing` preserve claim/preflight/projection/fresh-money/
740
+ create/fenced-finalize order. Hosts bind compatibility payloads, seller/date/tax
741
+ policy, captions, event descriptors and key scopes. Canonical and legacy line
742
+ checks share the unchanged donor allocation; handoff helpers register no job or
743
+ executor. Eight issue/line/payment/handoff cases join the existing package smoke
744
+ selection. Continuity cutover and activation remain separate.
745
+
746
+ Correction and reversal workers preserve durable identity, recovery and fenced
747
+ outcomes through host callbacks. The reference host requires a conforming
748
+ correction bridge; the legacy vendor method stays unselected. Reversal handlers pass
749
+ through the RPC action (`voided`, `correction_requested`, `corrected`, `no_invoice`). The required package
750
+ selection includes correction/reversal witnesses in `test/invoicing/contracts.test.ts`.
751
+ See the [invoicing design](../../docs/platform/plans/oss-m09-invoicing.md#p3-correction-and-reversal-boundary)
752
+ for managed migration and reconciliation limits.
753
+
754
+ See the [M09 forward owner](../../docs/platform/DATA_AND_MIGRATIONS.md#m09-invoicing-forward-design-and-control-approval) for the registered managed-correction limit and follow-up.
755
+
756
+ `createPostgresInvoiceStore` uses a caller-owned structural executor for native
757
+ owed duties, split fact leases, claims and fenced issue/correction results.
758
+ Missing host facts refuse before writes; clearance remains host work. Delivery
759
+ requires an explicit host envelope projection and M04 transport. `INVOICING_MIGRATIONS` separates native child-document and managed
760
+ base-row correction storage. Install the complete native catalogue, not its
761
+ seven sparse byte-identical SQL assets. The local-only Postgres adapter consumes
762
+ this store without claiming fiscal parity or registering a worker. The
763
+ [invoicing architecture owner](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#invoicing-contracts)
764
+ describes reversal recovery, capture faults and cutover fencing.
765
+ `collectInvoiceDuties` and `createIndependentInvoiceDutyBinding` collect retained
766
+ paid/handoff duties through separately committed store phases. Hosts invoke them
767
+ outside payment and fulfillment work; missing facts remain owed. The pinned
768
+ payment-decoupling SQL is a sparse catalogue projection, requiring the full P4
769
+ native chain. See the architecture owner for continuity and activation limits.
770
+
771
+ `runNativeInvoiceIssueJob` supplies the native create/readback binding over
772
+ explicit local/fresh money and provider ports. Fresh non-ready results settle
773
+ before returning; stale results write no failure. No scheduler or default
774
+ composition is registered. Operator DDL/ACL limits of the unchanged M07 readback
775
+ remain registered in the architecture owner.
776
+
777
+ For bounded duty discovery before facts exist, including cursor continuation past
778
+ unavailable facts, follow the [invoicing architecture owner](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#invoicing-contracts).
779
+
780
+ The invoicing read side adds `runInvoiceDeliveryJob`, `assertInvoicePdf`,
781
+ `invoicePdfFilename`, `syncAcceptedInvoiceDocuments`,
782
+ `syncInvoicePayoutSettlements`, `resolveInvoiceDocumentHistory` and
783
+ `createInvoiceReadModel`. Hosts supply authorization, storage, M04 transport,
784
+ terminal/backoff callbacks or the native store, and safe native history projection. See the
785
+ [read-helper recipe](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#invoicing-read-helpers).
786
+
787
+ `createInvoiceServices` and `createInvoiceContribution` compose the existing
788
+ helpers through `/invoicing`, with explicit selection, default-off and readiness
789
+ before execution. Native collection, issue, delivery, document/payout sync and
790
+ history use their module implementations; host facades may bind factories where
791
+ transport or query projection is host-owned. No executor is installed.
792
+ `claimInvoiceSellerRevision` selects a seller before leasing. The optional native
793
+ store seller binding and managed wrappers persist its first-ever claim revision;
794
+ legacy fallback and forward-only migration limits are owned by the
795
+ [M09 migration section](../../docs/platform/DATA_AND_MIGRATIONS.md#m09-invoicing-forward-design-and-control-approval).
@@ -90,6 +90,10 @@ export interface FulfillmentOperationError {
90
90
  status: number | null;
91
91
  retryable: boolean;
92
92
  mayHaveSucceeded: boolean;
93
+ /** Whether provider submission was attempted; absent means unknown, never proven false. */
94
+ submissionAttempted?: boolean;
95
+ /** Whether the HTTP response body was read, including empty or malformed JSON; absent means no body-read evidence. */
96
+ responseBodyRead?: boolean;
93
97
  /** Machine cause only; never an arbitrary exception message. */
94
98
  cause?: string;
95
99
  }
@@ -19,7 +19,7 @@ export declare function durableStrings(priorCount?: number): {
19
19
  /** Copy only the durable allowlist, including when an adapter object has extra runtime properties. */
20
20
  export declare function dispatchEvidence(evidence: DispatchEvidence): DispatchEvidence;
21
21
  /** Shadow has a local command identity; it never invokes a provider or resolves credentials. */
22
- export declare function shadowDispatchEvidence(request: FulfillmentDispatchRequest): DispatchEvidence;
22
+ export declare function shadowDispatchEvidence(request: FulfillmentDispatchRequest, commandFingerprint?: string): DispatchEvidence;
23
23
  export declare function dispatchResult(result: FulfillmentDispatchResult): FulfillmentDispatchResult;
24
24
  export declare function acceptedProviderProof(record: DispatchRecord | null): record is DispatchRecord & {
25
25
  externalRef: string;
@@ -22,6 +22,8 @@ export function operationError(error) {
22
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
23
  return { code, status: Number.isInteger(error.status) && error.status >= 100 && error.status <= 599 ? error.status : null,
24
24
  retryable: error.retryable === true, mayHaveSucceeded: error.mayHaveSucceeded === true,
25
+ ...(typeof error.submissionAttempted === "boolean" ? { submissionAttempted: error.submissionAttempted } : {}),
26
+ ...(typeof error.responseBodyRead === "boolean" ? { responseBodyRead: error.responseBodyRead } : {}),
25
27
  ...(cause ? { cause } : {}), ...(privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}) };
26
28
  }
27
29
  // The fulfillment opaque-ID rule; never accept free-form metadata as a reference.
@@ -78,10 +80,10 @@ export function dispatchEvidence(evidence) {
78
80
  };
79
81
  }
80
82
  /** Shadow has a local command identity; it never invokes a provider or resolves credentials. */
81
- export function shadowDispatchEvidence(request) {
83
+ export function shadowDispatchEvidence(request, commandFingerprint) {
82
84
  if (!request.selection.providerRef || !request.lines.length || request.lines.some(line => !Number.isSafeInteger(line.quantity) || line.quantity <= 0))
83
85
  throw new Error("dispatch_shadow_command_invalid");
84
- return dispatchEvidence({ commandFingerprint: createHash("sha256").update(JSON.stringify(request)).digest("hex"),
86
+ return dispatchEvidence({ commandFingerprint: commandFingerprint ?? createHash("sha256").update(JSON.stringify(request)).digest("hex"),
85
87
  contactRevision: request.contactRevision, providerRef: request.selection.providerRef,
86
88
  carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef,
87
89
  transcript: { itemCount: request.lines.length, items: request.lines.map(line => ({ skuRef: line.skuRef, quantity: line.quantity,
@@ -58,24 +58,32 @@ async function requestJson(fetchImpl, config, operation, path, init, refuseRedir
58
58
  let lastError = null;
59
59
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
60
60
  let submitted = false;
61
+ let responseBodyRead;
62
+ const dispatchFacts = () => operation === "createOrder" ? { submissionAttempted: submitted,
63
+ ...(responseBodyRead !== undefined ? { responseBodyRead } : {}) } : {};
61
64
  try {
62
65
  const base = new URL(config.baseUrl);
63
66
  const url = new URL(path, base);
64
67
  if (url.origin !== base.origin || url.username || url.password || url.protocol !== "https:")
65
68
  throw new Error("Invalid fulfillment path");
66
69
  const response = await fetchWithTimeout(fetchImpl, url, config, init, () => { submitted = true; });
70
+ responseBodyRead = false;
71
+ const body = await readJsonBody(response).then(value => { responseBodyRead = true; return value; }).catch(error => {
72
+ if (response.ok)
73
+ throw error;
74
+ return null;
75
+ });
67
76
  if (response.ok)
68
- return await readJsonBody(response);
77
+ return body;
69
78
  const retryable = method === "GET" && isRetryableStatus(response.status);
70
79
  // A failed body read cannot erase the rejection status already received.
71
- const body = await readJsonBody(response).catch(() => null);
72
- lastError = new OmnipackProviderError({
80
+ lastError = Object.assign(new OmnipackProviderError({
73
81
  operation,
74
82
  status: response.status,
75
83
  code: refuseRedirectRetry && response.status >= 300 && response.status < 400 ? "redirect_refused" : readProviderErrorCode(body) ?? `http_${response.status}`,
76
84
  retryable,
77
85
  mayHaveSucceeded: mayPostHaveSucceeded(method, response.status),
78
- });
86
+ }), dispatchFacts());
79
87
  if (!retryable || attempt === attempts)
80
88
  throw lastError;
81
89
  }
@@ -83,13 +91,13 @@ async function requestJson(fetchImpl, config, operation, path, init, refuseRedir
83
91
  const retryable = method === "GET" && attempt < attempts;
84
92
  lastError = submitted && error instanceof OmnipackProviderError
85
93
  ? error
86
- : new OmnipackProviderError({
94
+ : Object.assign(new OmnipackProviderError({
87
95
  operation,
88
96
  status: null,
89
97
  code: diagnoseRedirectFailure && isRedirectRefusal(error) ? "redirect_refused" : "network_error",
90
98
  retryable,
91
99
  mayHaveSucceeded: submitted && mayPostHaveSucceeded(method, null),
92
- });
100
+ }), dispatchFacts());
93
101
  if (!retryable || (refuseRedirectRetry && lastError.code === "redirect_refused"))
94
102
  throw lastError;
95
103
  }
@@ -242,10 +242,13 @@ export function createOmnipackFulfillmentAdapter(config) {
242
242
  return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
243
243
  const original = payload;
244
244
  // Validate calendar fields here only; keep compact offsets and local
245
- // timestamps accepted by the base parser, without rewriting key bytes.
245
+ // timestamps and minute precision accepted by the base parser,
246
+ // without rewriting key bytes.
246
247
  if (original.occurredAt !== undefined && original.occurredAt !== null
247
248
  && (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
+ || !z.iso.datetime({ offset: true, local: true })
250
+ .or(z.iso.datetime({ offset: true, local: true, precision: -1 }))
251
+ .safeParse(original.occurredAt.replace(/([+-]\d{2})(\d{2})$/u, "$1:$2")).success))
249
252
  return { kind: "refused", refusal: refusal("malformed", "normalize"), diagnostics: { invalidTimestamp: true } };
250
253
  const rejectedCorrelationRefs = [original.orderId, original.providerOrderId, original.orderNumber, original.fulfilmentNumber]
251
254
  .filter(value => value !== undefined && value !== null && (typeof value !== "string" || !correlationReference(value, config.observationCodecs))).length;
@@ -16,8 +16,46 @@ export declare function runFulfillmentDispatchWorker(input: {
16
16
  mode: DispatchRecord["mode"];
17
17
  batchLimit: number;
18
18
  };
19
+ /** Pure local shadow fingerprint: a lowercase 64-hex SHA-256 digest of the adopter's canonical command bytes. Invalid output or a throw refuses before evidence writes. Never return raw command/PII. Used across shadow claim/readback/attempt; shadow never begins, and live preparation must produce the same identity. */
20
+ shadowCommandFingerprint?: (request: FulfillmentDispatchRequest) => string;
19
21
  now?: () => Date;
20
22
  }): Promise<{
23
+ redactedFieldCount: number;
24
+ ok: boolean;
25
+ failures: number;
26
+ reason: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
27
+ refusal: {
28
+ reason: "malformed";
29
+ phase: "prepare";
30
+ retryable: false;
31
+ cause: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
32
+ };
33
+ skipped: boolean;
34
+ checked: number;
35
+ updated: number;
36
+ mode: "shadow" | "stage" | "live";
37
+ replayed: number;
38
+ providerCalls: number;
39
+ readBacks: number;
40
+ } | {
41
+ redactedFieldCount?: undefined;
42
+ ok: boolean;
43
+ failures: number;
44
+ reason: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
45
+ refusal: {
46
+ reason: "malformed";
47
+ phase: "prepare";
48
+ retryable: false;
49
+ cause: "adopter_fingerprint_invalid" | "adopter_fingerprint_failed";
50
+ };
51
+ skipped: boolean;
52
+ checked: number;
53
+ updated: number;
54
+ mode: "shadow" | "stage" | "live";
55
+ replayed: number;
56
+ providerCalls: number;
57
+ readBacks: number;
58
+ } | {
21
59
  redactedFieldCount: number;
22
60
  ok: boolean;
23
61
  skipped: boolean;
@@ -53,8 +53,26 @@ export async function runFulfillmentDispatchWorker(input) {
53
53
  for (let remapCount = 0;; remapCount += 1) {
54
54
  const request = { ...mappedCandidate, requestKey };
55
55
  stage = "dispatch_command_prepare_failed";
56
+ let shadowFingerprint;
57
+ if (input.config.mode === "shadow" && input.shadowCommandFingerprint) {
58
+ let value;
59
+ let cause;
60
+ try {
61
+ value = input.shadowCommandFingerprint(request);
62
+ }
63
+ catch {
64
+ cause = "adopter_fingerprint_failed";
65
+ }
66
+ if (!cause && typeof value === "string" && /^[a-f0-9]{64}$/.test(value))
67
+ shadowFingerprint = value;
68
+ else
69
+ cause ??= "adopter_fingerprint_invalid";
70
+ if (cause)
71
+ return { ...result, ok: false, failures: result.failures + 1, reason: cause,
72
+ refusal: { reason: "malformed", phase: "prepare", retryable: false, cause }, ...diagnostics() };
73
+ }
56
74
  const prepared = input.config.mode === "shadow"
57
- ? { kind: "prepared", evidence: shadowDispatchEvidence(request) }
75
+ ? { kind: "prepared", evidence: shadowDispatchEvidence(request, shadowFingerprint) }
58
76
  : await input.provider.prepare(request);
59
77
  if (prepared.kind === "refused") {
60
78
  result.ok = false;