@openlup/platform 0.23.0 → 0.25.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 (165) hide show
  1. package/CHANGELOG.md +327 -269
  2. package/README.md +123 -12
  3. package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +3 -2
  4. package/dist/communications/contracts/subscriptionPaymentExpired.js +1 -1
  5. package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +4 -3
  6. package/dist/communications/contracts/subscriptionPaymentFailed.js +4 -5
  7. package/dist/composition/index.d.ts +1 -0
  8. package/dist/composition/index.js +1 -0
  9. package/dist/composition/subscriptionContribution.d.ts +18 -0
  10. package/dist/composition/subscriptionContribution.js +19 -0
  11. package/dist/fulfillment/contracts/dispatch.d.ts +4 -0
  12. package/dist/fulfillment/dispatchProjection.d.ts +1 -1
  13. package/dist/fulfillment/dispatchProjection.js +4 -2
  14. package/dist/fulfillment/omnipack/client.js +14 -6
  15. package/dist/fulfillment/omnipack/index.js +5 -2
  16. package/dist/fulfillment/providerDispatchWorker.d.ts +38 -0
  17. package/dist/fulfillment/providerDispatchWorker.js +19 -1
  18. package/dist/inventory/index.d.ts +2 -0
  19. package/dist/inventory/index.js +1 -0
  20. package/dist/inventory/postgresStockAvailability.d.ts +24 -0
  21. package/dist/inventory/postgresStockAvailability.js +48 -0
  22. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  23. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  24. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  25. package/dist/invoicing/accountingLineAllocation.js +69 -0
  26. package/dist/invoicing/adapterConformance.d.ts +42 -0
  27. package/dist/invoicing/adapterConformance.js +296 -0
  28. package/dist/invoicing/artifacts.d.ts +7 -0
  29. package/dist/invoicing/artifacts.js +20 -0
  30. package/dist/invoicing/bindings.d.ts +46 -0
  31. package/dist/invoicing/bindings.js +69 -0
  32. package/dist/invoicing/channelPolicy.d.ts +28 -0
  33. package/dist/invoicing/channelPolicy.js +27 -0
  34. package/dist/invoicing/continuity.d.ts +12 -0
  35. package/dist/invoicing/continuity.js +18 -0
  36. package/dist/invoicing/contracts/facts.d.ts +11 -1
  37. package/dist/invoicing/contracts/host.d.ts +3 -4
  38. package/dist/invoicing/contracts/index.d.ts +1 -1
  39. package/dist/invoicing/contracts/store.d.ts +15 -5
  40. package/dist/invoicing/contribution.d.ts +21 -0
  41. package/dist/invoicing/contribution.js +19 -0
  42. package/dist/invoicing/correction.d.ts +42 -0
  43. package/dist/invoicing/correction.js +44 -0
  44. package/dist/invoicing/delivery.d.ts +85 -0
  45. package/dist/invoicing/delivery.js +79 -0
  46. package/dist/invoicing/documentHistory.d.ts +50 -0
  47. package/dist/invoicing/documentHistory.js +213 -0
  48. package/dist/invoicing/documentSync.d.ts +24 -0
  49. package/dist/invoicing/documentSync.js +26 -0
  50. package/dist/invoicing/dutyCollector.d.ts +15 -0
  51. package/dist/invoicing/dutyCollector.js +35 -0
  52. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  53. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  54. package/dist/invoicing/handoff.d.ts +37 -0
  55. package/dist/invoicing/handoff.js +62 -0
  56. package/dist/invoicing/index.d.ts +38 -0
  57. package/dist/invoicing/index.js +36 -0
  58. package/dist/invoicing/issue.d.ts +39 -0
  59. package/dist/invoicing/issue.js +55 -0
  60. package/dist/invoicing/migrations.d.ts +48 -0
  61. package/dist/invoicing/migrations.js +25 -0
  62. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  63. package/dist/invoicing/nativeUnsupported.js +22 -0
  64. package/dist/invoicing/paymentReadback.d.ts +20 -0
  65. package/dist/invoicing/paymentReadback.js +47 -0
  66. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  67. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  68. package/dist/invoicing/readModel.d.ts +74 -0
  69. package/dist/invoicing/readModel.js +90 -0
  70. package/dist/invoicing/readback.d.ts +4 -0
  71. package/dist/invoicing/readback.js +30 -0
  72. package/dist/invoicing/reversal.d.ts +37 -0
  73. package/dist/invoicing/reversal.js +28 -0
  74. package/dist/invoicing/sellerRevision.d.ts +19 -0
  75. package/dist/invoicing/sellerRevision.js +14 -0
  76. package/dist/invoicing/settlementSync.d.ts +18 -0
  77. package/dist/invoicing/settlementSync.js +32 -0
  78. package/dist/invoicing/snapshot.d.ts +52 -0
  79. package/dist/invoicing/snapshot.js +141 -0
  80. package/dist/subscriptions/commandExamples.d.ts +6 -0
  81. package/dist/subscriptions/commandExamples.js +20 -0
  82. package/dist/subscriptions/contracts/commands.d.ts +8 -1
  83. package/dist/subscriptions/contracts/index.d.ts +2 -2
  84. package/dist/subscriptions/contracts/stockWait.d.ts +12 -1
  85. package/dist/subscriptions/index.d.ts +22 -4
  86. package/dist/subscriptions/index.js +22 -3
  87. package/dist/subscriptions/lifecycleMessages.d.ts +9 -0
  88. package/dist/subscriptions/lifecycleMessages.js +38 -0
  89. package/dist/subscriptions/paymentOutcomes.js +1 -1
  90. package/dist/subscriptions/postgresCommandStore.d.ts +44 -0
  91. package/dist/subscriptions/postgresCommandStore.js +172 -0
  92. package/dist/subscriptions/postgresRetryStore.d.ts +6 -0
  93. package/dist/subscriptions/postgresRetryStore.js +11 -0
  94. package/dist/subscriptions/postgresStockWaitStore.d.ts +62 -0
  95. package/dist/subscriptions/postgresStockWaitStore.js +174 -0
  96. package/dist/subscriptions/services.d.ts +31 -0
  97. package/dist/subscriptions/services.js +15 -0
  98. package/package.json +3 -3
  99. package/release-gates.json +21 -7
  100. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  101. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  102. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  103. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  104. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  105. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  106. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  107. package/sql/20261024170000_subscription_command_lifecycle.sql +404 -0
  108. package/sql/20261024180000_subscription_stock_wait.sql +330 -0
  109. package/sql/20261024183000_inventory_available_to_promise.sql +70 -0
  110. package/sql/20261024184000_subscription_renewal_reserve_uses_seam.sql +147 -0
  111. package/sql/20261024184500_subscription_retry_late_positive_stock.sql +59 -0
  112. package/sql/20261024184600_subscription_retry_candidate_payment_filter.sql +22 -0
  113. package/src/communications/contracts/subscriptionPaymentExpired.ts +4 -3
  114. package/src/communications/contracts/subscriptionPaymentFailed.ts +8 -8
  115. package/src/composition/index.ts +1 -0
  116. package/src/composition/subscriptionContribution.ts +25 -0
  117. package/src/fulfillment/contracts/dispatch.ts +4 -0
  118. package/src/fulfillment/dispatchProjection.ts +4 -2
  119. package/src/fulfillment/omnipack/client.ts +13 -6
  120. package/src/fulfillment/omnipack/index.ts +5 -2
  121. package/src/fulfillment/providerDispatchWorker.ts +14 -1
  122. package/src/inventory/index.ts +2 -0
  123. package/src/inventory/postgresStockAvailability.ts +54 -0
  124. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  125. package/src/invoicing/accountingLineAllocation.ts +69 -0
  126. package/src/invoicing/adapterConformance.ts +249 -0
  127. package/src/invoicing/artifacts.ts +19 -0
  128. package/src/invoicing/bindings.ts +80 -0
  129. package/src/invoicing/channelPolicy.ts +50 -0
  130. package/src/invoicing/continuity.ts +15 -0
  131. package/src/invoicing/contracts/facts.ts +12 -1
  132. package/src/invoicing/contracts/host.ts +3 -4
  133. package/src/invoicing/contracts/index.ts +1 -1
  134. package/src/invoicing/contracts/store.ts +11 -7
  135. package/src/invoicing/contribution.ts +28 -0
  136. package/src/invoicing/correction.ts +56 -0
  137. package/src/invoicing/delivery.ts +122 -0
  138. package/src/invoicing/documentHistory.ts +301 -0
  139. package/src/invoicing/documentSync.ts +51 -0
  140. package/src/invoicing/dutyCollector.ts +31 -0
  141. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  142. package/src/invoicing/handoff.ts +71 -0
  143. package/src/invoicing/index.ts +38 -0
  144. package/src/invoicing/issue.ts +64 -0
  145. package/src/invoicing/migrations.ts +25 -0
  146. package/src/invoicing/nativeUnsupported.ts +24 -0
  147. package/src/invoicing/paymentReadback.ts +70 -0
  148. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  149. package/src/invoicing/readModel.ts +80 -0
  150. package/src/invoicing/readback.ts +26 -0
  151. package/src/invoicing/reversal.ts +34 -0
  152. package/src/invoicing/sellerRevision.ts +20 -0
  153. package/src/invoicing/settlementSync.ts +37 -0
  154. package/src/invoicing/snapshot.ts +192 -0
  155. package/src/subscriptions/commandExamples.ts +19 -0
  156. package/src/subscriptions/contracts/commands.ts +35 -2
  157. package/src/subscriptions/contracts/index.ts +2 -2
  158. package/src/subscriptions/contracts/stockWait.ts +14 -2
  159. package/src/subscriptions/index.ts +30 -3
  160. package/src/subscriptions/lifecycleMessages.ts +37 -0
  161. package/src/subscriptions/paymentOutcomes.ts +1 -1
  162. package/src/subscriptions/postgresCommandStore.ts +183 -0
  163. package/src/subscriptions/postgresRetryStore.ts +11 -0
  164. package/src/subscriptions/postgresStockWaitStore.ts +170 -0
  165. package/src/subscriptions/services.ts +21 -0
package/README.md CHANGED
@@ -1,25 +1,51 @@
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.
12
- 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.
13
+ For the prepared `0.25.0` set, read the [adjacent-release migration and known
14
+ limitations](CHANGELOG.md#0250) before selecting optional services.
15
+ Install the complete native chain through position 86, including N2 commands,
16
+ N3 stock wait, the ATP seam/allocator and late-payment/retry-selection forwards;
17
+ sparse SQL projections are not an install chain. Native runtime roles need the
18
+ ATP/exposure EXECUTE and underlying SELECT grants described in the release notes.
19
+ Managed hosts apply the complete prefix through `20261024185100` for stock-wait
20
+ and refund-evidence functions. Preserve captured obligations and receipts on rollback.
21
+ M10 subscription services and contributions remain **EXPERIMENTAL and DEFAULT-OFF**.
22
+ Hosts explicitly supply readiness, execution, commands, mandatory G23 admission,
23
+ a population lease and M04 delivery. Factories activate no serving work. The inert
24
+ native reference composes renewal, dunning and stock wait; managed TypeScript
25
+ stock recovery, native multi-cycle prepaid and M07 prepared-attempt-store
26
+ capabilities retain their named unsupported boundaries. Follow the older native-row
27
+ migration precautions when upgrading `0.22.0` data.
28
+ M09 invoicing remains **EXPERIMENTAL and DEFAULT-OFF**, available for explicit
29
+ provider/capability selection and ready host composition, including issue,
30
+ correction, independent duties, delivery/read-side and seller revision helpers.
31
+ Adopters must not activate unselected or unready invoicing; installation and
32
+ inert contributions register no runner, worker, route or schedule.
16
33
  M06 managed-host proof classes remain pending in the
17
34
  [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
35
+ Size comparison uses the published `0.24.0` tarball. Preparation and offline
19
36
  adapter evidence grant no publication or provider-account certification.
20
37
 
21
38
  Longer module recipes live in the [platform package reference](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md).
22
39
 
40
+ The prepared set requires Zod `^4.6.6` for GHSA-mrhx-c7xc-m3w5.
41
+ Public quote requests retain their collection limits and valid-input behavior;
42
+ oversized collections now fail with the size error before accumulating invalid
43
+ element errors. Package API and installed-consumer checks remain required.
44
+ The observation adapter explicitly retains minute-precision timestamps
45
+ alongside seconds/fractions, compact offsets and local timestamps. Calendar
46
+ validation still rejects impossible dates; validation never rewrites the raw
47
+ timestamp used in the webhook's idempotency key.
48
+
23
49
  ## Package surface
24
50
 
25
51
  `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 +71,7 @@ Longer module recipes live in the [platform package reference](../../docs/platfo
45
71
  | `./payments` | Experimental prepared-attempt ordering with injected persistence, execution and failure policy; contracts at `./payments/contracts`. |
46
72
  | `./fulfillment`, `./fulfillment/contracts` | [Experimental fulfillment services, contracts and status canon](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#fulfillment-contracts). |
47
73
  | `./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. |
74
+ | `./invoicing`, `./invoicing/contracts` | Default-off invoicing contracts and independent duty collection; no provider activation. |
49
75
  | `./inventory` | Experimental schemas/ports, handlers, native stores, projection, injected forecast and SQL descriptor; `./inventory/contracts` DTOs. |
50
76
  | `./catalog` | Experimental neutral catalog schemas, drafts, reads, media source packs and publication ports |
51
77
  | `./catalog/contracts` | Dependency-free catalog DTOs, media resolver, authority and canonicalization helpers |
@@ -703,6 +729,30 @@ The initial binding preserves case-before-outcome ordering with atomic bookkeepi
703
729
  N1's same-obligation recovery repairs the native decline stall (F16); it does not
704
730
  supply M07 prepared-store parity or activate a runner.
705
731
 
732
+ With N1 then N2 installed, `createPostgresSubscriptionCommandStore` and
733
+ `createPostgresSubscriptionCommands` support native generic bundle commands and
734
+ command-bound receipts. Preview persists a quote; apply atomically updates future
735
+ terms, event and receipt with no proration or current-cycle repricing. Hosts own
736
+ authorization, address validation, SKU/composition translation and the transaction
737
+ connection. Prepaid remains unsupported; the serving adapter refuses pause.
738
+
739
+ Native N3 plus the ATP seam/allocator and X/Y suffix provide
740
+ `createPostgresStockWaitStore` and `createPostgresStockAvailability` (the latter
741
+ at `./inventory`). Hosts supply transactions, clock, source and the existing
742
+ wire codec. Candidate discovery preserves uncaptured frozen operations; shortage
743
+ creates a durable wait, and elapsed stock-only refusals may reacquire on the same
744
+ identity with a new claim generation. Resumed means paid realization admitted.
745
+ The fixed 14-day soft priority is distinct from durable advance reservations.
746
+ The unbound descriptor keeps stock recovery unavailable. The opt-in native M10
747
+ reference composition selects it after composed PostgreSQL proof; managed
748
+ TypeScript recovery remains unavailable.
749
+
750
+ `createSubscriptionServices`, lifecycle-message and neutral command facades are
751
+ in `./subscriptions`; `createSubscriptionContribution` is in `./composition`.
752
+ They are default-off and describe one host executor/job. The
753
+ [composed module recipe](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#composed-subscription-module)
754
+ owns explicit selection, frozen mandate/replay, M04 delivery and M12 boundaries.
755
+
706
756
  ## Invoicing contracts
707
757
 
708
758
  Experimental `@openlup/platform/invoicing` exposes `readInvoiceCapabilities`;
@@ -710,4 +760,65 @@ Experimental `@openlup/platform/invoicing` exposes `readInvoiceCapabilities`;
710
760
  Frozen facts, correction recovery, exact absence, owed duties and host-only fiscal,
711
761
  artifact, delivery, payout and owned-history seams are described in the
712
762
  [invoicing reference](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#invoicing-contracts).
713
- No store, worker, adapter, route or activation is supplied by this contract step.
763
+ The contract entry supplies no executor or activation.
764
+ `createNativeUnsupportedInvoicePorts` supplies inert typed refusals for every
765
+ host, store and provider seam. `runInvoiceAdapterConformance` checks adapter-owned
766
+ offline fixtures; its sanitized evidence does not establish live fiscal readiness.
767
+
768
+ Issue helpers in `invoicing` preserve claim/preflight/projection/fresh-money/
769
+ create/fenced-finalize order. Hosts bind compatibility payloads, seller/date/tax
770
+ policy, captions, event descriptors and key scopes. Canonical and legacy line
771
+ checks share the unchanged donor allocation; handoff helpers register no job or
772
+ executor. Eight issue/line/payment/handoff cases join the existing package smoke
773
+ selection. Continuity cutover and activation remain separate.
774
+
775
+ Correction and reversal workers preserve durable identity, recovery and fenced
776
+ outcomes through host callbacks. The reference host requires a conforming
777
+ correction bridge; the legacy vendor method stays unselected. Reversal handlers pass
778
+ through the RPC action (`voided`, `correction_requested`, `corrected`, `no_invoice`). The required package
779
+ selection includes correction/reversal witnesses in `test/invoicing/contracts.test.ts`.
780
+ See the [invoicing design](../../docs/platform/plans/oss-m09-invoicing.md#p3-correction-and-reversal-boundary)
781
+ for managed migration and reconciliation limits.
782
+
783
+ 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.
784
+
785
+ `createPostgresInvoiceStore` uses a caller-owned structural executor for native
786
+ owed duties, split fact leases, claims and fenced issue/correction results.
787
+ Missing host facts refuse before writes; clearance remains host work. Delivery
788
+ requires an explicit host envelope projection and M04 transport. `INVOICING_MIGRATIONS` separates native child-document and managed
789
+ base-row correction storage. Install the complete native catalogue, not its
790
+ seven sparse byte-identical SQL assets. The local-only Postgres adapter consumes
791
+ this store without claiming fiscal parity or registering a worker. The
792
+ [invoicing architecture owner](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#invoicing-contracts)
793
+ describes reversal recovery, capture faults and cutover fencing.
794
+ `collectInvoiceDuties` and `createIndependentInvoiceDutyBinding` collect retained
795
+ paid/handoff duties through separately committed store phases. Hosts invoke them
796
+ outside payment and fulfillment work; missing facts remain owed. The pinned
797
+ payment-decoupling SQL is a sparse catalogue projection, requiring the full P4
798
+ native chain. See the architecture owner for continuity and activation limits.
799
+
800
+ `runNativeInvoiceIssueJob` supplies the native create/readback binding over
801
+ explicit local/fresh money and provider ports. Fresh non-ready results settle
802
+ before returning; stale results write no failure. No scheduler or default
803
+ composition is registered. Operator DDL/ACL limits of the unchanged M07 readback
804
+ remain registered in the architecture owner.
805
+
806
+ For bounded duty discovery before facts exist, including cursor continuation past
807
+ unavailable facts, follow the [invoicing architecture owner](../../docs/platform/ARCHITECTURE_AND_EXTENSIONS.md#invoicing-contracts).
808
+
809
+ The invoicing read side adds `runInvoiceDeliveryJob`, `assertInvoicePdf`,
810
+ `invoicePdfFilename`, `syncAcceptedInvoiceDocuments`,
811
+ `syncInvoicePayoutSettlements`, `resolveInvoiceDocumentHistory` and
812
+ `createInvoiceReadModel`. Hosts supply authorization, storage, M04 transport,
813
+ terminal/backoff callbacks or the native store, and safe native history projection. See the
814
+ [read-helper recipe](../../docs/platform/PLATFORM_PACKAGE_REFERENCE.md#invoicing-read-helpers).
815
+
816
+ `createInvoiceServices` and `createInvoiceContribution` compose the existing
817
+ helpers through `/invoicing`, with explicit selection, default-off and readiness
818
+ before execution. Native collection, issue, delivery, document/payout sync and
819
+ history use their module implementations; host facades may bind factories where
820
+ transport or query projection is host-owned. No executor is installed.
821
+ `claimInvoiceSellerRevision` selects a seller before leasing. The optional native
822
+ store seller binding and managed wrappers persist its first-ever claim revision;
823
+ legacy fallback and forward-only migration limits are owned by the
824
+ [M09 migration section](../../docs/platform/DATA_AND_MIGRATIONS.md#m09-invoicing-forward-design-and-control-approval).
@@ -14,8 +14,9 @@ export interface SubscriptionPaymentExpiredEmailVars {
14
14
  */
15
15
  cause?: PaymentFailureCustomerCause | null;
16
16
  /**
17
- * Recovery-link URL for the case's `resume_subscription` token. When present
18
- * the template renders the resume CTA; when absent it keeps the support-led
17
+ * Recovery-link URL for the case's payment-recovery token. Current exhaustion
18
+ * uses `repair_payment`; legacy expired cases may carry `resume_subscription`.
19
+ * When present the template renders the recovery CTA; otherwise it keeps the support-led
19
20
  * wording, because a button with no destination is worse than no button.
20
21
  */
21
22
  recoveryUrl?: string | null;
@@ -8,7 +8,7 @@ export function createSubscriptionPaymentExpiredContentFactory(input) {
8
8
  const causeSentence = subscriptionPaymentCauseSentence(locale, vars.cause ?? "unknown");
9
9
  const detail = [
10
10
  // Leads the box for the same reason as in the failed notice: it answers the
11
- // question the paused subscription raises, before the figures.
11
+ // question the unsuccessful payment raises, before the figures.
12
12
  ...(causeSentence ? [causeSentence] : []),
13
13
  ...(recoveryUrl
14
14
  ? [
@@ -12,9 +12,10 @@ export interface SubscriptionPaymentFailedEmailVars {
12
12
  recoveryUrl?: string | null;
13
13
  /**
14
14
  * Pre-formatted calendar day of the scheduled next charge, in the merchant's
15
- * zone (e.g. "11.08.2026"). The dunning row that selects THIS template always
16
- * carries a scheduled retry, so in production this is present; null keeps the
17
- * pre-date sentence rather than printing a placeholder date.
15
+ * zone (e.g. "11.08.2026"). Present only when a retry is actually scheduled:
16
+ * a first-rung hard decline selects this template with no next retry, and
17
+ * `retryScheduled: false` renders the no-retry copy. null/absent keeps the
18
+ * sentence without a date rather than printing a placeholder date.
18
19
  */
19
20
  nextRetryDateLabel?: string | null;
20
21
  /** Card scheme as stored on the method's consent snapshot, e.g. "visa". */
@@ -14,11 +14,10 @@ export function createSubscriptionPaymentFailedContentFactory(input) {
14
14
  // also why it is PREPENDED and never appended — the retry sentence must stay
15
15
  // last (see below).
16
16
  const causeSentence = subscriptionPaymentCauseSentence(locale, vars.cause ?? "unknown");
17
- // Only an explicit `false` turns the promises off. Absent means "no producer
18
- // told us", and the only rail that exists always schedules a retry, so absent
19
- // must keep rendering today's copy rather than the safer-sounding new copy:
20
- // telling a customer no attempt is coming when one is would push them to
21
- // cancel a subscription that was about to recover on its own.
17
+ // Only an explicit `false` turns the retry promises off. Absence preserves
18
+ // the legacy rendering default; it does not prove a retry is scheduled.
19
+ // Producers must pass the actual schedule fact when retries have stopped,
20
+ // including class termination or exhaustion.
22
21
  const retryScheduled = vars.retryScheduled !== false;
23
22
  const detail = [
24
23
  ...(causeSentence ? [causeSentence] : []),
@@ -5,3 +5,4 @@ export { createCatalogContribution, type CatalogContributionInput } from "./cata
5
5
  export { createInventoryContribution } from "./inventoryContribution.js";
6
6
  export { createShippingContribution } from "./shippingContribution.js";
7
7
  export { createFulfillmentContribution } from "./fulfillmentContribution.js";
8
+ export { createSubscriptionContribution } from "./subscriptionContribution.js";
@@ -6,3 +6,4 @@ export { createCatalogContribution } from "./catalogContribution.js";
6
6
  export { createInventoryContribution } from "./inventoryContribution.js";
7
7
  export { createShippingContribution } from "./shippingContribution.js";
8
8
  export { createFulfillmentContribution } from "./fulfillmentContribution.js";
9
+ export { createSubscriptionContribution } from "./subscriptionContribution.js";
@@ -0,0 +1,18 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+ /** One selected subscription job; no second scheduler or automatic registration. */
3
+ export declare function createSubscriptionContribution(input: {
4
+ version: string;
5
+ requiredSchema: RequiredSchema;
6
+ run: (() => Promise<unknown>) | null;
7
+ cadenceSeconds: number;
8
+ }): {
9
+ contributions: Contribution[];
10
+ ports: {
11
+ "@openlup/platform/subscriptions:run": () => Promise<unknown>;
12
+ } | {
13
+ "@openlup/platform/subscriptions:run"?: undefined;
14
+ };
15
+ triggers: {
16
+ [k: string]: () => Promise<unknown>;
17
+ };
18
+ };
@@ -0,0 +1,19 @@
1
+ /** One selected subscription job; no second scheduler or automatic registration. */
2
+ export function createSubscriptionContribution(input) {
3
+ if (!Number.isFinite(input.cadenceSeconds) || input.cadenceSeconds <= 0)
4
+ throw new Error("subscription_cadence_invalid");
5
+ const schedules = input.run ? [{ id: "subscriptions-renewal", cadenceSeconds: input.cadenceSeconds, run: input.run }] : [];
6
+ const contribution = {
7
+ manifest: {
8
+ name: "@openlup/platform", version: input.version, kind: "rail",
9
+ module: { id: "subscriptions", kind: "capability", provides: ["./subscriptions", "./subscriptions/contracts"], dependsOn: [] },
10
+ emits: [], handles: [], schedules: schedules.map(({ id, cadenceSeconds }) => ({ id, cadenceSeconds })),
11
+ env: [], requiredPorts: ["run"], routes: [], requiredSchema: input.requiredSchema,
12
+ }, handlers: [], schedules, routes: [],
13
+ };
14
+ return {
15
+ contributions: [contribution],
16
+ ports: input.run ? { "@openlup/platform/subscriptions:run": input.run } : {},
17
+ triggers: Object.fromEntries(schedules.map(schedule => [`@openlup/platform/subscriptions:${schedule.id}`, schedule.run])),
18
+ };
19
+ }
@@ -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;
@@ -12,3 +12,5 @@ export { forecastSubscriptionInventoryShortages, buildSubscriptionInventoryForec
12
12
  export { calculateSubscriptionForecastSkus, calculateSubscriptionForecastTotals } from "./subscriptionForecastCalculation.js";
13
13
  export { INVENTORY_MIGRATIONS } from "./sql.js";
14
14
  export { createInventoryBindings, type InventoryBindingDependencies, type InventoryBindings } from "./bindings.js";
15
+ export { createPostgresStockAvailability } from "./postgresStockAvailability.js";
16
+ export type { NativeStockPromise } from "./postgresStockAvailability.js";
@@ -9,3 +9,4 @@ export { forecastSubscriptionInventoryShortages, buildSubscriptionInventoryForec
9
9
  export { calculateSubscriptionForecastSkus, calculateSubscriptionForecastTotals } from "./subscriptionForecastCalculation.js";
10
10
  export { INVENTORY_MIGRATIONS } from "./sql.js";
11
11
  export { createInventoryBindings } from "./bindings.js";
12
+ export { createPostgresStockAvailability } from "./postgresStockAvailability.js";
@@ -0,0 +1,24 @@
1
+ import type { NewPurchaseStockReadPort } from "./newPurchaseStockAvailability.js";
2
+ interface StockQueryExecutor {
3
+ query(text: string, values?: unknown[]): Promise<{
4
+ rows: Record<string, unknown>[];
5
+ }>;
6
+ }
7
+ export interface NativeStockPromise {
8
+ availableQuantity: number;
9
+ forSaleQuantity: number;
10
+ hardHoldQuantity: number;
11
+ safetyQuantity: number;
12
+ softSubscriptionDemandQuantity: number;
13
+ channelExposureQuantity: number;
14
+ }
15
+ /** Native read-only ATP. The fixed 14-day priority cannot be waived by a new
16
+ * purchase caller. Only the fenced native SQL allocator uses the renewal exemption. */
17
+ export declare function createPostgresStockAvailability(executor: StockQueryExecutor, options: {
18
+ sourceKey: string;
19
+ now: () => string;
20
+ }): {
21
+ readPort: NewPurchaseStockReadPort;
22
+ availableToPromise(sku: string, asOf: string): Promise<NativeStockPromise>;
23
+ };
24
+ export {};
@@ -0,0 +1,48 @@
1
+ function object(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value))
3
+ throw new Error("inventory_stock_response_invalid");
4
+ return value;
5
+ }
6
+ function quantity(value) {
7
+ if ((typeof value !== "number" && typeof value !== "string") || String(value).trim() === "")
8
+ throw new Error("inventory_stock_quantity_invalid");
9
+ const n = Number(value);
10
+ if (!Number.isSafeInteger(n) || n < 0)
11
+ throw new Error("inventory_stock_quantity_invalid");
12
+ return n;
13
+ }
14
+ /** Native read-only ATP. The fixed 14-day priority cannot be waived by a new
15
+ * purchase caller. Only the fenced native SQL allocator uses the renewal exemption. */
16
+ export function createPostgresStockAvailability(executor, options) {
17
+ return {
18
+ async availableToPromise(sku, asOf) {
19
+ const row = object((await executor.query(`SELECT public.inventory_available_to_promise(
20
+ $1::text,$2::text,$3::timestamptz,false) AS response`, [options.sourceKey, sku, asOf])).rows[0]?.response);
21
+ return { availableQuantity: quantity(row.available_quantity), forSaleQuantity: quantity(row.for_sale_quantity),
22
+ hardHoldQuantity: quantity(row.hard_hold_quantity), safetyQuantity: quantity(row.safety_quantity),
23
+ softSubscriptionDemandQuantity: quantity(row.soft_subscription_demand_quantity), channelExposureQuantity: quantity(row.channel_exposure_quantity) };
24
+ },
25
+ readPort: {
26
+ async read(input) {
27
+ if (input.horizonDays !== 14 || input.excludeOrderId !== null)
28
+ throw new Error("native_stock_read_options_unsupported");
29
+ if (!input.skus.length)
30
+ return new Map();
31
+ const value = (await executor.query(`SELECT public.inventory_new_purchase_stock_read(
32
+ $1::text,$2::jsonb,$3::timestamptz) AS response`, [options.sourceKey, JSON.stringify(input.skus), options.now()])).rows[0]?.response;
33
+ if (!Array.isArray(value))
34
+ throw new Error("inventory_stock_response_invalid");
35
+ return new Map(value.map(value => {
36
+ const row = object(value);
37
+ if (typeof row.sku !== "string" || !["provider", "missing"].includes(String(row.source)) || typeof row.stale !== "boolean")
38
+ throw new Error("inventory_stock_response_invalid");
39
+ const result = { sku: row.sku, catalogSkuId: row.catalogSkuId == null ? null : String(row.catalogSkuId),
40
+ source: row.source, providerForSale: row.providerForSale == null ? null : quantity(row.providerForSale),
41
+ activeHolds: quantity(row.activeHolds), safetyStock: quantity(row.safetyStock), available: quantity(row.available), stale: row.stale,
42
+ renewalDemand: quantity(row.renewalDemand), availableForNewPurchases: quantity(row.availableForNewPurchases), excludedOrderHolds: quantity(row.excludedOrderHolds) };
43
+ return [result.sku, result];
44
+ }));
45
+ },
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,5 @@
1
+ import type { InvoiceLine } from "./contracts/facts.js";
2
+ export declare function mapCanonicalIssueLines(linesSnapshot: unknown[], chargedTotalGrossMinor: number, chargedTotalNetMinor: number, defaultLabel: string): Array<Pick<InvoiceLine, "quantity" | "unitGrossMinor" | "unitNetMinor" | "totalGrossMinor" | "totalNetMinor"> & {
3
+ name: string;
4
+ vatRate: string;
5
+ }>;