@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,249 @@
1
+ import type { InvoiceCapabilities, InvoiceDocumentIdentity, InvoiceIdentity, InvoiceWriteResult, InvoiceReadbackResult, InvoiceArtifactResult, InvoiceHostPorts, InvoiceServicesOptions } from "./contracts/index.js";
2
+
3
+ type Provider = NonNullable<InvoiceServicesOptions["provider"]>;
4
+ type Fixture<Input, Result> = { readonly input: Input; readonly expected: Result };
5
+ interface Subject {
6
+ readonly subjectId: string;
7
+ readonly capability: InvoiceCapabilities;
8
+ readonly provider: Provider;
9
+ readonly host?: InvoiceHostPorts;
10
+ /** Adapter-owned offline fixtures; count writes across every injected transport. */
11
+ readonly readWriteCount: () => number;
12
+ readonly sensitiveValues: readonly string[];
13
+ readonly fixtures: {
14
+ readonly issue: readonly (Fixture<Parameters<Provider["issue"]>[0], InvoiceWriteResult> & { readonly replay?: boolean })[];
15
+ readonly correction: readonly (Fixture<Parameters<Provider["correct"]>[0], InvoiceWriteResult> & { readonly replay?: boolean })[];
16
+ readonly readback: readonly Fixture<InvoiceIdentity, InvoiceReadbackResult>[];
17
+ readonly void: readonly Fixture<Parameters<Provider["void"]>[0], Awaited<ReturnType<Provider["void"]>>>[];
18
+ readonly artifact?: readonly Fixture<Parameters<InvoiceHostPorts["artifacts"]["read"]>[0], InvoiceArtifactResult>[];
19
+ readonly clearance?: readonly Fixture<Parameters<InvoiceHostPorts["clearance"]["observe"]>[0], Awaited<ReturnType<InvoiceHostPorts["clearance"]["observe"]>>>[];
20
+ readonly settlement?: readonly Fixture<Parameters<InvoiceHostPorts["settlement"]["record"]>[0], Awaited<ReturnType<InvoiceHostPorts["settlement"]["record"]>>>[];
21
+ readonly companyLookup?: readonly Fixture<Parameters<InvoiceHostPorts["registry"]["lookup"]>[0], Awaited<ReturnType<InvoiceHostPorts["registry"]["lookup"]>>>[];
22
+ };
23
+ }
24
+ class ContractFailure extends Error {}
25
+ function requireFact(value: unknown, code: string): asserts value {
26
+ if (!value) throw new ContractFailure(`INVOICE_CONFORMANCE:${code}`);
27
+ }
28
+ function same(a: unknown, b: unknown): boolean {
29
+ if (a === b) return true;
30
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
31
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
32
+ const left = Object.keys(a), right = Object.keys(b);
33
+ return left.length === right.length && left.every(key => Object.hasOwn(b, key) && same((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));
34
+ }
35
+ function keys(value: object, allowed: readonly string[]): void {
36
+ requireFact(value !== null && typeof value === "object", "proof_required");
37
+ requireFact(Object.keys(value).every(key => allowed.includes(key)), "unexpected_field");
38
+ }
39
+ function proof(value: { evidenceRef: string; fingerprint: string; observedAt: string }, extra: readonly string[] = []): void {
40
+ requireFact(value !== null && typeof value === "object", "proof_required");
41
+ keys(value, ["evidenceRef", "fingerprint", "observedAt", ...extra]);
42
+ requireFact([value.evidenceRef, value.fingerprint, value.observedAt].every(v => typeof v === "string" && v.trim().length > 0), "proof_required");
43
+ }
44
+ function identity(value: InvoiceIdentity | InvoiceDocumentIdentity, expected: InvoiceIdentity | InvoiceDocumentIdentity): void {
45
+ keys(value, "documentRef" in expected ? ["providerRef", "documentRef", "providerDocumentRef", "orderRef", "invoiceRef", "parentProviderDocumentRef"] : ["providerRef", "requestKey", "providerCreateIdentity", "factRevision", "orderRef", "invoiceRef", "operation", "correctionRef", "parentProviderDocumentRef"]);
46
+ requireFact(same(value, expected), "identity_scope");
47
+ }
48
+ function safe(result: unknown, subject: Subject): void {
49
+ const serialized = JSON.stringify(result);
50
+ requireFact(subject.sensitiveValues.every(value => value.length > 0 && !serialized.includes(value) && !serialized.includes(encodeURIComponent(value))), "sensitive_result");
51
+ }
52
+ function documentResult(result: InvoiceWriteResult | InvoiceReadbackResult | Awaited<ReturnType<Provider["void"]>>, expected: InvoiceIdentity, operation: "write" | "read" | "void"): void {
53
+ requireFact(operation === "void" ? result.kind === "unsupported" : operation === "read" ? ["found", "absent", "unknown", "unsupported"].includes(result.kind) : ["issued", "refused", "unknown", "unsupported"].includes(result.kind), "document_result_kind");
54
+ if (result.kind === "unsupported") {
55
+ keys(result, ["kind", "code", "retryable", "effectMayHaveSucceeded"]);
56
+ requireFact(result.retryable === false && result.effectMayHaveSucceeded === false && ["unsupported", "host_binding_required"].includes(result.code), "unsupported_before_write");
57
+ return;
58
+ }
59
+ identity(result.identity, expected);
60
+ if (result.kind === "issued" || result.kind === "found") {
61
+ keys(result, result.kind === "found" ? ["kind", "identity", "providerDocumentRef", "providerDocumentNumber", "proof"] : ["kind", "identity", "providerDocumentRef", "providerDocumentNumber", "proof", "replayed"]);
62
+ proof(result.proof);
63
+ if (result.kind === "issued") requireFact(typeof result.replayed === "boolean", "issued_replay");
64
+ requireFact(result.providerDocumentRef.trim().length > 0 && (result.providerDocumentNumber === null || typeof result.providerDocumentNumber === "string"), "issued_identity");
65
+ } else if (result.kind === "absent") {
66
+ keys(result, ["kind", "identity", "evidence"]); proof(result.evidence, ["exact", "complete", "scope"]);
67
+ requireFact(result.evidence.exact === true && result.evidence.complete === true, "exact_complete_absence");
68
+ identity(result.evidence.scope, expected);
69
+ } else if (result.kind === "unknown") {
70
+ keys(result, ["kind", "identity", "code", "effectMayHaveSucceeded"]);
71
+ requireFact(result.code === "unknown" && (operation === "write" ? "effectMayHaveSucceeded" in result && result.effectMayHaveSucceeded === true : !("effectMayHaveSucceeded" in result)), "unknown_not_absent");
72
+ } else {
73
+ keys(result, ["kind", "identity", "code", "retryable", "effectMayHaveSucceeded"]);
74
+ requireFact(result.kind === "refused" && result.effectMayHaveSucceeded === false && typeof result.retryable === "boolean" && ["blocked", "retryable", "missing_facts", "identity_mismatch", "parent_mismatch", "money_mismatch"].includes(result.code), "refusal_before_write");
75
+ }
76
+ }
77
+ function hostResult(operation: "artifact" | "clearance" | "settlement" | "companyLookup", input: unknown, value: unknown): void {
78
+ const result = value as Record<string, unknown>;
79
+ if (result.kind === "unsupported") {
80
+ keys(result, ["kind", "code", "retryable", "effectMayHaveSucceeded"]);
81
+ requireFact(result.retryable === false && result.effectMayHaveSucceeded === false && (result.code === "unsupported" || result.code === "host_binding_required"), "host_unsupported"); return;
82
+ }
83
+ if (operation === "artifact") {
84
+ const artifact = value as Exclude<InvoiceArtifactResult, { kind: "unsupported" }>, request = input as Parameters<InvoiceHostPorts["artifacts"]["read"]>[0];
85
+ identity(artifact.identity, request.identity);
86
+ if (artifact.kind === "found") {
87
+ keys(artifact, ["kind", "identity", "artifact"]); keys(artifact.artifact, ["kind", "ref", "owner", "mediaType", "proof"]);
88
+ requireFact(artifact.artifact.kind === request.kind && artifact.artifact.ref.length > 0 && artifact.artifact.mediaType.length > 0 && ["provider", "host_storage"].includes(artifact.artifact.owner), "artifact_reference"); proof(artifact.artifact.proof);
89
+ } else if (artifact.kind === "absent") {
90
+ keys(artifact, ["kind", "identity", "evidence"]); proof(artifact.evidence, ["exact", "complete", "scope"]);
91
+ keys(artifact.evidence.scope, ["identity", "kind", "artifactRef"]);
92
+ requireFact(artifact.evidence.exact === true && artifact.evidence.complete === true && artifact.evidence.scope.kind === request.kind && artifact.evidence.scope.artifactRef === null, "artifact_absence"); identity(artifact.evidence.scope.identity, request.identity);
93
+ } else { keys(artifact, ["kind", "identity", "code"]); requireFact(artifact.kind === "unknown" && artifact.code === "unknown", "artifact_unknown"); }
94
+ } else if (operation === "clearance") {
95
+ const observation = value as Awaited<ReturnType<InvoiceHostPorts["clearance"]["observe"]>>;
96
+ if (observation.kind === "observed") {
97
+ keys(observation, ["kind", "observation"]);
98
+ const observed = observation.observation;
99
+ keys(observed, ["state", "reference", "proof"]); proof(observed.proof);
100
+ requireFact(["not_submitted", "pending", "rejected", "accepted", "not_required", "unsupported"].includes(observed.state), "clearance_state");
101
+ requireFact(observed.state === "accepted" ? typeof observed.reference === "string" && observed.reference.trim().length > 0 : observed.reference === null || typeof observed.reference === "string", "clearance_reference");
102
+ } else { keys(observation, ["kind", "code"]); requireFact(observation.kind === "unknown" && observation.code === "unknown", "clearance_unknown"); }
103
+ } else if (operation === "settlement") {
104
+ const item = input as Parameters<InvoiceHostPorts["settlement"]["record"]>[0];
105
+ requireFact([item.grossMinor, item.feeMinor, item.netMinor].every(Number.isSafeInteger) && item.grossMinor - item.feeMinor === item.netMinor, "settlement_money");
106
+ keys(result, ["kind", "itemRef", "settlementItemRef", "replayed", "code"]); requireFact(result.itemRef === item.itemRef, "settlement_item_identity");
107
+ requireFact(["recorded", "conflict", "unknown"].includes(String(result.kind)), "settlement_result");
108
+ if (result.kind === "recorded") requireFact(typeof result.replayed === "boolean" && typeof result.settlementItemRef === "string" && result.settlementItemRef.length > 0, "settlement_receipt");
109
+ if (result.kind === "conflict") requireFact(result.code === "identity_mismatch" || result.code === "money_mismatch", "settlement_conflict");
110
+ if (result.kind === "unknown") requireFact(result.code === "unknown", "settlement_unknown");
111
+ } else {
112
+ const company = value as Exclude<Awaited<ReturnType<InvoiceHostPorts["registry"]["lookup"]>>, { kind: "unsupported" }>;
113
+ keys(result, ["kind", "identifier", "legalName", "registrationIds", "address", "taxRegistrations", "sourceRef", "proof", "code"]);
114
+ const request = input as Parameters<InvoiceHostPorts["registry"]["lookup"]>[0];
115
+ requireFact(company.sourceRef === request.sourceRef, "company_source");
116
+ if (company.kind !== "unknown") requireFact(same(company.identifier, request.identifier), "company_identity");
117
+ if (company.kind === "found" || company.kind === "partial") {
118
+ proof(company.proof);
119
+ for (const registration of company.taxRegistrations) { keys(registration, ["scheme", "status"]); requireFact(["active", "exempt", "not_registered", "unknown"].includes(registration.status), "company_tax_status"); }
120
+ } else if (company.kind === "not_found") proof(company.proof);
121
+ else requireFact(company.kind === "unknown" && company.code === "unknown", "company_unknown");
122
+ }
123
+ }
124
+ /** @consumer Offline provider bridge tests and installed cold consumer. Runner-
125
+ * agnostic contract evidence only; adapters own fixtures, transport and I/O counts.
126
+ * Throws closed failure codes. It neither certifies live fiscal readiness nor logs
127
+ * provider results, secrets, buyer facts or raw adapter errors.
128
+ */
129
+ export async function runInvoiceAdapterConformance(subjects: readonly Subject[]): Promise<{ readonly subjects: readonly { readonly subjectId: string; readonly providerRef: string; readonly cases: readonly string[] }[] }> {
130
+ const evidence: { subjectId: string; providerRef: string; cases: string[] }[] = [];
131
+ try {
132
+ requireFact(subjects.length > 0, "subject_required");
133
+ for (const subject of subjects) {
134
+ const { capability: caps, provider, fixtures } = subject;
135
+ const cases: string[] = [];
136
+ requireFact(caps.defaultOff === true, "default_off");
137
+ requireFact(caps.version === "invoicing.capabilities.v1" && caps.numberingOwner === "provider"
138
+ && [caps.providerRef, caps.fiscalScope.scopeRef, caps.fiscalScope.version, ...Object.values(caps.policies)].every(value => value.trim().length > 0), "capability_scope");
139
+ for (const operation of ["issue", "correction"] as const) {
140
+ const rows = fixtures[operation]; requireFact(rows.length > 0, "write_fixture_required");
141
+ if (caps.operations[operation].supported) requireFact(rows.some(row => row.expected.kind === "issued"), "issued_fixture_required");
142
+ if (caps.operations[operation].supported && caps.operations.recovery.supported) requireFact(rows.some(row => row.replay), "recovery_fixture_required");
143
+ const unknownRecovery = rows.filter(row => row.input.recoveryLookupRequired
144
+ && row.expected.kind === (caps.operations.recovery.supported ? "unknown" : "unsupported"));
145
+ if (caps.operations[operation].supported) {
146
+ requireFact(unknownRecovery.length > 0, "recovery_unknown_fixture_required");
147
+ if (caps.operations.recovery.supported && caps.operations.readback.supported) requireFact(unknownRecovery.every(row => fixtures.readback.some(read => read.expected.kind === "unknown" && same(read.input, row.input.identity))), "recovery_unknown_readback_required");
148
+ }
149
+ for (const row of rows) {
150
+ requireFact(row.input.identity.providerRef === caps.providerRef, "selected_provider");
151
+ const before = subject.readWriteCount();
152
+ const invoke = operation === "issue" ? () => provider.issue(row.input as Parameters<Provider["issue"]>[0]) : () => provider.correct(row.input as Parameters<Provider["correct"]>[0]);
153
+ const result = await invoke(); documentResult(result, row.input.identity, "write"); safe(result, subject);
154
+ if (unknownRecovery.includes(row)) requireFact(result.kind === row.expected.kind && subject.readWriteCount() === before, "recovery_unknown_no_write");
155
+ if (row.input.recoveryLookupRequired === true) {
156
+ if (!caps.operations.recovery.supported) requireFact(result.kind !== "issued" && subject.readWriteCount() === before, "recovery_create_without_absence");
157
+ if (subject.readWriteCount() > before || (result.kind === "issued" && result.replayed === false)) {
158
+ requireFact(caps.operations.recovery.supported && caps.operations.readback.supported && caps.operations.exactAbsence.supported
159
+ && fixtures.readback.some(read => same(read.input, row.input.identity) && read.expected.kind === "absent")
160
+ && !fixtures.readback.some(read => same(read.input, row.input.identity) && (read.expected.kind === "unknown" || read.expected.kind === "unsupported")), "recovery_create_without_absence");
161
+ }
162
+ }
163
+ requireFact(same(result, row.expected), "write_fixture");
164
+ if (!caps.operations[operation].supported) requireFact(result.kind === "unsupported", "declared_unsupported");
165
+ if (operation === "correction" && caps.operations.correction.supported) {
166
+ const request = row.input as Parameters<Provider["correct"]>[0];
167
+ requireFact(request.identity.correctionRef.length > 0 && request.identity.parentProviderDocumentRef.length > 0
168
+ && request.before.totalGrossMinor > 0 && Object.values(request.after).every(value => value === 0), "correction_before_after");
169
+ const parent = fixtures.correction.find(fixture => fixture.expected.kind === "issued" && fixture.input.identity.correctionRef === request.identity.correctionRef);
170
+ if (parent && parent.input.identity.parentProviderDocumentRef !== request.identity.parentProviderDocumentRef) requireFact((result.kind === "refused" || result.kind === "unknown") && subject.readWriteCount() === before, "parent_mismatch_no_write");
171
+ }
172
+ if (result.kind === "unsupported" || result.kind === "refused") requireFact(subject.readWriteCount() === before, "refused_write");
173
+ if (row.replay) {
174
+ requireFact(caps.operations.recovery.supported && result.kind === "issued", "replay_requires_issued");
175
+ const count = subject.readWriteCount();
176
+ const recovered = operation === "issue" ? await provider.issue({ ...row.input as Parameters<Provider["issue"]>[0], recoveryLookupRequired: true }) : await provider.correct({ ...row.input as Parameters<Provider["correct"]>[0], recoveryLookupRequired: true });
177
+ documentResult(recovered, row.input.identity, "write"); safe(recovered, subject);
178
+ requireFact(recovered.kind === "issued" && recovered.replayed === true && recovered.providerDocumentRef === result.providerDocumentRef && recovered.providerDocumentNumber === result.providerDocumentNumber, "recovery_identity");
179
+ requireFact(subject.readWriteCount() === count, "recovery_no_second_write");
180
+ }
181
+ cases.push(operation + ":" + result.kind + (row.replay ? ":recovery" : ""));
182
+ }
183
+ }
184
+ if (caps.operations.correction.supported) {
185
+ const issued = fixtures.correction.find(row => row.expected.kind === "issued");
186
+ requireFact(issued && fixtures.correction.some(row => row.input.recoveryLookupRequired
187
+ && row.input.identity.correctionRef === issued.input.identity.correctionRef
188
+ && row.input.identity.providerCreateIdentity === issued.input.identity.providerCreateIdentity
189
+ && row.input.identity.parentProviderDocumentRef !== issued.input.identity.parentProviderDocumentRef
190
+ && (row.expected.kind === "refused" || row.expected.kind === "unknown")), "correction_parent_fixture_required");
191
+ }
192
+ requireFact(fixtures.readback.length > 0 && fixtures.void.length > 0, "read_void_fixture_required");
193
+ if (caps.operations.readback.supported) requireFact(fixtures.readback.some(row => row.expected.kind === "unknown") && fixtures.readback.some(row => row.expected.kind === "found"), "found_unknown_fixtures_required");
194
+ if (caps.operations.exactAbsence.supported) requireFact(fixtures.readback.some(row => row.expected.kind === "absent"), "absence_fixture_required");
195
+ for (const row of fixtures.readback) {
196
+ const before = subject.readWriteCount(), result = await provider.read(row.input);
197
+ documentResult(result, row.input, "read"); safe(result, subject); requireFact(same(result, row.expected), "read_fixture");
198
+ requireFact(subject.readWriteCount() === before, "read_no_write");
199
+ if (!caps.operations.readback.supported) requireFact(result.kind === "unsupported", "declared_unsupported");
200
+ if (result.kind === "absent") requireFact(caps.operations.exactAbsence.supported, "absence_capability");
201
+ cases.push("read:" + result.kind);
202
+ }
203
+ requireFact(caps.operations.remoteVoid.supported === false && caps.operations.remoteVoid.reason === "not_implemented", "remote_void_cut");
204
+ for (const row of fixtures.void) {
205
+ const before = subject.readWriteCount(), result = await provider.void(row.input);
206
+ documentResult(result, row.input, "void"); safe(result, subject); requireFact(same(result, row.expected), "void_fixture");
207
+ requireFact(subject.readWriteCount() === before, "void_refused_write");
208
+ cases.push("void:" + result.kind);
209
+ }
210
+ const settlementReceipts = new Map<string, string>();
211
+ const settlementRows = fixtures.settlement ?? [];
212
+ const distinctPayoutRows = settlementRows.filter(row => row.expected.kind === "recorded" && settlementRows.some(other => other.expected.kind === "recorded"
213
+ && row.input.itemRef !== other.input.itemRef && row.input.providerRef === other.input.providerRef
214
+ && row.input.batchRef === other.input.batchRef && row.input.providerPaymentRef === other.input.providerPaymentRef));
215
+ if (caps.operations.settlement.supported) requireFact(distinctPayoutRows.length >= 2, "distinct_payout_fixtures_required");
216
+ // Optional host-owned capabilities require their own fixtures when declared.
217
+ for (const operation of ["artifact", "clearance", "settlement", "companyLookup"] as const) {
218
+ const rows = fixtures[operation] ?? [];
219
+ if (caps.operations[operation].supported) requireFact(subject.host && rows.some(row => ["found", "partial", "recorded", "observed"].includes(row.expected.kind) && !("observation" in row.expected && row.expected.observation.state === "unsupported")), "host_fixture_required");
220
+ for (const row of rows) {
221
+ requireFact(subject.host, "host_required"); const before = subject.readWriteCount();
222
+ const result = operation === "artifact" ? await subject.host.artifacts.read(row.input as Parameters<InvoiceHostPorts["artifacts"]["read"]>[0])
223
+ : operation === "clearance" ? await subject.host.clearance.observe(row.input as Parameters<InvoiceHostPorts["clearance"]["observe"]>[0])
224
+ : operation === "settlement" ? await subject.host.settlement.record(row.input as Parameters<InvoiceHostPorts["settlement"]["record"]>[0])
225
+ : await subject.host.registry.lookup(row.input as Parameters<InvoiceHostPorts["registry"]["lookup"]>[0]);
226
+ hostResult(operation, row.input, result);
227
+ if (operation === "settlement" && distinctPayoutRows.some(fixture => fixture === row)) requireFact(result.kind === "recorded", "distinct_payout_items");
228
+ if (operation === "settlement" && "kind" in result && result.kind === "recorded") {
229
+ const input = row.input as Parameters<InvoiceHostPorts["settlement"]["record"]>[0];
230
+ const item = JSON.stringify([input.providerRef, input.batchRef, input.itemRef]);
231
+ requireFact(!settlementReceipts.has(result.settlementItemRef) || settlementReceipts.get(result.settlementItemRef) === item, "distinct_payout_items");
232
+ settlementReceipts.set(result.settlementItemRef, item);
233
+ }
234
+ safe(result, subject); requireFact(same(result, row.expected), "host_fixture");
235
+ if (result.kind === "unsupported") requireFact(subject.readWriteCount() === before, "host_unsupported_write");
236
+ if (operation !== "settlement") requireFact(subject.readWriteCount() === before, "host_read_no_write");
237
+ if (!caps.operations[operation].supported) requireFact((result.kind === "unsupported" || ("observation" in result && result.observation.state === "unsupported")) && subject.readWriteCount() === before, "host_unsupported_before_write");
238
+ cases.push(operation);
239
+ }
240
+ }
241
+ const row = { subjectId: subject.subjectId, providerRef: caps.providerRef, cases };
242
+ safe(row, subject); evidence.push(row);
243
+ }
244
+ return { subjects: evidence };
245
+ } catch (error) {
246
+ if (error instanceof ContractFailure) throw error;
247
+ throw new ContractFailure("INVOICE_CONFORMANCE:adapter_threw");
248
+ }
249
+ }
@@ -0,0 +1,19 @@
1
+ const MAX_INVOICE_PDF_BYTES = 25 * 1024 * 1024;
2
+
3
+ /** Validate transient bytes; this is neither storage proof nor delivery proof. */
4
+ export function assertInvoicePdf(pdf: { content: Uint8Array; contentType: string }): void {
5
+ const contentType = pdf.contentType.split(";", 1)[0]?.trim().toLowerCase();
6
+ if (contentType !== "application/pdf") throw new Error("invoice_pdf_content_type_invalid");
7
+ if (!pdf.content || pdf.content.byteLength === 0) throw new Error("invoice_pdf_empty");
8
+ if (pdf.content.byteLength > MAX_INVOICE_PDF_BYTES) throw new Error("invoice_pdf_too_large");
9
+ if (String.fromCharCode(...pdf.content.subarray(0, 5)) !== "%PDF-") {
10
+ throw new Error("invoice_pdf_signature_invalid");
11
+ }
12
+ }
13
+
14
+ /** The host supplies its existing fallback caption. No locale/content default. */
15
+ export function invoicePdfFilename(reference: string, fallback = "invoice"): string {
16
+ const safe = (value: string) => value.normalize("NFKD")
17
+ .replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
18
+ return `${safe(reference) || safe(fallback) || "invoice"}.pdf`;
19
+ }
@@ -0,0 +1,80 @@
1
+ import type { InvoiceServicesOptions } from "./contracts/host.js";
2
+ import type { InvoiceUnsupported } from "./contracts/provider.js";
3
+ import { createIndependentInvoiceDutyBinding } from "./continuity.js";
4
+ import { runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
5
+ import { runInvoiceDeliveryJob } from "./delivery.js";
6
+ import { syncAcceptedInvoiceDocuments } from "./documentSync.js";
7
+ import { syncInvoicePayoutSettlements } from "./settlementSync.js";
8
+ import { createInvoiceReadModel } from "./readModel.js";
9
+ import { readInvoiceCapabilities } from "./capabilities.js";
10
+
11
+ type Operation = "issue" | "correction" | "delivery" | "documentSync" | "settlementSync" | "collectDuties" | "history";
12
+ type Ports = Pick<InvoiceServicesOptions, "host"> & Required<{
13
+ provider: NonNullable<InvoiceServicesOptions["provider"]>;
14
+ store: NonNullable<InvoiceServicesOptions["store"]>;
15
+ capability: NonNullable<InvoiceServicesOptions["capability"]>;
16
+ }>;
17
+ type Refusal = InvoiceUnsupported | { readonly kind: "refused"; readonly code: "disabled" | "selection_required"; readonly retryable: false; readonly effectMayHaveSucceeded: false };
18
+ /** Host factories bind existing workers/readers, never another executor or cron. */
19
+ export interface InvoiceServices<Result = unknown> {
20
+ readiness(): Promise<{ readonly kind: "ready" } | Refusal>;
21
+ run(operation: Operation): Promise<Result | Refusal>;
22
+ }
23
+ /** Explicit selection and host readiness precede factory construction and claim.
24
+ * Native jobs use the installed module helpers; transport-specific host facades
25
+ * may supply factories. Auth, invocation scope and leases stay host-owned.
26
+ */
27
+ export function createInvoiceServices<Result = unknown>(options: InvoiceServicesOptions & {
28
+ readonly factories?: Partial<Record<Operation, (ports: Ports) => () => Promise<Result>>>;
29
+ readonly jobs?: {
30
+ readonly issue?: Omit<Parameters<typeof runNativeInvoiceIssueJob>[0], "store" | "provider" | "money" | "providerRef">;
31
+ readonly delivery?: { readonly claimOptions: Parameters<NonNullable<InvoiceServicesOptions["store"]>["claimDeliveries"]>[0]; filename(claim: Exclude<Awaited<ReturnType<NonNullable<InvoiceServicesOptions["store"]>["claimDeliveries"]>>, InvoiceUnsupported>[number]): string };
32
+ readonly documentSync?: Parameters<typeof syncAcceptedInvoiceDocuments>[0];
33
+ readonly settlementSync?: Omit<Parameters<typeof syncInvoicePayoutSettlements>[0], "settlement">;
34
+ readonly history?: Parameters<ReturnType<typeof createInvoiceReadModel>["history"]>[0];
35
+ readonly collectDuties?: { readonly limit?: number };
36
+ };
37
+ }): InvoiceServices<Result> {
38
+ const { enabled, selection, capability, provider, store, host, readiness: observe } = options;
39
+ const factories = { ...options.factories };
40
+ const unsupported = { kind: "unsupported", code: "host_binding_required", retryable: false, effectMayHaveSucceeded: false } as const;
41
+ const readiness: InvoiceServices<Result>["readiness"] = async () => {
42
+ const selected = readInvoiceCapabilities({ enabled, selection, capability });
43
+ if (selected.kind !== "ready") return selected;
44
+ if (!provider || !store) return unsupported;
45
+ return observe();
46
+ };
47
+ return {
48
+ readiness,
49
+ async run(operation) {
50
+ const state = await readiness();
51
+ if (state.kind !== "ready") return state;
52
+ const factory = factories[operation];
53
+ const capabilityKey = operation === "documentSync" ? "readback"
54
+ : operation === "settlementSync" ? "settlement" : operation;
55
+ if ((capabilityKey === "issue" || capabilityKey === "correction" || capabilityKey === "readback" || capabilityKey === "settlement")
56
+ && !capability!.operations[capabilityKey].supported) return unsupported;
57
+ if (factory) return factory({ provider: provider!, store: store!, capability: capability!, host })();
58
+ const jobs = options.jobs;
59
+ // Inputs are explicit host selections. No operation acquires a host run lease.
60
+ if (operation === "collectDuties" && jobs?.collectDuties) return createIndependentInvoiceDutyBinding({ ...jobs.collectDuties, store: store! }).collect() as Promise<Result>;
61
+ // The native claim batch includes issue and correction/readback duties.
62
+ if (operation === "issue" && jobs?.issue && (!capability!.operations.correction.supported || !capability!.operations.readback.supported)) return unsupported;
63
+ if (operation === "issue" && jobs?.issue) return runNativeInvoiceIssueJob({ ...jobs.issue, store: store!, provider: provider!, money: host.money, providerRef: selection!.providerRef }) as Promise<Result>;
64
+ if (operation === "delivery" && jobs?.delivery) return runInvoiceDeliveryJob({
65
+ enabled: true, providerEnabled: true, deliveryEnabled: true, store: store!, ...jobs.delivery,
66
+ download: async claim => {
67
+ const artifact = await host.artifacts.read({ identity: claim.identity, kind: "pdf" });
68
+ if (artifact.kind !== "found") throw new Error("invoicing_artifact_unavailable");
69
+ const result = await host.artifacts.download({ identity: claim.identity, artifactRef: artifact.artifact.ref });
70
+ if (result.kind !== "downloaded") throw new Error("invoicing_artifact_unavailable");
71
+ return result;
72
+ }, send: input => host.delivery.send(input),
73
+ }) as Promise<Result>;
74
+ if (operation === "documentSync" && jobs?.documentSync) return syncAcceptedInvoiceDocuments(jobs.documentSync) as Promise<Result>;
75
+ if (operation === "settlementSync" && jobs?.settlementSync) return syncInvoicePayoutSettlements({ ...jobs.settlementSync, settlement: host.settlement }) as Promise<Result>;
76
+ if (operation === "history" && jobs?.history) return createInvoiceReadModel({ accountHistory: host.accountHistory }).history(jobs.history) as Promise<Result>;
77
+ return unsupported;
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,50 @@
1
+ type ChannelInvoiceAction = "issue" | "channel_issues" | "suppress";
2
+
3
+ export type InvoicePolicyLookup =
4
+ | { by: "order"; orderUuid: string }
5
+ | { by: "fulfillmentOrder"; fulfillmentOrderId: string };
6
+
7
+ /** Reads the resolved sales-source invoice action for either producer key.
8
+ * Read failures propagate to the caller.
9
+ */
10
+ export interface OrderInvoicePolicyReader {
11
+ readOrderInvoicePolicy(lookup: InvoicePolicyLookup): Promise<ChannelInvoiceAction>;
12
+ }
13
+
14
+ export interface ChannelInvoiceDecision {
15
+ issue: boolean;
16
+ action: ChannelInvoiceAction;
17
+ detail: Record<string, unknown>;
18
+ }
19
+
20
+ const ISSUE: ChannelInvoiceDecision = { issue: true, action: "issue", detail: {} };
21
+
22
+ function refusal(action: ChannelInvoiceAction): ChannelInvoiceDecision {
23
+ return {
24
+ issue: false,
25
+ action,
26
+ detail: action === "channel_issues"
27
+ // The far side mints the document. Stamped with the same key an order
28
+ // metadata write would have used, so a later wave that gains a metadata
29
+ // path moves the value rather than renaming it.
30
+ ? { invoicePolicy: action, invoiceIssuedBy: "channel", accountingInvoiceSkipped: action }
31
+ // The operator declared that no sales document is produced at all.
32
+ : { invoicePolicy: action, accountingInvoiceSkipped: action },
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Whether this deployment should request the sales document for an order.
38
+ *
39
+ * An ABSENT reader answers `issue`. Composition roots that predate this wave —
40
+ * and every test that builds a producer without one — keep today's behaviour
41
+ * exactly, which is what makes the storefront path provably unchanged.
42
+ */
43
+ export async function decideChannelInvoice(
44
+ reader: OrderInvoicePolicyReader | undefined,
45
+ lookup: InvoicePolicyLookup,
46
+ ): Promise<ChannelInvoiceDecision> {
47
+ if (!reader) return ISSUE;
48
+ const action = await reader.readOrderInvoicePolicy(lookup);
49
+ return action === "issue" ? ISSUE : refusal(action);
50
+ }
@@ -0,0 +1,15 @@
1
+ import { collectInvoiceDuties } from "./dutyCollector.js";
2
+
3
+ /** Explicit independent host invocation, never an inline fulfillment/payment hook.
4
+ * Durable source receipts and fact leases own replay, backoff and fault ceilings.
5
+ * A collector outage remains visible and can be retried by the host later.
6
+ * @consumer Reference native accounting runtime and installed cold consumer.
7
+ */
8
+ export function createIndependentInvoiceDutyBinding(input: Parameters<typeof collectInvoiceDuties>[0]) {
9
+ return {
10
+ async collect(): Promise<Awaited<ReturnType<typeof collectInvoiceDuties>> | { kind: "unavailable"; code: "retryable" }> {
11
+ try { return await collectInvoiceDuties(input); }
12
+ catch { return { kind: "unavailable", code: "retryable" }; }
13
+ },
14
+ };
15
+ }
@@ -48,7 +48,7 @@ export interface InvoiceFacts {
48
48
  readonly clearanceRequired: boolean;
49
49
  readonly currency: string;
50
50
  readonly buyer: InvoiceBuyer;
51
- /** Supplied separately for execution; P8 freezes it at first claim before provider I/O. */
51
+ /** Claim projection only: the first durable claim freezes the seller; legacy obligations retain execution-time fallback. */
52
52
  readonly seller?: InvoiceSeller;
53
53
  readonly payment: { readonly intentRef: string; readonly providerRef: string | null; readonly providerPaymentRef: string | null; readonly amountMinor: number; readonly currency: string; readonly localSettlementState: "matched" | "unavailable" };
54
54
  readonly orderMoney: { readonly subtotalMinor: number; readonly discountMinor: number; readonly shippingMinor: number; readonly shippingDiscountMinor: number; readonly totalMinor: number } | null;
@@ -58,3 +58,14 @@ export interface InvoiceFacts {
58
58
  readonly totalGrossMinor: number;
59
59
  readonly chargedTotalGrossMinor: number;
60
60
  }
61
+
62
+ /** Host-selected immutable revision; it contains no provider or country defaults. */
63
+ export interface InvoiceSellerRevision<Seller = InvoiceSeller> {
64
+ readonly seller: Seller;
65
+ readonly revisionRef: string;
66
+ }
67
+ /** Added only to returned claim facts, never to installed historical revisions. */
68
+ export interface InvoiceClaimedFacts extends InvoiceFacts {
69
+ readonly sellerRevision?: string;
70
+ readonly sellerMode?: "frozen" | "legacy_fallback";
71
+ }
@@ -12,8 +12,8 @@ import type { InvoiceStorePort } from "./store.js";
12
12
  export interface InvoiceHostPorts {
13
13
  facts: { read(orderId: string): Promise<{ readonly kind: "found"; readonly facts: InvoiceFacts } | { readonly kind: "unknown"; readonly code: "missing_facts" } | InvoiceUnsupported> };
14
14
  /** Read the current host seller for the next claim, never per duty. Null orderId
15
- * means an unscoped claim (including corrections). P8 freezes this seller at
16
- * first claim; revisionRef feeds the claim wrapper's revision argument.
15
+ * means an unscoped claim (including corrections). The durable wrapper freezes this seller at
16
+ * the first-ever claim; revisionRef feeds the claim wrapper's revision argument.
17
17
  */
18
18
  seller: { read(input: { readonly orderId: string | null }): Promise<{ readonly kind: "found"; readonly seller: InvoiceSeller; readonly revisionRef: string } | { readonly kind: "unknown"; readonly code: "unknown" } | InvoiceUnsupported> };
19
19
  /** Read-only projection of the final claimed M07 identity, never a refund writer.
@@ -71,8 +71,7 @@ export interface InvoiceHostPorts {
71
71
  resolveDownload(input: { readonly accountRef: string; readonly orderId: string; readonly documentRef: string }): Promise<{ readonly kind: "authorized"; readonly identity: InvoiceDocumentIdentity; readonly artifactRef: string; readonly fileName: string; readonly parentDocumentRef: string | null } | { readonly kind: "denied" } | { readonly kind: "unknown"; readonly code: "unknown" } | InvoiceUnsupported>;
72
72
  };
73
73
  }
74
- /** Future service composition inputs only; no constructor, worker or runtime route
75
- * is supplied in P1. Closures/credentials must never be serialized with options.
74
+ /** Explicit service composition inputs; the host retains worker and route authority. Closures/credentials must never be serialized with options.
76
75
  */
77
76
  export interface InvoiceServicesOptions {
78
77
  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: { readonly before: InvoiceFacts; readonly reason: string; readonly after: { readonly totalNetMinor: 0; readonly totalTaxMinor: 0; readonly totalGrossMinor: 0 } } | null;
20
20
  }
@@ -40,14 +40,15 @@ export interface InvoiceDutyReadback {
40
40
  }
41
41
  /** Every operation is one local transaction; commit before external I/O. Native
42
42
  * success atomically saves late identity and compensation; stale catch writes nothing.
43
- * Managed and native stores are bound to the selected provider at construction.
43
+ * Compatibility stores bind provider selection at construction; the structural
44
+ * native store accepts it per claim and persists the first provider on the duty.
44
45
  * No implementation, executor or migration is installed by these declarations.
45
46
  */
46
47
  export interface InvoiceStorePort {
47
48
  /** Never call inside a renewal money, dunning or dispatch transaction without
48
49
  * isolation: capture failure must not propagate; the sweep recovers.
49
50
  */
50
- captureDuty(input: { readonly requestKey: string; readonly sourceKind: "paid" | "handoff"; readonly sourceId: string; readonly orderId: string; readonly occurredAt: string; readonly evidence: { readonly settlementIntentId: string; readonly amountMinor: number; readonly currency: string } | { readonly shipmentId: string; readonly handedOverAt: string } }): Promise<{ readonly dutyId: string; readonly status: InvoiceDutyStatus } | InvoiceUnsupported>;
51
+ captureDuty(input: { readonly requestKey: string; readonly sourceKind: "paid" | "handoff"; readonly sourceId: string; readonly orderId: string; readonly occurredAt: string; readonly evidence: { readonly settlementIntentId: string; readonly amountMinor: number; readonly currency: string } | { readonly shipmentId: string; readonly handedOverAt: string } }): Promise<{ readonly dutyId: string; readonly status: InvoiceDutyStatus } | { readonly kind: "conflict"; readonly code: "identity_mismatch" } | InvoiceUnsupported>;
51
52
  installFacts(input: { readonly dutyId: string; readonly facts: InvoiceFacts }): Promise<{ readonly dutyId: string; readonly status: InvoiceDutyStatus; readonly replayed?: boolean } | InvoiceUnsupported>;
52
53
  /** Pinned split facts lease: sweep and prepare commit separately; finishing
53
54
  * requires the same token and authoritative document/fact match. No host I/O
@@ -58,7 +59,7 @@ export interface InvoiceStorePort {
58
59
  finishFacts(input: { readonly dutyId: string; readonly token: string; readonly result: Omit<Exclude<Awaited<ReturnType<InvoiceStorePort["prepareFacts"]>>, InvoiceUnsupported>, "status"> & { readonly status: "owed" | "ready" | "issued" | "voided" | "unsupported" } }): Promise<{ readonly dutyId: string; readonly status: "owed" | "ready" | "issued" | "voided" | "unsupported" | "faulted" | "stale" } | InvoiceUnsupported>;
59
60
  readDuties(orderId: string): Promise<InvoiceDutyReadback | InvoiceUnsupported>;
60
61
  /** Filter order before locking/limit; never lease unrelated orders then filter.
61
- * P8 reads the injected host seller before calling the first-claim SQL wrapper;
62
+ * P7 host composition reads the injected seller before the separate first-claim SQL wrapper;
62
63
  * its revisionRef supplies p_revision, without requiring a duty from the caller.
63
64
  */
64
65
  claim(input: { readonly limit: number; readonly visibilitySeconds: number; readonly providerRef: string; readonly orderId?: string | null }): Promise<readonly InvoiceClaim[] | InvoiceUnsupported>;
@@ -80,7 +81,7 @@ export interface InvoiceStorePort {
80
81
  * Missing means no M09 duty, never invoice absence: retain/retry the reversal
81
82
  * intent and route legacy documents to the incumbent path.
82
83
  */
83
- requestReversal(input: { readonly requestKey: string; readonly orderId: string; readonly reason: "order_canceled" | "order_refunded"; readonly proof: InvoiceProof }): Promise<{ readonly kind: "missing" | "voided" | "pending_readback" | "correction_requested"; readonly dutyId?: string; readonly replayed?: boolean } | { readonly kind: "unsupported_recorded"; readonly dutyId?: string; readonly replayed?: boolean } | InvoiceUnsupported>;
84
+ requestReversal(input: { readonly requestKey: string; readonly orderId: string; readonly reason: "order_canceled" | "order_refunded"; readonly proof: InvoiceProof }): Promise<{ readonly kind: "missing" | "voided" | "pending_readback" | "correction_requested"; readonly dutyId?: string; readonly replayed?: boolean } | { readonly kind: "unsupported_recorded"; readonly dutyId?: string; readonly replayed?: boolean } | { readonly kind: "conflict"; readonly code: "identity_mismatch" } | InvoiceUnsupported>;
84
85
  /** Existing local-only request remains distinct from fiscal issuance. */
85
86
  requestLocalDocument(input: { readonly requestKey: string; readonly orderId: string; readonly providerRef: string }): Promise<{ readonly kind: "requested"; readonly documentId: string; readonly invoiceRef: string; readonly status: string; readonly replayed: boolean } | InvoiceUnsupported>;
86
87
  listClearanceTargets(limit: number): Promise<readonly { readonly documentId: string; readonly identity: InvoiceDocumentIdentity; readonly providerDocumentRef: string; readonly providerDocumentNumber: string | null; readonly state: InvoiceClearanceObservation["state"] }[] | InvoiceUnsupported>;
@@ -92,7 +93,10 @@ export interface InvoiceStorePort {
92
93
  claimDeliveries(input: { readonly limit: number; readonly visibilitySeconds: number; readonly requireClearance: boolean; readonly orderId?: string | null }): Promise<readonly { readonly token: string; readonly generation: number; readonly identity: InvoiceDocumentIdentity; readonly providerDocumentRef: string; readonly delivery: Omit<Parameters<InvoiceDeliveryPort["send"]>[0], "filename" | "content" | "contentType"> }[] | InvoiceUnsupported>;
93
94
  /** Native uncertain settlement passes p_receipt NULL and retains a non-null
94
95
  * receiptRef hint in p_evidence; managed uses it as the provider message id.
95
- * Managed failed_before_send uses retrySeconds for backoff; native ignores it.
96
+ * Managed failed_before_send uses the retrySeconds RPC argument. Native
97
+ * failed_before_send carries { phase: 'before_send', retryable, retrySeconds }
98
+ * in p_evidence: whole seconds clamp to 60..86400, absent/invalid uses 900.
99
+ * retryable:false parks at infinity, with failed status and last_error visible.
96
100
  */
97
101
  settleDelivery(input: { readonly deliveryRef: string; readonly token: string; readonly generation: number; readonly providerDocumentRef: string; readonly retrySeconds: number; readonly result: Exclude<Awaited<ReturnType<InvoiceDeliveryPort["send"]>>, InvoiceUnsupported> }): Promise<{ readonly kind: "recorded" | "stale"; readonly replayed: boolean } | InvoiceUnsupported>;
98
102
  }
@@ -0,0 +1,28 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+
3
+ /** Inert descriptors for the host's existing registry and executor. No registration. */
4
+ export function createInvoiceContribution(input: {
5
+ version: string;
6
+ requiredSchema: RequiredSchema;
7
+ invoicing: Readonly<Record<string, ((...args: never[]) => unknown) | null>>;
8
+ handlers: Contribution["handlers"];
9
+ schedules: Contribution["schedules"];
10
+ routes: Contribution["routes"];
11
+ }) {
12
+ const contribution: Contribution = {
13
+ manifest: {
14
+ name: "@openlup/platform", version: input.version, kind: "rail",
15
+ module: { id: "invoicing", kind: "capability", provides: ["./invoicing"], dependsOn: [] },
16
+ emits: [], handles: input.handlers.map(handler => handler.eventType), env: [],
17
+ schedules: input.schedules.map(({ id, cadenceSeconds }) => ({ id, cadenceSeconds })),
18
+ requiredPorts: Object.keys(input.invoicing), routes: input.routes.map(route => route.id), requiredSchema: input.requiredSchema,
19
+ },
20
+ handlers: input.handlers, schedules: input.schedules, routes: input.routes,
21
+ };
22
+ return {
23
+ contributions: [contribution],
24
+ ports: Object.fromEntries(Object.entries(input.invoicing).filter(([, fn]) => typeof fn === "function").map(([name, fn]) => [`@openlup/platform/invoicing:${name}`, fn])),
25
+ routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/invoicing:${route.id}`, route.handle])),
26
+ triggers: Object.fromEntries(input.schedules.map(schedule => [`@openlup/platform/invoicing:${schedule.id}`, schedule.run])),
27
+ };
28
+ }