@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
@@ -0,0 +1,69 @@
1
+ import { createIndependentInvoiceDutyBinding } from "./continuity.js";
2
+ import { runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
3
+ import { runInvoiceDeliveryJob } from "./delivery.js";
4
+ import { syncAcceptedInvoiceDocuments } from "./documentSync.js";
5
+ import { syncInvoicePayoutSettlements } from "./settlementSync.js";
6
+ import { createInvoiceReadModel } from "./readModel.js";
7
+ import { readInvoiceCapabilities } from "./capabilities.js";
8
+ /** Explicit selection and host readiness precede factory construction and claim.
9
+ * Native jobs use the installed module helpers; transport-specific host facades
10
+ * may supply factories. Auth, invocation scope and leases stay host-owned.
11
+ */
12
+ export function createInvoiceServices(options) {
13
+ const { enabled, selection, capability, provider, store, host, readiness: observe } = options;
14
+ const factories = { ...options.factories };
15
+ const unsupported = { kind: "unsupported", code: "host_binding_required", retryable: false, effectMayHaveSucceeded: false };
16
+ const readiness = async () => {
17
+ const selected = readInvoiceCapabilities({ enabled, selection, capability });
18
+ if (selected.kind !== "ready")
19
+ return selected;
20
+ if (!provider || !store)
21
+ return unsupported;
22
+ return observe();
23
+ };
24
+ return {
25
+ readiness,
26
+ async run(operation) {
27
+ const state = await readiness();
28
+ if (state.kind !== "ready")
29
+ return state;
30
+ const factory = factories[operation];
31
+ const capabilityKey = operation === "documentSync" ? "readback"
32
+ : operation === "settlementSync" ? "settlement" : operation;
33
+ if ((capabilityKey === "issue" || capabilityKey === "correction" || capabilityKey === "readback" || capabilityKey === "settlement")
34
+ && !capability.operations[capabilityKey].supported)
35
+ return unsupported;
36
+ if (factory)
37
+ return factory({ provider: provider, store: store, capability: capability, host })();
38
+ const jobs = options.jobs;
39
+ // Inputs are explicit host selections. No operation acquires a host run lease.
40
+ if (operation === "collectDuties" && jobs?.collectDuties)
41
+ return createIndependentInvoiceDutyBinding({ ...jobs.collectDuties, store: store }).collect();
42
+ // The native claim batch includes issue and correction/readback duties.
43
+ if (operation === "issue" && jobs?.issue && (!capability.operations.correction.supported || !capability.operations.readback.supported))
44
+ return unsupported;
45
+ if (operation === "issue" && jobs?.issue)
46
+ return runNativeInvoiceIssueJob({ ...jobs.issue, store: store, provider: provider, money: host.money, providerRef: selection.providerRef });
47
+ if (operation === "delivery" && jobs?.delivery)
48
+ return runInvoiceDeliveryJob({
49
+ enabled: true, providerEnabled: true, deliveryEnabled: true, store: store, ...jobs.delivery,
50
+ download: async (claim) => {
51
+ const artifact = await host.artifacts.read({ identity: claim.identity, kind: "pdf" });
52
+ if (artifact.kind !== "found")
53
+ throw new Error("invoicing_artifact_unavailable");
54
+ const result = await host.artifacts.download({ identity: claim.identity, artifactRef: artifact.artifact.ref });
55
+ if (result.kind !== "downloaded")
56
+ throw new Error("invoicing_artifact_unavailable");
57
+ return result;
58
+ }, send: input => host.delivery.send(input),
59
+ });
60
+ if (operation === "documentSync" && jobs?.documentSync)
61
+ return syncAcceptedInvoiceDocuments(jobs.documentSync);
62
+ if (operation === "settlementSync" && jobs?.settlementSync)
63
+ return syncInvoicePayoutSettlements({ ...jobs.settlementSync, settlement: host.settlement });
64
+ if (operation === "history" && jobs?.history)
65
+ return createInvoiceReadModel({ accountHistory: host.accountHistory }).history(jobs.history);
66
+ return unsupported;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,28 @@
1
+ type ChannelInvoiceAction = "issue" | "channel_issues" | "suppress";
2
+ export type InvoicePolicyLookup = {
3
+ by: "order";
4
+ orderUuid: string;
5
+ } | {
6
+ by: "fulfillmentOrder";
7
+ fulfillmentOrderId: string;
8
+ };
9
+ /** Reads the resolved sales-source invoice action for either producer key.
10
+ * Read failures propagate to the caller.
11
+ */
12
+ export interface OrderInvoicePolicyReader {
13
+ readOrderInvoicePolicy(lookup: InvoicePolicyLookup): Promise<ChannelInvoiceAction>;
14
+ }
15
+ export interface ChannelInvoiceDecision {
16
+ issue: boolean;
17
+ action: ChannelInvoiceAction;
18
+ detail: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Whether this deployment should request the sales document for an order.
22
+ *
23
+ * An ABSENT reader answers `issue`. Composition roots that predate this wave —
24
+ * and every test that builds a producer without one — keep today's behaviour
25
+ * exactly, which is what makes the storefront path provably unchanged.
26
+ */
27
+ export declare function decideChannelInvoice(reader: OrderInvoicePolicyReader | undefined, lookup: InvoicePolicyLookup): Promise<ChannelInvoiceDecision>;
28
+ export {};
@@ -0,0 +1,27 @@
1
+ const ISSUE = { issue: true, action: "issue", detail: {} };
2
+ function refusal(action) {
3
+ return {
4
+ issue: false,
5
+ action,
6
+ detail: action === "channel_issues"
7
+ // The far side mints the document. Stamped with the same key an order
8
+ // metadata write would have used, so a later wave that gains a metadata
9
+ // path moves the value rather than renaming it.
10
+ ? { invoicePolicy: action, invoiceIssuedBy: "channel", accountingInvoiceSkipped: action }
11
+ // The operator declared that no sales document is produced at all.
12
+ : { invoicePolicy: action, accountingInvoiceSkipped: action },
13
+ };
14
+ }
15
+ /**
16
+ * Whether this deployment should request the sales document for an order.
17
+ *
18
+ * An ABSENT reader answers `issue`. Composition roots that predate this wave —
19
+ * and every test that builds a producer without one — keep today's behaviour
20
+ * exactly, which is what makes the storefront path provably unchanged.
21
+ */
22
+ export async function decideChannelInvoice(reader, lookup) {
23
+ if (!reader)
24
+ return ISSUE;
25
+ const action = await reader.readOrderInvoicePolicy(lookup);
26
+ return action === "issue" ? ISSUE : refusal(action);
27
+ }
@@ -0,0 +1,12 @@
1
+ import { collectInvoiceDuties } from "./dutyCollector.js";
2
+ /** Explicit independent host invocation, never an inline fulfillment/payment hook.
3
+ * Durable source receipts and fact leases own replay, backoff and fault ceilings.
4
+ * A collector outage remains visible and can be retried by the host later.
5
+ * @consumer Reference native accounting runtime and installed cold consumer.
6
+ */
7
+ export declare function createIndependentInvoiceDutyBinding(input: Parameters<typeof collectInvoiceDuties>[0]): {
8
+ collect(): Promise<Awaited<ReturnType<typeof collectInvoiceDuties>> | {
9
+ kind: "unavailable";
10
+ code: "retryable";
11
+ }>;
12
+ };
@@ -0,0 +1,18 @@
1
+ import { collectInvoiceDuties } from "./dutyCollector.js";
2
+ /** Explicit independent host invocation, never an inline fulfillment/payment hook.
3
+ * Durable source receipts and fact leases own replay, backoff and fault ceilings.
4
+ * A collector outage remains visible and can be retried by the host later.
5
+ * @consumer Reference native accounting runtime and installed cold consumer.
6
+ */
7
+ export function createIndependentInvoiceDutyBinding(input) {
8
+ return {
9
+ async collect() {
10
+ try {
11
+ return await collectInvoiceDuties(input);
12
+ }
13
+ catch {
14
+ return { kind: "unavailable", code: "retryable" };
15
+ }
16
+ },
17
+ };
18
+ }
@@ -75,7 +75,7 @@ export interface InvoiceFacts {
75
75
  readonly clearanceRequired: boolean;
76
76
  readonly currency: string;
77
77
  readonly buyer: InvoiceBuyer;
78
- /** Supplied separately for execution; P8 freezes it at first claim before provider I/O. */
78
+ /** Claim projection only: the first durable claim freezes the seller; legacy obligations retain execution-time fallback. */
79
79
  readonly seller?: InvoiceSeller;
80
80
  readonly payment: {
81
81
  readonly intentRef: string;
@@ -98,3 +98,13 @@ export interface InvoiceFacts {
98
98
  readonly totalGrossMinor: number;
99
99
  readonly chargedTotalGrossMinor: number;
100
100
  }
101
+ /** Host-selected immutable revision; it contains no provider or country defaults. */
102
+ export interface InvoiceSellerRevision<Seller = InvoiceSeller> {
103
+ readonly seller: Seller;
104
+ readonly revisionRef: string;
105
+ }
106
+ /** Added only to returned claim facts, never to installed historical revisions. */
107
+ export interface InvoiceClaimedFacts extends InvoiceFacts {
108
+ readonly sellerRevision?: string;
109
+ readonly sellerMode?: "frozen" | "legacy_fallback";
110
+ }
@@ -20,8 +20,8 @@ export interface InvoiceHostPorts {
20
20
  } | InvoiceUnsupported>;
21
21
  };
22
22
  /** Read the current host seller for the next claim, never per duty. Null orderId
23
- * means an unscoped claim (including corrections). P8 freezes this seller at
24
- * first claim; revisionRef feeds the claim wrapper's revision argument.
23
+ * means an unscoped claim (including corrections). The durable wrapper freezes this seller at
24
+ * the first-ever claim; revisionRef feeds the claim wrapper's revision argument.
25
25
  */
26
26
  seller: {
27
27
  read(input: {
@@ -228,8 +228,7 @@ export interface InvoiceHostPorts {
228
228
  } | InvoiceUnsupported>;
229
229
  };
230
230
  }
231
- /** Future service composition inputs only; no constructor, worker or runtime route
232
- * is supplied in P1. Closures/credentials must never be serialized with options.
231
+ /** Explicit service composition inputs; the host retains worker and route authority. Closures/credentials must never be serialized with options.
233
232
  */
234
233
  export interface InvoiceServicesOptions {
235
234
  readonly enabled?: boolean;
@@ -1,5 +1,5 @@
1
1
  /** @consumer Frozen fact provider and installed declaration consumer. */
2
- export type { InvoiceFacts, InvoiceLine, InvoiceSeller, InvoiceBuyer } from "./facts.js";
2
+ export type { InvoiceFacts, InvoiceLine, InvoiceSeller, InvoiceBuyer, InvoiceSellerRevision, InvoiceClaimedFacts } from "./facts.js";
3
3
  /** @consumer A4 issue/correction bridge, adapters and operator proof readback. */
4
4
  export type { InvoiceIdentity, InvoiceDocumentIdentity, InvoiceWriteResult, InvoiceReadbackResult, InvoiceProviderPort, InvoiceUnsupported, InvoiceDiagnosticCode, InvoiceProof } from "./provider.js";
5
5
  /** @consumer Host artifact storage/download binding. */
@@ -1,5 +1,5 @@
1
1
  import type { InvoiceHostPorts } from "./host.js";
2
- import type { InvoiceFacts } from "./facts.js";
2
+ import type { InvoiceFacts, InvoiceClaimedFacts } from "./facts.js";
3
3
  import type { InvoiceIdentity, InvoiceDocumentIdentity, InvoiceProof, InvoiceUnsupported, InvoiceWriteResult, InvoiceReadbackResult } from "./provider.js";
4
4
  import type { InvoiceClearanceObservation, InvoiceArtifactResult, InvoiceDeliveryPort } from "./delivery.js";
5
5
  type InvoiceDutyStatus = "owed" | "blocked" | "ready" | "processing" | "unknown" | "issued" | "voided" | "unsupported" | "faulted";
@@ -14,7 +14,7 @@ export interface InvoiceClaim {
14
14
  readonly generation: number;
15
15
  readonly leaseUntil: string;
16
16
  readonly identity: InvoiceIdentity;
17
- readonly facts: InvoiceFacts;
17
+ readonly facts: InvoiceClaimedFacts;
18
18
  readonly action: "create" | "readback";
19
19
  readonly correction: {
20
20
  readonly before: InvoiceFacts;
@@ -53,7 +53,8 @@ export interface InvoiceDutyReadback {
53
53
  }
54
54
  /** Every operation is one local transaction; commit before external I/O. Native
55
55
  * success atomically saves late identity and compensation; stale catch writes nothing.
56
- * Managed and native stores are bound to the selected provider at construction.
56
+ * Compatibility stores bind provider selection at construction; the structural
57
+ * native store accepts it per claim and persists the first provider on the duty.
57
58
  * No implementation, executor or migration is installed by these declarations.
58
59
  */
59
60
  export interface InvoiceStorePort {
@@ -77,6 +78,9 @@ export interface InvoiceStorePort {
77
78
  }): Promise<{
78
79
  readonly dutyId: string;
79
80
  readonly status: InvoiceDutyStatus;
81
+ } | {
82
+ readonly kind: "conflict";
83
+ readonly code: "identity_mismatch";
80
84
  } | InvoiceUnsupported>;
81
85
  installFacts(input: {
82
86
  readonly dutyId: string;
@@ -118,7 +122,7 @@ export interface InvoiceStorePort {
118
122
  } | InvoiceUnsupported>;
119
123
  readDuties(orderId: string): Promise<InvoiceDutyReadback | InvoiceUnsupported>;
120
124
  /** Filter order before locking/limit; never lease unrelated orders then filter.
121
- * P8 reads the injected host seller before calling the first-claim SQL wrapper;
125
+ * P7 host composition reads the injected seller before the separate first-claim SQL wrapper;
122
126
  * its revisionRef supplies p_revision, without requiring a duty from the caller.
123
127
  */
124
128
  claim(input: {
@@ -194,6 +198,9 @@ export interface InvoiceStorePort {
194
198
  readonly kind: "unsupported_recorded";
195
199
  readonly dutyId?: string;
196
200
  readonly replayed?: boolean;
201
+ } | {
202
+ readonly kind: "conflict";
203
+ readonly code: "identity_mismatch";
197
204
  } | InvoiceUnsupported>;
198
205
  /** Existing local-only request remains distinct from fiscal issuance. */
199
206
  requestLocalDocument(input: {
@@ -250,7 +257,10 @@ export interface InvoiceStorePort {
250
257
  }[] | InvoiceUnsupported>;
251
258
  /** Native uncertain settlement passes p_receipt NULL and retains a non-null
252
259
  * receiptRef hint in p_evidence; managed uses it as the provider message id.
253
- * Managed failed_before_send uses retrySeconds for backoff; native ignores it.
260
+ * Managed failed_before_send uses the retrySeconds RPC argument. Native
261
+ * failed_before_send carries { phase: 'before_send', retryable, retrySeconds }
262
+ * in p_evidence: whole seconds clamp to 60..86400, absent/invalid uses 900.
263
+ * retryable:false parks at infinity, with failed status and last_error visible.
254
264
  */
255
265
  settleDelivery(input: {
256
266
  readonly deliveryRef: string;
@@ -0,0 +1,21 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+ /** Inert descriptors for the host's existing registry and executor. No registration. */
3
+ export declare function createInvoiceContribution(input: {
4
+ version: string;
5
+ requiredSchema: RequiredSchema;
6
+ invoicing: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
7
+ handlers: Contribution["handlers"];
8
+ schedules: Contribution["schedules"];
9
+ routes: Contribution["routes"];
10
+ }): {
11
+ contributions: Contribution[];
12
+ ports: {
13
+ [k: string]: ((...args: never[]) => unknown) | null;
14
+ };
15
+ routes: {
16
+ [k: string]: (...args: never[]) => unknown;
17
+ };
18
+ triggers: {
19
+ [k: string]: (...args: never[]) => unknown;
20
+ };
21
+ };
@@ -0,0 +1,19 @@
1
+ /** Inert descriptors for the host's existing registry and executor. No registration. */
2
+ export function createInvoiceContribution(input) {
3
+ const contribution = {
4
+ manifest: {
5
+ name: "@openlup/platform", version: input.version, kind: "rail",
6
+ module: { id: "invoicing", kind: "capability", provides: ["./invoicing"], dependsOn: [] },
7
+ emits: [], handles: input.handlers.map(handler => handler.eventType), env: [],
8
+ schedules: input.schedules.map(({ id, cadenceSeconds }) => ({ id, cadenceSeconds })),
9
+ requiredPorts: Object.keys(input.invoicing), routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
10
+ },
11
+ handlers: input.handlers, schedules: input.schedules, routes: input.routes,
12
+ };
13
+ return {
14
+ contributions: [contribution],
15
+ ports: Object.fromEntries(Object.entries(input.invoicing).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/invoicing:${name}`, fn])),
16
+ routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/invoicing:${route.id}`, route.handle])),
17
+ triggers: Object.fromEntries(input.schedules.map(schedule => [`@openlup/platform/invoicing:${schedule.id}`, schedule.run])),
18
+ };
19
+ }
@@ -0,0 +1,42 @@
1
+ import type { InvoiceWriteResult } from "./contracts/provider.js";
2
+ /** Durable correction identity, recovery and claim fencing from the donor worker.
3
+ * Hosts project immutable BEFORE facts and bind a conforming recovery provider.
4
+ * @consumer server/domains/accounting/accountingJobService.ts
5
+ */
6
+ export declare function runInvoiceCorrectionJob<Claim extends {
7
+ outboxId: string;
8
+ attemptCount: number;
9
+ providerInvoiceId: string;
10
+ correctionReason: string;
11
+ payload: Record<string, unknown>;
12
+ }, Snapshot, Result extends Pick<Extract<InvoiceWriteResult, {
13
+ kind: "issued";
14
+ }>, "providerDocumentRef" | "providerDocumentNumber">>(input: {
15
+ createEnabled: boolean;
16
+ providerEnabled: boolean;
17
+ claim(): Promise<readonly Claim[]>;
18
+ project(claim: Claim): Snapshot;
19
+ /** Resolve accepted/recovered receipts only; unknown is never an ACK or absence. */
20
+ create(snapshot: Snapshot, options: {
21
+ correctionReason: string;
22
+ correctedProviderInvoiceId: string;
23
+ correctionRef: string;
24
+ recoveryLookupRequired: boolean;
25
+ }): Promise<Result>;
26
+ finish(claim: Claim, result: Result): Promise<unknown>;
27
+ isClaimFenceError(error: unknown): boolean;
28
+ fail(claim: Claim, error: unknown, retrySeconds: number): Promise<unknown>;
29
+ }): Promise<{
30
+ ok: boolean;
31
+ checked: number;
32
+ updated: number;
33
+ skipped: boolean;
34
+ reason: string;
35
+ failures: number;
36
+ } | {
37
+ ok: boolean;
38
+ checked: number;
39
+ updated: number;
40
+ skipped: boolean;
41
+ failures: number;
42
+ }>;
@@ -0,0 +1,44 @@
1
+ /** Durable correction identity, recovery and claim fencing from the donor worker.
2
+ * Hosts project immutable BEFORE facts and bind a conforming recovery provider.
3
+ * @consumer server/domains/accounting/accountingJobService.ts
4
+ */
5
+ export async function runInvoiceCorrectionJob(input) {
6
+ if (!input.createEnabled)
7
+ return skipped("create_disabled");
8
+ if (!input.providerEnabled)
9
+ return skipped("provider_disabled");
10
+ const claims = await input.claim();
11
+ let updated = 0;
12
+ let failures = 0;
13
+ for (const claim of claims) {
14
+ try {
15
+ const correctionRef = `accounting-correction:${claim.outboxId}`;
16
+ if (claim.payload.providerCreateIdentity !== correctionRef) {
17
+ throw new Error("accounting_correction_recovery_identity_missing");
18
+ }
19
+ const snapshot = input.project(claim);
20
+ const result = await input.create(snapshot, {
21
+ correctionReason: claim.correctionReason,
22
+ correctedProviderInvoiceId: claim.providerInvoiceId,
23
+ correctionRef,
24
+ recoveryLookupRequired: claim.attemptCount > 1,
25
+ });
26
+ await input.finish(claim, result);
27
+ updated += 1;
28
+ }
29
+ catch (error) {
30
+ failures += 1;
31
+ if (input.isClaimFenceError(error))
32
+ continue;
33
+ try {
34
+ await input.fail(claim, error, Math.min(3600, 300 * Math.max(1, claim.attemptCount)));
35
+ }
36
+ catch (failureError) {
37
+ if (!input.isClaimFenceError(failureError))
38
+ throw failureError;
39
+ }
40
+ }
41
+ }
42
+ return { ok: failures === 0, checked: claims.length, updated, skipped: false, failures };
43
+ }
44
+ function skipped(reason) { return { ok: true, checked: 0, updated: 0, skipped: true, reason, failures: 0 }; }
@@ -0,0 +1,85 @@
1
+ import type { InvoiceUnsupported } from "./contracts/provider.js";
2
+ import type { InvoiceStorePort } from "./contracts/store.js";
3
+ import type { InvoiceDeliveryPort } from "./contracts/delivery.js";
4
+ type DeliveryOutcome = Omit<Extract<Awaited<ReturnType<InvoiceDeliveryPort["send"]>>, {
5
+ kind: "provider_accepted";
6
+ }>, "proof"> | Exclude<Awaited<ReturnType<InvoiceDeliveryPort["send"]>>, {
7
+ kind: "provider_accepted";
8
+ }>;
9
+ /** Donor sequencing over host claims, original keys, M04 transport and fenced
10
+ * finalizers. Native hosts MUST bind terminalFailure before leasing work.
11
+ * With CTL-M09-3's 191000 pin, failedBeforeSend settles failed_before_send with
12
+ * evidence { phase: 'before_send', retrySeconds }: whole seconds clamped
13
+ * to 60..86400, absent/invalid uses 900;
14
+ * terminalFailure uses { phase: 'before_send', retryable: false } for no reclaim.
15
+ * Native store mode refuses before claim unless its host projection is bound.
16
+ * @consumer Host delivery facade and explicit native M04 binding.
17
+ */
18
+ type DeliveryJobInput<Claim, Result extends DeliveryOutcome> = {
19
+ enabled: boolean;
20
+ providerEnabled: boolean;
21
+ deliveryEnabled: boolean;
22
+ claim(): Promise<readonly Claim[] | InvoiceUnsupported>;
23
+ attemptCount(claim: Claim): number;
24
+ validate(claim: Claim): void;
25
+ download(claim: Claim): Promise<{
26
+ content: Uint8Array;
27
+ contentType: string;
28
+ }>;
29
+ filename(claim: Claim): string;
30
+ /** A rejected promise proves failure BEFORE transport entry. Once entered,
31
+ * the M04 binding must return uncertain, including timeout/lost ACK. */
32
+ send(claim: Claim, document: {
33
+ filename: string;
34
+ content: Uint8Array;
35
+ contentType: string;
36
+ }): Promise<Result>;
37
+ failedBeforeSend(claim: Claim, error: unknown, retrySeconds: number): Promise<unknown>;
38
+ /** Must durably stop reclaim using the same fence. Native failed_before_send
39
+ * evidence { phase: 'before_send', retryable: false } sets available_at to infinity. */
40
+ terminalFailure(claim: Claim, result: Extract<Result, {
41
+ kind: "failed_before_send" | "unsupported";
42
+ }>): Promise<unknown>;
43
+ uncertain(claim: Claim, result: Extract<Result, {
44
+ kind: "uncertain";
45
+ }>): Promise<unknown>;
46
+ accepted(claim: Claim, result: Extract<Result, {
47
+ kind: "provider_accepted";
48
+ }>): Promise<unknown>;
49
+ finalizeUncertain(claim: Claim, receiptRef: string): Promise<unknown>;
50
+ };
51
+ type NativeClaim = Exclude<Awaited<ReturnType<InvoiceStorePort["claimDeliveries"]>>, InvoiceUnsupported>[number];
52
+ type NativeDeliveryJobInput = {
53
+ enabled: boolean;
54
+ providerEnabled: boolean;
55
+ deliveryEnabled: boolean;
56
+ store: Pick<InvoiceStorePort, "claimDeliveries" | "settleDelivery">;
57
+ claimOptions: Parameters<InvoiceStorePort["claimDeliveries"]>[0];
58
+ download(claim: NativeClaim): Promise<{
59
+ content: Uint8Array;
60
+ contentType: string;
61
+ }>;
62
+ filename(claim: NativeClaim): string;
63
+ send: InvoiceDeliveryPort["send"];
64
+ };
65
+ /** Host callback mode preserves donor sequencing. Native store mode supplies
66
+ * fenced terminal/backoff finalizers and the M04 envelope from claimDeliveries.
67
+ * @consumer Host delivery facade and explicit native M04 binding.
68
+ */
69
+ export declare function runInvoiceDeliveryJob(input: NativeDeliveryJobInput): ReturnType<typeof runDeliveryJob>;
70
+ export declare function runInvoiceDeliveryJob<Claim, Result extends DeliveryOutcome>(input: DeliveryJobInput<Claim, Result>): ReturnType<typeof runDeliveryJob>;
71
+ declare function runDeliveryJob<Claim, Result extends DeliveryOutcome>(input: DeliveryJobInput<Claim, Result>): Promise<{
72
+ ok: boolean;
73
+ checked: number;
74
+ updated: number;
75
+ skipped: boolean;
76
+ reason: string;
77
+ failures: number;
78
+ } | {
79
+ ok: boolean;
80
+ checked: number;
81
+ updated: number;
82
+ skipped: boolean;
83
+ failures: number;
84
+ }>;
85
+ export {};
@@ -0,0 +1,79 @@
1
+ import { assertInvoicePdf } from "./artifacts.js";
2
+ export function runInvoiceDeliveryJob(input) {
3
+ if (!("store" in input))
4
+ return runDeliveryJob(input);
5
+ const settle = async (claim, result, retrySeconds = 900) => {
6
+ const outcome = await input.store.settleDelivery({ deliveryRef: claim.delivery.deliveryRef, token: claim.token,
7
+ generation: claim.generation, providerDocumentRef: claim.providerDocumentRef, retrySeconds, result });
8
+ if (outcome.kind !== "recorded")
9
+ throw new Error("invoicing_delivery_fence");
10
+ };
11
+ return runDeliveryJob({
12
+ enabled: input.enabled, providerEnabled: input.providerEnabled, deliveryEnabled: input.deliveryEnabled,
13
+ claim: () => input.store.claimDeliveries(input.claimOptions), attemptCount: claim => claim.generation,
14
+ validate: () => { }, download: input.download, filename: input.filename,
15
+ send: (claim, document) => input.send({ ...claim.delivery, ...document }),
16
+ failedBeforeSend: (claim, _error, retrySeconds) => settle(claim, { kind: "failed_before_send", retryable: true, code: "retryable" }, retrySeconds),
17
+ terminalFailure: (claim, result) => settle(claim, result.kind === "failed_before_send" ? result
18
+ : { kind: "failed_before_send", retryable: false, code: "retryable" }),
19
+ uncertain: (claim, result) => settle(claim, result), accepted: (claim, result) => settle(claim, result),
20
+ finalizeUncertain: (claim, receiptRef) => settle(claim, { kind: "uncertain", code: "uncertain_delivery", receiptRef }),
21
+ });
22
+ }
23
+ async function runDeliveryJob(input) {
24
+ if (!input.enabled)
25
+ return skipped("provider_email_disabled");
26
+ if (!input.providerEnabled)
27
+ return skipped("provider_disabled");
28
+ if (!input.deliveryEnabled)
29
+ return skipped("email_delivery_provider_disabled");
30
+ // JS hosts missing a mandatory terminal policy must refuse before claiming.
31
+ if (typeof input.terminalFailure !== "function")
32
+ return skipped("host_binding_required");
33
+ const claims = await input.claim();
34
+ if (!Array.isArray(claims))
35
+ return skipped("host_binding_required");
36
+ let updated = 0;
37
+ let failures = 0;
38
+ for (const claim of claims) {
39
+ let result;
40
+ const retrySeconds = Math.min(3600, 300 * Math.max(1, input.attemptCount(claim)));
41
+ try {
42
+ input.validate(claim);
43
+ const pdf = await input.download(claim);
44
+ assertInvoicePdf(pdf);
45
+ result = await input.send(claim, { ...pdf, filename: input.filename(claim) });
46
+ }
47
+ catch (error) {
48
+ failures += 1;
49
+ await input.failedBeforeSend(claim, error, retrySeconds);
50
+ continue;
51
+ }
52
+ if (result.kind === "failed_before_send" || result.kind === "unsupported") {
53
+ failures += 1;
54
+ if (result.kind === "failed_before_send" && result.retryable) {
55
+ await input.failedBeforeSend(claim, result, retrySeconds);
56
+ }
57
+ else {
58
+ await input.terminalFailure(claim, result);
59
+ }
60
+ continue;
61
+ }
62
+ if (result.kind === "uncertain") {
63
+ failures += 1;
64
+ await input.uncertain(claim, result);
65
+ continue;
66
+ }
67
+ try {
68
+ await input.accepted(claim, result);
69
+ updated += 1;
70
+ }
71
+ catch {
72
+ failures += 1;
73
+ // Acceptance plus a lost local ACK cannot become a retryable send.
74
+ await input.finalizeUncertain(claim, result.receiptRef).catch(() => undefined);
75
+ }
76
+ }
77
+ return { ok: failures === 0, checked: claims.length, updated, skipped: false, failures };
78
+ }
79
+ function skipped(reason) { return { ok: true, checked: 0, updated: 0, skipped: true, reason, failures: 0 }; }
@@ -0,0 +1,50 @@
1
+ type InvoiceDocumentRole = "original" | "correction" | "replacement";
2
+ type InvoiceDocumentArtifact = "invoice" | "correction";
3
+ type InvoiceDocumentEmailState = "not_requested" | "pending" | "provider_accepted" | "failed";
4
+ type InvoiceDocumentHistoryRow = Record<string, unknown>;
5
+ interface InvoiceDocumentHistoryEntry {
6
+ documentKey: string;
7
+ invoiceId: string;
8
+ parentDocumentRef: string | null;
9
+ orderId: string;
10
+ invoiceRef: string;
11
+ role: InvoiceDocumentRole;
12
+ artifact: InvoiceDocumentArtifact;
13
+ status: string;
14
+ blockedReason: string | null;
15
+ documentKind: string | null;
16
+ providerKind: string | null;
17
+ providerDocumentRef: string | null;
18
+ providerDocumentNumber: string | null;
19
+ clearanceReference: string | null;
20
+ clearanceState: string;
21
+ totalGrossMinor: number | null;
22
+ /**
23
+ * The currency the document was **issued in**, copied from
24
+ * `accounting_invoices.currency` (`NOT NULL CHECK (char_length = 3)`).
25
+ *
26
+ * It travels with the amount rather than being resolved by whoever renders it. An
27
+ * issued invoice is the most historical money the platform holds: it is a document
28
+ * of record whose denomination was fixed the day it was written, and a reader that
29
+ * asked the current settlement profile instead would re-label the customer's own
30
+ * accounting the first time a deployment changed what it sells in.
31
+ */
32
+ currency: string;
33
+ issuedAt: string | null;
34
+ createdAt: string;
35
+ isCurrent: boolean;
36
+ downloadAvailable: boolean;
37
+ emailState: InvoiceDocumentEmailState;
38
+ }
39
+ interface InvoiceDocumentHistory {
40
+ current: InvoiceDocumentHistoryEntry | null;
41
+ documents: InvoiceDocumentHistoryEntry[];
42
+ }
43
+ interface ResolveInvoiceDocumentHistoryInput {
44
+ invoices: InvoiceDocumentHistoryRow[];
45
+ operations?: InvoiceDocumentHistoryRow[];
46
+ deliveryOutboxes?: InvoiceDocumentHistoryRow[];
47
+ providerArtifactEligible?: (providerKind: string) => boolean;
48
+ }
49
+ export declare function resolveInvoiceDocumentHistory({ invoices, operations, deliveryOutboxes, providerArtifactEligible, }: ResolveInvoiceDocumentHistoryInput): InvoiceDocumentHistory;
50
+ export {};