@happyvertical/smrt-commerce 0.57.2 → 0.57.4

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 (162) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +130 -0
  3. package/agents/accounting-snapshot-mapper.md +11 -0
  4. package/agents/contract-billing-schedules.md +67 -0
  5. package/agents/contract-money.md +81 -0
  6. package/agents/customer-funds.md +96 -0
  7. package/agents/historical-sales.md +132 -0
  8. package/agents/money-calculator.md +36 -0
  9. package/agents/money-model.md +6 -3
  10. package/agents/observed-purchase-prices.md +37 -0
  11. package/agents/payment-allocations.md +59 -0
  12. package/agents/received-sales-contract-money.md +38 -0
  13. package/dist/accounting-snapshot.d.ts +8 -1
  14. package/dist/accounting-snapshot.d.ts.map +1 -1
  15. package/dist/allocation-integrity.d.ts.map +1 -1
  16. package/dist/billing/period-close.d.ts.map +1 -1
  17. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js → ContractLineItemCollection-J4xtRtzE.js} +6 -6
  18. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js.map → ContractLineItemCollection-J4xtRtzE.js.map} +1 -1
  19. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js +4385 -0
  20. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js.map +1 -0
  21. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js +170 -0
  22. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js.map +1 -0
  23. package/dist/chunks/{InvoiceCollection-cinzAqt6.js → InvoiceCollection-BIcge9Wl.js} +131 -18
  24. package/dist/chunks/InvoiceCollection-BIcge9Wl.js.map +1 -0
  25. package/dist/chunks/{TaxCodeCollection-DaBOPG9c.js → TaxCodeCollection-RWdVaheF.js} +3 -4
  26. package/dist/chunks/TaxCodeCollection-RWdVaheF.js.map +1 -0
  27. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js → TaxComponentCollection-Dt2jeWiC.js} +3 -3
  28. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js.map → TaxComponentCollection-Dt2jeWiC.js.map} +1 -1
  29. package/dist/chunks/{allocation-integrity-WmWyXhtG.js → allocation-integrity-DJ7-LY1j.js} +7 -7
  30. package/dist/chunks/{allocation-integrity-WmWyXhtG.js.map → allocation-integrity-DJ7-LY1j.js.map} +1 -1
  31. package/dist/chunks/{contract-money-EuToOghh.js → contract-money-DSRTMoXm.js} +45 -12
  32. package/dist/chunks/contract-money-DSRTMoXm.js.map +1 -0
  33. package/dist/chunks/customer-funds-internal-BQzoJXK5.js +24 -0
  34. package/dist/chunks/customer-funds-internal-BQzoJXK5.js.map +1 -0
  35. package/dist/chunks/{detect-CY9ufaFb.js → detect-DAE-_NAk.js} +5 -2
  36. package/dist/chunks/{detect-CY9ufaFb.js.map → detect-DAE-_NAk.js.map} +1 -1
  37. package/dist/chunks/historical-commerce-internal-C9ButfuH.js +18 -0
  38. package/dist/chunks/historical-commerce-internal-C9ButfuH.js.map +1 -0
  39. package/dist/chunks/{invoice-posting-C2i5dOVZ.js → invoice-posting-BeTPw3h9.js} +5 -5
  40. package/dist/chunks/invoice-posting-BeTPw3h9.js.map +1 -0
  41. package/dist/chunks/retention-accounting-Brdj9x-V.js +2323 -0
  42. package/dist/chunks/retention-accounting-Brdj9x-V.js.map +1 -0
  43. package/dist/chunks/retention-accounting-internal-CBQRvd74.js +17 -0
  44. package/dist/chunks/retention-accounting-internal-CBQRvd74.js.map +1 -0
  45. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js → tenant-money-barrier-DoF6lR8h.js} +5 -5
  46. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js.map → tenant-money-barrier-DoF6lR8h.js.map} +1 -1
  47. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  48. package/dist/collections/DocumentTaxCollection.d.ts +4 -1
  49. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -1
  50. package/dist/collections/HistoricalCustomerFundsCollections.d.ts +10 -0
  51. package/dist/collections/HistoricalCustomerFundsCollections.d.ts.map +1 -0
  52. package/dist/collections/HistoricalSalesLineCollection.d.ts +6 -0
  53. package/dist/collections/HistoricalSalesLineCollection.d.ts.map +1 -0
  54. package/dist/collections/PaymentAllocationCollection.d.ts +2 -6
  55. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  56. package/dist/collections/PurchasePriceObservationCollection.d.ts +6 -0
  57. package/dist/collections/PurchasePriceObservationCollection.d.ts.map +1 -0
  58. package/dist/collections/index.d.ts +1 -0
  59. package/dist/collections/index.d.ts.map +1 -1
  60. package/dist/contract-billing-schedule.d.ts +83 -0
  61. package/dist/contract-billing-schedule.d.ts.map +1 -0
  62. package/dist/contract-money.d.ts +2 -0
  63. package/dist/contract-money.d.ts.map +1 -1
  64. package/dist/customer-funds-internal.d.ts +6 -0
  65. package/dist/customer-funds-internal.d.ts.map +1 -0
  66. package/dist/customer-funds.d.ts +108 -0
  67. package/dist/customer-funds.d.ts.map +1 -0
  68. package/dist/document-money.d.ts +11 -0
  69. package/dist/document-money.d.ts.map +1 -1
  70. package/dist/historical-commerce-internal.d.ts +3 -0
  71. package/dist/historical-commerce-internal.d.ts.map +1 -0
  72. package/dist/historical-customer-funds-types.d.ts +113 -0
  73. package/dist/historical-customer-funds-types.d.ts.map +1 -0
  74. package/dist/historical-customer-funds.d.ts +23 -0
  75. package/dist/historical-customer-funds.d.ts.map +1 -0
  76. package/dist/historical-funds-internal.d.ts +5 -0
  77. package/dist/historical-funds-internal.d.ts.map +1 -0
  78. package/dist/historical-sales-money.d.ts +22 -0
  79. package/dist/historical-sales-money.d.ts.map +1 -0
  80. package/dist/historical-sales-reports.d.ts +31 -0
  81. package/dist/historical-sales-reports.d.ts.map +1 -0
  82. package/dist/historical-sales-types.d.ts +106 -0
  83. package/dist/historical-sales-types.d.ts.map +1 -0
  84. package/dist/historical-sales-validation.d.ts +12 -0
  85. package/dist/historical-sales-validation.d.ts.map +1 -0
  86. package/dist/index.d.ts +21 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1685 -1640
  89. package/dist/index.js.map +1 -1
  90. package/dist/invoice-accounting-sync.d.ts +0 -1
  91. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  92. package/dist/invoice-mcp.d.ts.map +1 -1
  93. package/dist/invoice-posting.d.ts.map +1 -1
  94. package/dist/invoice-retention.d.ts +16 -0
  95. package/dist/invoice-retention.d.ts.map +1 -0
  96. package/dist/invoices.d.ts +2 -0
  97. package/dist/invoices.d.ts.map +1 -1
  98. package/dist/invoices.js +2 -2
  99. package/dist/manifest.json +7563 -4419
  100. package/dist/models/Contract.d.ts +14 -0
  101. package/dist/models/Contract.d.ts.map +1 -1
  102. package/dist/models/DocumentTax.d.ts +2 -0
  103. package/dist/models/DocumentTax.d.ts.map +1 -1
  104. package/dist/models/HistoricalCustomerFunds.d.ts +36 -0
  105. package/dist/models/HistoricalCustomerFunds.d.ts.map +1 -0
  106. package/dist/models/HistoricalCustomerFundsAllocation.d.ts +32 -0
  107. package/dist/models/HistoricalCustomerFundsAllocation.d.ts.map +1 -0
  108. package/dist/models/HistoricalSalesLine.d.ts +29 -0
  109. package/dist/models/HistoricalSalesLine.d.ts.map +1 -0
  110. package/dist/models/Invoice.d.ts +10 -1
  111. package/dist/models/Invoice.d.ts.map +1 -1
  112. package/dist/models/Payment.d.ts +7 -1
  113. package/dist/models/Payment.d.ts.map +1 -1
  114. package/dist/models/PaymentAllocation.d.ts +3 -1
  115. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  116. package/dist/models/PaymentIntent.d.ts.map +1 -1
  117. package/dist/models/Payout.d.ts.map +1 -1
  118. package/dist/models/PurchasePriceObservation.d.ts +15 -0
  119. package/dist/models/PurchasePriceObservation.d.ts.map +1 -0
  120. package/dist/models/TaxCode.d.ts +1 -1
  121. package/dist/models/TaxCode.d.ts.map +1 -1
  122. package/dist/models/index.d.ts +1 -0
  123. package/dist/models/index.d.ts.map +1 -1
  124. package/dist/money.d.ts +5 -1
  125. package/dist/money.d.ts.map +1 -1
  126. package/dist/money.js +62 -15
  127. package/dist/money.js.map +1 -1
  128. package/dist/observed-purchase-price-internal.d.ts +3 -0
  129. package/dist/observed-purchase-price-internal.d.ts.map +1 -0
  130. package/dist/observed-purchase-price.d.ts +71 -0
  131. package/dist/observed-purchase-price.d.ts.map +1 -0
  132. package/dist/received-purchase-money.d.ts +87 -0
  133. package/dist/received-purchase-money.d.ts.map +1 -0
  134. package/dist/recipes.d.ts +4 -1
  135. package/dist/recipes.d.ts.map +1 -1
  136. package/dist/retention-accounting-internal.d.ts +3 -0
  137. package/dist/retention-accounting-internal.d.ts.map +1 -0
  138. package/dist/retention-accounting.d.ts +164 -0
  139. package/dist/retention-accounting.d.ts.map +1 -0
  140. package/dist/retention-money.d.ts +43 -0
  141. package/dist/retention-money.d.ts.map +1 -0
  142. package/dist/smrt-knowledge.json +1866 -113
  143. package/dist/tax/cookbook.d.ts.map +1 -1
  144. package/dist/tax-summary.d.ts +1 -0
  145. package/dist/tax-summary.d.ts.map +1 -1
  146. package/dist/types/index.d.ts +20 -1
  147. package/dist/types/index.d.ts.map +1 -1
  148. package/package.json +16 -16
  149. package/dist/chunks/CustomerCollection-BIPH2ZUx.js +0 -99
  150. package/dist/chunks/CustomerCollection-BIPH2ZUx.js.map +0 -1
  151. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js +0 -1666
  152. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +0 -1
  153. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +0 -1
  154. package/dist/chunks/TaxCodeCollection-DaBOPG9c.js.map +0 -1
  155. package/dist/chunks/VendorCollection-D6ZcUYy8.js +0 -100
  156. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +0 -1
  157. package/dist/chunks/calculations-C9daQLsh.js +0 -82
  158. package/dist/chunks/calculations-C9daQLsh.js.map +0 -1
  159. package/dist/chunks/contract-money-EuToOghh.js.map +0 -1
  160. package/dist/chunks/document-money-DV5t4vlK.js +0 -1186
  161. package/dist/chunks/document-money-DV5t4vlK.js.map +0 -1
  162. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +0 -1
@@ -1,1186 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { t as DocumentTaxCollection } from "./DocumentTaxCollection-DDzlywMW.js";
3
- import { s as InvoiceStatus } from "./types-Bbm0Xflc.js";
4
- import { t as CustomerCollection } from "./CustomerCollection-BIPH2ZUx.js";
5
- import { MoneyValidationError, calculateMoneyDocument, resolveLineTaxCode } from "../money.js";
6
- import { t as allocateFrozenCredit } from "./credit-allocation-yspz0Lab.js";
7
- import { a as assertCommerceMoneySchemaCurrent, f as DOCUMENT_MONEY_READ_REBIND, g as withDocumentMoneyTransaction, h as withDocumentMoneyObjectTransaction, m as lockDocumentMoneyParents, p as DOCUMENT_MONEY_WRITE } from "./detect-CY9ufaFb.js";
8
- import { t as InvoiceCollection } from "./InvoiceCollection-cinzAqt6.js";
9
- import { n as withTenantMoneyCollectionRead, r as withTenantMoneyRuntime, t as lockTenantMoneyRuntime } from "./tenant-money-barrier-6TlqUeVj.js";
10
- import { t as TaxCodeCollection } from "./TaxCodeCollection-DaBOPG9c.js";
11
- import { t as TaxComponentCollection } from "./TaxComponentCollection-C4zxnyVu.js";
12
- import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
13
- import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
14
- //#region src/models/InvoiceLineItem.ts
15
- var __defProp = Object.defineProperty;
16
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
17
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
18
- enumerable: true,
19
- configurable: true,
20
- writable: true,
21
- value
22
- }) : obj[key] = value;
23
- var __decorateClass = (decorators, target, key, kind) => {
24
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
25
- for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
26
- if (kind && result) __defProp(target, key, result);
27
- return result;
28
- };
29
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
30
- var InvoiceLineItem = class extends SmrtObject {
31
- /** @internal Collection read guard returns this snapshot on its root DB. */
32
- rebindMoneyRead(capability, db) {
33
- if (capability !== DOCUMENT_MONEY_READ_REBIND) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice line read rebinding requires the owning capability");
34
- this._db = db;
35
- this.options.db = db;
36
- return this;
37
- }
38
- tenantId = null;
39
- invoiceId = "";
40
- creditedLineId = null;
41
- creditCalculationBasisJson = "";
42
- get creditCalculationBasis() {
43
- try {
44
- const value = JSON.parse(this.creditCalculationBasisJson);
45
- if (!value || typeof value !== "object" || !("kind" in value)) return null;
46
- const basis = value;
47
- if (basis.kind !== "quantity" && basis.kind !== "net_amount" || typeof basis.sourceQuantity !== "string" || !Number.isSafeInteger(basis.sourceUnitPrice) || basis.sourceDiscountType !== "none" && basis.sourceDiscountType !== "percent" && basis.sourceDiscountType !== "amount" || typeof basis.sourceDiscountRate !== "string" || !Number.isSafeInteger(basis.sourceDiscountMinor) || basis.kind === "quantity" && typeof basis.quantity !== "string" || basis.kind === "net_amount" && !Number.isSafeInteger(basis.netMinor)) return null;
48
- return basis;
49
- } catch {
50
- return null;
51
- }
52
- }
53
- set creditCalculationBasis(value) {
54
- this.creditCalculationBasisJson = value ? JSON.stringify(value) : "";
55
- }
56
- description = "";
57
- /**
58
- * SKU or item code
59
- */
60
- sku = "";
61
- productId = null;
62
- quantity = 1;
63
- unitPrice = 0;
64
- discount = 0;
65
- discountType = "none";
66
- discountRate = "0.0";
67
- taxRate = 0;
68
- taxMode = "inherit";
69
- taxCodeId = null;
70
- taxCodeCode = null;
71
- taxCodeName = null;
72
- taxComponentsJson = null;
73
- get taxComponents() {
74
- if (this.taxComponentsJson === null) return [];
75
- try {
76
- const value = JSON.parse(this.taxComponentsJson);
77
- if (!Array.isArray(value)) return [];
78
- return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.name === "string" && typeof entry.percent === "string" && Number.isSafeInteger(entry.taxMinor));
79
- } catch {
80
- return [];
81
- }
82
- }
83
- set taxComponents(value) {
84
- this.taxComponentsJson = JSON.stringify(value);
85
- }
86
- taxAmount = 0;
87
- /** Stable document snapshot identity; ordinary lifecycle saves do not change it. */
88
- moneySnapshotId = "";
89
- amount = 0;
90
- /**
91
- * Type of source ('campaign' | 'contract' | 'manual' | etc.)
92
- */
93
- sourceType = "";
94
- /**
95
- * ID of the source (e.g., campaign ID, contract ID)
96
- */
97
- sourceId = "";
98
- /**
99
- * Service period start (for time-based billing)
100
- */
101
- periodStart = null;
102
- /**
103
- * Service period end (for time-based billing)
104
- */
105
- periodEnd = null;
106
- revenueAccountId = null;
107
- /**
108
- * Sort order within the invoice
109
- */
110
- sortOrder = 0;
111
- constructor(options = {}) {
112
- super(options);
113
- if (options.tenantId !== void 0) this.tenantId = options.tenantId;
114
- if (options.invoiceId !== void 0) this.invoiceId = options.invoiceId;
115
- if (options.creditedLineId !== void 0) this.creditedLineId = options.creditedLineId;
116
- if (options.creditCalculationBasisJson !== void 0) this.creditCalculationBasisJson = options.creditCalculationBasisJson;
117
- if (options.creditCalculationBasis !== void 0) this.creditCalculationBasis = structuredClone(options.creditCalculationBasis);
118
- if (options.description !== void 0) this.description = options.description;
119
- if (options.sku !== void 0) this.sku = options.sku;
120
- if (options.productId !== void 0) this.productId = options.productId;
121
- if (options.quantity !== void 0) this.quantity = options.quantity;
122
- if (options.unitPrice !== void 0) this.unitPrice = options.unitPrice;
123
- if (options.discount !== void 0) this.discount = options.discount;
124
- if (options.discountType !== void 0) this.discountType = options.discountType;
125
- if (options.discountRate !== void 0) this.discountRate = options.discountRate;
126
- if (options.taxRate !== void 0) this.taxRate = options.taxRate;
127
- if (options.taxMode !== void 0) this.taxMode = options.taxMode;
128
- if (options.taxCodeId !== void 0) this.taxCodeId = options.taxCodeId;
129
- if (options.taxCodeCode !== void 0) this.taxCodeCode = options.taxCodeCode;
130
- if (options.taxCodeName !== void 0) this.taxCodeName = options.taxCodeName;
131
- if (options.taxComponentsJson !== void 0) this.taxComponentsJson = options.taxComponentsJson;
132
- if (options.taxComponents !== void 0) this.taxComponents = structuredClone(options.taxComponents);
133
- if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
134
- if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
135
- if (options.amount !== void 0) this.amount = options.amount;
136
- if (options.sourceType !== void 0) this.sourceType = options.sourceType;
137
- if (options.sourceId !== void 0) this.sourceId = options.sourceId;
138
- if (options.periodStart !== void 0) this.periodStart = options.periodStart;
139
- if (options.periodEnd !== void 0) this.periodEnd = options.periodEnd;
140
- if (options.revenueAccountId !== void 0) this.revenueAccountId = options.revenueAccountId;
141
- if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
142
- }
143
- /** Refuse an old-format row read from the database. */
144
- async initialize() {
145
- await super.initialize();
146
- if (this.isPersisted) {
147
- const { assertHydratedMoneyRowCurrent } = await import("./detect-CY9ufaFb.js").then((n) => n.c);
148
- await assertHydratedMoneyRowCurrent(this);
149
- }
150
- return this;
151
- }
152
- /** Persist through the invoice money aggregate. */
153
- async save() {
154
- const { saveInvoiceLineItem } = await Promise.resolve().then(() => document_money_exports);
155
- return await saveInvoiceLineItem(this);
156
- }
157
- /** @internal Aggregate persistence only. */
158
- async saveDerived(capability) {
159
- if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice line money write capability is required");
160
- for (const [field2, value] of [["unitPrice", this.unitPrice], ["discount", this.discount]]) if (!Number.isSafeInteger(value)) throw new MoneyValidationError("UNSAFE_MONEY", `Invoice line ${field2} ${value} must be a safe integer number of minor units`);
161
- return await super.save();
162
- }
163
- async delete(options = {}) {
164
- const { deleteInvoiceLineItem } = await Promise.resolve().then(() => document_money_exports);
165
- await deleteInvoiceLineItem(this, options);
166
- }
167
- /** @internal Aggregate persistence only. */
168
- async deleteDerived(capability, options = {}) {
169
- if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice line money write capability is required");
170
- await super.delete(options);
171
- }
172
- /**
173
- * Check if line item has source tracking
174
- */
175
- hasSource() {
176
- return !!this.sourceType && !!this.sourceId;
177
- }
178
- /**
179
- * Check if line item has a service period
180
- */
181
- hasPeriod() {
182
- return !!this.periodStart && !!this.periodEnd;
183
- }
184
- };
185
- __publicField(InvoiceLineItem, "__smrtPackage__", "@happyvertical/smrt-commerce");
186
- __decorateClass([tenantId({ nullable: true })], InvoiceLineItem.prototype, "tenantId", 2);
187
- __decorateClass([field({ description: "The invoice this line belongs to." }), foreignKey("Invoice", { role: "items" })], InvoiceLineItem.prototype, "invoiceId", 2);
188
- __decorateClass([foreignKey("InvoiceLineItem", { nullable: true })], InvoiceLineItem.prototype, "creditedLineId", 2);
189
- __decorateClass([field({ description: "Frozen source basis used to cap and reproduce an issued credit line." })], InvoiceLineItem.prototype, "creditCalculationBasisJson", 2);
190
- __decorateClass([field({ description: "What is being billed on this line." })], InvoiceLineItem.prototype, "description", 2);
191
- __decorateClass([crossPackageRef("@happyvertical/smrt-products:Product", { nullable: true })], InvoiceLineItem.prototype, "productId", 2);
192
- __decorateClass([field({ description: "How many." })], InvoiceLineItem.prototype, "quantity", 2);
193
- __decorateClass([field({
194
- description: "The price of one, before discount; includes tax when the document's prices include tax.",
195
- ui: { format: "money" }
196
- })], InvoiceLineItem.prototype, "unitPrice", 2);
197
- __decorateClass([field({ ui: { format: "money" } })], InvoiceLineItem.prototype, "discount", 2);
198
- __decorateClass([field({ type: "decimal" })], InvoiceLineItem.prototype, "discountRate", 2);
199
- __decorateClass([field({ description: "The stored fractional tax rate for this line (0.05 means 5%)." })], InvoiceLineItem.prototype, "taxRate", 2);
200
- __decorateClass([foreignKey("TaxCode", { nullable: true })], InvoiceLineItem.prototype, "taxCodeId", 2);
201
- __decorateClass([field({ ui: { format: "money" } })], InvoiceLineItem.prototype, "taxAmount", 2);
202
- __decorateClass([field({
203
- description: "The line amount: net of discount, and including tax only when the document's prices include tax.",
204
- ui: { format: "money" }
205
- })], InvoiceLineItem.prototype, "amount", 2);
206
- __decorateClass([crossPackageRef("@happyvertical/smrt-ledgers:Account", { nullable: true })], InvoiceLineItem.prototype, "revenueAccountId", 2);
207
- InvoiceLineItem = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
208
- api: {
209
- include: [
210
- "list",
211
- "get",
212
- "create",
213
- "update",
214
- "delete"
215
- ],
216
- writable: [
217
- "invoiceId",
218
- "description",
219
- "sku",
220
- "productId",
221
- "quantity",
222
- "unitPrice",
223
- "discount",
224
- "discountType",
225
- "discountRate",
226
- "taxRate",
227
- "taxMode",
228
- "taxCodeId",
229
- "sourceType",
230
- "sourceId",
231
- "periodStart",
232
- "periodEnd",
233
- "revenueAccountId",
234
- "sortOrder"
235
- ]
236
- },
237
- mcp: { include: ["list", "get"] },
238
- cli: { skipApiCheck: true }
239
- })], InvoiceLineItem);
240
- //#endregion
241
- //#region src/collections/InvoiceLineItemCollection.ts
242
- var InvoiceLineItemCollection = class extends SmrtCollection {
243
- static _itemClass = InvoiceLineItem;
244
- async withMoneyRead(operation) {
245
- const rootDb = this.db;
246
- const tenantId = getTenantId();
247
- if (!tenantId) {
248
- await assertCommerceMoneySchemaCurrent(rootDb);
249
- return operation();
250
- }
251
- const result = await withTenantMoneyCollectionRead(rootDb, tenantId, (boundDb) => this.withDatabase(boundDb, operation));
252
- if (result instanceof InvoiceLineItem) result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
253
- else if (Array.isArray(result)) {
254
- for (const item of result) if (item instanceof InvoiceLineItem) item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
255
- }
256
- return result;
257
- }
258
- async get(filter, options = {}) {
259
- return this.withMoneyRead(() => super.get(filter, options));
260
- }
261
- async list(options = {}) {
262
- return this.withMoneyRead(() => super.list(options));
263
- }
264
- /**
265
- * Find line items by invoice
266
- *
267
- * @param invoiceId - Invoice ID
268
- * @returns Array of line items, sorted by sortOrder
269
- */
270
- async findByInvoice(invoiceId) {
271
- return await this.list({
272
- where: { invoiceId },
273
- orderBy: "sortOrder ASC"
274
- });
275
- }
276
- /**
277
- * Find line items by source
278
- *
279
- * @param sourceType - Source type (e.g., 'campaign', 'contract')
280
- * @param sourceId - Source ID
281
- * @returns Array of line items
282
- */
283
- async findBySource(sourceType, sourceId) {
284
- return await this.list({
285
- where: {
286
- sourceType,
287
- sourceId
288
- },
289
- orderBy: "created_at DESC"
290
- });
291
- }
292
- /**
293
- * Find line items by source type only
294
- *
295
- * @param sourceType - Source type (e.g., 'campaign')
296
- * @returns Array of line items
297
- */
298
- async findBySourceType(sourceType) {
299
- return await this.list({
300
- where: { sourceType },
301
- orderBy: "created_at DESC"
302
- });
303
- }
304
- /**
305
- * Calculate total amount for an invoice
306
- *
307
- * @param invoiceId - Invoice ID
308
- * @returns Total amount
309
- */
310
- async getTotalForInvoice(invoiceId) {
311
- return (await this.findByInvoice(invoiceId)).reduce((sum, item) => sum + item.amount, 0);
312
- }
313
- /**
314
- * Get the next sort order for an invoice
315
- *
316
- * @param invoiceId - Invoice ID
317
- * @returns Next sort order number
318
- */
319
- async getNextSortOrder(invoiceId) {
320
- const lineItems = await this.findByInvoice(invoiceId);
321
- if (lineItems.length === 0) return 0;
322
- return Math.max(...lineItems.map((item) => item.sortOrder)) + 1;
323
- }
324
- /**
325
- * Find all invoice line items belonging to a specific tenant
326
- *
327
- * @param tenantId - Tenant ID
328
- * @returns Array of invoice line items for the tenant
329
- */
330
- async findByTenant(tenantId) {
331
- return this.list({ where: { tenantId } });
332
- }
333
- /**
334
- * Find all global invoice line items (not associated with any tenant).
335
- *
336
- * Routes through the shared tenant-global helper so it does not throw under
337
- * an active tenant context (an explicit `tenant_id IS NULL` filter would be
338
- * flagged as an isolation violation). (#1600)
339
- *
340
- * @returns Array of global invoice line items
341
- */
342
- async findGlobal() {
343
- return queryGlobal(this);
344
- }
345
- /**
346
- * Find invoice line items for a tenant including global line items.
347
- *
348
- * Fails closed if an active tenant context requests a different tenant's
349
- * rows; the admin/system path keeps the cross-tenant capability. (#1600)
350
- *
351
- * @param tenantId - Tenant ID
352
- * @returns Array of tenant-specific and global invoice line items
353
- */
354
- async findWithGlobals(tenantId) {
355
- return queryWithGlobals(this, tenantId, "InvoiceLineItem.findWithGlobals");
356
- }
357
- };
358
- //#endregion
359
- //#region src/document-money.ts
360
- var document_money_exports = /* @__PURE__ */ __exportAll({
361
- deleteInvoice: () => deleteInvoice,
362
- deleteInvoiceLineItem: () => deleteInvoiceLineItem,
363
- getInvoiceMoneyFacts: () => getInvoiceMoneyFacts,
364
- lockAndReloadInvoice: () => lockAndReloadInvoice,
365
- previewInvoiceMoney: () => previewInvoiceMoney,
366
- saveInvoice: () => saveInvoice,
367
- saveInvoiceLineItem: () => saveInvoiceLineItem
368
- });
369
- var FROZEN_INVOICE_FIELDS = [
370
- "kind",
371
- "creditedInvoiceId",
372
- "tenantId",
373
- "customerId",
374
- "contractId",
375
- "invoiceNumber",
376
- "reference",
377
- "issueDate",
378
- "dueDate",
379
- "subtotal",
380
- "discountTotal",
381
- "taxAmount",
382
- "providerTaxAmount",
383
- "totalAmount",
384
- "currency",
385
- "lineAmountType",
386
- "taxRoundingScope",
387
- "sellerTaxRegistrationsJson",
388
- "partyTaxId",
389
- "moneySnapshotId",
390
- "notes",
391
- "customerNotes",
392
- "terms"
393
- ];
394
- var FROZEN_LINE_FIELDS = [
395
- "tenantId",
396
- "invoiceId",
397
- "creditedLineId",
398
- "creditCalculationBasisJson",
399
- "description",
400
- "sku",
401
- "productId",
402
- "quantity",
403
- "unitPrice",
404
- "discountType",
405
- "discountRate",
406
- "discount",
407
- "taxMode",
408
- "taxCodeId",
409
- "taxCodeCode",
410
- "taxCodeName",
411
- "taxComponentsJson",
412
- "taxAmount",
413
- "taxRate",
414
- "amount",
415
- "sourceType",
416
- "sourceId",
417
- "periodStart",
418
- "periodEnd",
419
- "revenueAccountId",
420
- "sortOrder",
421
- "moneySnapshotId"
422
- ];
423
- function comparable(value) {
424
- if (value instanceof Date) return value.toISOString();
425
- return value === "" || value === void 0 ? null : value;
426
- }
427
- function assertFieldsUnchanged(proposed, persisted, fields, label) {
428
- for (const field of fields) if (comparable(proposed[field]) !== comparable(persisted[field])) throw new MoneyValidationError("FROZEN_DOCUMENT", `${label} ${String(field)} is frozen`);
429
- }
430
- function assertTenantId(value) {
431
- if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money requires a tenant-owned invoice");
432
- }
433
- function requireId(value, label) {
434
- if (!value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} requires a persisted ID`);
435
- return value;
436
- }
437
- async function lockRuntimeSettings(db, tenantId) {
438
- if (!tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Commerce money documents require a tenant");
439
- return lockTenantMoneyRuntime(db, tenantId);
440
- }
441
- function isFrozen(invoice) {
442
- return invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId || !!invoice.moneySnapshotId;
443
- }
444
- function aggregateSignature(invoice) {
445
- return JSON.stringify([
446
- invoice.lineAmountType,
447
- invoice.taxRoundingScope,
448
- invoice.subtotal,
449
- invoice.discountTotal,
450
- invoice.taxAmount,
451
- invoice.totalAmount
452
- ]);
453
- }
454
- async function loadInvoice(db, id) {
455
- const collection = await InvoiceCollection.create({ db });
456
- const invoice = await collection.get({ id }, { cache: false });
457
- if (!invoice) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice ${id} was not found`);
458
- return {
459
- collection,
460
- invoice
461
- };
462
- }
463
- async function findInvoice(db, id) {
464
- return (await InvoiceCollection.create({ db })).get({ id }, { cache: false });
465
- }
466
- async function lockAndReloadInvoice(db, invoiceId, tenantId) {
467
- const first = await loadInvoice(db, invoiceId);
468
- await lockDocumentMoneyParents([{
469
- id: invoiceId,
470
- tenantId,
471
- collection: first.collection
472
- }]);
473
- const invoice = await first.collection.get({ id: invoiceId }, { cache: false });
474
- if (!invoice || invoice.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant identity changed while acquiring its money lock");
475
- return {
476
- collection: first.collection,
477
- invoice
478
- };
479
- }
480
- async function lockAndReloadMoneyInvoice(db, invoiceId, tenantId, creditedInvoiceId) {
481
- const collection = await InvoiceCollection.create({ db });
482
- const ids = [invoiceId, ...creditedInvoiceId ? [creditedInvoiceId] : []];
483
- if ((await Promise.all(ids.map((id) => collection.get({ id }, { cache: false })))).some((row) => !row || row.tenantId !== tenantId)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money parents must exist in the active tenant");
484
- await lockDocumentMoneyParents(ids.map((id) => ({
485
- id,
486
- tenantId,
487
- collection
488
- })));
489
- const invoice = await collection.get({ id: invoiceId }, { cache: false });
490
- const source = creditedInvoiceId ? await collection.get({ id: creditedInvoiceId }, { cache: false }) : null;
491
- if (!invoice || invoice.tenantId !== tenantId || source && source.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money parent identity changed while acquiring its lock");
492
- return {
493
- collection,
494
- invoice,
495
- source
496
- };
497
- }
498
- function discountInput(line) {
499
- if (line.discountType === "percent") return {
500
- kind: "percent",
501
- rate: String(line.discountRate)
502
- };
503
- if (line.discountType === "amount" || line.discount !== 0) return {
504
- kind: "amount",
505
- minor: line.discount
506
- };
507
- if (line.discountType !== "none") throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown discount type: ${line.discountType}`);
508
- return { kind: "none" };
509
- }
510
- function issuedCredit(invoice) {
511
- return invoice.kind === "credit_note" && invoice.status !== InvoiceStatus.DRAFT && invoice.status !== InvoiceStatus.CANCELLED;
512
- }
513
- async function refreshIssuedCreditBalance(db, source) {
514
- const sourceId = requireId(source.id, "Credited invoice");
515
- const credits = await (await InvoiceCollection.create({ db })).list({ where: {
516
- tenantId: source.tenantId,
517
- creditedInvoiceId: sourceId
518
- } });
519
- const issued = credits.filter(issuedCredit).sort((left, right) => {
520
- const date = left.issueDate.getTime() - right.issueDate.getTime();
521
- if (date) return date;
522
- return String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0;
523
- });
524
- const total = issued.reduce((sum, credit) => sum + credit.totalAmount, 0);
525
- if (!Number.isSafeInteger(total) || total < 0) throw new MoneyValidationError("UNSAFE_MONEY", "Issued credit total is not a nonnegative safe integer");
526
- let remainingSource = Math.max(0, source.totalAmount - source.amountPaid);
527
- for (const credit of credits) {
528
- const included = issued.includes(credit);
529
- const applied = included ? Math.min(credit.totalAmount, remainingSource) : 0;
530
- if (included) remainingSource -= applied;
531
- if (credit.creditedAmount !== applied) {
532
- credit.creditedAmount = applied;
533
- credit.updatePaymentStatus(credit.amountPaid);
534
- await credit.saveDerived(DOCUMENT_MONEY_WRITE);
535
- }
536
- }
537
- source.creditedAmount = total;
538
- source.updatePaymentStatus(source.amountPaid);
539
- await source.saveDerived(DOCUMENT_MONEY_WRITE);
540
- }
541
- function lineNet(line, parent) {
542
- return parent.lineAmountType === "inclusive" ? line.amount - line.taxAmount : line.amount;
543
- }
544
- async function prepareCreditLines(db, invoice, lines) {
545
- if (!invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
546
- const sourceInvoice = await findInvoice(db, invoice.creditedInvoiceId);
547
- if (sourceInvoice?.kind !== "invoice" || sourceInvoice.tenantId !== invoice.tenantId || sourceInvoice.status === InvoiceStatus.DRAFT || sourceInvoice.status === InvoiceStatus.CANCELLED || !sourceInvoice.moneySnapshotId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must be an issued, non-cancelled invoice in the same tenant");
548
- if (sourceInvoice.currency !== invoice.currency) throw new MoneyValidationError("CURRENCY_MISMATCH", "Credit note currency must match its credited invoice");
549
- if (sourceInvoice.customerId !== invoice.customerId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit note customer must match its credited invoice");
550
- invoice.lineAmountType = sourceInvoice.lineAmountType;
551
- invoice.taxRoundingScope = sourceInvoice.taxRoundingScope;
552
- const lineCollection = await InvoiceLineItemCollection.create({ db });
553
- const sourceLines = await lineCollection.findByInvoice(requireId(sourceInvoice.id, "Credited invoice"));
554
- const sourceTaxes = await (await DocumentTaxCollection.create({ db })).findByDocument(requireId(sourceInvoice.tenantId, "Credited invoice tenant"), "invoice", requireId(sourceInvoice.id, "Credited invoice"));
555
- const sourceByLineId = new Map(sourceLines.map((line) => [requireId(line.id, "Credited line"), line]));
556
- const priorCredits = (await (await InvoiceCollection.create({ db })).list({ where: {
557
- tenantId: invoice.tenantId,
558
- creditedInvoiceId: invoice.creditedInvoiceId
559
- } })).filter((row) => row.id !== invoice.id && issuedCredit(row));
560
- const issuedLines = [];
561
- const issuedParentById = new Map(priorCredits.map((row) => [requireId(row.id, "Issued credit note"), row]));
562
- const consumptions = [];
563
- for (const credit of priorCredits) for (const line of await lineCollection.findByInvoice(requireId(credit.id, "Issued credit note"))) {
564
- if (!line.creditedLineId) continue;
565
- issuedLines.push(line);
566
- const basis = line.creditCalculationBasis;
567
- consumptions.push({
568
- creditedLineId: line.creditedLineId,
569
- quantity: basis?.kind === "quantity" ? basis.quantity : "0",
570
- netMinor: lineNet(line, credit),
571
- discountMinor: line.discount
572
- });
573
- }
574
- const targeted = lines.filter((line) => line.creditedLineId);
575
- const allocation = targeted.length ? allocateFrozenCredit(sourceLines.map((line) => ({
576
- id: requireId(line.id, "Credited line"),
577
- quantity: String(line.quantity),
578
- netMinor: lineNet(line, sourceInvoice),
579
- discountMinor: line.discount,
580
- unitPrice: line.unitPrice,
581
- discountType: line.discountType,
582
- discountRate: line.discountRate
583
- })), consumptions, targeted.map((line) => {
584
- const basis = line.creditCalculationBasis;
585
- return basis?.kind === "net_amount" ? {
586
- creditedLineId: requireId(line.creditedLineId, "Credited line"),
587
- netMinor: basis.netMinor
588
- } : {
589
- creditedLineId: requireId(line.creditedLineId, "Credited line"),
590
- quantity: basis?.kind === "quantity" ? basis.quantity : String(line.quantity)
591
- };
592
- })) : {
593
- allocations: [],
594
- exhaustsDocument: false
595
- };
596
- const allocationByLineId = /* @__PURE__ */ new Map();
597
- const targetBySource = new Map(targeted.map((line) => [requireId(line.creditedLineId, "Credited line"), line]));
598
- for (const row of allocation.allocations) {
599
- const line = targetBySource.get(row.creditedLineId);
600
- if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit allocation lost its target line");
601
- line.creditCalculationBasis = row.basis;
602
- allocationByLineId.set(requireId(line.id, "Credit line"), row);
603
- }
604
- return {
605
- allocationByLineId,
606
- sourceByLineId,
607
- issuedLines,
608
- issuedParentById,
609
- sourceInvoice,
610
- sourceTaxes,
611
- exhaustsDocument: allocation.exhaustsDocument
612
- };
613
- }
614
- async function recomputeInvoiceAggregate(db, invoice, settings, freezeSnapshotId = "", persist = true, proposedLines) {
615
- assertTenantId(invoice.tenantId);
616
- const tenantId = invoice.tenantId;
617
- const linesCollection = await InvoiceLineItemCollection.create({ db });
618
- const codesCollection = await TaxCodeCollection.create({ db });
619
- const componentsCollection = await TaxComponentCollection.create({ db });
620
- const invoiceId = requireId(invoice.id, "Invoice calculation");
621
- const lines = proposedLines ?? await linesCollection.findByInvoice(invoiceId);
622
- const credit = invoice.kind === "credit_note" ? await prepareCreditLines(db, invoice, lines) : null;
623
- const codes = await codesCollection.list({ where: { tenantId } });
624
- const components = await componentsCollection.list({ where: { tenantId } });
625
- const componentById = new Map(components.map((row) => [requireId(row.id, "Tax component"), row]));
626
- const codeById = new Map(codes.map((row) => [requireId(row.id, "Tax code"), row]));
627
- const codeInputs = {};
628
- for (const code of codes) {
629
- const codeId = requireId(code.id, "Tax code");
630
- codeInputs[codeId] = {
631
- id: codeId,
632
- kind: code.kind,
633
- active: code.active,
634
- appliesTo: code.appliesTo,
635
- components: code.components.map((part) => {
636
- const component = componentById.get(part.taxComponentId);
637
- if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
638
- return {
639
- taxComponentId: requireId(component.id, "Tax component"),
640
- percent: component.percent,
641
- compound: part.compound
642
- };
643
- })
644
- };
645
- }
646
- if (credit) for (const source of credit.sourceByLineId.values()) {
647
- if (!source.taxCodeId) continue;
648
- const frozenKind = credit.sourceTaxes.find((row) => row.taxCodeId === source.taxCodeId)?.kind;
649
- codeInputs[source.taxCodeId] = {
650
- id: source.taxCodeId,
651
- kind: frozenKind ?? (source.taxComponents.length ? "taxable" : "zero-rated"),
652
- active: true,
653
- appliesTo: "sales",
654
- components: source.taxComponents.map((component) => ({
655
- taxComponentId: component.taxComponentId,
656
- percent: component.percent,
657
- compound: false
658
- }))
659
- };
660
- }
661
- const exempt = await codesCollection.findByCode("exempt", tenantId);
662
- const outOfScope = await codesCollection.findByCode("out-of-scope", tenantId);
663
- const customers = await CustomerCollection.create({ db });
664
- const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
665
- if (customer && customer.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice customer must belong to its tenant");
666
- const productIds = [...new Set(lines.filter((line) => !line.creditedLineId).map((line) => line.productId).filter((productId) => !!productId))];
667
- const productById = /* @__PURE__ */ new Map();
668
- if (productIds.length > 0) {
669
- const { ProductCollection } = await import("@happyvertical/smrt-products/collections");
670
- const products = await ProductCollection.create({ db });
671
- for (const productId of productIds) {
672
- const product = await products.get({ id: productId }, { cache: false });
673
- if (!product) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} was not found`);
674
- if (product.tenantId !== null && product.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} must be global or belong to its tenant`);
675
- productById.set(productId, product);
676
- }
677
- }
678
- const resolvedCodes = /* @__PURE__ */ new Map();
679
- const calculated = calculateMoneyDocument({
680
- currency: invoice.currency,
681
- lineAmountType: invoice.lineAmountType,
682
- roundingScope: invoice.taxRoundingScope,
683
- lines: lines.map((line) => {
684
- if (line.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice line ${line.id} must belong to its parent tenant`);
685
- const lineId = requireId(line.id, "Invoice line");
686
- if (line.creditedLineId) {
687
- if (!credit) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited line");
688
- const source = credit.sourceByLineId.get(line.creditedLineId);
689
- const allocation = credit.allocationByLineId.get(lineId);
690
- if (!source || !allocation) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credited line is not part of the frozen source invoice");
691
- const taxCode = source.taxCodeId ? codeInputs[source.taxCodeId] ?? null : null;
692
- resolvedCodes.set(lineId, source.taxCodeId ? {
693
- id: source.taxCodeId,
694
- code: source.taxCodeCode,
695
- name: source.taxCodeName,
696
- kind: taxCode?.kind ?? "zero-rated",
697
- components: structuredClone(source.taxComponents)
698
- } : null);
699
- line.description ||= source.description;
700
- line.sku ||= source.sku;
701
- line.productId = source.productId;
702
- line.revenueAccountId = source.revenueAccountId;
703
- if (allocation.basis.kind === "quantity") {
704
- line.quantity = Number(allocation.basis.quantity);
705
- line.unitPrice = allocation.basis.sourceUnitPrice;
706
- line.discountType = allocation.discountMinor ? "amount" : "none";
707
- line.discountRate = "0";
708
- } else {
709
- line.quantity = 1;
710
- line.unitPrice = allocation.netMinor ?? allocation.basis.netMinor;
711
- line.discountType = "none";
712
- line.discountRate = "0";
713
- }
714
- line.discount = allocation.discountMinor;
715
- return {
716
- id: lineId,
717
- quantity: line.quantity,
718
- unitPrice: line.unitPrice,
719
- netAmountMinor: allocation.basis.kind === "net_amount" ? allocation.netMinor ?? allocation.basis.netMinor : void 0,
720
- discount: allocation.discountMinor ? {
721
- kind: "amount",
722
- minor: allocation.discountMinor
723
- } : { kind: "none" },
724
- taxCode
725
- };
726
- }
727
- if (invoice.kind === "invoice" && line.creditCalculationBasis) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot carry a credit calculation basis");
728
- if (invoice.kind === "credit_note" && line.taxMode !== "override") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Free-form credit lines require an explicit tax-code override");
729
- const resolved = resolveLineTaxCode({
730
- lineAmountType: invoice.lineAmountType,
731
- lineOverride: line.taxMode === "override" ? line.taxCodeId : null
732
- }, {
733
- direction: "sales",
734
- codes: codeInputs,
735
- customerTaxExempt: customer?.taxExempt ?? false,
736
- exemptCodeId: exempt?.id,
737
- productCodeId: line.productId ? productById.get(line.productId)?.defaultSalesTaxCodeId || null : null,
738
- partyCodeId: customer?.defaultTaxCodeId,
739
- appDefaultCodeId: settings.defaultSalesTaxCodeId,
740
- outOfScopeCodeId: outOfScope?.id
741
- });
742
- resolvedCodes.set(lineId, resolved.taxCode ? (() => {
743
- const code = codeById.get(resolved.taxCode.id);
744
- if (!code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax code ${resolved.taxCode.id} was not loaded`);
745
- return {
746
- id: requireId(code.id, "Tax code"),
747
- code: code.code,
748
- name: code.name,
749
- kind: code.kind,
750
- components: code.components.map((part) => {
751
- const component = componentById.get(part.taxComponentId);
752
- if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
753
- return {
754
- taxComponentId: part.taxComponentId,
755
- name: component.name,
756
- percent: component.percent,
757
- taxMinor: 0
758
- };
759
- })
760
- };
761
- })() : null);
762
- return {
763
- id: requireId(line.id, "Invoice line"),
764
- quantity: line.quantity,
765
- unitPrice: line.unitPrice,
766
- discount: discountInput(line),
767
- taxCode: resolved.taxCode
768
- };
769
- })
770
- });
771
- if (credit?.exhaustsDocument) {
772
- const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
773
- const targeted = lines.filter((line) => !!line.creditedLineId);
774
- for (const line of targeted) {
775
- const source = credit.sourceByLineId.get(requireId(line.creditedLineId, "Credited line"));
776
- const result = resultById.get(requireId(line.id, "Credit line"));
777
- if (!source || !result) continue;
778
- const prior = credit.issuedLines.filter((candidate) => candidate.creditedLineId === source.id);
779
- const priorNet = prior.reduce((sum, candidate) => {
780
- const parent = credit.issuedParentById.get(candidate.invoiceId);
781
- return sum + (parent ? lineNet(candidate, parent) : 0);
782
- }, 0);
783
- const desiredNet = lineNet(source, credit.sourceInvoice) - priorNet;
784
- const netDelta = desiredNet - result.netMinor;
785
- result.netMinor = desiredNet;
786
- result.amountMinor += netDelta;
787
- for (const sourceComponent of source.taxComponents) {
788
- const component = result.components.find((candidate) => candidate.taxComponentId === sourceComponent.taxComponentId);
789
- if (!component) continue;
790
- const priorTax = prior.reduce((sum, candidate) => sum + (candidate.taxComponents.find((part) => part.taxComponentId === sourceComponent.taxComponentId)?.taxMinor ?? 0), 0);
791
- const desiredTax = sourceComponent.taxMinor - priorTax;
792
- const taxDelta = desiredTax - component.taxMinor;
793
- component.taxMinor = desiredTax;
794
- result.taxMinor += taxDelta;
795
- if (invoice.lineAmountType === "inclusive") result.amountMinor += taxDelta;
796
- }
797
- }
798
- const rebuilt = /* @__PURE__ */ new Map();
799
- for (const result of calculated.lines) {
800
- const metadata = resolvedCodes.get(result.id) ?? null;
801
- const parts = result.components.length ? result.components : [{
802
- taxComponentId: null,
803
- taxMinor: 0
804
- }];
805
- for (const part of parts) {
806
- const key = `${metadata?.id ?? ""}\0${part.taxComponentId ?? ""}`;
807
- const row = rebuilt.get(key) ?? {
808
- taxCodeId: metadata?.id ?? null,
809
- taxComponentId: part.taxComponentId,
810
- taxableMinor: 0,
811
- taxMinor: 0
812
- };
813
- row.taxableMinor += result.netMinor;
814
- row.taxMinor += part.taxMinor;
815
- rebuilt.set(key, row);
816
- }
817
- }
818
- calculated.taxes = [...rebuilt.values()].sort((left, right) => {
819
- const a = `${left.taxCodeId ?? ""}\0${left.taxComponentId ?? ""}`;
820
- const b = `${right.taxCodeId ?? ""}\0${right.taxComponentId ?? ""}`;
821
- return a < b ? -1 : a > b ? 1 : 0;
822
- });
823
- calculated.subtotalMinor = calculated.lines.reduce((sum, row) => sum + row.netMinor, 0);
824
- calculated.taxMinor = calculated.lines.reduce((sum, row) => sum + row.taxMinor, 0);
825
- calculated.totalMinor = calculated.subtotalMinor + calculated.taxMinor;
826
- }
827
- if (credit) {
828
- const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
829
- for (const [lineId, allocation] of credit.allocationByLineId) {
830
- if (allocation.basis.kind !== "quantity") continue;
831
- const result = resultById.get(lineId);
832
- const hasAmountBasisConsumption = credit.issuedLines.some((line) => line.creditedLineId === allocation.creditedLineId && line.creditCalculationBasis?.kind === "net_amount");
833
- if (result && Math.abs(result.netMinor) > Math.abs(allocation.remainingNetMinor) && (!allocation.exhaustsLine || hasAmountBasisConsumption)) throw new MoneyValidationError("CREDIT_EXCEEDS_LINE", "Credit quantity exceeds the remaining frozen net amount");
834
- }
835
- }
836
- const lineById = new Map(lines.map((line) => [requireId(line.id, "Invoice line"), line]));
837
- for (const result of calculated.lines) {
838
- const line = lineById.get(result.id);
839
- if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Calculated line ${result.id} was not loaded`);
840
- const code = resolvedCodes.get(requireId(line.id, "Invoice line")) ?? null;
841
- line.discount = result.discountMinor;
842
- line.amount = result.amountMinor;
843
- line.taxAmount = result.taxMinor;
844
- line.taxCodeId = code?.id ?? null;
845
- line.taxCodeCode = code?.code ?? null;
846
- line.taxCodeName = code?.name ?? null;
847
- line.taxComponents = result.components.map((entry) => {
848
- const component = code?.components.find((candidate) => candidate.taxComponentId === entry.taxComponentId);
849
- if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Calculated component ${entry.taxComponentId} was not loaded`);
850
- return {
851
- taxComponentId: component.taxComponentId,
852
- name: component.name,
853
- percent: component.percent,
854
- taxMinor: entry.taxMinor
855
- };
856
- });
857
- line.taxRate = code ? code.components.reduce((sum, part) => sum + Number(part.percent) / 100, 0) : 0;
858
- if (freezeSnapshotId) line.moneySnapshotId = freezeSnapshotId;
859
- if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE);
860
- }
861
- const taxRows = calculated.taxes.map((tax) => {
862
- const code = tax.taxCodeId ? [...resolvedCodes.values()].find((candidate) => candidate?.id === tax.taxCodeId) ?? null : null;
863
- const snapshotComponent = tax.taxComponentId ? code?.components.find((candidate) => candidate.taxComponentId === tax.taxComponentId) : null;
864
- const currentComponent = tax.taxComponentId ? componentById.get(tax.taxComponentId) : null;
865
- const sourceTax = credit?.sourceTaxes.find((row) => row.taxCodeId === tax.taxCodeId && row.taxComponentId === tax.taxComponentId);
866
- if (tax.taxCodeId && !code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax code ${tax.taxCodeId} was not loaded`);
867
- if (tax.taxComponentId && !snapshotComponent) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax component ${tax.taxComponentId} was not loaded`);
868
- return {
869
- tenantId,
870
- documentType: invoice.kind,
871
- documentId: invoiceId,
872
- currency: invoice.currency,
873
- taxCodeId: code ? requireId(code.id, "Tax code") : null,
874
- taxCodeCode: code?.code ?? null,
875
- kind: code?.kind ?? null,
876
- taxComponentId: snapshotComponent?.taxComponentId ?? null,
877
- taxComponentName: snapshotComponent?.name ?? null,
878
- percent: snapshotComponent?.percent ?? null,
879
- liabilityAccountId: sourceTax?.liabilityAccountId || currentComponent?.liabilityAccountId || null,
880
- taxableMinor: tax.taxableMinor,
881
- taxMinor: tax.taxMinor,
882
- recoverableMinor: 0
883
- };
884
- });
885
- if (persist) await (await DocumentTaxCollection.create({ db })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
886
- tenantId,
887
- documentType: invoice.kind,
888
- documentId: invoiceId,
889
- rows: taxRows
890
- });
891
- invoice.subtotal = calculated.subtotalMinor;
892
- invoice.discountTotal = calculated.discountTotalMinor;
893
- invoice.taxAmount = calculated.taxMinor + invoice.providerTaxAmount;
894
- invoice.totalAmount = calculated.totalMinor + invoice.providerTaxAmount;
895
- return {
896
- lines,
897
- taxRows
898
- };
899
- }
900
- async function previewInvoiceMoneyOnScope(input, settings) {
901
- if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview requires its active principal tenant");
902
- const { invoice } = await loadInvoice(input.db, input.invoiceId);
903
- if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview cannot cross tenant boundaries");
904
- if (invoice.status !== InvoiceStatus.DRAFT || invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Frozen invoice money cannot be previewed as a proposed draft");
905
- const proposedHeader = input.draft?.header;
906
- if (proposedHeader) {
907
- for (const field of [
908
- "currency",
909
- "lineAmountType",
910
- "taxRoundingScope",
911
- "providerTaxAmount",
912
- "customerId"
913
- ]) if (proposedHeader[field] !== void 0) invoice[field] = proposedHeader[field];
914
- }
915
- const storedLines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
916
- const storedById = new Map(storedLines.map((line) => [line.id, line]));
917
- const providedIds = input.draft?.lines?.map((line) => line.id).filter((id) => !!id) ?? [];
918
- if (new Set(providedIds).size !== providedIds.length) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview line ids must be distinct");
919
- const unknownId = providedIds.find((id) => !storedById.has(id));
920
- if (unknownId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice preview line ${unknownId} was not found on the invoice`);
921
- const proposedLines = input.draft?.lines ? input.draft.lines.map((proposed) => {
922
- const result = (proposed.id ? storedById.get(proposed.id) : void 0) ?? new InvoiceLineItem();
923
- for (const field of [
924
- "description",
925
- "productId",
926
- "quantity",
927
- "unitPrice",
928
- "discountType",
929
- "discountRate",
930
- "discount",
931
- "taxMode",
932
- "taxCodeId"
933
- ]) if (proposed[field] !== void 0) result[field] = proposed[field];
934
- result.id ||= crypto.randomUUID();
935
- result.invoiceId = input.invoiceId;
936
- result.tenantId = input.tenantId;
937
- return result;
938
- }) : storedLines;
939
- const calculated = await recomputeInvoiceAggregate(input.db, invoice, settings, "", false, proposedLines);
940
- return {
941
- source: "calculated",
942
- header: {
943
- currency: invoice.currency,
944
- lineAmountType: invoice.lineAmountType,
945
- taxRoundingScope: invoice.taxRoundingScope,
946
- subtotalMinor: invoice.subtotal,
947
- discountTotalMinor: invoice.discountTotal,
948
- taxAmountMinor: invoice.taxAmount,
949
- providerTaxAmountMinor: invoice.providerTaxAmount,
950
- totalAmountMinor: invoice.totalAmount
951
- },
952
- lines: calculated.lines.map((line) => ({
953
- id: requireId(line.id, "Preview line"),
954
- grossMinor: line.amount + line.discount,
955
- discountMinor: line.discount,
956
- amountMinor: line.amount,
957
- taxMinor: line.taxAmount,
958
- taxCodeId: line.taxCodeId,
959
- taxCodeCode: line.taxCodeCode,
960
- taxCodeName: line.taxCodeName,
961
- taxComponents: structuredClone(line.taxComponents)
962
- })),
963
- taxes: calculated.taxRows
964
- };
965
- }
966
- async function previewInvoiceMoney(input) {
967
- return withTenantMoneyRuntime(input.db, input.tenantId, async (scope) => previewInvoiceMoneyOnScope({
968
- ...input,
969
- db: scope.db
970
- }, scope.settings));
971
- }
972
- async function getInvoiceMoneyFactsOnScope(input) {
973
- if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts require their active principal tenant");
974
- const { invoice } = await loadInvoice(input.db, input.invoiceId);
975
- if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts cannot cross tenant boundaries");
976
- const lines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
977
- const taxes = await (await DocumentTaxCollection.create({ db: input.db })).findByDocument(input.tenantId, invoice.kind, input.invoiceId);
978
- return {
979
- source: "stored",
980
- invoiceId: input.invoiceId,
981
- moneySnapshotId: invoice.moneySnapshotId,
982
- kind: invoice.kind,
983
- status: invoice.status,
984
- header: {
985
- currency: invoice.currency,
986
- lineAmountType: invoice.lineAmountType,
987
- taxRoundingScope: invoice.taxRoundingScope,
988
- subtotalMinor: invoice.subtotal,
989
- discountTotalMinor: invoice.discountTotal,
990
- taxAmountMinor: invoice.taxAmount,
991
- providerTaxAmountMinor: invoice.providerTaxAmount,
992
- totalAmountMinor: invoice.totalAmount
993
- },
994
- lines: lines.map((line) => ({
995
- id: requireId(line.id, "Invoice line"),
996
- grossMinor: line.amount + line.discount,
997
- discountMinor: line.discount,
998
- amountMinor: line.amount,
999
- taxMinor: line.taxAmount,
1000
- taxCodeId: line.taxCodeId,
1001
- taxCodeCode: line.taxCodeCode,
1002
- taxCodeName: line.taxCodeName,
1003
- taxComponents: structuredClone(line.taxComponents)
1004
- })),
1005
- taxes: taxes.map((row) => ({
1006
- tenantId: requireId(row.tenantId, "Document tax tenant"),
1007
- documentType: row.documentType,
1008
- documentId: row.documentId,
1009
- currency: row.currency,
1010
- taxCodeId: row.taxCodeId,
1011
- taxCodeCode: row.taxCodeCode,
1012
- kind: row.kind,
1013
- taxComponentId: row.taxComponentId,
1014
- taxComponentName: row.taxComponentName,
1015
- percent: row.percent,
1016
- liabilityAccountId: row.liabilityAccountId,
1017
- taxableMinor: row.taxableMinor,
1018
- taxMinor: row.taxMinor,
1019
- recoverableMinor: row.recoverableMinor
1020
- }))
1021
- };
1022
- }
1023
- async function getInvoiceMoneyFacts(input) {
1024
- return withTenantMoneyRuntime(input.db, input.tenantId, async (scope) => getInvoiceMoneyFactsOnScope({
1025
- ...input,
1026
- db: scope.db
1027
- }));
1028
- }
1029
- async function saveInvoice(input) {
1030
- const priorRevision = input.updated_at;
1031
- try {
1032
- return await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
1033
- if (invoice.kind !== "invoice" && invoice.kind !== "credit_note") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice kind must be invoice or credit_note");
1034
- if (invoice.kind === "invoice" && invoice.creditedInvoiceId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited invoice");
1035
- if (invoice.kind === "credit_note" && !invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
1036
- const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
1037
- const settings = await lockRuntimeSettings(tx, candidateTenantId);
1038
- const existing = invoice.id ? await findInvoice(tx, invoice.id) : null;
1039
- const tenantId = existing?.tenantId ?? candidateTenantId;
1040
- if (candidateTenantId && existing?.tenantId && existing.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
1041
- assertTenantId(tenantId);
1042
- if (!existing) {
1043
- invoice.tenantId = tenantId;
1044
- invoice.creditedAmount = 0;
1045
- let source = null;
1046
- if (invoice.kind === "credit_note") {
1047
- source = await findInvoice(tx, requireId(invoice.creditedInvoiceId, "Credited invoice"));
1048
- if (!source || source.tenantId !== tenantId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must exist in the active tenant");
1049
- await lockDocumentMoneyParents([{
1050
- id: requireId(source.id, "Credited invoice"),
1051
- tenantId,
1052
- collection: await InvoiceCollection.create({ db: tx })
1053
- }]);
1054
- if (source.kind !== "invoice" || source.status === InvoiceStatus.DRAFT || source.status === InvoiceStatus.CANCELLED || !source.moneySnapshotId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must be an issued, non-cancelled invoice");
1055
- if (invoice.currency !== source.currency || invoice.customerId !== source.customerId) throw new MoneyValidationError(invoice.currency !== source.currency ? "CURRENCY_MISMATCH" : "INVALID_MONEY_INPUT", "Credit note currency and customer must match its source invoice");
1056
- }
1057
- invoice.lineAmountType = invoice.kind === "credit_note" && source ? source.lineAmountType : settings.defaultLineAmountType;
1058
- invoice.taxRoundingScope = invoice.kind === "credit_note" && source ? source.taxRoundingScope : settings.taxRoundingScope;
1059
- invoice.subtotal = 0;
1060
- invoice.discountTotal = 0;
1061
- invoice.taxAmount = invoice.providerTaxAmount;
1062
- invoice.totalAmount = invoice.providerTaxAmount;
1063
- if (invoice.status !== InvoiceStatus.DRAFT || invoice.arJournalId) {
1064
- const customers = await CustomerCollection.create({ db: tx });
1065
- const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
1066
- invoice.moneySnapshotId = crypto.randomUUID();
1067
- invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
1068
- invoice.partyTaxId = customer?.taxId ?? "";
1069
- }
1070
- return invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1071
- }
1072
- if (invoice.tenantId !== existing.tenantId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice tenant identity cannot change");
1073
- if (invoice.kind !== existing.kind || invoice.creditedInvoiceId !== existing.creditedInvoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice kind and credited invoice identity are immutable");
1074
- const locked = await lockAndReloadMoneyInvoice(tx, requireId(existing.id, "Invoice"), tenantId, existing.creditedInvoiceId);
1075
- invoice.creditedAmount = locked.invoice.creditedAmount;
1076
- if (isFrozen(locked.invoice)) {
1077
- if (invoice.arReversalJournalId !== locked.invoice.arReversalJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice reversal journal identity is server-managed");
1078
- if (locked.invoice.arJournalId && invoice.arJournalId !== locked.invoice.arJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice revenue journal identity is frozen once assigned");
1079
- if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.kind === "invoice" && locked.invoice.isPaid()) throw new MoneyValidationError("FROZEN_DOCUMENT", "A paid invoice cannot be cancelled");
1080
- if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.kind === "credit_note" && locked.invoice.amountPaid > 0) throw new MoneyValidationError("FROZEN_DOCUMENT", "A refunded credit note cannot be cancelled");
1081
- if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.arJournalId && !locked.invoice.arReversalJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "A posted invoice requires an atomic reversal before cancellation");
1082
- assertFieldsUnchanged(invoice, locked.invoice, FROZEN_INVOICE_FIELDS, "Invoice");
1083
- const saved2 = await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1084
- if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
1085
- return saved2;
1086
- }
1087
- const freezing = invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId;
1088
- const snapshotId = freezing ? locked.invoice.moneySnapshotId || crypto.randomUUID() : "";
1089
- await recomputeInvoiceAggregate(tx, invoice, settings, snapshotId);
1090
- if (freezing) {
1091
- const customers = await CustomerCollection.create({ db: tx });
1092
- const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
1093
- invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
1094
- invoice.partyTaxId = customer?.taxId ?? "";
1095
- invoice.moneySnapshotId = snapshotId;
1096
- }
1097
- const saved = await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1098
- if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
1099
- return saved;
1100
- }));
1101
- } catch (error) {
1102
- input.updated_at = priorRevision;
1103
- throw error;
1104
- }
1105
- }
1106
- async function saveInvoiceLineItem(input) {
1107
- return withDocumentMoneyObjectTransaction(input, async (line, tx) => {
1108
- const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
1109
- const settings = await lockRuntimeSettings(tx, candidateTenantId);
1110
- const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
1111
- const parentId = persisted?.invoiceId ?? line.invoiceId;
1112
- const first = await loadInvoice(tx, parentId);
1113
- if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
1114
- assertTenantId(first.invoice.tenantId);
1115
- const { invoice, source } = await lockAndReloadMoneyInvoice(tx, parentId, first.invoice.tenantId, first.invoice.creditedInvoiceId);
1116
- if (persisted && line.invoiceId !== persisted.invoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice line cannot move between parents");
1117
- line.invoiceId = requireId(invoice.id, "Invoice");
1118
- line.tenantId = invoice.tenantId;
1119
- if (invoice.kind === "invoice" && line.creditedLineId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot reference credited lines");
1120
- if (invoice.kind === "credit_note" && line.creditedLineId && (!source || source.id !== invoice.creditedInvoiceId)) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit source changed while saving its line");
1121
- if (isFrozen(invoice)) {
1122
- if (!persisted) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot add a line to a frozen invoice");
1123
- assertFieldsUnchanged(line, persisted, FROZEN_LINE_FIELDS, "Invoice line");
1124
- return line.saveDerived(DOCUMENT_MONEY_WRITE);
1125
- }
1126
- await line.saveDerived(DOCUMENT_MONEY_WRITE);
1127
- const priorAggregate = aggregateSignature(invoice);
1128
- await recomputeInvoiceAggregate(tx, invoice, settings);
1129
- if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1130
- const refreshed = await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: requireId(line.id, "Invoice line") }, { cache: false });
1131
- if (refreshed) for (const field of [
1132
- "discount",
1133
- "amount",
1134
- "taxAmount",
1135
- "taxRate",
1136
- "taxCodeId",
1137
- "taxCodeCode",
1138
- "taxCodeName",
1139
- "taxComponentsJson",
1140
- "moneySnapshotId",
1141
- "updated_at"
1142
- ]) line[field] = refreshed[field];
1143
- return line;
1144
- });
1145
- }
1146
- async function deleteInvoiceLineItem(input, options) {
1147
- await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (line) => {
1148
- const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
1149
- const settings = await lockRuntimeSettings(tx, candidateTenantId);
1150
- const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
1151
- if (!persisted) return;
1152
- const first = await loadInvoice(tx, persisted.invoiceId);
1153
- if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
1154
- assertTenantId(first.invoice.tenantId);
1155
- const { invoice } = await lockAndReloadInvoice(tx, persisted.invoiceId, first.invoice.tenantId);
1156
- if (isFrozen(invoice)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a line from a frozen invoice");
1157
- await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);
1158
- const priorAggregate = aggregateSignature(invoice);
1159
- await recomputeInvoiceAggregate(tx, invoice, settings);
1160
- if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1161
- }));
1162
- }
1163
- async function deleteInvoice(input, options) {
1164
- await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
1165
- const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
1166
- await lockRuntimeSettings(tx, candidateTenantId);
1167
- const persisted = invoice.id ? await findInvoice(tx, invoice.id) : null;
1168
- if (!persisted) return;
1169
- if (candidateTenantId && persisted.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
1170
- if (isFrozen(persisted)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a frozen invoice");
1171
- assertTenantId(persisted.tenantId);
1172
- const persistedId = requireId(persisted.id, "Invoice");
1173
- await lockAndReloadInvoice(tx, persistedId, persisted.tenantId);
1174
- await (await DocumentTaxCollection.create({ db: tx })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
1175
- tenantId: persisted.tenantId,
1176
- documentType: "invoice",
1177
- documentId: persistedId,
1178
- rows: []
1179
- });
1180
- await invoice.deleteDerived(DOCUMENT_MONEY_WRITE, options);
1181
- }));
1182
- }
1183
- //#endregion
1184
- export { InvoiceLineItemCollection as a, previewInvoiceMoney as i, getInvoiceMoneyFacts as n, InvoiceLineItem as o, lockAndReloadInvoice as r, document_money_exports as t };
1185
-
1186
- //# sourceMappingURL=document-money-DV5t4vlK.js.map