@openlup/platform 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +218 -59
  2. package/README.md +93 -11
  3. package/dist/fulfillment/contracts/dispatch.d.ts +4 -0
  4. package/dist/fulfillment/dispatchProjection.d.ts +1 -1
  5. package/dist/fulfillment/dispatchProjection.js +4 -2
  6. package/dist/fulfillment/omnipack/client.js +14 -6
  7. package/dist/fulfillment/omnipack/index.js +5 -2
  8. package/dist/fulfillment/providerDispatchWorker.d.ts +38 -0
  9. package/dist/fulfillment/providerDispatchWorker.js +19 -1
  10. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  11. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  12. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  13. package/dist/invoicing/accountingLineAllocation.js +69 -0
  14. package/dist/invoicing/adapterConformance.d.ts +42 -0
  15. package/dist/invoicing/adapterConformance.js +296 -0
  16. package/dist/invoicing/artifacts.d.ts +7 -0
  17. package/dist/invoicing/artifacts.js +20 -0
  18. package/dist/invoicing/bindings.d.ts +46 -0
  19. package/dist/invoicing/bindings.js +69 -0
  20. package/dist/invoicing/channelPolicy.d.ts +28 -0
  21. package/dist/invoicing/channelPolicy.js +27 -0
  22. package/dist/invoicing/continuity.d.ts +12 -0
  23. package/dist/invoicing/continuity.js +18 -0
  24. package/dist/invoicing/contracts/facts.d.ts +11 -1
  25. package/dist/invoicing/contracts/host.d.ts +3 -4
  26. package/dist/invoicing/contracts/index.d.ts +1 -1
  27. package/dist/invoicing/contracts/store.d.ts +15 -5
  28. package/dist/invoicing/contribution.d.ts +21 -0
  29. package/dist/invoicing/contribution.js +19 -0
  30. package/dist/invoicing/correction.d.ts +42 -0
  31. package/dist/invoicing/correction.js +44 -0
  32. package/dist/invoicing/delivery.d.ts +85 -0
  33. package/dist/invoicing/delivery.js +79 -0
  34. package/dist/invoicing/documentHistory.d.ts +50 -0
  35. package/dist/invoicing/documentHistory.js +213 -0
  36. package/dist/invoicing/documentSync.d.ts +24 -0
  37. package/dist/invoicing/documentSync.js +26 -0
  38. package/dist/invoicing/dutyCollector.d.ts +15 -0
  39. package/dist/invoicing/dutyCollector.js +35 -0
  40. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  41. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  42. package/dist/invoicing/handoff.d.ts +37 -0
  43. package/dist/invoicing/handoff.js +62 -0
  44. package/dist/invoicing/index.d.ts +38 -0
  45. package/dist/invoicing/index.js +36 -0
  46. package/dist/invoicing/issue.d.ts +39 -0
  47. package/dist/invoicing/issue.js +55 -0
  48. package/dist/invoicing/migrations.d.ts +48 -0
  49. package/dist/invoicing/migrations.js +25 -0
  50. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  51. package/dist/invoicing/nativeUnsupported.js +22 -0
  52. package/dist/invoicing/paymentReadback.d.ts +20 -0
  53. package/dist/invoicing/paymentReadback.js +47 -0
  54. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  55. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  56. package/dist/invoicing/readModel.d.ts +74 -0
  57. package/dist/invoicing/readModel.js +90 -0
  58. package/dist/invoicing/readback.d.ts +4 -0
  59. package/dist/invoicing/readback.js +30 -0
  60. package/dist/invoicing/reversal.d.ts +37 -0
  61. package/dist/invoicing/reversal.js +28 -0
  62. package/dist/invoicing/sellerRevision.d.ts +19 -0
  63. package/dist/invoicing/sellerRevision.js +14 -0
  64. package/dist/invoicing/settlementSync.d.ts +18 -0
  65. package/dist/invoicing/settlementSync.js +32 -0
  66. package/dist/invoicing/snapshot.d.ts +52 -0
  67. package/dist/invoicing/snapshot.js +141 -0
  68. package/package.json +3 -3
  69. package/release-gates.json +21 -7
  70. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  71. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  72. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  73. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  74. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  75. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  76. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  77. package/src/fulfillment/contracts/dispatch.ts +4 -0
  78. package/src/fulfillment/dispatchProjection.ts +4 -2
  79. package/src/fulfillment/omnipack/client.ts +13 -6
  80. package/src/fulfillment/omnipack/index.ts +5 -2
  81. package/src/fulfillment/providerDispatchWorker.ts +14 -1
  82. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  83. package/src/invoicing/accountingLineAllocation.ts +69 -0
  84. package/src/invoicing/adapterConformance.ts +249 -0
  85. package/src/invoicing/artifacts.ts +19 -0
  86. package/src/invoicing/bindings.ts +80 -0
  87. package/src/invoicing/channelPolicy.ts +50 -0
  88. package/src/invoicing/continuity.ts +15 -0
  89. package/src/invoicing/contracts/facts.ts +12 -1
  90. package/src/invoicing/contracts/host.ts +3 -4
  91. package/src/invoicing/contracts/index.ts +1 -1
  92. package/src/invoicing/contracts/store.ts +11 -7
  93. package/src/invoicing/contribution.ts +28 -0
  94. package/src/invoicing/correction.ts +56 -0
  95. package/src/invoicing/delivery.ts +122 -0
  96. package/src/invoicing/documentHistory.ts +301 -0
  97. package/src/invoicing/documentSync.ts +51 -0
  98. package/src/invoicing/dutyCollector.ts +31 -0
  99. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  100. package/src/invoicing/handoff.ts +71 -0
  101. package/src/invoicing/index.ts +38 -0
  102. package/src/invoicing/issue.ts +64 -0
  103. package/src/invoicing/migrations.ts +25 -0
  104. package/src/invoicing/nativeUnsupported.ts +24 -0
  105. package/src/invoicing/paymentReadback.ts +70 -0
  106. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  107. package/src/invoicing/readModel.ts +80 -0
  108. package/src/invoicing/readback.ts +26 -0
  109. package/src/invoicing/reversal.ts +34 -0
  110. package/src/invoicing/sellerRevision.ts +20 -0
  111. package/src/invoicing/settlementSync.ts +37 -0
  112. package/src/invoicing/snapshot.ts +192 -0
@@ -0,0 +1,192 @@
1
+ import type { InvoiceBuyer } from "./contracts/facts.js";
2
+ import { mapCanonicalIssueLines } from "./accountingCanonicalProviderLines.js";
3
+ import { allocateChargedGrossTotals } from "./accountingLineAllocation.js";
4
+
5
+ type IssueLines = ReturnType<typeof mapCanonicalIssueLines>;
6
+ interface Options {
7
+ defaultLineLabel: string;
8
+ deliveryLabel: string;
9
+ defaultTaxRate: string;
10
+ }
11
+ /** Compatibility projection of a claimed host snapshot, not a second store contract.
12
+ * Seller/date/kind/payment selection remains with the host; no environment is read.
13
+ */
14
+ export function mapInvoiceClaimToProviderSnapshot<Seller, Kind, Payment>(input: {
15
+ orderRef: string;
16
+ issueDate: string;
17
+ sellDate: string;
18
+ currency: string;
19
+ seller: Seller;
20
+ buyerSnapshot: Record<string, unknown>;
21
+ documentKind: Kind;
22
+ governmentClearanceRequired: boolean;
23
+ payment: Payment;
24
+ linesSnapshot: unknown[];
25
+ totalGrossMinor: number;
26
+ totalNetMinor: number;
27
+ orderMoney: unknown;
28
+ }, options: Options & { defaultBuyerLabel: string }) {
29
+ return {
30
+ orderRef: input.orderRef,
31
+ issueDate: input.issueDate,
32
+ sellDate: input.sellDate,
33
+ currency: input.currency,
34
+ seller: input.seller,
35
+ buyer: {
36
+ name: textValue(input.buyerSnapshot.name) ?? options.defaultBuyerLabel,
37
+ email: textValue(input.buyerSnapshot.email),
38
+ taxId: textValue(input.buyerSnapshot.taxId),
39
+ companyName: textValue(input.buyerSnapshot.companyName),
40
+ address: mapBuyerAddress(input.buyerSnapshot.address),
41
+ },
42
+ documentKind: input.documentKind,
43
+ governmentClearanceRequired: input.governmentClearanceRequired,
44
+ payment: input.payment,
45
+ lines: mapIssueLinesToChargedTotal(input.linesSnapshot, input.totalGrossMinor, input.totalNetMinor, input.orderMoney, options),
46
+ chargedTotalGrossMinor: input.totalGrossMinor,
47
+ };
48
+ }
49
+
50
+ // Canonical issue snapshots already carry effective item and explicit delivery
51
+ // positions and are verified as a zero-delta passthrough. Historical snapshots
52
+ // without positionKind retain the independent #1693 reconciliation below.
53
+ function mapIssueLinesToChargedTotal(
54
+ linesSnapshot: unknown[],
55
+ chargedTotalGrossMinor: number,
56
+ chargedTotalNetMinor: number,
57
+ orderMoney: unknown,
58
+ options: Options,
59
+ ): IssueLines {
60
+ const canonicalKinds = linesSnapshot.map((line) => textValue(isRecord(line) ? line.positionKind : null));
61
+ if (canonicalKinds.some(Boolean)) {
62
+ if (!canonicalKinds.every((kind) => kind === "item" || kind === "delivery")) {
63
+ throw new Error("accounting_invoice_canonical_positions_mixed_or_invalid");
64
+ }
65
+ return mapCanonicalIssueLines(
66
+ linesSnapshot,
67
+ chargedTotalGrossMinor,
68
+ chargedTotalNetMinor,
69
+ options.defaultLineLabel,
70
+ );
71
+ }
72
+
73
+ const parsed = linesSnapshot.map((line) => parseLineSnapshot(line, options));
74
+ const deliveryGrossMinor = readDeliveryGrossMinor(orderMoney, chargedTotalGrossMinor);
75
+ const chargedTotals = allocateChargedGrossTotals(
76
+ parsed.map((line) => line.totalGrossMinor),
77
+ chargedTotalGrossMinor - deliveryGrossMinor,
78
+ );
79
+ const positions: IssueLines = parsed.map((line, index) => {
80
+ const totalGrossMinor = chargedTotals[index];
81
+ const totalNetMinor = estimateNetMinor(totalGrossMinor, line.vatRate);
82
+ return {
83
+ name: line.name,
84
+ quantity: line.quantity,
85
+ unitGrossMinor: Math.round(totalGrossMinor / line.quantity),
86
+ unitNetMinor: Math.round(totalNetMinor / line.quantity),
87
+ totalGrossMinor,
88
+ totalNetMinor,
89
+ vatRate: line.vatRate,
90
+ };
91
+ });
92
+ if (deliveryGrossMinor > 0) {
93
+ positions.push(buildDeliveryPosition(deliveryGrossMinor, parsed, options));
94
+ }
95
+ return positions;
96
+ }
97
+
98
+ // Legacy-only compatibility: pre-canonical claims had no explicit delivery
99
+ // position, so their effective delivery is the closed header residual. Current
100
+ // canonical claims never enter this path and use persisted shipping columns.
101
+ // Every inconsistent legacy shape degrades to 0 and then fails closed if the
102
+ // catalog lines cannot reconcile to the charged header.
103
+ function readDeliveryGrossMinor(orderMoney: unknown, chargedTotalGrossMinor: number): number {
104
+ if (!isRecord(orderMoney)) return 0;
105
+ const subtotal = numberValue(orderMoney.subtotalCents);
106
+ const discount = numberValue(orderMoney.discountCents);
107
+ const total = numberValue(orderMoney.totalCents);
108
+ if (subtotal === null || discount === null || total === null) return 0;
109
+ if (![subtotal, discount, total].every((value) => Number.isInteger(value) && value >= 0)) return 0;
110
+ if (discount > subtotal) return 0;
111
+ if (total !== chargedTotalGrossMinor) return 0;
112
+ const deliveryGrossMinor = chargedTotalGrossMinor - (subtotal - discount);
113
+ return deliveryGrossMinor > 0 ? deliveryGrossMinor : 0;
114
+ }
115
+
116
+ // Delivery is an ancillary supply and follows the goods' VAT rate; with mixed
117
+ // item rates (or no item line to source a rate from) the correct rate is
118
+ // ambiguous, so the claim fails closed into the outbox last_error.
119
+ function buildDeliveryPosition(
120
+ deliveryGrossMinor: number,
121
+ itemLines: Array<{ vatRate: string }>,
122
+ options: Options,
123
+ ): IssueLines[number] {
124
+ const rates = [...new Set(itemLines.map((line) => line.vatRate.trim()))];
125
+ if (rates.length === 0) {
126
+ throw new Error(`accounting_invoice_delivery_vat_rate_unavailable delivery=${deliveryGrossMinor}`);
127
+ }
128
+ if (rates.length > 1) {
129
+ throw new Error(`accounting_invoice_delivery_vat_rates_mixed rates=${rates.join(",")}`);
130
+ }
131
+ const vatRate = rates[0];
132
+ const totalNetMinor = estimateNetMinor(deliveryGrossMinor, vatRate);
133
+ return {
134
+ name: options.deliveryLabel,
135
+ quantity: 1,
136
+ unitGrossMinor: deliveryGrossMinor,
137
+ unitNetMinor: totalNetMinor,
138
+ totalGrossMinor: deliveryGrossMinor,
139
+ totalNetMinor,
140
+ vatRate,
141
+ };
142
+ }
143
+
144
+ function mapBuyerAddress(value: unknown): NonNullable<InvoiceBuyer["address"]> {
145
+ const address = isRecord(value) ? value : {};
146
+ return {
147
+ line1: textValue(address.line1),
148
+ postalCode: textValue(address.postalCode),
149
+ city: textValue(address.city),
150
+ country: textValue(address.country),
151
+ };
152
+ }
153
+
154
+ function parseLineSnapshot(value: unknown, options: Options): {
155
+ name: string;
156
+ quantity: number;
157
+ totalGrossMinor: number;
158
+ vatRate: string;
159
+ } {
160
+ const row = isRecord(value) ? value : {};
161
+ const quantity = numberValue(row.quantity) || 1;
162
+ const vatRate = textValue(row.vatRate) ?? options.defaultTaxRate;
163
+ // Legacy (pre-#1693) lines_snapshot rows carry only the per-unit
164
+ // unitGrossMinor, so the line total is unit price times quantity.
165
+ const unitGrossMinor = numberValue(row.unitGrossMinor);
166
+ const totalGrossMinor = numberValue(row.totalGrossMinor) ??
167
+ (unitGrossMinor === null ? 0 : unitGrossMinor * quantity);
168
+ return {
169
+ name: textValue(row.name) ?? options.defaultLineLabel,
170
+ quantity,
171
+ totalGrossMinor,
172
+ vatRate,
173
+ };
174
+ }
175
+
176
+ function estimateNetMinor(grossMinor: number, vatRate: string): number {
177
+ const rate = Number(vatRate.replace(",", "."));
178
+ return Number.isFinite(rate) ? Math.round(grossMinor / (1 + rate / 100)) : grossMinor;
179
+ }
180
+
181
+ function textValue(value: unknown): string | null {
182
+ return typeof value === "string" && value.trim() ? value.trim() : null;
183
+ }
184
+
185
+ function numberValue(value: unknown): number | null {
186
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
187
+ }
188
+
189
+
190
+ function isRecord(value: unknown): value is Record<string, unknown> {
191
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
192
+ }