@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,213 @@
1
+ const CURRENT_STATUSES = new Set(["issued", "accepted"]);
2
+ const HISTORICAL_INVOICE_STATUSES = new Set(["issued", "accepted", "corrected"]);
3
+ export function resolveInvoiceDocumentHistory({ invoices, operations = [], deliveryOutboxes = [], providerArtifactEligible = () => true, }) {
4
+ const orderedInvoices = [...invoices].sort(compareRowsChronologically);
5
+ const correctionOperations = operations.filter((row) => text(row.operation) === "correction_issued");
6
+ const activeInvoice = [...orderedInvoices]
7
+ .filter((row) => currentInvoiceEligible(row, providerArtifactEligible))
8
+ .sort(compareRowsNewestFirst)[0] ?? null;
9
+ const activeInvoiceId = activeInvoice ? text(activeInvoice.id) : null;
10
+ const documents = [];
11
+ for (const invoice of orderedInvoices) {
12
+ const invoiceId = text(invoice.id);
13
+ const replacement = nullableText(invoice.correction_of_invoice_id) !== null;
14
+ const providerDocumentRef = nullableText(invoice.provider_document_ref);
15
+ documents.push({
16
+ documentKey: `invoice:${invoiceId}`,
17
+ invoiceId,
18
+ parentDocumentRef: replacement ? `invoice:${nullableText(invoice.correction_of_invoice_id)}` : null,
19
+ orderId: text(invoice.order_id),
20
+ invoiceRef: text(invoice.invoice_ref),
21
+ role: replacement ? "replacement" : "original",
22
+ artifact: "invoice",
23
+ status: text(invoice.status),
24
+ blockedReason: nullableText(invoice.blocked_reason),
25
+ documentKind: nullableText(invoice.document_kind),
26
+ providerKind: nullableText(invoice.provider_kind),
27
+ providerDocumentRef,
28
+ providerDocumentNumber: nullableText(invoice.provider_document_number),
29
+ clearanceReference: nullableText(invoice.clearance_reference),
30
+ clearanceState: text(invoice.clearance_state),
31
+ totalGrossMinor: nonnegativeNumber(invoice.total_gross_minor),
32
+ currency: text(invoice.currency),
33
+ issuedAt: nullableText(invoice.created_at),
34
+ createdAt: text(invoice.created_at),
35
+ isCurrent: invoiceId === activeInvoiceId,
36
+ downloadAvailable: historicalInvoiceDownloadEligible(invoice, providerArtifactEligible),
37
+ emailState: resolveEmailState({
38
+ invoice,
39
+ providerDocumentRef,
40
+ operations,
41
+ deliveryOutboxes,
42
+ preferInvoiceFallback: !correctionProviderDocumentRef(invoice),
43
+ }),
44
+ });
45
+ const correctionProviderId = correctionProviderDocumentRef(invoice);
46
+ if (!correctionProviderId)
47
+ continue;
48
+ const correctionOperation = correctionOperations
49
+ .filter((row) => text(row.invoice_id) === invoiceId)
50
+ .sort(compareRowsNewestFirst)[0] ?? null;
51
+ const correctionNumber = correctionProviderDocumentNumber(invoice);
52
+ const correctionCreatedAt = nullableText(correctionOperation?.created_at)
53
+ ?? laterTimestamp(text(invoice.updated_at), text(invoice.created_at));
54
+ const providerKind = nullableText(invoice.provider_kind);
55
+ const persistedCorrectionStatus = normalize(invoice.correction_status);
56
+ const correctionStatus = ["issued", "accepted"].includes(persistedCorrectionStatus)
57
+ ? persistedCorrectionStatus
58
+ : "not_ready";
59
+ documents.push({
60
+ documentKey: `correction:${invoiceId}`,
61
+ invoiceId,
62
+ parentDocumentRef: `invoice:${invoiceId}`,
63
+ orderId: text(invoice.order_id),
64
+ invoiceRef: correctionNumber ?? `correction-${text(invoice.invoice_ref)}`.slice(0, 160),
65
+ role: "correction",
66
+ artifact: "correction",
67
+ status: correctionStatus,
68
+ blockedReason: nullableText(invoice.blocked_reason),
69
+ documentKind: "correction",
70
+ providerKind,
71
+ providerDocumentRef: correctionProviderId,
72
+ providerDocumentNumber: correctionNumber,
73
+ clearanceReference: null,
74
+ clearanceState: "not_required",
75
+ totalGrossMinor: null,
76
+ // A correction has no total of its own on this row, but it is a document of the
77
+ // same invoice and therefore of the same currency. Stating it keeps the field
78
+ // total, so a consumer never has to ask "which document has a currency?".
79
+ currency: text(invoice.currency),
80
+ issuedAt: correctionCreatedAt || null,
81
+ createdAt: correctionCreatedAt,
82
+ isCurrent: false,
83
+ downloadAvailable: Boolean(providerKind
84
+ && providerArtifactEligible(providerKind)
85
+ && nullableText(invoice.blocked_reason) === null
86
+ && ["issued", "accepted"].includes(correctionStatus)),
87
+ emailState: resolveEmailState({
88
+ invoice,
89
+ providerDocumentRef: correctionProviderId,
90
+ operations,
91
+ deliveryOutboxes,
92
+ preferInvoiceFallback: true,
93
+ }),
94
+ });
95
+ }
96
+ documents.sort(compareDocumentsChronologically);
97
+ return {
98
+ current: documents.find((document) => document.isCurrent) ?? null,
99
+ documents,
100
+ };
101
+ }
102
+ function currentInvoiceEligible(row, providerArtifactEligible) {
103
+ const providerKind = nullableText(row.provider_kind);
104
+ return Boolean(providerKind
105
+ && providerArtifactEligible(providerKind)
106
+ && nullableText(row.provider_document_ref)
107
+ && nullableText(row.blocked_reason) === null
108
+ && CURRENT_STATUSES.has(normalize(row.status)));
109
+ }
110
+ function historicalInvoiceDownloadEligible(row, providerArtifactEligible) {
111
+ const providerKind = nullableText(row.provider_kind);
112
+ return Boolean(providerKind
113
+ && providerArtifactEligible(providerKind)
114
+ && nullableText(row.provider_document_ref)
115
+ && nullableText(row.blocked_reason) === null
116
+ && HISTORICAL_INVOICE_STATUSES.has(normalize(row.status)));
117
+ }
118
+ function resolveEmailState({ invoice, providerDocumentRef, operations, deliveryOutboxes, preferInvoiceFallback, }) {
119
+ if (!providerDocumentRef)
120
+ return "not_requested";
121
+ const invoiceId = text(invoice.id);
122
+ const accepted = operations.some((row) => {
123
+ if (text(row.invoice_id) !== invoiceId || text(row.operation) !== "provider_email_sent")
124
+ return false;
125
+ const payloadProviderId = nullableText(record(row.payload).providerDocumentRef);
126
+ return (payloadProviderId ?? nullableText(row.provider_ref)) === providerDocumentRef;
127
+ });
128
+ if (accepted)
129
+ return "provider_accepted";
130
+ const outbox = deliveryOutboxes
131
+ .filter((row) => text(row.invoice_id) === invoiceId)
132
+ .filter((row) => {
133
+ const targetProviderId = nullableText(record(row.metadata).providerDocumentRef);
134
+ return targetProviderId ? targetProviderId === providerDocumentRef : preferInvoiceFallback;
135
+ })
136
+ .sort(compareRowsNewestFirst)[0];
137
+ const outboxStatus = normalize(outbox?.status);
138
+ if (["failed", "uncertain", "cancelled"].includes(outboxStatus))
139
+ return "failed";
140
+ if (["pending", "processing", "queued"].includes(outboxStatus))
141
+ return "pending";
142
+ if (outboxStatus === "succeeded")
143
+ return "provider_accepted";
144
+ if (preferInvoiceFallback) {
145
+ const emailStatus = normalize(invoice.email_status);
146
+ if (emailStatus === "failed")
147
+ return "failed";
148
+ if (emailStatus === "pending")
149
+ return "pending";
150
+ if (emailStatus === "sent")
151
+ return "provider_accepted";
152
+ }
153
+ return "not_requested";
154
+ }
155
+ function correctionProviderDocumentRef(row) {
156
+ return nullableText(record(row.metadata).correctionProviderDocumentRef);
157
+ }
158
+ function correctionProviderDocumentNumber(row) {
159
+ return nullableText(record(row.metadata).correctionProviderDocumentNumber);
160
+ }
161
+ function compareRowsChronologically(a, b) {
162
+ return compareTimestamps(a.created_at, b.created_at) || text(a.id).localeCompare(text(b.id));
163
+ }
164
+ function compareRowsNewestFirst(a, b) {
165
+ return compareTimestamps(b.created_at ?? b.updated_at, a.created_at ?? a.updated_at) || text(b.id).localeCompare(text(a.id));
166
+ }
167
+ function compareDocumentsChronologically(a, b) {
168
+ return compareTimestamps(a.createdAt, b.createdAt)
169
+ || documentRolePriority(a.role) - documentRolePriority(b.role)
170
+ || a.documentKey.localeCompare(b.documentKey);
171
+ }
172
+ function documentRolePriority(role) {
173
+ if (role === "original")
174
+ return 0;
175
+ if (role === "correction")
176
+ return 1;
177
+ return 2;
178
+ }
179
+ function laterTimestamp(a, b) {
180
+ const aTime = Date.parse(a);
181
+ const bTime = Date.parse(b);
182
+ if (Number.isFinite(aTime) && Number.isFinite(bTime))
183
+ return aTime >= bTime ? a : b;
184
+ return a || b;
185
+ }
186
+ function compareTimestamps(a, b) {
187
+ const aTime = Date.parse(text(a));
188
+ const bTime = Date.parse(text(b));
189
+ if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime)
190
+ return aTime - bTime;
191
+ if (Number.isFinite(aTime) && !Number.isFinite(bTime))
192
+ return -1;
193
+ if (!Number.isFinite(aTime) && Number.isFinite(bTime))
194
+ return 1;
195
+ return 0;
196
+ }
197
+ function nonnegativeNumber(value) {
198
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
199
+ }
200
+ function record(value) {
201
+ return value && typeof value === "object" && !Array.isArray(value)
202
+ ? value
203
+ : {};
204
+ }
205
+ function normalize(value) {
206
+ return text(value).trim().toLowerCase();
207
+ }
208
+ function text(value) {
209
+ return typeof value === "string" ? value : "";
210
+ }
211
+ function nullableText(value) {
212
+ return typeof value === "string" && value.trim() ? value.trim() : null;
213
+ }
@@ -0,0 +1,24 @@
1
+ /** Direct donor extraction: host supplies attachment kinds, durable provider
2
+ * references, status/event keys, hashing and recording. Downloads confer no blob
3
+ * storage proof. The optional observation mode retains polling without adding
4
+ * another public runner/export or provider-specific status vocabulary. */
5
+ export declare function syncAcceptedInvoiceDocuments<Target, Status, Document>(input: {
6
+ target: Target;
7
+ status: Status;
8
+ downloads: readonly ((target: Target) => Promise<Document>)[];
9
+ record(target: Target, status: Status, documents: readonly Document[]): Promise<unknown>;
10
+ }): Promise<void>;
11
+ export declare function syncAcceptedInvoiceDocuments<Target, Status, Document>(input: {
12
+ targets(): Promise<readonly Target[]>;
13
+ observe(target: Target): Promise<Status>;
14
+ recordStatus(target: Target, status: Status): Promise<unknown>;
15
+ accepted(status: Status): boolean;
16
+ downloads: readonly ((target: Target) => Promise<Document>)[];
17
+ record(target: Target, status: Status, documents: readonly Document[]): Promise<unknown>;
18
+ }): Promise<{
19
+ ok: boolean;
20
+ checked: number;
21
+ updated: number;
22
+ skipped: false;
23
+ failures: number;
24
+ }>;
@@ -0,0 +1,26 @@
1
+ export async function syncAcceptedInvoiceDocuments(input) {
2
+ const sync = async (target, status) => {
3
+ const documents = await Promise.all(input.downloads.map(download => download(target)));
4
+ await input.record(target, status, documents);
5
+ };
6
+ if (!input.targets) {
7
+ await sync(input.target, input.status);
8
+ return;
9
+ }
10
+ const targets = await input.targets();
11
+ let updated = 0;
12
+ let failures = 0;
13
+ for (const target of targets) {
14
+ try {
15
+ const status = await input.observe(target);
16
+ await input.recordStatus(target, status);
17
+ if (input.accepted(status))
18
+ await sync(target, status);
19
+ updated += 1;
20
+ }
21
+ catch {
22
+ failures += 1;
23
+ }
24
+ }
25
+ return { ok: failures === 0, checked: targets.length, updated, skipped: false, failures };
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { InvoiceStorePort, InvoiceUnsupported } from "./contracts/index.js";
2
+ /** Collect retained sources, then prepare each leased duty in separate transactions.
3
+ * The host commits every store call; no money/parcel callback or provider I/O is
4
+ * accepted here. Missing facts stay owed until the host installs their revision.
5
+ * @consumer Independent invoice duty binding.
6
+ */
7
+ export declare function collectInvoiceDuties(input: {
8
+ store: Pick<InvoiceStorePort, "sweep" | "prepareFacts" | "finishFacts">;
9
+ limit?: number;
10
+ }): Promise<{
11
+ kind: "collected";
12
+ collected: number;
13
+ prepared: number;
14
+ failures: number;
15
+ } | InvoiceUnsupported>;
@@ -0,0 +1,35 @@
1
+ /** Collect retained sources, then prepare each leased duty in separate transactions.
2
+ * The host commits every store call; no money/parcel callback or provider I/O is
3
+ * accepted here. Missing facts stay owed until the host installs their revision.
4
+ * @consumer Independent invoice duty binding.
5
+ */
6
+ export async function collectInvoiceDuties(input) {
7
+ const batch = await input.store.sweep(input.limit ?? 10);
8
+ if ("kind" in batch)
9
+ return batch;
10
+ let prepared = 0, failures = 0;
11
+ for (const claim of batch.claims) {
12
+ try {
13
+ const result = await input.store.prepareFacts(claim);
14
+ if ("kind" in result) {
15
+ failures++;
16
+ continue;
17
+ }
18
+ if (result.status === "stale")
19
+ continue;
20
+ const finished = await input.store.finishFacts({ ...claim, result: { ...result, status: result.status } });
21
+ if ("kind" in finished)
22
+ failures++;
23
+ else if (finished.status === "ready" || finished.status === "issued" || finished.status === "voided")
24
+ prepared++;
25
+ else if (finished.status !== "stale")
26
+ failures++;
27
+ }
28
+ catch {
29
+ // A failed prepare/finalize leaves the persisted lease reclaimable. Never
30
+ // manufacture a successful finish or prevent another claim's progress.
31
+ failures++;
32
+ }
33
+ }
34
+ return { kind: "collected", collected: batch.collected, prepared, failures };
35
+ }
@@ -0,0 +1,56 @@
1
+ interface AccountingInvoiceIssuePort {
2
+ requestInvoiceIssueFromPaidOrder(input: {
3
+ idempotencyKey: string;
4
+ orderId: string;
5
+ providerKind: string;
6
+ }): Promise<unknown>;
7
+ requestInvoiceIssueFromFulfillmentHandoff(input: {
8
+ idempotencyKey: string;
9
+ fulfillmentOrderId: string;
10
+ providerKind: string;
11
+ }): Promise<unknown>;
12
+ }
13
+ import { type OrderInvoicePolicyReader } from "./channelPolicy.js";
14
+ export type AutoDispatchFulfillmentResult = {
15
+ kind: "completed" | "skipped";
16
+ detail?: Record<string, unknown>;
17
+ } | {
18
+ kind: "retryable" | "snooze" | "fatal";
19
+ reason: string;
20
+ } | {
21
+ kind: "manual_review";
22
+ reason: string;
23
+ detail?: Record<string, unknown>;
24
+ };
25
+ export interface AutoDispatchFulfillmentPort {
26
+ ensureFulfilledFromPaidOrder(input: {
27
+ orderUuid: string;
28
+ outboxEventId: string;
29
+ signal: AbortSignal;
30
+ }): Promise<AutoDispatchFulfillmentResult>;
31
+ }
32
+ type FulfillmentHandoffRequest = {
33
+ idempotencyKey: string;
34
+ fulfillmentOrderId: string;
35
+ actorUserId: string;
36
+ };
37
+ type FulfillmentHandoffPort<TResult = unknown> = {
38
+ handOffCommerceFulfillmentOrder: (request: FulfillmentHandoffRequest) => Promise<TResult>;
39
+ };
40
+ export interface AccountingHandoffTriggerOptions {
41
+ accountingPort: AccountingInvoiceIssuePort;
42
+ providerKind: string;
43
+ keys: {
44
+ shadow(key: string): string;
45
+ paid(orderId: string): string;
46
+ handoff(orderId: string): string;
47
+ };
48
+ failOnAccountingError?: boolean;
49
+ issueTrigger?: "handoff" | "paid";
50
+ invoicePolicyReader?: OrderInvoicePolicyReader;
51
+ }
52
+ export declare function withAccountingInvoiceShadowTrigger<TResult, TPort extends FulfillmentHandoffPort<TResult>>(fulfillmentPort: TPort, options: Omit<AccountingHandoffTriggerOptions, "accountingPort"> & {
53
+ accountingPort: Pick<AccountingInvoiceIssuePort, "requestInvoiceIssueFromFulfillmentHandoff"> & Partial<AccountingInvoiceIssuePort>;
54
+ }): TPort;
55
+ export declare function withAutoDispatchAccountingInvoiceTrigger(fulfillmentPort: AutoDispatchFulfillmentPort, options: AccountingHandoffTriggerOptions): AutoDispatchFulfillmentPort;
56
+ export {};
@@ -0,0 +1,90 @@
1
+ import { decideChannelInvoice, } from "./channelPolicy.js";
2
+ export function withAccountingInvoiceShadowTrigger(fulfillmentPort, options) {
3
+ return {
4
+ ...fulfillmentPort,
5
+ async handOffCommerceFulfillmentOrder(request) {
6
+ const result = await fulfillmentPort.handOffCommerceFulfillmentOrder(request);
7
+ try {
8
+ // The operator hand-off reaches this wrapper holding only the
9
+ // fulfillment order id, so the policy is read by that key.
10
+ const decision = await decideChannelInvoice(options.invoicePolicyReader, {
11
+ by: "fulfillmentOrder",
12
+ fulfillmentOrderId: request.fulfillmentOrderId,
13
+ });
14
+ if (decision.issue) {
15
+ await options.accountingPort.requestInvoiceIssueFromFulfillmentHandoff({
16
+ idempotencyKey: options.keys.shadow(request.idempotencyKey),
17
+ fulfillmentOrderId: request.fulfillmentOrderId,
18
+ providerKind: options.providerKind,
19
+ });
20
+ }
21
+ }
22
+ catch (error) {
23
+ if (options.failOnAccountingError)
24
+ throw error;
25
+ }
26
+ return result;
27
+ },
28
+ };
29
+ }
30
+ // Auto-dispatch sibling of withAccountingInvoiceShadowTrigger. The outbox
31
+ // commerce.order.paid -> fulfillment path drives handoff through the
32
+ // OrderPaidFulfillmentPort (ensureFulfilledFromPaidOrder), which issues the
33
+ // mark-handed-over RPC directly and therefore never reaches the admin
34
+ // hand-off wrapper above. Without this, an auto-dispatched paid order reaches
35
+ // handed_over but no accounting invoice is ever requested. Same semantics:
36
+ // idempotent (the issue RPC dedups on the order-derived invoice_ref, so any
37
+ // key is duplicate-safe) and fail-soft (accounting errors never fail the
38
+ // fulfillment dispatch — the outbox event still settles as processed).
39
+ export function withAutoDispatchAccountingInvoiceTrigger(fulfillmentPort, options) {
40
+ return {
41
+ async ensureFulfilledFromPaidOrder(input) {
42
+ // Both branches hold the order uuid, so both ask by the order key — the
43
+ // strongest key available, and the one that needs no join.
44
+ const lookup = { by: "order", orderUuid: input.orderUuid };
45
+ if (options.issueTrigger === "paid") {
46
+ try {
47
+ const decision = await decideChannelInvoice(options.invoicePolicyReader, lookup);
48
+ if (decision.issue) {
49
+ await options.accountingPort.requestInvoiceIssueFromPaidOrder({
50
+ idempotencyKey: options.keys.paid(input.orderUuid),
51
+ orderId: input.orderUuid,
52
+ providerKind: options.providerKind,
53
+ });
54
+ }
55
+ }
56
+ catch (error) {
57
+ if (options.failOnAccountingError)
58
+ throw error;
59
+ }
60
+ }
61
+ const result = await fulfillmentPort.ensureFulfilledFromPaidOrder(input);
62
+ if (options.issueTrigger === "paid")
63
+ return result;
64
+ const fulfillmentOrderId = readHandedOverFulfillmentOrderId(result);
65
+ if (fulfillmentOrderId) {
66
+ try {
67
+ const decision = await decideChannelInvoice(options.invoicePolicyReader, lookup);
68
+ if (decision.issue) {
69
+ await options.accountingPort.requestInvoiceIssueFromFulfillmentHandoff({
70
+ idempotencyKey: options.keys.handoff(input.orderUuid),
71
+ fulfillmentOrderId,
72
+ providerKind: options.providerKind,
73
+ });
74
+ }
75
+ }
76
+ catch (error) {
77
+ if (options.failOnAccountingError)
78
+ throw error;
79
+ }
80
+ }
81
+ return result;
82
+ },
83
+ };
84
+ }
85
+ function readHandedOverFulfillmentOrderId(result) {
86
+ if (result.kind !== "completed" && result.kind !== "skipped")
87
+ return null;
88
+ const id = result.detail?.fulfillmentOrderId;
89
+ return typeof id === "string" ? id : null;
90
+ }
@@ -0,0 +1,37 @@
1
+ import type { ChannelInvoiceDecision } from "./channelPolicy.js";
2
+ /** Host retains event schema, key scopes, RPC error class and compatibility detail. */
3
+ export declare function createFulfillmentHandoffInvoiceHandler<Row extends {
4
+ payload: unknown;
5
+ }, Payload>(input: {
6
+ descriptor: {
7
+ eventType: string;
8
+ timeoutMs: number;
9
+ };
10
+ parsePayload(value: unknown): {
11
+ success: true;
12
+ data: Payload;
13
+ } | {
14
+ success: false;
15
+ };
16
+ decide(payload: Payload): Promise<ChannelInvoiceDecision>;
17
+ request(row: Row, payload: Payload): Promise<Record<string, unknown>>;
18
+ errorDetails(error: unknown): {
19
+ message: string;
20
+ causeCode?: string;
21
+ causeMessage?: string;
22
+ };
23
+ }): {
24
+ handle(row: Row, signal: AbortSignal): Promise<{
25
+ kind: "processed";
26
+ detail: Record<string, unknown>;
27
+ } | {
28
+ kind: "discard";
29
+ reason: string;
30
+ benign?: boolean;
31
+ } | {
32
+ kind: "retry";
33
+ reason: string;
34
+ }>;
35
+ eventType: string;
36
+ timeoutMs: number;
37
+ };
@@ -0,0 +1,62 @@
1
+ // SQLSTATEs the issue-request RPCs raise for permanently non-retryable
2
+ // conditions. Retrying these can never succeed, so the event must be discarded
3
+ // (terminal) rather than retry-looped into a `failed` backlog that the staging
4
+ // outbox-preview gate treats as blocking (see outboxOrderPaidFulfillmentHandler,
5
+ // which likewise maps fatal -> discard).
6
+ // - 22023 (invalid_parameter_value): every validation RAISE in
7
+ // accounting_invoice_issue_request_from_handoff / _from_paid_order — invalid
8
+ // input, missing fulfillment order (deleted), missing order/client/address,
9
+ // payment-not-succeeded, package-not-shipped. The handed_over event commits
10
+ // atomically with handed_over_at, so a missing row means it was deleted, not
11
+ // a visibility race.
12
+ // - 23514 (check_violation): the #1811 provider-immutability guard
13
+ // (accounting_invoice_provider_mismatch). The provider is immutable, so a
14
+ // replay against a different provider can never be persisted.
15
+ const PERMANENT_ISSUE_SQLSTATES = new Set(["22023", "23514"]);
16
+ // The subset of permanent (22023) RAISE identifiers that mean the aggregate or
17
+ // its graph was legitimately deleted before this async event dispatched — an
18
+ // orphan from smoke/order cleanup or a real order deletion. Discarding these is
19
+ // the expected, correct outcome, so they are classified `benign` (logged at warn,
20
+ // not error). Every OTHER permanent condition — payment-not-succeeded,
21
+ // package-not-shipped, invalid-input, and the 23514 provider-immutability guard —
22
+ // is still discarded but kept at error level because it signals a genuine anomaly.
23
+ const BENIGN_ISSUE_RAISE_IDENTIFIERS = new Set([
24
+ "accounting_invoice_handoff_not_found",
25
+ "accounting_invoice_order_not_found",
26
+ "accounting_invoice_client_not_found",
27
+ "accounting_invoice_address_not_found",
28
+ ]);
29
+ /** Host retains event schema, key scopes, RPC error class and compatibility detail. */
30
+ export function createFulfillmentHandoffInvoiceHandler(input) {
31
+ return {
32
+ ...input.descriptor,
33
+ async handle(row, signal) {
34
+ const parsed = input.parsePayload(row.payload);
35
+ if (!parsed.success)
36
+ return { kind: "discard", reason: "fulfillment_handoff_payload_invalid" };
37
+ if (signal.aborted)
38
+ return { kind: "retry", reason: "fulfillment_handoff_handler_aborted" };
39
+ try {
40
+ const decision = await input.decide(parsed.data);
41
+ if (!decision.issue)
42
+ return { kind: "processed", detail: decision.detail };
43
+ return { kind: "processed", detail: await input.request(row, parsed.data) };
44
+ }
45
+ catch (error) {
46
+ const { message, causeCode, causeMessage } = input.errorDetails(error);
47
+ // Surface the RPC's own RAISE identifier so a discard reads explicitly
48
+ // (e.g. "…: accounting_invoice_handoff_not_found (22023)") instead of an
49
+ // opaque SQLSTATE — this is what lets an orphan discard be told apart
50
+ // from a real payment-not-succeeded failure in the logs.
51
+ const detail = causeMessage && causeMessage !== message ? `${message}: ${causeMessage}` : message;
52
+ const reason = (causeCode ? `${detail} (${causeCode})` : detail).slice(0, 300);
53
+ if (causeCode !== undefined && PERMANENT_ISSUE_SQLSTATES.has(causeCode)) {
54
+ const benign = causeMessage !== undefined
55
+ && BENIGN_ISSUE_RAISE_IDENTIFIERS.has(causeMessage);
56
+ return benign ? { kind: "discard", reason, benign: true } : { kind: "discard", reason };
57
+ }
58
+ return { kind: "retry", reason };
59
+ }
60
+ },
61
+ };
62
+ }
@@ -1 +1,39 @@
1
1
  export { readInvoiceCapabilities } from "./capabilities.js";
2
+ export { createNativeUnsupportedInvoicePorts } from "./nativeUnsupported.js";
3
+ export { runInvoiceAdapterConformance } from "./adapterConformance.js";
4
+ export { runInvoiceIssueJob } from "./issue.js";
5
+ /** @consumer server/domains/accounting/accountingProviderSnapshot.ts */
6
+ export { mapInvoiceClaimToProviderSnapshot } from "./snapshot.js";
7
+ /** @consumer server/domains/accounting/accountingCanonicalProviderLines.ts */
8
+ export { mapCanonicalIssueLines } from "./accountingCanonicalProviderLines.js";
9
+ /** @consumer server/domains/accounting/accountingLineAllocation.ts */
10
+ export { allocateChargedGrossTotals } from "./accountingLineAllocation.js";
11
+ /** @consumer server/domains/accounting/accountingLineAllocation.ts */
12
+ export { allocateDiscountedGrossTotals } from "./accountingLineAllocation.js";
13
+ /** @consumer server/domains/accounting/accountingPaymentReadback.ts */
14
+ export { readInvoicePaymentEvidence } from "./paymentReadback.js";
15
+ /** @consumer server/domains/accounting/fulfillmentHandoffInvoiceHandler.ts */
16
+ export { createFulfillmentHandoffInvoiceHandler } from "./handoff.js";
17
+ /** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
18
+ export { withAccountingInvoiceShadowTrigger } from "./fulfillmentTrigger.js";
19
+ /** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
20
+ export { withAutoDispatchAccountingInvoiceTrigger } from "./fulfillmentTrigger.js";
21
+ export { runInvoiceCorrectionJob } from "./correction.js";
22
+ export { createInvoiceReversalHandlers } from "./reversal.js";
23
+ /** @consumer Reference native accounting adapter/runtime and installed-consumer conformance. */
24
+ export { createPostgresInvoiceStore, runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
25
+ /** @consumer Native migration conformance and installed-consumer asset verification. */
26
+ export { INVOICING_MIGRATIONS } from "./migrations.js";
27
+ export { collectInvoiceDuties } from "./dutyCollector.js";
28
+ export { createIndependentInvoiceDutyBinding } from "./continuity.js";
29
+ export { runInvoiceDeliveryJob } from "./delivery.js";
30
+ export { assertInvoicePdf, invoicePdfFilename } from "./artifacts.js";
31
+ export { syncAcceptedInvoiceDocuments } from "./documentSync.js";
32
+ export { syncInvoicePayoutSettlements } from "./settlementSync.js";
33
+ export { resolveInvoiceDocumentHistory } from "./documentHistory.js";
34
+ export { createInvoiceReadModel } from "./readModel.js";
35
+ export { createInvoiceServices } from "./bindings.js";
36
+ export type { InvoiceServices } from "./bindings.js";
37
+ export { createInvoiceContribution } from "./contribution.js";
38
+ export { claimInvoiceSellerRevision } from "./sellerRevision.js";
39
+ export type { InvoiceSellerRevision, InvoiceClaimedFacts } from "./contracts/facts.js";
@@ -1 +1,37 @@
1
1
  export { readInvoiceCapabilities } from "./capabilities.js";
2
+ export { createNativeUnsupportedInvoicePorts } from "./nativeUnsupported.js";
3
+ export { runInvoiceAdapterConformance } from "./adapterConformance.js";
4
+ export { runInvoiceIssueJob } from "./issue.js";
5
+ /** @consumer server/domains/accounting/accountingProviderSnapshot.ts */
6
+ export { mapInvoiceClaimToProviderSnapshot } from "./snapshot.js";
7
+ /** @consumer server/domains/accounting/accountingCanonicalProviderLines.ts */
8
+ export { mapCanonicalIssueLines } from "./accountingCanonicalProviderLines.js";
9
+ /** @consumer server/domains/accounting/accountingLineAllocation.ts */
10
+ export { allocateChargedGrossTotals } from "./accountingLineAllocation.js";
11
+ /** @consumer server/domains/accounting/accountingLineAllocation.ts */
12
+ export { allocateDiscountedGrossTotals } from "./accountingLineAllocation.js";
13
+ /** @consumer server/domains/accounting/accountingPaymentReadback.ts */
14
+ export { readInvoicePaymentEvidence } from "./paymentReadback.js";
15
+ /** @consumer server/domains/accounting/fulfillmentHandoffInvoiceHandler.ts */
16
+ export { createFulfillmentHandoffInvoiceHandler } from "./handoff.js";
17
+ /** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
18
+ export { withAccountingInvoiceShadowTrigger } from "./fulfillmentTrigger.js";
19
+ /** @consumer server/domains/accounting/fulfillmentInvoiceTrigger.ts */
20
+ export { withAutoDispatchAccountingInvoiceTrigger } from "./fulfillmentTrigger.js";
21
+ export { runInvoiceCorrectionJob } from "./correction.js";
22
+ export { createInvoiceReversalHandlers } from "./reversal.js";
23
+ /** @consumer Reference native accounting adapter/runtime and installed-consumer conformance. */
24
+ export { createPostgresInvoiceStore, runNativeInvoiceIssueJob } from "./postgresInvoiceStore.js";
25
+ /** @consumer Native migration conformance and installed-consumer asset verification. */
26
+ export { INVOICING_MIGRATIONS } from "./migrations.js";
27
+ export { collectInvoiceDuties } from "./dutyCollector.js";
28
+ export { createIndependentInvoiceDutyBinding } from "./continuity.js";
29
+ export { runInvoiceDeliveryJob } from "./delivery.js";
30
+ export { assertInvoicePdf, invoicePdfFilename } from "./artifacts.js";
31
+ export { syncAcceptedInvoiceDocuments } from "./documentSync.js";
32
+ export { syncInvoicePayoutSettlements } from "./settlementSync.js";
33
+ export { resolveInvoiceDocumentHistory } from "./documentHistory.js";
34
+ export { createInvoiceReadModel } from "./readModel.js";
35
+ export { createInvoiceServices } from "./bindings.js";
36
+ export { createInvoiceContribution } from "./contribution.js";
37
+ export { claimInvoiceSellerRevision } from "./sellerRevision.js";