@happyvertical/smrt-commerce 0.57.3 → 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 (161) 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/chunks/{ContractLineItemCollection-BZcrlrrY.js → ContractLineItemCollection-J4xtRtzE.js} +6 -6
  17. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js.map → ContractLineItemCollection-J4xtRtzE.js.map} +1 -1
  18. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js +4385 -0
  19. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js.map +1 -0
  20. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js +170 -0
  21. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js.map +1 -0
  22. package/dist/chunks/{InvoiceCollection-cinzAqt6.js → InvoiceCollection-BIcge9Wl.js} +131 -18
  23. package/dist/chunks/InvoiceCollection-BIcge9Wl.js.map +1 -0
  24. package/dist/chunks/{TaxCodeCollection-DaBOPG9c.js → TaxCodeCollection-RWdVaheF.js} +3 -4
  25. package/dist/chunks/TaxCodeCollection-RWdVaheF.js.map +1 -0
  26. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js → TaxComponentCollection-Dt2jeWiC.js} +3 -3
  27. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js.map → TaxComponentCollection-Dt2jeWiC.js.map} +1 -1
  28. package/dist/chunks/{allocation-integrity-WmWyXhtG.js → allocation-integrity-DJ7-LY1j.js} +7 -7
  29. package/dist/chunks/{allocation-integrity-WmWyXhtG.js.map → allocation-integrity-DJ7-LY1j.js.map} +1 -1
  30. package/dist/chunks/{contract-money-EuToOghh.js → contract-money-DSRTMoXm.js} +45 -12
  31. package/dist/chunks/contract-money-DSRTMoXm.js.map +1 -0
  32. package/dist/chunks/customer-funds-internal-BQzoJXK5.js +24 -0
  33. package/dist/chunks/customer-funds-internal-BQzoJXK5.js.map +1 -0
  34. package/dist/chunks/{detect-CY9ufaFb.js → detect-DAE-_NAk.js} +5 -2
  35. package/dist/chunks/{detect-CY9ufaFb.js.map → detect-DAE-_NAk.js.map} +1 -1
  36. package/dist/chunks/historical-commerce-internal-C9ButfuH.js +18 -0
  37. package/dist/chunks/historical-commerce-internal-C9ButfuH.js.map +1 -0
  38. package/dist/chunks/{invoice-posting-C2i5dOVZ.js → invoice-posting-BeTPw3h9.js} +5 -5
  39. package/dist/chunks/invoice-posting-BeTPw3h9.js.map +1 -0
  40. package/dist/chunks/retention-accounting-Brdj9x-V.js +2323 -0
  41. package/dist/chunks/retention-accounting-Brdj9x-V.js.map +1 -0
  42. package/dist/chunks/retention-accounting-internal-CBQRvd74.js +17 -0
  43. package/dist/chunks/retention-accounting-internal-CBQRvd74.js.map +1 -0
  44. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js → tenant-money-barrier-DoF6lR8h.js} +5 -5
  45. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js.map → tenant-money-barrier-DoF6lR8h.js.map} +1 -1
  46. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  47. package/dist/collections/DocumentTaxCollection.d.ts +4 -1
  48. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -1
  49. package/dist/collections/HistoricalCustomerFundsCollections.d.ts +10 -0
  50. package/dist/collections/HistoricalCustomerFundsCollections.d.ts.map +1 -0
  51. package/dist/collections/HistoricalSalesLineCollection.d.ts +6 -0
  52. package/dist/collections/HistoricalSalesLineCollection.d.ts.map +1 -0
  53. package/dist/collections/PaymentAllocationCollection.d.ts +2 -6
  54. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  55. package/dist/collections/PurchasePriceObservationCollection.d.ts +6 -0
  56. package/dist/collections/PurchasePriceObservationCollection.d.ts.map +1 -0
  57. package/dist/collections/index.d.ts +1 -0
  58. package/dist/collections/index.d.ts.map +1 -1
  59. package/dist/contract-billing-schedule.d.ts +83 -0
  60. package/dist/contract-billing-schedule.d.ts.map +1 -0
  61. package/dist/contract-money.d.ts +2 -0
  62. package/dist/contract-money.d.ts.map +1 -1
  63. package/dist/customer-funds-internal.d.ts +6 -0
  64. package/dist/customer-funds-internal.d.ts.map +1 -0
  65. package/dist/customer-funds.d.ts +108 -0
  66. package/dist/customer-funds.d.ts.map +1 -0
  67. package/dist/document-money.d.ts +11 -0
  68. package/dist/document-money.d.ts.map +1 -1
  69. package/dist/historical-commerce-internal.d.ts +3 -0
  70. package/dist/historical-commerce-internal.d.ts.map +1 -0
  71. package/dist/historical-customer-funds-types.d.ts +113 -0
  72. package/dist/historical-customer-funds-types.d.ts.map +1 -0
  73. package/dist/historical-customer-funds.d.ts +23 -0
  74. package/dist/historical-customer-funds.d.ts.map +1 -0
  75. package/dist/historical-funds-internal.d.ts +5 -0
  76. package/dist/historical-funds-internal.d.ts.map +1 -0
  77. package/dist/historical-sales-money.d.ts +22 -0
  78. package/dist/historical-sales-money.d.ts.map +1 -0
  79. package/dist/historical-sales-reports.d.ts +31 -0
  80. package/dist/historical-sales-reports.d.ts.map +1 -0
  81. package/dist/historical-sales-types.d.ts +106 -0
  82. package/dist/historical-sales-types.d.ts.map +1 -0
  83. package/dist/historical-sales-validation.d.ts +12 -0
  84. package/dist/historical-sales-validation.d.ts.map +1 -0
  85. package/dist/index.d.ts +21 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1668 -1643
  88. package/dist/index.js.map +1 -1
  89. package/dist/invoice-accounting-sync.d.ts +0 -1
  90. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  91. package/dist/invoice-mcp.d.ts.map +1 -1
  92. package/dist/invoice-posting.d.ts.map +1 -1
  93. package/dist/invoice-retention.d.ts +16 -0
  94. package/dist/invoice-retention.d.ts.map +1 -0
  95. package/dist/invoices.d.ts +2 -0
  96. package/dist/invoices.d.ts.map +1 -1
  97. package/dist/invoices.js +2 -2
  98. package/dist/manifest.json +7563 -4419
  99. package/dist/models/Contract.d.ts +14 -0
  100. package/dist/models/Contract.d.ts.map +1 -1
  101. package/dist/models/DocumentTax.d.ts +2 -0
  102. package/dist/models/DocumentTax.d.ts.map +1 -1
  103. package/dist/models/HistoricalCustomerFunds.d.ts +36 -0
  104. package/dist/models/HistoricalCustomerFunds.d.ts.map +1 -0
  105. package/dist/models/HistoricalCustomerFundsAllocation.d.ts +32 -0
  106. package/dist/models/HistoricalCustomerFundsAllocation.d.ts.map +1 -0
  107. package/dist/models/HistoricalSalesLine.d.ts +29 -0
  108. package/dist/models/HistoricalSalesLine.d.ts.map +1 -0
  109. package/dist/models/Invoice.d.ts +10 -1
  110. package/dist/models/Invoice.d.ts.map +1 -1
  111. package/dist/models/Payment.d.ts +7 -1
  112. package/dist/models/Payment.d.ts.map +1 -1
  113. package/dist/models/PaymentAllocation.d.ts +3 -1
  114. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  115. package/dist/models/PaymentIntent.d.ts.map +1 -1
  116. package/dist/models/Payout.d.ts.map +1 -1
  117. package/dist/models/PurchasePriceObservation.d.ts +15 -0
  118. package/dist/models/PurchasePriceObservation.d.ts.map +1 -0
  119. package/dist/models/TaxCode.d.ts +1 -1
  120. package/dist/models/TaxCode.d.ts.map +1 -1
  121. package/dist/models/index.d.ts +1 -0
  122. package/dist/models/index.d.ts.map +1 -1
  123. package/dist/money.d.ts +5 -1
  124. package/dist/money.d.ts.map +1 -1
  125. package/dist/money.js +62 -15
  126. package/dist/money.js.map +1 -1
  127. package/dist/observed-purchase-price-internal.d.ts +3 -0
  128. package/dist/observed-purchase-price-internal.d.ts.map +1 -0
  129. package/dist/observed-purchase-price.d.ts +71 -0
  130. package/dist/observed-purchase-price.d.ts.map +1 -0
  131. package/dist/received-purchase-money.d.ts +87 -0
  132. package/dist/received-purchase-money.d.ts.map +1 -0
  133. package/dist/recipes.d.ts +4 -1
  134. package/dist/recipes.d.ts.map +1 -1
  135. package/dist/retention-accounting-internal.d.ts +3 -0
  136. package/dist/retention-accounting-internal.d.ts.map +1 -0
  137. package/dist/retention-accounting.d.ts +164 -0
  138. package/dist/retention-accounting.d.ts.map +1 -0
  139. package/dist/retention-money.d.ts +43 -0
  140. package/dist/retention-money.d.ts.map +1 -0
  141. package/dist/smrt-knowledge.json +1866 -113
  142. package/dist/tax/cookbook.d.ts.map +1 -1
  143. package/dist/tax-summary.d.ts +1 -0
  144. package/dist/tax-summary.d.ts.map +1 -1
  145. package/dist/types/index.d.ts +20 -1
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/package.json +16 -16
  148. package/dist/chunks/CustomerCollection-BIPH2ZUx.js +0 -99
  149. package/dist/chunks/CustomerCollection-BIPH2ZUx.js.map +0 -1
  150. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js +0 -1666
  151. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +0 -1
  152. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +0 -1
  153. package/dist/chunks/TaxCodeCollection-DaBOPG9c.js.map +0 -1
  154. package/dist/chunks/VendorCollection-D6ZcUYy8.js +0 -100
  155. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +0 -1
  156. package/dist/chunks/calculations-C9daQLsh.js +0 -82
  157. package/dist/chunks/calculations-C9daQLsh.js.map +0 -1
  158. package/dist/chunks/contract-money-EuToOghh.js.map +0 -1
  159. package/dist/chunks/document-money-DV5t4vlK.js +0 -1186
  160. package/dist/chunks/document-money-DV5t4vlK.js.map +0 -1
  161. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +0 -1
@@ -0,0 +1,4385 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { i as CustomerType, n as ContractType, p as VendorStatus, r as CustomerStatus, s as InvoiceStatus, t as ContractStatus } from "./types-Bbm0Xflc.js";
3
+ import { MoneyValidationError, assertIntegerMinorUnits, calculateMoneyDocument, resolveLineTaxCode } from "../money.js";
4
+ import { n as withHistoricalCommerceWrite, t as assertHistoricalCommerceWrite } from "./historical-commerce-internal-C9ButfuH.js";
5
+ import { t as allocateFrozenCredit } from "./credit-allocation-yspz0Lab.js";
6
+ import { _ as withDocumentMoneyTransaction, a as assertCommerceMoneySchemaCurrent, f as DOCUMENT_MONEY_READ_REBIND, g as withDocumentMoneyObjectTransaction, h as lockDocumentMoneyParents, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
7
+ import { s as parseInvoiceRetention, t as InvoiceCollection } from "./InvoiceCollection-BIcge9Wl.js";
8
+ import { n as withTenantMoneyCollectionRead, r as withTenantMoneyRuntime, t as lockTenantMoneyRuntime } from "./tenant-money-barrier-DoF6lR8h.js";
9
+ import { t as TaxCodeCollection } from "./TaxCodeCollection-RWdVaheF.js";
10
+ import { t as ContractLineItemCollection } from "./ContractLineItemCollection-J4xtRtzE.js";
11
+ import { t as TaxComponentCollection } from "./TaxComponentCollection-Dt2jeWiC.js";
12
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
13
+ import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
14
+ import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
15
+ import { createHash } from "node:crypto";
16
+ //#region src/party-contacts.ts
17
+ var DEFAULT_PARTY_CONTACT_FIELD_NAMES = {
18
+ contactId: "contactId",
19
+ contactName: "contactName",
20
+ contactLabel: "contactLabel",
21
+ contactEmail: "contactEmail",
22
+ contactPhone: "contactPhone",
23
+ contactAddress: "contactAddress",
24
+ contactCount: "contactCount"
25
+ };
26
+ var PARTY_CONTACT_MAX_LENGTHS = {
27
+ name: 160,
28
+ label: 80,
29
+ email: 320,
30
+ phone: 80,
31
+ address: 500
32
+ };
33
+ var FIELDS = [
34
+ "name",
35
+ "label",
36
+ "email",
37
+ "phone",
38
+ "address"
39
+ ];
40
+ var PartyContactValidationError = class extends Error {
41
+ /** Errors aligned with the submitted rows; `undefined` for a valid row. */
42
+ errors;
43
+ constructor(errors) {
44
+ super("One or more contact rows are invalid");
45
+ this.name = "PartyContactValidationError";
46
+ this.errors = errors;
47
+ }
48
+ };
49
+ var PartyContactPayloadError = class extends Error {
50
+ reason;
51
+ constructor(reason, message) {
52
+ super(message);
53
+ this.name = "PartyContactPayloadError";
54
+ this.reason = reason;
55
+ }
56
+ };
57
+ var PartyContactPartyNotFoundError = class extends Error {
58
+ constructor(partyId) {
59
+ super(`Customer or vendor ${partyId} was not found`);
60
+ this.name = "PartyContactPartyNotFoundError";
61
+ }
62
+ };
63
+ function text$2(value) {
64
+ return typeof value === "string" ? value : "";
65
+ }
66
+ function normalizePartyContacts(value) {
67
+ let input = value;
68
+ if (typeof input === "string") try {
69
+ input = JSON.parse(input);
70
+ } catch {
71
+ return [];
72
+ }
73
+ if (!Array.isArray(input)) return [];
74
+ const contacts = [];
75
+ for (const entry of input) {
76
+ if (!entry || typeof entry !== "object") continue;
77
+ const row = entry;
78
+ contacts.push({
79
+ id: text$2(row.id) || globalThis.crypto.randomUUID(),
80
+ name: text$2(row.name),
81
+ label: text$2(row.label),
82
+ email: text$2(row.email),
83
+ phone: text$2(row.phone),
84
+ address: text$2(row.address)
85
+ });
86
+ }
87
+ return contacts;
88
+ }
89
+ function readPartyContactRows(form, fieldNames = {}) {
90
+ const names = {
91
+ ...DEFAULT_PARTY_CONTACT_FIELD_NAMES,
92
+ ...fieldNames
93
+ };
94
+ const column = (name) => form.getAll(name).map(text$2);
95
+ const columns = {
96
+ id: column(names.contactId),
97
+ name: column(names.contactName),
98
+ label: column(names.contactLabel),
99
+ email: column(names.contactEmail),
100
+ phone: column(names.contactPhone),
101
+ address: column(names.contactAddress)
102
+ };
103
+ const markers = column(names.contactCount);
104
+ if (markers.length !== 1) throw new PartyContactPayloadError(markers.length === 0 ? "missing" : "misaligned", markers.length === 0 ? `Contact payload has no ${names.contactCount} marker; the contact fields were not posted under these names` : `Contact payload has ${markers.length} ${names.contactCount} markers`);
105
+ const count = columns.id.length;
106
+ if (markers[0] !== String(count)) throw new PartyContactPayloadError("misaligned", `Contact payload is misaligned: ${names.contactCount} is ${markers[0]} but ${count} contact ids were posted`);
107
+ for (const [key, values] of Object.entries(columns)) if (values.length !== count) throw new PartyContactPayloadError("misaligned", `Contact payload is misaligned: ${count} contact ids but ${values.length} ${key} values`);
108
+ return columns.id.map((id, index) => ({
109
+ id,
110
+ name: columns.name[index],
111
+ label: columns.label[index],
112
+ email: columns.email[index],
113
+ phone: columns.phone[index],
114
+ address: columns.address[index]
115
+ }));
116
+ }
117
+ function defaultParseRemoveIntent(intent) {
118
+ const match = /^removeContact:(\d+)$/.exec(intent);
119
+ return match ? Number(match[1]) : void 0;
120
+ }
121
+ function applyPartyContactIntent(rows, intent, intents = {}) {
122
+ if (intent === (intents.addIntent ?? "addContact")) return {
123
+ contacts: [...rows, {}],
124
+ handled: true
125
+ };
126
+ const index = (intents.parseRemoveIntent ?? defaultParseRemoveIntent)(intent);
127
+ if (index !== void 0) return {
128
+ contacts: rows.filter((_, position) => position !== index),
129
+ handled: true
130
+ };
131
+ return {
132
+ contacts: [...rows],
133
+ handled: false
134
+ };
135
+ }
136
+ var EMAIL = /^[^\s@]+@[^\s@]+$/;
137
+ function validatePartyContactRows(rows) {
138
+ const contacts = [];
139
+ const errors = [];
140
+ let invalid = false;
141
+ rows.forEach((row, index) => {
142
+ const trimmed = { id: text$2(row.id).trim() };
143
+ let rowErrors;
144
+ for (const field of FIELDS) {
145
+ const value = text$2(row[field]).trim();
146
+ trimmed[field] = value;
147
+ if (value.length > PARTY_CONTACT_MAX_LENGTHS[field]) rowErrors = {
148
+ ...rowErrors,
149
+ [field]: "too_long"
150
+ };
151
+ }
152
+ if (trimmed.email && !rowErrors?.email && !EMAIL.test(trimmed.email)) rowErrors = {
153
+ ...rowErrors,
154
+ email: "invalid_email"
155
+ };
156
+ errors[index] = rowErrors;
157
+ if (rowErrors) invalid = true;
158
+ if (FIELDS.some((field) => trimmed[field])) contacts.push(trimmed);
159
+ });
160
+ return invalid ? {
161
+ contacts,
162
+ errors
163
+ } : { contacts };
164
+ }
165
+ async function savePartyContacts(collection, partyId, posted, options = {}) {
166
+ const rows = Array.isArray(posted) ? posted : readPartyContactRows(posted, options.fieldNames);
167
+ const party = await collection.get({ id: partyId });
168
+ if (!party || options.expectedTenantId !== void 0 && party.tenantId !== options.expectedTenantId) throw new PartyContactPartyNotFoundError(partyId);
169
+ const validation = validatePartyContactRows(rows);
170
+ const { contacts } = validation;
171
+ const errors = validation.errors ?? rows.map(() => void 0);
172
+ const saved = new Set(normalizePartyContacts(party.contacts).map((c) => c.id));
173
+ let invalid = validation.errors !== void 0;
174
+ rows.forEach((row, index) => {
175
+ const id = text$2(row.id).trim();
176
+ if (id && !saved.has(id)) {
177
+ errors[index] = {
178
+ ...errors[index],
179
+ id: "unknown_contact"
180
+ };
181
+ invalid = true;
182
+ }
183
+ });
184
+ if (invalid) throw new PartyContactValidationError(errors);
185
+ const seen = /* @__PURE__ */ new Set();
186
+ party.contacts = contacts.map((row) => {
187
+ const id = row.id && !seen.has(row.id) ? row.id : globalThis.crypto.randomUUID();
188
+ seen.add(id);
189
+ return {
190
+ id,
191
+ name: row.name ?? "",
192
+ label: row.label ?? "",
193
+ email: row.email ?? "",
194
+ phone: row.phone ?? "",
195
+ address: row.address ?? ""
196
+ };
197
+ });
198
+ await party.save();
199
+ return party.contacts;
200
+ }
201
+ //#endregion
202
+ //#region src/models/Customer.ts
203
+ var __defProp$5 = Object.defineProperty;
204
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
205
+ var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, {
206
+ enumerable: true,
207
+ configurable: true,
208
+ writable: true,
209
+ value
210
+ }) : obj[key] = value;
211
+ var __decorateClass$5 = (decorators, target, key, kind) => {
212
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
213
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
214
+ if (kind && result) __defProp$5(target, key, result);
215
+ return result;
216
+ };
217
+ var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
218
+ var Customer = class extends SmrtObject {
219
+ tenantId = null;
220
+ profileId = "";
221
+ creditLimit = 0;
222
+ paymentTerms = "";
223
+ taxExempt = false;
224
+ taxId = "";
225
+ defaultTaxCodeId = "";
226
+ defaultShippingAddress = {};
227
+ defaultBillingAddress = {};
228
+ status = CustomerStatus.ACTIVE;
229
+ customerType = CustomerType.DTC;
230
+ notes = "";
231
+ contacts = [];
232
+ constructor(options = {}) {
233
+ super(options);
234
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
235
+ if (options.profileId !== void 0) this.profileId = options.profileId;
236
+ if (options.creditLimit !== void 0) this.creditLimit = options.creditLimit;
237
+ if (options.paymentTerms !== void 0) this.paymentTerms = options.paymentTerms;
238
+ if (options.taxExempt !== void 0) this.taxExempt = options.taxExempt;
239
+ if (options.taxId !== void 0) this.taxId = options.taxId;
240
+ if (options.defaultTaxCodeId !== void 0) this.defaultTaxCodeId = options.defaultTaxCodeId;
241
+ if (options.defaultShippingAddress !== void 0) this.defaultShippingAddress = options.defaultShippingAddress;
242
+ if (options.defaultBillingAddress !== void 0) this.defaultBillingAddress = options.defaultBillingAddress;
243
+ if (options.status !== void 0) this.status = options.status;
244
+ if (options.customerType !== void 0 && options.customerType !== null) this.customerType = options.customerType;
245
+ if (options.notes !== void 0) this.notes = options.notes;
246
+ if (options.contacts !== void 0) this.contacts = normalizePartyContacts(options.contacts);
247
+ }
248
+ /**
249
+ * Re-normalize contacts after the framework's option-override pass, which
250
+ * re-applies raw option values (including a stored JSON string).
251
+ */
252
+ async initialize() {
253
+ await super.initialize();
254
+ this.contacts = normalizePartyContacts(this.contacts);
255
+ return this;
256
+ }
257
+ /** Save with a final contacts normalization against direct assignment. */
258
+ async save() {
259
+ this.contacts = normalizePartyContacts(this.contacts);
260
+ return super.save();
261
+ }
262
+ /**
263
+ * Convenience predicate: is this a wholesale customer?
264
+ */
265
+ isWholesale() {
266
+ return this.customerType === CustomerType.WHOLESALE;
267
+ }
268
+ /**
269
+ * Check if customer is active
270
+ */
271
+ isActive() {
272
+ return this.status === CustomerStatus.ACTIVE;
273
+ }
274
+ /**
275
+ * Check if customer has available credit
276
+ */
277
+ hasAvailableCredit(amount) {
278
+ return this.creditLimit >= amount;
279
+ }
280
+ /**
281
+ * Get the profile ID for external lookup.
282
+ *
283
+ * To get the actual Profile object, use the ProfileCollection from smrt-profiles:
284
+ * ```typescript
285
+ * const profiles = await ProfileCollection.create(options);
286
+ * const profile = await profiles.get({ id: customer.profileId });
287
+ * ```
288
+ *
289
+ * @returns The profile ID or empty string if not set
290
+ */
291
+ getProfileId() {
292
+ return this.profileId;
293
+ }
294
+ };
295
+ __publicField$5(Customer, "__smrtPackage__", "@happyvertical/smrt-commerce");
296
+ __decorateClass$5([tenantId({ nullable: true })], Customer.prototype, "tenantId", 2);
297
+ __decorateClass$5([crossPackageRef("@happyvertical/smrt-profiles:Profile", { description: "Links this customer to their profile record, where their name and contact details are kept." })], Customer.prototype, "profileId", 2);
298
+ __decorateClass$5([field({
299
+ description: "The most this customer may owe you at one time. Leave it at zero if you do not set limits.",
300
+ ui: { format: "money" }
301
+ })], Customer.prototype, "creditLimit", 2);
302
+ __decorateClass$5([field({ description: "How long the customer has to pay, written the way you would say it, for example \"Net 30\"." })], Customer.prototype, "paymentTerms", 2);
303
+ __decorateClass$5([field({ description: "Turn this on if you do not charge this customer sales tax." })], Customer.prototype, "taxExempt", 2);
304
+ __decorateClass$5([field({
305
+ sensitive: true,
306
+ description: "The tax or VAT number of the customer, kept on file for tax-exempt sales."
307
+ })], Customer.prototype, "taxId", 2);
308
+ __decorateClass$5([foreignKey("TaxCode", { description: "The tax code normally used when selling to this customer." })], Customer.prototype, "defaultTaxCodeId", 2);
309
+ __decorateClass$5([field({ description: "Where you usually send goods to this customer, so you do not retype it on every order." })], Customer.prototype, "defaultShippingAddress", 2);
310
+ __decorateClass$5([field({ description: "Where you send invoices to this customer." })], Customer.prototype, "defaultBillingAddress", 2);
311
+ __decorateClass$5([field({ description: "Whether you are still dealing with this customer: active, inactive or suspended." })], Customer.prototype, "status", 2);
312
+ __decorateClass$5([field({ description: "The kind of customer: a direct shopper, a wholesale buyer or a retail customer." })], Customer.prototype, "customerType", 2);
313
+ __decorateClass$5([field({
314
+ description: "Anything worth remembering about this customer.",
315
+ ui: { widget: "textarea" }
316
+ })], Customer.prototype, "notes", 2);
317
+ __decorateClass$5([field({ description: "The people you deal with at this customer, with their role and how to reach them." })], Customer.prototype, "contacts", 2);
318
+ Customer = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
319
+ api: { include: [
320
+ "list",
321
+ "get",
322
+ "create",
323
+ "update"
324
+ ] },
325
+ mcp: { include: ["list", "get"] },
326
+ cli: { skipApiCheck: true }
327
+ })], Customer);
328
+ //#endregion
329
+ //#region src/collections/CustomerCollection.ts
330
+ var CustomerCollection = class extends SmrtCollection {
331
+ static _itemClass = Customer;
332
+ /**
333
+ * Find customers by profile ID
334
+ *
335
+ * @param profileId - Profile ID from smrt-profiles
336
+ * @returns Array of customers linked to this profile
337
+ */
338
+ async findByProfile(profileId) {
339
+ return await this.list({
340
+ where: { profileId },
341
+ orderBy: "created_at DESC"
342
+ });
343
+ }
344
+ /**
345
+ * Find all active customers
346
+ *
347
+ * @returns Array of active customers
348
+ */
349
+ async findActive() {
350
+ return await this.list({
351
+ where: { status: CustomerStatus.ACTIVE },
352
+ orderBy: "created_at DESC"
353
+ });
354
+ }
355
+ /**
356
+ * Find customers by status
357
+ *
358
+ * @param status - Customer status
359
+ * @returns Array of customers
360
+ */
361
+ async findByStatus(status) {
362
+ return await this.list({
363
+ where: { status },
364
+ orderBy: "created_at DESC"
365
+ });
366
+ }
367
+ /**
368
+ * Get or create a customer for a profile
369
+ *
370
+ * @param profileId - Profile ID
371
+ * @param defaults - Default values if creating
372
+ * @returns Customer
373
+ */
374
+ async getOrCreateForProfile(profileId, defaults = {}) {
375
+ const existing = await this.findByProfile(profileId);
376
+ if (existing.length > 0) return existing[0];
377
+ const customer = await this.create({
378
+ profileId,
379
+ creditLimit: defaults.creditLimit ?? 0,
380
+ paymentTerms: defaults.paymentTerms ?? "",
381
+ status: CustomerStatus.ACTIVE
382
+ });
383
+ await customer.save();
384
+ return customer;
385
+ }
386
+ /**
387
+ * Find all customers belonging to a specific tenant
388
+ *
389
+ * @param tenantId - Tenant ID
390
+ * @returns Array of customers for the tenant
391
+ */
392
+ async findByTenant(tenantId) {
393
+ return this.list({ where: { tenantId } });
394
+ }
395
+ /**
396
+ * Find all global customers (not associated with any tenant).
397
+ *
398
+ * Routes through the shared tenant-global helper so it does not throw under
399
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
400
+ * flagged as an isolation violation). (#1600)
401
+ *
402
+ * @returns Array of global customers
403
+ */
404
+ async findGlobal() {
405
+ return queryGlobal(this);
406
+ }
407
+ /**
408
+ * Find customers for a tenant including global customers.
409
+ *
410
+ * Fails closed if an active tenant context requests a different tenant's
411
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
412
+ *
413
+ * @param tenantId - Tenant ID
414
+ * @returns Array of tenant-specific and global customers
415
+ */
416
+ async findWithGlobals(tenantId) {
417
+ return queryWithGlobals(this, tenantId, "Customer.findWithGlobals");
418
+ }
419
+ };
420
+ //#endregion
421
+ //#region src/models/InvoiceLineItem.ts
422
+ var __defProp$4 = Object.defineProperty;
423
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
424
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
425
+ enumerable: true,
426
+ configurable: true,
427
+ writable: true,
428
+ value
429
+ }) : obj[key] = value;
430
+ var __decorateClass$4 = (decorators, target, key, kind) => {
431
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
432
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
433
+ if (kind && result) __defProp$4(target, key, result);
434
+ return result;
435
+ };
436
+ var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
437
+ var InvoiceLineItem = class extends SmrtObject {
438
+ /** @internal Collection read guard returns this snapshot on its root DB. */
439
+ rebindMoneyRead(capability, db) {
440
+ if (capability !== DOCUMENT_MONEY_READ_REBIND) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice line read rebinding requires the owning capability");
441
+ this._db = db;
442
+ this.options.db = db;
443
+ return this;
444
+ }
445
+ tenantId = null;
446
+ invoiceId = "";
447
+ creditedLineId = null;
448
+ creditCalculationBasisJson = "";
449
+ get creditCalculationBasis() {
450
+ try {
451
+ const value = JSON.parse(this.creditCalculationBasisJson);
452
+ if (!value || typeof value !== "object" || !("kind" in value)) return null;
453
+ const basis = value;
454
+ 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;
455
+ return basis;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ set creditCalculationBasis(value) {
461
+ this.creditCalculationBasisJson = value ? JSON.stringify(value) : "";
462
+ }
463
+ description = "";
464
+ /**
465
+ * SKU or item code
466
+ */
467
+ sku = "";
468
+ productId = null;
469
+ quantity = 1;
470
+ unitPrice = 0;
471
+ discount = 0;
472
+ discountType = "none";
473
+ discountRate = "0.0";
474
+ taxRate = 0;
475
+ taxMode = "inherit";
476
+ taxCodeId = null;
477
+ taxCodeCode = null;
478
+ taxCodeName = null;
479
+ taxComponentsJson = null;
480
+ get taxComponents() {
481
+ if (this.taxComponentsJson === null) return [];
482
+ try {
483
+ const value = JSON.parse(this.taxComponentsJson);
484
+ if (!Array.isArray(value)) return [];
485
+ return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.name === "string" && typeof entry.percent === "string" && Number.isSafeInteger(entry.taxMinor));
486
+ } catch {
487
+ return [];
488
+ }
489
+ }
490
+ set taxComponents(value) {
491
+ this.taxComponentsJson = JSON.stringify(value);
492
+ }
493
+ taxAmount = 0;
494
+ /** Stable document snapshot identity; ordinary lifecycle saves do not change it. */
495
+ moneySnapshotId = "";
496
+ amount = 0;
497
+ /**
498
+ * Type of source ('campaign' | 'contract' | 'manual' | etc.)
499
+ */
500
+ sourceType = "";
501
+ /**
502
+ * ID of the source (e.g., campaign ID, contract ID)
503
+ */
504
+ sourceId = "";
505
+ /**
506
+ * Service period start (for time-based billing)
507
+ */
508
+ periodStart = null;
509
+ /**
510
+ * Service period end (for time-based billing)
511
+ */
512
+ periodEnd = null;
513
+ revenueAccountId = null;
514
+ /**
515
+ * Sort order within the invoice
516
+ */
517
+ sortOrder = 0;
518
+ constructor(options = {}) {
519
+ super(options);
520
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
521
+ if (options.invoiceId !== void 0) this.invoiceId = options.invoiceId;
522
+ if (options.creditedLineId !== void 0) this.creditedLineId = options.creditedLineId;
523
+ if (options.creditCalculationBasisJson !== void 0) this.creditCalculationBasisJson = options.creditCalculationBasisJson;
524
+ if (options.creditCalculationBasis !== void 0) this.creditCalculationBasis = structuredClone(options.creditCalculationBasis);
525
+ if (options.description !== void 0) this.description = options.description;
526
+ if (options.sku !== void 0) this.sku = options.sku;
527
+ if (options.productId !== void 0) this.productId = options.productId;
528
+ if (options.quantity !== void 0) this.quantity = options.quantity;
529
+ if (options.unitPrice !== void 0) this.unitPrice = options.unitPrice;
530
+ if (options.discount !== void 0) this.discount = options.discount;
531
+ if (options.discountType !== void 0) this.discountType = options.discountType;
532
+ if (options.discountRate !== void 0) this.discountRate = options.discountRate;
533
+ if (options.taxRate !== void 0) this.taxRate = options.taxRate;
534
+ if (options.taxMode !== void 0) this.taxMode = options.taxMode;
535
+ if (options.taxCodeId !== void 0) this.taxCodeId = options.taxCodeId;
536
+ if (options.taxCodeCode !== void 0) this.taxCodeCode = options.taxCodeCode;
537
+ if (options.taxCodeName !== void 0) this.taxCodeName = options.taxCodeName;
538
+ if (options.taxComponentsJson !== void 0) this.taxComponentsJson = options.taxComponentsJson;
539
+ if (options.taxComponents !== void 0) this.taxComponents = structuredClone(options.taxComponents);
540
+ if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
541
+ if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
542
+ if (options.amount !== void 0) this.amount = options.amount;
543
+ if (options.sourceType !== void 0) this.sourceType = options.sourceType;
544
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
545
+ if (options.periodStart !== void 0) this.periodStart = options.periodStart;
546
+ if (options.periodEnd !== void 0) this.periodEnd = options.periodEnd;
547
+ if (options.revenueAccountId !== void 0) this.revenueAccountId = options.revenueAccountId;
548
+ if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
549
+ }
550
+ /** Refuse an old-format row read from the database. */
551
+ async initialize() {
552
+ await super.initialize();
553
+ if (this.isPersisted) {
554
+ const { assertHydratedMoneyRowCurrent } = await import("./detect-DAE-_NAk.js").then((n) => n.c);
555
+ await assertHydratedMoneyRowCurrent(this);
556
+ }
557
+ return this;
558
+ }
559
+ /** Persist through the invoice money aggregate. */
560
+ async save() {
561
+ const { saveInvoiceLineItem } = await Promise.resolve().then(() => document_money_exports);
562
+ return await saveInvoiceLineItem(this);
563
+ }
564
+ /** @internal Aggregate persistence only. */
565
+ async saveDerived(capability) {
566
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice line money write capability is required");
567
+ 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`);
568
+ return await super.save();
569
+ }
570
+ async delete(options = {}) {
571
+ const { deleteInvoiceLineItem } = await Promise.resolve().then(() => document_money_exports);
572
+ await deleteInvoiceLineItem(this, options);
573
+ }
574
+ /** @internal Aggregate persistence only. */
575
+ async deleteDerived(capability, options = {}) {
576
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice line money write capability is required");
577
+ await super.delete(options);
578
+ }
579
+ /**
580
+ * Check if line item has source tracking
581
+ */
582
+ hasSource() {
583
+ return !!this.sourceType && !!this.sourceId;
584
+ }
585
+ /**
586
+ * Check if line item has a service period
587
+ */
588
+ hasPeriod() {
589
+ return !!this.periodStart && !!this.periodEnd;
590
+ }
591
+ };
592
+ __publicField$4(InvoiceLineItem, "__smrtPackage__", "@happyvertical/smrt-commerce");
593
+ __decorateClass$4([tenantId({ nullable: true })], InvoiceLineItem.prototype, "tenantId", 2);
594
+ __decorateClass$4([field({ description: "The invoice this line belongs to." }), foreignKey("Invoice", { role: "items" })], InvoiceLineItem.prototype, "invoiceId", 2);
595
+ __decorateClass$4([foreignKey("InvoiceLineItem", { nullable: true })], InvoiceLineItem.prototype, "creditedLineId", 2);
596
+ __decorateClass$4([field({ description: "Frozen source basis used to cap and reproduce an issued credit line." })], InvoiceLineItem.prototype, "creditCalculationBasisJson", 2);
597
+ __decorateClass$4([field({ description: "What is being billed on this line." })], InvoiceLineItem.prototype, "description", 2);
598
+ __decorateClass$4([crossPackageRef("@happyvertical/smrt-products:Product", { nullable: true })], InvoiceLineItem.prototype, "productId", 2);
599
+ __decorateClass$4([field({ description: "How many." })], InvoiceLineItem.prototype, "quantity", 2);
600
+ __decorateClass$4([field({
601
+ description: "The price of one, before discount; includes tax when the document's prices include tax.",
602
+ ui: { format: "money" }
603
+ })], InvoiceLineItem.prototype, "unitPrice", 2);
604
+ __decorateClass$4([field({ ui: { format: "money" } })], InvoiceLineItem.prototype, "discount", 2);
605
+ __decorateClass$4([field({ type: "decimal" })], InvoiceLineItem.prototype, "discountRate", 2);
606
+ __decorateClass$4([field({ description: "The stored fractional tax rate for this line (0.05 means 5%)." })], InvoiceLineItem.prototype, "taxRate", 2);
607
+ __decorateClass$4([foreignKey("TaxCode", { nullable: true })], InvoiceLineItem.prototype, "taxCodeId", 2);
608
+ __decorateClass$4([field({ ui: { format: "money" } })], InvoiceLineItem.prototype, "taxAmount", 2);
609
+ __decorateClass$4([field({
610
+ description: "The line amount: net of discount, and including tax only when the document's prices include tax.",
611
+ ui: { format: "money" }
612
+ })], InvoiceLineItem.prototype, "amount", 2);
613
+ __decorateClass$4([crossPackageRef("@happyvertical/smrt-ledgers:Account", { nullable: true })], InvoiceLineItem.prototype, "revenueAccountId", 2);
614
+ InvoiceLineItem = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
615
+ api: {
616
+ include: [
617
+ "list",
618
+ "get",
619
+ "create",
620
+ "update",
621
+ "delete"
622
+ ],
623
+ writable: [
624
+ "invoiceId",
625
+ "description",
626
+ "sku",
627
+ "productId",
628
+ "quantity",
629
+ "unitPrice",
630
+ "discount",
631
+ "discountType",
632
+ "discountRate",
633
+ "taxRate",
634
+ "taxMode",
635
+ "taxCodeId",
636
+ "sourceType",
637
+ "sourceId",
638
+ "periodStart",
639
+ "periodEnd",
640
+ "revenueAccountId",
641
+ "sortOrder"
642
+ ]
643
+ },
644
+ mcp: { include: ["list", "get"] },
645
+ cli: { skipApiCheck: true }
646
+ })], InvoiceLineItem);
647
+ //#endregion
648
+ //#region src/collections/InvoiceLineItemCollection.ts
649
+ var InvoiceLineItemCollection = class extends SmrtCollection {
650
+ static _itemClass = InvoiceLineItem;
651
+ async withMoneyRead(operation) {
652
+ const rootDb = this.db;
653
+ const tenantId = getTenantId();
654
+ if (!tenantId) {
655
+ await assertCommerceMoneySchemaCurrent(rootDb);
656
+ return operation();
657
+ }
658
+ const result = await withTenantMoneyCollectionRead(rootDb, tenantId, (boundDb) => this.withDatabase(boundDb, operation));
659
+ if (result instanceof InvoiceLineItem) result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
660
+ else if (Array.isArray(result)) {
661
+ for (const item of result) if (item instanceof InvoiceLineItem) item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
662
+ }
663
+ return result;
664
+ }
665
+ async get(filter, options = {}) {
666
+ return this.withMoneyRead(() => super.get(filter, options));
667
+ }
668
+ async list(options = {}) {
669
+ return this.withMoneyRead(() => super.list(options));
670
+ }
671
+ /**
672
+ * Find line items by invoice
673
+ *
674
+ * @param invoiceId - Invoice ID
675
+ * @returns Array of line items, sorted by sortOrder
676
+ */
677
+ async findByInvoice(invoiceId) {
678
+ return await this.list({
679
+ where: { invoiceId },
680
+ orderBy: "sortOrder ASC"
681
+ });
682
+ }
683
+ /**
684
+ * Find line items by source
685
+ *
686
+ * @param sourceType - Source type (e.g., 'campaign', 'contract')
687
+ * @param sourceId - Source ID
688
+ * @returns Array of line items
689
+ */
690
+ async findBySource(sourceType, sourceId) {
691
+ return await this.list({
692
+ where: {
693
+ sourceType,
694
+ sourceId
695
+ },
696
+ orderBy: "created_at DESC"
697
+ });
698
+ }
699
+ /**
700
+ * Find line items by source type only
701
+ *
702
+ * @param sourceType - Source type (e.g., 'campaign')
703
+ * @returns Array of line items
704
+ */
705
+ async findBySourceType(sourceType) {
706
+ return await this.list({
707
+ where: { sourceType },
708
+ orderBy: "created_at DESC"
709
+ });
710
+ }
711
+ /**
712
+ * Calculate total amount for an invoice
713
+ *
714
+ * @param invoiceId - Invoice ID
715
+ * @returns Total amount
716
+ */
717
+ async getTotalForInvoice(invoiceId) {
718
+ return (await this.findByInvoice(invoiceId)).reduce((sum, item) => sum + item.amount, 0);
719
+ }
720
+ /**
721
+ * Get the next sort order for an invoice
722
+ *
723
+ * @param invoiceId - Invoice ID
724
+ * @returns Next sort order number
725
+ */
726
+ async getNextSortOrder(invoiceId) {
727
+ const lineItems = await this.findByInvoice(invoiceId);
728
+ if (lineItems.length === 0) return 0;
729
+ return Math.max(...lineItems.map((item) => item.sortOrder)) + 1;
730
+ }
731
+ /**
732
+ * Find all invoice line items belonging to a specific tenant
733
+ *
734
+ * @param tenantId - Tenant ID
735
+ * @returns Array of invoice line items for the tenant
736
+ */
737
+ async findByTenant(tenantId) {
738
+ return this.list({ where: { tenantId } });
739
+ }
740
+ /**
741
+ * Find all global invoice line items (not associated with any tenant).
742
+ *
743
+ * Routes through the shared tenant-global helper so it does not throw under
744
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
745
+ * flagged as an isolation violation). (#1600)
746
+ *
747
+ * @returns Array of global invoice line items
748
+ */
749
+ async findGlobal() {
750
+ return queryGlobal(this);
751
+ }
752
+ /**
753
+ * Find invoice line items for a tenant including global line items.
754
+ *
755
+ * Fails closed if an active tenant context requests a different tenant's
756
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
757
+ *
758
+ * @param tenantId - Tenant ID
759
+ * @returns Array of tenant-specific and global invoice line items
760
+ */
761
+ async findWithGlobals(tenantId) {
762
+ return queryWithGlobals(this, tenantId, "InvoiceLineItem.findWithGlobals");
763
+ }
764
+ };
765
+ //#endregion
766
+ //#region src/models/HistoricalSalesLine.ts
767
+ var __defProp$3 = Object.defineProperty;
768
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
769
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, {
770
+ enumerable: true,
771
+ configurable: true,
772
+ writable: true,
773
+ value
774
+ }) : obj[key] = value;
775
+ var __decorateClass$3 = (decorators, target, key, kind) => {
776
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
777
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
778
+ if (kind && result) __defProp$3(target, key, result);
779
+ return result;
780
+ };
781
+ var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
782
+ var HistoricalSalesLine = class extends SmrtObject {
783
+ tenantId = "";
784
+ invoiceId = "";
785
+ creditedLineId = null;
786
+ sourceKey = "";
787
+ sourceKeyHash = "";
788
+ description = "";
789
+ netMinor = 0;
790
+ taxMinor = null;
791
+ grossMinor = null;
792
+ quantityText = null;
793
+ unitRateText = null;
794
+ sourceFieldsJson = "{}";
795
+ moneySnapshotId = "";
796
+ constructor(options = {}) {
797
+ super(options);
798
+ for (const key of [
799
+ "tenantId",
800
+ "invoiceId",
801
+ "creditedLineId",
802
+ "sourceKey",
803
+ "sourceKeyHash",
804
+ "description",
805
+ "netMinor",
806
+ "taxMinor",
807
+ "grossMinor",
808
+ "quantityText",
809
+ "unitRateText",
810
+ "sourceFieldsJson",
811
+ "moneySnapshotId"
812
+ ]) if (options[key] !== void 0) Object.assign(this, { [key]: options[key] });
813
+ if (options.sourceFields !== void 0) this.sourceFieldsJson = JSON.stringify(options.sourceFields);
814
+ }
815
+ async save(options) {
816
+ assertHistoricalCommerceWrite(this);
817
+ if (!this.tenantId || !this.invoiceId || !this.sourceKey || !this.sourceKeyHash || !this.moneySnapshotId) throw new Error("Historical source line identity is incomplete");
818
+ if (this.id && await this.db.get(this.tableName, { id: this.id })) throw new Error("Historical sales source lines are immutable");
819
+ return super.save(options);
820
+ }
821
+ async delete() {
822
+ throw new Error("Historical sales source lines cannot be deleted");
823
+ }
824
+ };
825
+ __publicField$3(HistoricalSalesLine, "__smrtPackage__", "@happyvertical/smrt-commerce");
826
+ __decorateClass$3([tenantId()], HistoricalSalesLine.prototype, "tenantId", 2);
827
+ __decorateClass$3([foreignKey("Invoice")], HistoricalSalesLine.prototype, "invoiceId", 2);
828
+ __decorateClass$3([foreignKey("HistoricalSalesLine", { nullable: true })], HistoricalSalesLine.prototype, "creditedLineId", 2);
829
+ __decorateClass$3([field({ ui: { format: "money" } })], HistoricalSalesLine.prototype, "netMinor", 2);
830
+ __decorateClass$3([field({
831
+ type: "integer",
832
+ nullable: true,
833
+ ui: { format: "money" }
834
+ })], HistoricalSalesLine.prototype, "taxMinor", 2);
835
+ __decorateClass$3([field({
836
+ type: "integer",
837
+ nullable: true,
838
+ ui: { format: "money" }
839
+ })], HistoricalSalesLine.prototype, "grossMinor", 2);
840
+ __decorateClass$3([field({
841
+ type: "text",
842
+ nullable: true
843
+ })], HistoricalSalesLine.prototype, "quantityText", 2);
844
+ __decorateClass$3([field({
845
+ type: "text",
846
+ nullable: true
847
+ })], HistoricalSalesLine.prototype, "unitRateText", 2);
848
+ HistoricalSalesLine = __decorateClass$3([TenantScoped({ mode: "required" }), smrt({
849
+ tableName: "historical_sales_lines",
850
+ indexes: [{
851
+ name: "historical_sales_line_source_key",
852
+ columns: [
853
+ "tenantId",
854
+ "invoiceId",
855
+ "sourceKeyHash"
856
+ ],
857
+ unique: true
858
+ }],
859
+ api: { include: ["get", "list"] },
860
+ cli: { include: ["get", "list"] },
861
+ mcp: { include: ["get", "list"] }
862
+ })], HistoricalSalesLine);
863
+ //#endregion
864
+ //#region src/collections/HistoricalSalesLineCollection.ts
865
+ var HistoricalSalesLineCollection = class extends SmrtCollection {
866
+ static _itemClass = HistoricalSalesLine;
867
+ };
868
+ //#endregion
869
+ //#region src/models/Vendor.ts
870
+ var __defProp$2 = Object.defineProperty;
871
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
872
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
873
+ enumerable: true,
874
+ configurable: true,
875
+ writable: true,
876
+ value
877
+ }) : obj[key] = value;
878
+ var __decorateClass$2 = (decorators, target, key, kind) => {
879
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
880
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
881
+ if (kind && result) __defProp$2(target, key, result);
882
+ return result;
883
+ };
884
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
885
+ var Vendor = class extends SmrtObject {
886
+ tenantId = null;
887
+ profileId = "";
888
+ leadTimeDays = 0;
889
+ minimumOrderAmount = 0;
890
+ paymentTerms = "";
891
+ currency = "USD";
892
+ taxId = "";
893
+ defaultTaxCodeId = "";
894
+ defaultContactEmail = "";
895
+ defaultContactPhone = "";
896
+ status = VendorStatus.ACTIVE;
897
+ notes = "";
898
+ payoutAddresses = {};
899
+ contacts = [];
900
+ constructor(options = {}) {
901
+ super(options);
902
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
903
+ if (options.profileId !== void 0) this.profileId = options.profileId;
904
+ if (options.leadTimeDays !== void 0) this.leadTimeDays = options.leadTimeDays;
905
+ if (options.minimumOrderAmount !== void 0) this.minimumOrderAmount = options.minimumOrderAmount;
906
+ if (options.paymentTerms !== void 0) this.paymentTerms = options.paymentTerms;
907
+ if (options.currency !== void 0) this.currency = options.currency;
908
+ if (options.taxId !== void 0) this.taxId = options.taxId;
909
+ if (options.defaultTaxCodeId !== void 0) this.defaultTaxCodeId = options.defaultTaxCodeId;
910
+ if (options.defaultContactEmail !== void 0) this.defaultContactEmail = options.defaultContactEmail;
911
+ if (options.defaultContactPhone !== void 0) this.defaultContactPhone = options.defaultContactPhone;
912
+ if (options.status !== void 0) this.status = options.status;
913
+ if (options.notes !== void 0) this.notes = options.notes;
914
+ if (options.payoutAddresses !== void 0) this.payoutAddresses = Vendor.normalizePayoutAddresses(options.payoutAddresses);
915
+ if (options.contacts !== void 0) this.contacts = normalizePartyContacts(options.contacts);
916
+ }
917
+ /**
918
+ * The framework's {@link SmrtObject.initialize} re-applies `options` keys on
919
+ * top of constructor-set values so option data always wins over field
920
+ * initializers. That means our constructor's normalization runs *before*
921
+ * `initializePropertiesFromOptions` overwrites `payoutAddresses` with the
922
+ * raw cloned value. Re-normalize once super has finished so consumers
923
+ * who pass a string, a partially-typed map, or `null` end up with a
924
+ * clean `Record<string, string>` before any save / read happens.
925
+ */
926
+ async initialize() {
927
+ await super.initialize();
928
+ this.payoutAddresses = Vendor.normalizePayoutAddresses(this.payoutAddresses);
929
+ this.contacts = normalizePartyContacts(this.contacts);
930
+ return this;
931
+ }
932
+ /**
933
+ * Save with a final normalization pass so direct field assignments
934
+ * (`vendor.payoutAddresses = somethingFunny`) can't smuggle non-string
935
+ * values into the persisted row.
936
+ */
937
+ async save() {
938
+ this.payoutAddresses = Vendor.normalizePayoutAddresses(this.payoutAddresses);
939
+ this.contacts = normalizePartyContacts(this.contacts);
940
+ return super.save();
941
+ }
942
+ /**
943
+ * Check if vendor is active
944
+ */
945
+ isActive() {
946
+ return this.status === VendorStatus.ACTIVE;
947
+ }
948
+ /**
949
+ * Check if order meets minimum requirements
950
+ */
951
+ meetsMinimumOrder(amount) {
952
+ return amount >= this.minimumOrderAmount;
953
+ }
954
+ /**
955
+ * Get the profile ID for external lookup.
956
+ *
957
+ * To get the actual Profile object, use the ProfileCollection from smrt-profiles:
958
+ * ```typescript
959
+ * const profiles = await ProfileCollection.create(options);
960
+ * const profile = await profiles.get({ id: vendor.profileId });
961
+ * ```
962
+ *
963
+ * @returns The profile ID or empty string if not set
964
+ */
965
+ getProfileId() {
966
+ return this.profileId;
967
+ }
968
+ /**
969
+ * Look up the payout destination for a single currency. Returns
970
+ * `undefined` if the vendor has no entry for that currency — caller's
971
+ * job to decide whether that means "skip the option" or "raise a
972
+ * configuration error". The lookup is case-sensitive; the contract is
973
+ * that the currency code passed in matches exactly what was stored
974
+ * (`USDC-base`, not `usdc-base`).
975
+ */
976
+ getPayoutAddress(currency) {
977
+ const map = this.payoutAddresses;
978
+ if (!map || typeof map !== "object") return void 0;
979
+ const value = map[currency];
980
+ return typeof value === "string" ? value : void 0;
981
+ }
982
+ /**
983
+ * Set the payout destination for a single currency. Mutates the live
984
+ * map and returns `this` for chaining. Use {@link clearPayoutAddress}
985
+ * to remove a single entry.
986
+ */
987
+ setPayoutAddress(currency, destination) {
988
+ this.payoutAddresses = {
989
+ ...this.payoutAddresses ?? {},
990
+ [currency]: destination
991
+ };
992
+ return this;
993
+ }
994
+ /**
995
+ * Remove a single currency entry from the payout map. No-op if the
996
+ * currency was already absent.
997
+ */
998
+ clearPayoutAddress(currency) {
999
+ if (!this.payoutAddresses || !(currency in this.payoutAddresses)) return this;
1000
+ const next = { ...this.payoutAddresses };
1001
+ delete next[currency];
1002
+ this.payoutAddresses = next;
1003
+ return this;
1004
+ }
1005
+ /**
1006
+ * Normalize an input value (object, pre-serialized JSON string, or
1007
+ * `null`/`undefined`) into a plain `Record<string, string>`. Used by
1008
+ * the constructor so consumers can pass either shape into
1009
+ * `VendorCollection.create({ payoutAddresses: ... })`.
1010
+ *
1011
+ * Non-string values inside an input object are silently dropped — a
1012
+ * `Record<string, string>` invariant is what consumers downstream
1013
+ * (`PaymentIntent` builders, payout SDK adapters) rely on.
1014
+ */
1015
+ static normalizePayoutAddresses(value) {
1016
+ if (value == null) return {};
1017
+ let parsed = value;
1018
+ if (typeof value === "string") try {
1019
+ parsed = JSON.parse(value);
1020
+ } catch {
1021
+ return {};
1022
+ }
1023
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
1024
+ const out = {};
1025
+ for (const [key, v] of Object.entries(parsed)) if (typeof v === "string") out[key] = v;
1026
+ return out;
1027
+ }
1028
+ };
1029
+ __publicField$2(Vendor, "__smrtPackage__", "@happyvertical/smrt-commerce");
1030
+ __decorateClass$2([tenantId({ nullable: true })], Vendor.prototype, "tenantId", 2);
1031
+ __decorateClass$2([crossPackageRef("@happyvertical/smrt-profiles:Profile", { description: "Links this vendor to their profile record, where their name and contact details are kept." })], Vendor.prototype, "profileId", 2);
1032
+ __decorateClass$2([field({ description: "How many days the vendor usually takes between your order and delivery." })], Vendor.prototype, "leadTimeDays", 2);
1033
+ __decorateClass$2([field({
1034
+ description: "The smallest order the vendor will accept.",
1035
+ ui: { format: "money" }
1036
+ })], Vendor.prototype, "minimumOrderAmount", 2);
1037
+ __decorateClass$2([field({ description: "How long you have to pay this vendor, written the way you would say it, for example \"Net 30\"." })], Vendor.prototype, "paymentTerms", 2);
1038
+ __decorateClass$2([field({
1039
+ description: "The currency this vendor bills you in.",
1040
+ ui: { widget: "currency" }
1041
+ })], Vendor.prototype, "currency", 2);
1042
+ __decorateClass$2([field({
1043
+ sensitive: true,
1044
+ description: "The vendor tax or VAT registration number kept on file."
1045
+ })], Vendor.prototype, "taxId", 2);
1046
+ __decorateClass$2([foreignKey("TaxCode", { description: "The tax code normally used when buying from this vendor." })], Vendor.prototype, "defaultTaxCodeId", 2);
1047
+ __decorateClass$2([field({
1048
+ description: "The email address you normally use to reach this vendor.",
1049
+ ui: { widget: "email" }
1050
+ })], Vendor.prototype, "defaultContactEmail", 2);
1051
+ __decorateClass$2([field({
1052
+ description: "The phone number you normally use to reach this vendor.",
1053
+ ui: { widget: "phone" }
1054
+ })], Vendor.prototype, "defaultContactPhone", 2);
1055
+ __decorateClass$2([field({ description: "Whether you still buy from this vendor: active, inactive or suspended." })], Vendor.prototype, "status", 2);
1056
+ __decorateClass$2([field({
1057
+ description: "Anything worth remembering about this vendor.",
1058
+ ui: { widget: "textarea" }
1059
+ })], Vendor.prototype, "notes", 2);
1060
+ __decorateClass$2([field({
1061
+ sensitive: true,
1062
+ description: "Where payments to this vendor should be sent."
1063
+ })], Vendor.prototype, "payoutAddresses", 2);
1064
+ __decorateClass$2([field({ description: "The people you deal with at this vendor, with their role and how to reach them." })], Vendor.prototype, "contacts", 2);
1065
+ Vendor = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
1066
+ api: { include: [
1067
+ "list",
1068
+ "get",
1069
+ "create",
1070
+ "update"
1071
+ ] },
1072
+ mcp: { include: ["list", "get"] },
1073
+ cli: { skipApiCheck: true }
1074
+ })], Vendor);
1075
+ //#endregion
1076
+ //#region src/models/Contract.ts
1077
+ var __defProp$1 = Object.defineProperty;
1078
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
1079
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
1080
+ enumerable: true,
1081
+ configurable: true,
1082
+ writable: true,
1083
+ value
1084
+ }) : obj[key] = value;
1085
+ var __decorateClass$1 = (decorators, target, key, kind) => {
1086
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
1087
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1088
+ if (kind && result) __defProp$1(target, key, result);
1089
+ return result;
1090
+ };
1091
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
1092
+ var CONTRACT_STATUS_TRANSITIONS = {
1093
+ [ContractStatus.DRAFT]: [
1094
+ ContractStatus.SENT,
1095
+ ContractStatus.ACCEPTED,
1096
+ ContractStatus.DECLINED,
1097
+ ContractStatus.CANCELLED
1098
+ ],
1099
+ [ContractStatus.SENT]: [
1100
+ ContractStatus.ACCEPTED,
1101
+ ContractStatus.DECLINED,
1102
+ ContractStatus.CANCELLED
1103
+ ],
1104
+ [ContractStatus.ACCEPTED]: [ContractStatus.COMPLETED, ContractStatus.CANCELLED],
1105
+ [ContractStatus.DECLINED]: [],
1106
+ [ContractStatus.COMPLETED]: [],
1107
+ [ContractStatus.CANCELLED]: []
1108
+ };
1109
+ var loadedContractStatus = /* @__PURE__ */ new WeakMap();
1110
+ function classifyContractPurchase(contract) {
1111
+ return "vendorId" in contract;
1112
+ }
1113
+ function isContractSnapshotReportEligible(contract) {
1114
+ return !(contract instanceof SupplierQuote) && contract.status !== ContractStatus.DRAFT && contract.status !== ContractStatus.CANCELLED;
1115
+ }
1116
+ var Contract = class extends SmrtObject {
1117
+ /** @internal Collection read guard returns this snapshot on its root DB. */
1118
+ rebindMoneyRead(capability, db) {
1119
+ if (capability !== DOCUMENT_MONEY_READ_REBIND) throw new Error("Contract read rebinding requires the owning capability");
1120
+ this._db = db;
1121
+ this.options.db = db;
1122
+ return this;
1123
+ }
1124
+ tenantId = null;
1125
+ contractType = ContractType.ORDER;
1126
+ status = ContractStatus.DRAFT;
1127
+ subtotal = 0;
1128
+ discountTotal = 0;
1129
+ taxAmount = 0;
1130
+ totalAmount = 0;
1131
+ currency = "USD";
1132
+ moneyAuthority = "calculated";
1133
+ /** Guarded received evidence; write through the public received purchase/sales contract commands only. */
1134
+ receivedPurchaseMoneyJson = "";
1135
+ billingScheduleJson = "";
1136
+ /** Pricing interpretation fixed before the Contract leaves draft. */
1137
+ lineAmountType = "exclusive";
1138
+ /** Component rounding scope fixed before the Contract leaves draft. */
1139
+ taxRoundingScope = "line";
1140
+ /** Immutable seller registration snapshot once the Contract leaves draft. */
1141
+ sellerTaxRegistrationsJson = "[]";
1142
+ get sellerTaxRegistrations() {
1143
+ try {
1144
+ const value = JSON.parse(this.sellerTaxRegistrationsJson);
1145
+ if (!Array.isArray(value)) return [];
1146
+ return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.number === "string" && typeof entry.label === "string");
1147
+ } catch {
1148
+ return [];
1149
+ }
1150
+ }
1151
+ set sellerTaxRegistrations(value) {
1152
+ this.sellerTaxRegistrationsJson = JSON.stringify(value);
1153
+ }
1154
+ /** Immutable customer or vendor tax identifier snapshot after issue. */
1155
+ partyTaxId = "";
1156
+ /** Stable issue/freeze identity, independent of lifecycle updated_at. */
1157
+ moneySnapshotId = "";
1158
+ issueDate = /* @__PURE__ */ new Date();
1159
+ dueDate = null;
1160
+ expiryDate = null;
1161
+ reference = "";
1162
+ notes = "";
1163
+ terms = "";
1164
+ channelId = "";
1165
+ constructor(options = {}) {
1166
+ super(options);
1167
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1168
+ if (options.contractType !== void 0) this.contractType = options.contractType;
1169
+ if (options.status !== void 0) this.status = options.status;
1170
+ if (options.billingScheduleJson !== void 0) this.billingScheduleJson = options.billingScheduleJson;
1171
+ if (options.moneyAuthority !== void 0) this.moneyAuthority = options.moneyAuthority;
1172
+ if (options.receivedPurchaseMoneyJson !== void 0) this.receivedPurchaseMoneyJson = options.receivedPurchaseMoneyJson;
1173
+ if (options.subtotal !== void 0) this.subtotal = options.subtotal;
1174
+ if (options.discountTotal !== void 0) this.discountTotal = options.discountTotal;
1175
+ if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
1176
+ if (options.totalAmount !== void 0) this.totalAmount = options.totalAmount;
1177
+ if (options.currency !== void 0) this.currency = options.currency;
1178
+ if (options.lineAmountType !== void 0) this.lineAmountType = options.lineAmountType;
1179
+ if (options.taxRoundingScope !== void 0) this.taxRoundingScope = options.taxRoundingScope;
1180
+ if (options.sellerTaxRegistrationsJson !== void 0) this.sellerTaxRegistrationsJson = options.sellerTaxRegistrationsJson;
1181
+ if (options.sellerTaxRegistrations !== void 0) this.sellerTaxRegistrations = structuredClone(options.sellerTaxRegistrations);
1182
+ if (options.partyTaxId !== void 0) this.partyTaxId = options.partyTaxId;
1183
+ if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
1184
+ if (options.issueDate !== void 0) this.issueDate = options.issueDate;
1185
+ if (options.dueDate !== void 0) this.dueDate = options.dueDate;
1186
+ if (options.expiryDate !== void 0) this.expiryDate = options.expiryDate;
1187
+ if (options.reference !== void 0) this.reference = options.reference;
1188
+ if (options.notes !== void 0) this.notes = options.notes;
1189
+ if (options.terms !== void 0) this.terms = options.terms;
1190
+ if (options.channelId !== void 0) this.channelId = options.channelId;
1191
+ }
1192
+ /**
1193
+ * Check if contract is in draft state
1194
+ */
1195
+ isDraft() {
1196
+ return this.status === ContractStatus.DRAFT;
1197
+ }
1198
+ /**
1199
+ * Check if contract is accepted
1200
+ */
1201
+ isAccepted() {
1202
+ return this.status === ContractStatus.ACCEPTED;
1203
+ }
1204
+ /**
1205
+ * Check if contract is completed
1206
+ */
1207
+ isCompleted() {
1208
+ return this.status === ContractStatus.COMPLETED;
1209
+ }
1210
+ /**
1211
+ * Check if contract is expired (for estimates)
1212
+ */
1213
+ isExpired() {
1214
+ if (!this.expiryDate) return false;
1215
+ return /* @__PURE__ */ new Date() > this.expiryDate;
1216
+ }
1217
+ /**
1218
+ * Check if contract is overdue
1219
+ */
1220
+ isOverdue() {
1221
+ if (!this.dueDate) return false;
1222
+ if (this.status === ContractStatus.COMPLETED) return false;
1223
+ return /* @__PURE__ */ new Date() > this.dueDate;
1224
+ }
1225
+ /**
1226
+ * Calculate and update totals from line items
1227
+ */
1228
+ async recalculateTotals() {
1229
+ const { recalculateContractTotals } = await import("./contract-money-DSRTMoXm.js");
1230
+ await recalculateContractTotals(this);
1231
+ }
1232
+ /**
1233
+ * Capture the status the row was loaded with so the save-time transition
1234
+ * guard can reject illegal status flips made via raw field assignment
1235
+ * (mass-assignment on the generated update route, a stale caller, etc.).
1236
+ * Only persisted rows carry a prior status.
1237
+ */
1238
+ async initialize() {
1239
+ await super.initialize();
1240
+ if (this.isPersisted) {
1241
+ const { assertHydratedMoneyRowCurrent } = await import("./detect-DAE-_NAk.js").then((n) => n.c);
1242
+ await assertHydratedMoneyRowCurrent(this);
1243
+ }
1244
+ if (await this.isSaved()) loadedContractStatus.set(this, this.status);
1245
+ return this;
1246
+ }
1247
+ /**
1248
+ * Validate the amounts and the status transition before persisting, then
1249
+ * save. Inherited by every STI subtype (Estimate/Order/Lease/.../LicenseSale),
1250
+ * so a forged `status` on any of them is caught here. LicenseSale layers its
1251
+ * own rights-immutability guard on top via `super.save()`. See S5 audit
1252
+ * #1390.
1253
+ *
1254
+ * `subtotal` / `taxAmount` / `totalAmount` are mass-assignable on the
1255
+ * generated create/update routes, so the minor-units check runs on every
1256
+ * write surface (#2401).
1257
+ */
1258
+ async save() {
1259
+ const { saveContract } = await import("./contract-money-DSRTMoXm.js");
1260
+ return await saveContract(this);
1261
+ }
1262
+ /** Received purchase monetary history cannot be deleted. */
1263
+ async delete(options = {}) {
1264
+ const { deleteContract } = await import("./contract-money-DSRTMoXm.js");
1265
+ await deleteContract(this, options);
1266
+ }
1267
+ /** @internal Contract aggregate persistence only. */
1268
+ async deleteDerived(capability, options = {}) {
1269
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Contract money write capability is required");
1270
+ await super.delete(options);
1271
+ }
1272
+ /** @internal Contract aggregate persistence only. */
1273
+ async saveDerived(capability) {
1274
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Contract money write capability is required");
1275
+ assertIntegerMinorUnits("Contract", this.reference || this.id || "<new>", [
1276
+ ["subtotal", this.subtotal],
1277
+ ["discountTotal", this.discountTotal],
1278
+ ["taxAmount", this.taxAmount],
1279
+ ["totalAmount", this.totalAmount]
1280
+ ]);
1281
+ const prior = await this.resolvePriorStatus();
1282
+ this.assertContractStatusTransition(prior);
1283
+ const result = await super.save();
1284
+ loadedContractStatus.set(this, this.status);
1285
+ return result;
1286
+ }
1287
+ /**
1288
+ * Reject an illegal status flip done via raw assignment. No-op transitions
1289
+ * and brand-new rows are always allowed.
1290
+ */
1291
+ assertContractStatusTransition(prior) {
1292
+ if (prior === void 0) return;
1293
+ if (prior === this.status) return;
1294
+ if (!(CONTRACT_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Contract ${this.reference || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
1295
+ }
1296
+ /**
1297
+ * Resolve the AUTHORITATIVE prior status (S5 audit #1390 round 5, codex
1298
+ * MEDIUM#2). The WeakMap is only populated when {@link initialize} loaded the
1299
+ * row from the DB; it is empty for an instance built via
1300
+ * `collection.create({ id: <existing>, _skipLoad: true })` — the upsert path
1301
+ * that lets a caller write onto an existing row without hydrating it. Trusting
1302
+ * an empty WeakMap there would treat the write as a brand-new row and skip the
1303
+ * transition guard entirely (a poisonable prior-state). Mirrors the
1304
+ * authoritative-prior-load applied to Payment/PaymentIntent/Invoice/Payout.
1305
+ *
1306
+ * So when this instance carries an `id`, read the persisted row straight from
1307
+ * the database and treat its `status` as the prior — a create-onto-existing is
1308
+ * an update. Only when no row exists in the DB (truly new) do we fall back to
1309
+ * the WeakMap (which is also empty then), i.e. `undefined` = genuinely new.
1310
+ * The raw row's `status` column is single-word (no snake_case transform).
1311
+ */
1312
+ async resolvePriorStatus() {
1313
+ if (this.id) try {
1314
+ const row = await this.db.get(this.tableName, { id: this.id });
1315
+ if (row && row.status != null) return row.status;
1316
+ } catch {}
1317
+ return loadedContractStatus.get(this);
1318
+ }
1319
+ };
1320
+ __publicField$1(Contract, "__smrtPackage__", "@happyvertical/smrt-commerce");
1321
+ /**
1322
+ * STI discriminator field
1323
+ */
1324
+ __publicField$1(Contract, "_stiField", "contractType");
1325
+ __decorateClass$1([tenantId({ nullable: true })], Contract.prototype, "tenantId", 2);
1326
+ __decorateClass$1([field({ description: "The kind of contract: order, estimate, lease, agreement and so on." })], Contract.prototype, "contractType", 2);
1327
+ __decorateClass$1([field({ description: "Where the order or agreement stands, for example draft, sent, accepted or completed." })], Contract.prototype, "status", 2);
1328
+ __decorateClass$1([field({
1329
+ description: "The amount before tax.",
1330
+ ui: { format: "money" }
1331
+ })], Contract.prototype, "subtotal", 2);
1332
+ __decorateClass$1([field({ ui: { format: "money" } })], Contract.prototype, "discountTotal", 2);
1333
+ __decorateClass$1([field({
1334
+ description: "The tax charged.",
1335
+ ui: { format: "money" }
1336
+ })], Contract.prototype, "taxAmount", 2);
1337
+ __decorateClass$1([field({
1338
+ description: "The full amount, tax included.",
1339
+ ui: { format: "money" }
1340
+ })], Contract.prototype, "totalAmount", 2);
1341
+ __decorateClass$1([field({
1342
+ description: "The currency of the amounts.",
1343
+ ui: { widget: "currency" }
1344
+ })], Contract.prototype, "currency", 2);
1345
+ __decorateClass$1([field({ description: "Whether amounts were calculated or recorded from reviewed original paperwork." })], Contract.prototype, "moneyAuthority", 2);
1346
+ __decorateClass$1([field({ type: "text" })], Contract.prototype, "billingScheduleJson", 2);
1347
+ __decorateClass$1([field({ description: "The date it was issued or placed." })], Contract.prototype, "issueDate", 2);
1348
+ __decorateClass$1([field({ description: "The date payment or delivery is due." })], Contract.prototype, "dueDate", 2);
1349
+ __decorateClass$1([field({ description: "The last day the price or offer is good for." })], Contract.prototype, "expiryDate", 2);
1350
+ __decorateClass$1([field({ description: "Your own number or note for finding this later, such as a purchase order or quote number." })], Contract.prototype, "reference", 2);
1351
+ __decorateClass$1([field({
1352
+ description: "Anything worth remembering about this.",
1353
+ ui: { widget: "textarea" }
1354
+ })], Contract.prototype, "notes", 2);
1355
+ __decorateClass$1([field({
1356
+ description: "The conditions agreed, such as deposits, payment or delivery conditions.",
1357
+ ui: { widget: "textarea" }
1358
+ })], Contract.prototype, "terms", 2);
1359
+ __decorateClass$1([field({ description: "Where it came from, for example your shop, a marketplace or a phone call." })], Contract.prototype, "channelId", 2);
1360
+ Contract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
1361
+ tableStrategy: "sti",
1362
+ api: { include: [
1363
+ "list",
1364
+ "get",
1365
+ "create",
1366
+ "update"
1367
+ ] },
1368
+ mcp: { include: ["list", "get"] },
1369
+ cli: { skipApiCheck: true }
1370
+ })], Contract);
1371
+ var SalesContract = class extends Contract {
1372
+ customerId = null;
1373
+ constructor(options = {}) {
1374
+ super(options);
1375
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1376
+ }
1377
+ };
1378
+ __publicField$1(SalesContract, "__smrtPackage__", "@happyvertical/smrt-commerce");
1379
+ __decorateClass$1([foreignKey(Customer, { description: "The customer a sales order or agreement is for." })], SalesContract.prototype, "customerId", 2);
1380
+ SalesContract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({ abstract: true })], SalesContract);
1381
+ var PurchaseContract = class extends Contract {
1382
+ vendorId = null;
1383
+ constructor(options = {}) {
1384
+ super(options);
1385
+ if (options.vendorId !== void 0) this.vendorId = options.vendorId;
1386
+ }
1387
+ };
1388
+ __publicField$1(PurchaseContract, "__smrtPackage__", "@happyvertical/smrt-commerce");
1389
+ __decorateClass$1([foreignKey(Vendor, { description: "The vendor supplying this quote or purchase contract." })], PurchaseContract.prototype, "vendorId", 2);
1390
+ PurchaseContract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({ abstract: true })], PurchaseContract);
1391
+ var Estimate = class extends SalesContract {
1392
+ customerId = null;
1393
+ contractType = ContractType.ESTIMATE;
1394
+ constructor(options = {}) {
1395
+ super(options);
1396
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1397
+ }
1398
+ };
1399
+ __publicField$1(Estimate, "__smrtPackage__", "@happyvertical/smrt-commerce");
1400
+ __decorateClass$1([foreignKey(Customer, { required: true })], Estimate.prototype, "customerId", 2);
1401
+ Estimate = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Estimate);
1402
+ var Order = class extends SalesContract {
1403
+ customerId = null;
1404
+ contractType = ContractType.ORDER;
1405
+ constructor(options = {}) {
1406
+ super(options);
1407
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1408
+ }
1409
+ };
1410
+ __publicField$1(Order, "__smrtPackage__", "@happyvertical/smrt-commerce");
1411
+ __decorateClass$1([foreignKey(Customer, { required: true })], Order.prototype, "customerId", 2);
1412
+ Order = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Order);
1413
+ var Lease = class extends SalesContract {
1414
+ customerId = null;
1415
+ contractType = ContractType.LEASE;
1416
+ constructor(options = {}) {
1417
+ super(options);
1418
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1419
+ }
1420
+ };
1421
+ __publicField$1(Lease, "__smrtPackage__", "@happyvertical/smrt-commerce");
1422
+ __decorateClass$1([foreignKey(Customer, { required: true })], Lease.prototype, "customerId", 2);
1423
+ Lease = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Lease);
1424
+ var Agreement = class extends SalesContract {
1425
+ customerId = null;
1426
+ contractType = ContractType.AGREEMENT;
1427
+ constructor(options = {}) {
1428
+ super(options);
1429
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1430
+ }
1431
+ };
1432
+ __publicField$1(Agreement, "__smrtPackage__", "@happyvertical/smrt-commerce");
1433
+ __decorateClass$1([foreignKey(Customer, { required: true })], Agreement.prototype, "customerId", 2);
1434
+ Agreement = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Agreement);
1435
+ var SupplierQuote = class extends PurchaseContract {
1436
+ vendorId = null;
1437
+ contractType = ContractType.SUPPLIER_QUOTE;
1438
+ constructor(options = {}) {
1439
+ super(options);
1440
+ if (options.vendorId !== void 0) this.vendorId = options.vendorId;
1441
+ }
1442
+ };
1443
+ __publicField$1(SupplierQuote, "__smrtPackage__", "@happyvertical/smrt-commerce");
1444
+ __decorateClass$1([foreignKey(Vendor, { required: true })], SupplierQuote.prototype, "vendorId", 2);
1445
+ SupplierQuote = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], SupplierQuote);
1446
+ var PurchaseOrder = class extends PurchaseContract {
1447
+ vendorId = null;
1448
+ contractType = ContractType.PURCHASE_ORDER;
1449
+ expectedDate = "";
1450
+ shipTo = "";
1451
+ /** Construct a purchase order with its optional delivery header. */
1452
+ constructor(options = {}) {
1453
+ super(options);
1454
+ if (options.vendorId !== void 0) this.vendorId = options.vendorId;
1455
+ if (options.expectedDate !== void 0) this.expectedDate = options.expectedDate;
1456
+ if (options.shipTo !== void 0) this.shipTo = options.shipTo;
1457
+ }
1458
+ };
1459
+ __publicField$1(PurchaseOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
1460
+ __decorateClass$1([foreignKey(Vendor, { required: true })], PurchaseOrder.prototype, "vendorId", 2);
1461
+ __decorateClass$1([field({ description: "The date you expect the delivery, as YYYY-MM-DD. Leave blank if it is not known." })], PurchaseOrder.prototype, "expectedDate", 2);
1462
+ __decorateClass$1([field({ description: "Where this order should be delivered, including any delivery instructions." })], PurchaseOrder.prototype, "shipTo", 2);
1463
+ PurchaseOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], PurchaseOrder);
1464
+ var WholesaleOrder = class extends SalesContract {
1465
+ customerId = null;
1466
+ contractType = ContractType.WHOLESALE_ORDER;
1467
+ constructor(options = {}) {
1468
+ super(options);
1469
+ if (options.customerId !== void 0) this.customerId = options.customerId;
1470
+ }
1471
+ };
1472
+ __publicField$1(WholesaleOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
1473
+ __decorateClass$1([foreignKey(Customer, { required: true })], WholesaleOrder.prototype, "customerId", 2);
1474
+ WholesaleOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], WholesaleOrder);
1475
+ var ProductionOrder = class extends PurchaseContract {
1476
+ contractType = ContractType.PRODUCTION_ORDER;
1477
+ /**
1478
+ * Plain string reference to the upstream product the order is asked
1479
+ * to make. Cross-package id; never `@foreignKey()`. Required by
1480
+ * `ProductionService.consumeMaterials` unless `bomId` is supplied.
1481
+ */
1482
+ productId = "";
1483
+ /**
1484
+ * Optional explicit BOM revision the order locked to at posting
1485
+ * time. When set, manufacturing uses this exact row even if the
1486
+ * product's active BOM has since changed. When empty, falls back
1487
+ * to the active BOM for `productId`.
1488
+ */
1489
+ bomId = "";
1490
+ };
1491
+ __publicField$1(ProductionOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
1492
+ ProductionOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], ProductionOrder);
1493
+ var Cart = class extends SalesContract {
1494
+ contractType = ContractType.CART;
1495
+ };
1496
+ __publicField$1(Cart, "__smrtPackage__", "@happyvertical/smrt-commerce");
1497
+ Cart = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Cart);
1498
+ var issuedRightsSnapshot = /* @__PURE__ */ new WeakMap();
1499
+ var LicenseSale = class extends SalesContract {
1500
+ contractType = ContractType.LICENSE_SALE;
1501
+ /**
1502
+ * Plain string reference to the upstream `Sku` (from
1503
+ * `@happyvertical/smrt-products`) that this license was sold against.
1504
+ * Cross-package id; never `@foreignKey()`.
1505
+ */
1506
+ skuId = "";
1507
+ /**
1508
+ * Plain string reference to the `Payment` that satisfied the
1509
+ * purchase. Cross-model reference matches the rest of the
1510
+ * package's convention.
1511
+ */
1512
+ paymentId = "";
1513
+ /**
1514
+ * The licensee's email address — the primary identity carried
1515
+ * forward into PDF generation and any downstream hash-registry
1516
+ * publication. Required; without it, downstream rights cannot be
1517
+ * attributed.
1518
+ */
1519
+ licenseeEmail = "";
1520
+ /**
1521
+ * Optional legal-entity name of the licensee for purchases that
1522
+ * warrant it (typically high-tier B2B licenses). Empty string for
1523
+ * individual-buyer licenses where the email *is* the legal identity.
1524
+ */
1525
+ licenseeLegalEntity = "";
1526
+ /**
1527
+ * Optional ISO-3166 jurisdiction code of the licensee, captured for
1528
+ * tax / disclosure / governing-law purposes. Empty when not
1529
+ * required.
1530
+ */
1531
+ licenseeJurisdiction = "";
1532
+ /** Permitted media — `web,print,social`, `broadcast`, etc. */
1533
+ rightsMedium = "";
1534
+ /** Distribution scope — `worldwide`, `territorial`, `private`, etc. */
1535
+ rightsDistributionScope = "";
1536
+ /** Exclusivity — `exclusive` / `non-exclusive` / `co-exclusive`. */
1537
+ rightsExclusivity = "";
1538
+ /** Duration — `perpetual` / `12-months` / `event-only`, etc. */
1539
+ rightsDuration = "";
1540
+ /** Territory — ISO-3166 list, `worldwide`, region name, etc. */
1541
+ rightsTerritory = "";
1542
+ /** Whether the licensee may sublicense the rights. */
1543
+ rightsSublicensing = false;
1544
+ /** Whether the licensee may create derivative works. */
1545
+ rightsDerivatives = false;
1546
+ /** URL of the signed PDF (typically a cloud-storage public URL). */
1547
+ pdfUrl = "";
1548
+ /**
1549
+ * SHA-256 of the signed PDF bytes (hex string, no `0x` prefix).
1550
+ * Lets downstream consumers verify the PDF the licensee holds is
1551
+ * the one this row was issued for, without re-fetching.
1552
+ */
1553
+ pdfHash = "";
1554
+ /**
1555
+ * Optional reference to an on-chain hash-registry entry — set when
1556
+ * the LicenseSale's `pdfHash` has been anchored on a public chain
1557
+ * for tamper-evidence. Format is whatever the hash-registry
1558
+ * service emits (e.g. `chain:tx-hash:log-index`). Empty for
1559
+ * deployments that don't publish hashes on-chain.
1560
+ */
1561
+ onChainHashRegistryRef = "";
1562
+ constructor(options = {}) {
1563
+ super(options);
1564
+ if (options.skuId !== void 0) this.skuId = options.skuId;
1565
+ if (options.paymentId !== void 0) this.paymentId = options.paymentId;
1566
+ if (options.licenseeEmail !== void 0) this.licenseeEmail = options.licenseeEmail;
1567
+ if (options.licenseeLegalEntity !== void 0) this.licenseeLegalEntity = options.licenseeLegalEntity;
1568
+ if (options.licenseeJurisdiction !== void 0) this.licenseeJurisdiction = options.licenseeJurisdiction;
1569
+ if (options.rightsMedium !== void 0) this.rightsMedium = options.rightsMedium;
1570
+ if (options.rightsDistributionScope !== void 0) this.rightsDistributionScope = options.rightsDistributionScope;
1571
+ if (options.rightsExclusivity !== void 0) this.rightsExclusivity = options.rightsExclusivity;
1572
+ if (options.rightsDuration !== void 0) this.rightsDuration = options.rightsDuration;
1573
+ if (options.rightsTerritory !== void 0) this.rightsTerritory = options.rightsTerritory;
1574
+ if (options.rightsSublicensing !== void 0) this.rightsSublicensing = options.rightsSublicensing;
1575
+ if (options.rightsDerivatives !== void 0) this.rightsDerivatives = options.rightsDerivatives;
1576
+ if (options.pdfUrl !== void 0) this.pdfUrl = options.pdfUrl;
1577
+ if (options.pdfHash !== void 0) this.pdfHash = options.pdfHash;
1578
+ if (options.onChainHashRegistryRef !== void 0) this.onChainHashRegistryRef = options.onChainHashRegistryRef;
1579
+ }
1580
+ /**
1581
+ * Once `super.initialize()` has applied the framework's option-
1582
+ * override pass, capture the rights snapshot if (and only if) the
1583
+ * row arrived already issued. That's how the immutability guard
1584
+ * tells "freshly drafting a license" (snapshot still mutable) apart
1585
+ * from "loaded an already-issued row" (snapshot frozen).
1586
+ */
1587
+ async initialize() {
1588
+ await super.initialize();
1589
+ if (this.status === ContractStatus.ACCEPTED && await this.isSaved()) issuedRightsSnapshot.set(this, this.serializeRightsSnapshot());
1590
+ return this;
1591
+ }
1592
+ /**
1593
+ * Return the current rights snapshot as a typed object. Read-only
1594
+ * for callers — mutation should go through assignment to the
1595
+ * individual `rights*` fields, gated by the issued-immutability
1596
+ * guard.
1597
+ */
1598
+ getRightsSnapshot() {
1599
+ return {
1600
+ medium: this.rightsMedium,
1601
+ distributionScope: this.rightsDistributionScope,
1602
+ exclusivity: this.rightsExclusivity,
1603
+ duration: this.rightsDuration,
1604
+ territory: this.rightsTerritory,
1605
+ sublicensing: this.rightsSublicensing,
1606
+ derivatives: this.rightsDerivatives
1607
+ };
1608
+ }
1609
+ /**
1610
+ * Revoke an issued license — moves status to `CANCELLED` without
1611
+ * touching the rights snapshot (which stays frozen). Throws if
1612
+ * called on a non-issued license; a draft license should be
1613
+ * deleted, not revoked.
1614
+ */
1615
+ revoke() {
1616
+ if (this.status !== ContractStatus.ACCEPTED) throw new Error(`LicenseSale ${this.id ?? "<new>"}: cannot revoke from status '${this.status}' \u2014 only ACCEPTED licenses can be revoked`);
1617
+ this.status = ContractStatus.CANCELLED;
1618
+ }
1619
+ /**
1620
+ * Save with the immutability check: if the row was loaded with
1621
+ * `ACCEPTED` status, the rights snapshot at save time must match
1622
+ * the snapshot at load time. Anything else means an out-of-band
1623
+ * mutation crept in and we surface it instead of letting the row
1624
+ * drift silently.
1625
+ *
1626
+ * Newly-drafted licenses (where the row was constructed without
1627
+ * `ACCEPTED` status) capture their snapshot at the moment the
1628
+ * caller transitions to `ACCEPTED` and saves — `validateImmutable`
1629
+ * picks that up post-save on the next call.
1630
+ */
1631
+ async save() {
1632
+ this.validateImmutable();
1633
+ const result = await super.save();
1634
+ if (this.status === ContractStatus.ACCEPTED && !issuedRightsSnapshot.has(this)) issuedRightsSnapshot.set(this, this.serializeRightsSnapshot());
1635
+ return result;
1636
+ }
1637
+ /**
1638
+ * Compare the current rights against the captured snapshot
1639
+ * (populated either at load time by `initialize` or at first
1640
+ * issuance by `save`). Throws on mismatch.
1641
+ */
1642
+ validateImmutable() {
1643
+ const captured = issuedRightsSnapshot.get(this);
1644
+ if (!captured) return;
1645
+ if (captured !== this.serializeRightsSnapshot()) throw new Error(`LicenseSale ${this.id ?? "<new>"}: rights snapshot is immutable once issued (status=ACCEPTED). Use revoke() to cancel the existing license and issue a new one with the updated rights.`);
1646
+ }
1647
+ serializeRightsSnapshot() {
1648
+ return JSON.stringify({
1649
+ medium: this.rightsMedium,
1650
+ distributionScope: this.rightsDistributionScope,
1651
+ exclusivity: this.rightsExclusivity,
1652
+ duration: this.rightsDuration,
1653
+ territory: this.rightsTerritory,
1654
+ sublicensing: this.rightsSublicensing,
1655
+ derivatives: this.rightsDerivatives
1656
+ });
1657
+ }
1658
+ };
1659
+ __publicField$1(LicenseSale, "__smrtPackage__", "@happyvertical/smrt-commerce");
1660
+ LicenseSale = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], LicenseSale);
1661
+ //#endregion
1662
+ //#region src/collections/PurchaseContractCollection.ts
1663
+ var PurchaseContractCollection = class extends SmrtCollection {
1664
+ static _itemClass = PurchaseContract;
1665
+ async findByVendor(vendorId) {
1666
+ return this.list({
1667
+ where: { vendorId },
1668
+ orderBy: "created_at DESC"
1669
+ });
1670
+ }
1671
+ };
1672
+ //#endregion
1673
+ //#region src/collections/SalesContractCollection.ts
1674
+ var SalesContractCollection = class extends SmrtCollection {
1675
+ static _itemClass = SalesContract;
1676
+ async findByCustomer(customerId) {
1677
+ return this.list({
1678
+ where: { customerId },
1679
+ orderBy: "created_at DESC"
1680
+ });
1681
+ }
1682
+ };
1683
+ //#endregion
1684
+ //#region src/collections/ContractCollection.ts
1685
+ var ContractCollection = class extends SmrtCollection {
1686
+ static _itemClass = Contract;
1687
+ async withMoneyRead(operation) {
1688
+ const rootDb = this.db;
1689
+ const tenantId = getTenantId();
1690
+ if (!tenantId) {
1691
+ await assertCommerceMoneySchemaCurrent(rootDb);
1692
+ return operation();
1693
+ }
1694
+ const result = await withTenantMoneyCollectionRead(rootDb, tenantId, (boundDb) => this.withDatabase(boundDb, operation));
1695
+ if (result instanceof Contract) result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
1696
+ else if (Array.isArray(result)) {
1697
+ for (const item of result) if (item instanceof Contract) item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
1698
+ }
1699
+ return result;
1700
+ }
1701
+ async get(filter, options = {}) {
1702
+ return this.withMoneyRead(() => super.get(filter, options));
1703
+ }
1704
+ async list(options = {}) {
1705
+ return this.withMoneyRead(() => super.list(options));
1706
+ }
1707
+ /**
1708
+ * Find contracts by customer
1709
+ *
1710
+ * @param customerId - Customer ID
1711
+ * @returns Array of contracts
1712
+ * @deprecated Use SalesContractCollection.findByCustomer.
1713
+ */
1714
+ async findByCustomer(customerId) {
1715
+ return (await SalesContractCollection.create({
1716
+ ...this.options,
1717
+ db: this.db
1718
+ })).findByCustomer(customerId);
1719
+ }
1720
+ /**
1721
+ * Find contracts by vendor
1722
+ *
1723
+ * @param vendorId - Vendor ID
1724
+ * @returns Array of contracts
1725
+ * @deprecated Use PurchaseContractCollection.findByVendor.
1726
+ */
1727
+ async findByVendor(vendorId) {
1728
+ return (await PurchaseContractCollection.create({
1729
+ ...this.options,
1730
+ db: this.db
1731
+ })).findByVendor(vendorId);
1732
+ }
1733
+ /**
1734
+ * Hydrate a bounded tenant/date report candidate set once, retaining exact
1735
+ * STI descendants so callers can classify custom purchase types safely.
1736
+ */
1737
+ async listSnapshotReportContracts(options) {
1738
+ return (await this.list({
1739
+ where: {
1740
+ tenantId: options.tenantId,
1741
+ "issueDate >=": options.from.toISOString(),
1742
+ "issueDate <=": options.to.toISOString()
1743
+ },
1744
+ orderBy: "issueDate ASC"
1745
+ })).filter(isContractSnapshotReportEligible);
1746
+ }
1747
+ /**
1748
+ * Find contracts by type (STI)
1749
+ *
1750
+ * @param contractType - Contract type
1751
+ * @returns Array of contracts
1752
+ */
1753
+ async findByType(contractType) {
1754
+ return await this.list({
1755
+ where: { contractType },
1756
+ orderBy: "created_at DESC"
1757
+ });
1758
+ }
1759
+ /**
1760
+ * Find contracts by status
1761
+ *
1762
+ * @param status - Contract status
1763
+ * @returns Array of contracts
1764
+ */
1765
+ async findByStatus(status) {
1766
+ return await this.list({
1767
+ where: { status },
1768
+ orderBy: "created_at DESC"
1769
+ });
1770
+ }
1771
+ /**
1772
+ * Find all draft contracts
1773
+ *
1774
+ * @returns Array of draft contracts
1775
+ */
1776
+ async findDrafts() {
1777
+ return await this.findByStatus(ContractStatus.DRAFT);
1778
+ }
1779
+ /**
1780
+ * Find all accepted contracts
1781
+ *
1782
+ * @returns Array of accepted contracts
1783
+ */
1784
+ async findAccepted() {
1785
+ return await this.findByStatus(ContractStatus.ACCEPTED);
1786
+ }
1787
+ /**
1788
+ * Find overdue contracts
1789
+ *
1790
+ * @returns Array of overdue contracts
1791
+ */
1792
+ async findOverdue() {
1793
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1794
+ return (await this.list({
1795
+ where: { "dueDate <": now },
1796
+ orderBy: "dueDate ASC"
1797
+ })).filter((c) => c.status !== ContractStatus.COMPLETED && c.status !== ContractStatus.CANCELLED);
1798
+ }
1799
+ /**
1800
+ * Find expired estimates/quotes
1801
+ *
1802
+ * @returns Array of expired estimates
1803
+ */
1804
+ async findExpired() {
1805
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1806
+ return (await this.list({
1807
+ where: {
1808
+ contractType: ContractType.ESTIMATE,
1809
+ "expiryDate <": now
1810
+ },
1811
+ orderBy: "expiryDate ASC"
1812
+ })).filter((c) => c.status !== ContractStatus.ACCEPTED && c.status !== ContractStatus.DECLINED && c.status !== ContractStatus.CANCELLED);
1813
+ }
1814
+ /**
1815
+ * Find contracts in date range
1816
+ *
1817
+ * @param startDate - Start date
1818
+ * @param endDate - End date
1819
+ * @returns Array of contracts
1820
+ */
1821
+ async findByDateRange(startDate, endDate) {
1822
+ return await this.list({
1823
+ where: {
1824
+ "issueDate >=": startDate.toISOString(),
1825
+ "issueDate <=": endDate.toISOString()
1826
+ },
1827
+ orderBy: "issueDate DESC"
1828
+ });
1829
+ }
1830
+ /**
1831
+ * Find all contracts belonging to a specific tenant
1832
+ *
1833
+ * @param tenantId - Tenant ID
1834
+ * @returns Array of contracts for the tenant
1835
+ */
1836
+ async findByTenant(tenantId) {
1837
+ return this.list({ where: { tenantId } });
1838
+ }
1839
+ /**
1840
+ * Find all global contracts (not associated with any tenant).
1841
+ *
1842
+ * Routes through the shared tenant-global helper so it does not throw under
1843
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1844
+ * flagged as an isolation violation). (#1600)
1845
+ *
1846
+ * @returns Array of global contracts
1847
+ */
1848
+ async findGlobal() {
1849
+ return queryGlobal(this);
1850
+ }
1851
+ /**
1852
+ * Find contracts for a tenant including global contracts.
1853
+ *
1854
+ * Fails closed if an active tenant context requests a different tenant's
1855
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1856
+ *
1857
+ * @param tenantId - Tenant ID
1858
+ * @returns Array of tenant-specific and global contracts
1859
+ */
1860
+ async findWithGlobals(tenantId) {
1861
+ return queryWithGlobals(this, tenantId, "Contract.findWithGlobals");
1862
+ }
1863
+ };
1864
+ //#endregion
1865
+ //#region src/contract-billing-schedule.ts
1866
+ function invalid$3(message) {
1867
+ throw new MoneyValidationError("INVALID_MONEY_INPUT", message);
1868
+ }
1869
+ function text$1(value, label) {
1870
+ if (typeof value !== "string" || !value || value.trim() !== value) invalid$3(`${label} must be nonempty trimmed text`);
1871
+ return value;
1872
+ }
1873
+ function integer$1(value, label, signed = false) {
1874
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || !signed && value < 0) invalid$3(`${label} must be a ${signed ? "" : "nonnegative "}safe integer`);
1875
+ return value;
1876
+ }
1877
+ function safe(value) {
1878
+ return integer$1(Number(value), "Computed amount");
1879
+ }
1880
+ function context(input) {
1881
+ text$1(input.tenantId, "Tenant");
1882
+ text$1(input.contractId, "Contract");
1883
+ text$1(input.requestId, "Request");
1884
+ if (getTenantId() !== input.tenantId) invalid$3("Schedule command requires its active tenant context");
1885
+ }
1886
+ function milestones(value) {
1887
+ if (!Array.isArray(value) || value.length === 0) invalid$3("A schedule requires milestones");
1888
+ const keys = /* @__PURE__ */ new Set();
1889
+ return value.map((entry) => {
1890
+ if (!entry || typeof entry !== "object") invalid$3("Invalid milestone");
1891
+ const key = text$1(entry.key, "Milestone key");
1892
+ if (keys.has(key)) invalid$3("Milestone keys must be unique");
1893
+ keys.add(key);
1894
+ return {
1895
+ key,
1896
+ label: text$1(entry.label, "Milestone label"),
1897
+ amountMinor: integer$1(entry.amountMinor, "Milestone amount")
1898
+ };
1899
+ });
1900
+ }
1901
+ function money(value) {
1902
+ if (!value || typeof value !== "object") invalid$3("Approved invoice money is required");
1903
+ const netMinor = integer$1(value.netMinor, "Approved net");
1904
+ const taxMinor = integer$1(value.taxMinor, "Approved tax");
1905
+ const grossMinor = integer$1(value.grossMinor, "Approved gross");
1906
+ if (BigInt(netMinor) + BigInt(taxMinor) !== BigInt(grossMinor)) invalid$3("Approved invoice money must reconcile");
1907
+ return {
1908
+ netMinor,
1909
+ taxMinor,
1910
+ grossMinor
1911
+ };
1912
+ }
1913
+ function allocateContractMilestones(totalMinor, weights) {
1914
+ integer$1(totalMinor, "Total");
1915
+ if (!Array.isArray(weights) || !weights.length) invalid$3("Milestone weights are required");
1916
+ const values = weights.map((value) => {
1917
+ integer$1(value, "Weight");
1918
+ if (!value) invalid$3("Weights must be positive");
1919
+ return BigInt(value);
1920
+ });
1921
+ const sum = values.reduce((a, b) => a + b, 0n);
1922
+ let remaining = BigInt(totalMinor);
1923
+ return values.map((value, index) => {
1924
+ const amount = index === values.length - 1 ? remaining : BigInt(totalMinor) * value / sum;
1925
+ remaining -= amount;
1926
+ return safe(amount);
1927
+ });
1928
+ }
1929
+ function getContractBillingSchedule(contract) {
1930
+ if (!contract.billingScheduleJson) return null;
1931
+ let parsed;
1932
+ try {
1933
+ parsed = JSON.parse(contract.billingScheduleJson);
1934
+ } catch {
1935
+ invalid$3("Stored billing schedule is invalid");
1936
+ }
1937
+ if (parsed.version !== 1 || parsed.capacityPolicy !== "issued-consumed" || !Array.isArray(parsed.revisions) || !parsed.revisions.length || !Array.isArray(parsed.claims)) invalid$3("Stored billing schedule is invalid");
1938
+ return parsed;
1939
+ }
1940
+ async function loadContract(db, tenantId, contractId) {
1941
+ const collection = await ContractCollection.create({ db });
1942
+ await lockDocumentMoneyParents([{
1943
+ id: contractId,
1944
+ tenantId,
1945
+ collection
1946
+ }]);
1947
+ const contract = await collection.get({ id: contractId }, { cache: false });
1948
+ if (!(contract instanceof Order || contract instanceof Agreement) || contract.tenantId !== tenantId || !contract.customerId || contract.status !== ContractStatus.ACCEPTED || !contract.moneySnapshotId) invalid$3("Schedule requires an accepted customer contract in the active tenant");
1949
+ return contract;
1950
+ }
1951
+ async function approveContractBillingSchedule(input) {
1952
+ context(input);
1953
+ const expectedRevision = integer$1(input.expectedRevision, "Expected revision");
1954
+ const proposal = {
1955
+ requestId: input.requestId,
1956
+ approvalEvidenceRef: text$1(input.approvalEvidenceRef, "Approval evidence"),
1957
+ taxPolicyEvidenceRef: text$1(input.taxPolicyEvidenceRef, "Tax policy evidence"),
1958
+ currency: text$1(input.currency, "Currency"),
1959
+ basis: input.basis,
1960
+ amendmentMinor: integer$1(input.amendmentMinor, "Amendment", true),
1961
+ milestones: milestones(input.milestones)
1962
+ };
1963
+ if (proposal.basis !== "net" && proposal.basis !== "gross") invalid$3("Schedule basis must explicitly be net or gross");
1964
+ return withTenantMoneyRuntime(input.db, input.tenantId, async ({ db }) => {
1965
+ const contract = await loadContract(db, input.tenantId, input.contractId);
1966
+ const stored = getContractBillingSchedule(contract);
1967
+ const replay = stored?.revisions.find((entry) => entry.requestId === input.requestId);
1968
+ if (replay) {
1969
+ const { revision: revision2, approvedTotalMinor: _total, ...old } = replay;
1970
+ if (revision2 !== expectedRevision + 1 || JSON.stringify(old) !== JSON.stringify(proposal)) invalid$3("Schedule request changed");
1971
+ return replay;
1972
+ }
1973
+ if (stored?.claims.some((entry) => entry.requestId === input.requestId)) invalid$3("Request identity already used");
1974
+ const previous = stored?.revisions.at(-1);
1975
+ if ((previous?.revision ?? 0) !== expectedRevision) invalid$3("Schedule revision changed");
1976
+ if (contract.currency !== proposal.currency || previous && (previous.currency !== proposal.currency || previous.basis !== proposal.basis)) invalid$3("Schedule currency and basis must match original approval");
1977
+ const originalAmountMinor = stored?.originalAmountMinor ?? (proposal.basis === "net" ? contract.subtotal : contract.totalAmount);
1978
+ integer$1(originalAmountMinor, "Original contract amount");
1979
+ if (!stored) {
1980
+ if (proposal.amendmentMinor !== 0) invalid$3("Initial schedule cannot amend original contract value");
1981
+ if ((await (await InvoiceCollection.create({ db })).findByContract(input.contractId)).some((invoice) => invoice.kind === "invoice" && !!invoice.moneySnapshotId)) invalid$3("Existing issued contract invoices prevent initial schedule approval");
1982
+ }
1983
+ const approvedTotalMinor = safe(BigInt(previous?.approvedTotalMinor ?? originalAmountMinor) + BigInt(proposal.amendmentMinor));
1984
+ if (proposal.milestones.reduce((sum, entry) => sum + BigInt(entry.amountMinor), 0n) !== BigInt(approvedTotalMinor)) invalid$3("Milestone total must equal original contract plus approved amendments");
1985
+ for (const claim of stored?.claims ?? []) {
1986
+ const prior = stored?.revisions.find((entry) => entry.revision === claim.revision)?.milestones.find((entry) => entry.key === claim.milestoneKey);
1987
+ const next = proposal.milestones.find((entry) => entry.key === claim.milestoneKey);
1988
+ if (!prior || !next || JSON.stringify(prior) !== JSON.stringify(next)) invalid$3("An issued milestone cannot be removed or changed");
1989
+ }
1990
+ const revision = {
1991
+ revision: expectedRevision + 1,
1992
+ ...proposal,
1993
+ approvedTotalMinor
1994
+ };
1995
+ const state = stored ?? {
1996
+ version: 1,
1997
+ originalAmountMinor,
1998
+ capacityPolicy: "issued-consumed",
1999
+ revisions: [],
2000
+ claims: []
2001
+ };
2002
+ state.revisions.push(revision);
2003
+ contract.billingScheduleJson = JSON.stringify(state);
2004
+ await contract.saveDerived(DOCUMENT_MONEY_WRITE);
2005
+ return revision;
2006
+ });
2007
+ }
2008
+ var issuePermissions = /* @__PURE__ */ new WeakMap();
2009
+ async function assertContractScheduleInvoiceFreeze(db, invoice) {
2010
+ if (invoice.kind !== "invoice" || !invoice.contractId) return;
2011
+ const contract = await (await ContractCollection.create({ db })).get({ id: invoice.contractId }, { cache: false });
2012
+ if (!contract) return;
2013
+ if (contract.tenantId !== invoice.tenantId) invalid$3("Invoice contract must belong to its active tenant");
2014
+ if (!getContractBillingSchedule(contract)) return;
2015
+ if (invoice.status === InvoiceStatus.CANCELLED && !invoice.moneySnapshotId && !invoice.arJournalId) return;
2016
+ if (!(contract instanceof Order || contract instanceof Agreement)) invalid$3("Scheduled contract must be a customer contract");
2017
+ const permission = invoice.id ? issuePermissions.get(db)?.get(invoice.id) : void 0;
2018
+ if (!permission || permission.contractId !== invoice.contractId) invalid$3("Scheduled contract invoices must use issueContractMilestoneInvoice");
2019
+ if (invoice.customerId !== contract.customerId || invoice.currency !== contract.currency) invalid$3("Milestone invoice must match contract customer and currency");
2020
+ const actual = {
2021
+ netMinor: invoice.subtotal,
2022
+ taxMinor: invoice.taxAmount,
2023
+ grossMinor: invoice.totalAmount
2024
+ };
2025
+ if (JSON.stringify(actual) !== JSON.stringify(permission.approvedMoney) || (permission.basis === "net" ? invoice.subtotal : invoice.totalAmount) !== permission.amountMinor) invalid$3("Milestone invoice money differs from approved amounts");
2026
+ }
2027
+ async function issueContractMilestoneInvoice(input) {
2028
+ context(input);
2029
+ const request = {
2030
+ requestId: input.requestId,
2031
+ revision: integer$1(input.revision, "Revision"),
2032
+ milestoneKey: text$1(input.milestoneKey, "Milestone key"),
2033
+ invoiceId: text$1(input.invoiceId, "Invoice"),
2034
+ approvedMoney: money(input.approvedMoney),
2035
+ invoiceApprovalEvidenceRef: text$1(input.invoiceApprovalEvidenceRef, "Invoice approval evidence")
2036
+ };
2037
+ return withTenantMoneyRuntime(input.db, input.tenantId, async ({ db }) => {
2038
+ const contract = await loadContract(db, input.tenantId, input.contractId);
2039
+ const state = getContractBillingSchedule(contract);
2040
+ if (!state) invalid$3("Contract has no approved billing schedule");
2041
+ const replay = state.claims.find((entry) => entry.requestId === request.requestId);
2042
+ if (replay) {
2043
+ const { moneySnapshotId: _snapshot, ...prior } = replay;
2044
+ if (JSON.stringify(prior) !== JSON.stringify(request)) invalid$3("Milestone issue request changed");
2045
+ return replay;
2046
+ }
2047
+ if (state.revisions.some((entry) => entry.requestId === request.requestId)) invalid$3("Request identity already used");
2048
+ const revision = state.revisions.at(-1);
2049
+ if (!revision || revision.revision !== request.revision) invalid$3("Schedule revision changed");
2050
+ const milestone = revision.milestones.find((entry) => entry.key === request.milestoneKey);
2051
+ if (!milestone) invalid$3("Milestone does not exist in approved revision");
2052
+ if (state.claims.some((entry) => entry.milestoneKey === request.milestoneKey || entry.invoiceId === request.invoiceId)) invalid$3("Milestone or invoice already consumed");
2053
+ const invoices = await InvoiceCollection.create({ db });
2054
+ await lockDocumentMoneyParents([{
2055
+ id: request.invoiceId,
2056
+ tenantId: input.tenantId,
2057
+ collection: invoices
2058
+ }]);
2059
+ const invoice = await invoices.get({ id: request.invoiceId }, { cache: false });
2060
+ if (!invoice || invoice.tenantId !== input.tenantId || invoice.kind !== "invoice" || invoice.status !== InvoiceStatus.DRAFT || invoice.moneySnapshotId || invoice.contractId !== input.contractId || invoice.customerId !== contract.customerId || invoice.currency !== revision.currency) invalid$3("Milestone requires a matching unissued draft in the active tenant");
2061
+ const permissions = issuePermissions.get(db) ?? /* @__PURE__ */ new Map();
2062
+ permissions.set(request.invoiceId, {
2063
+ contractId: input.contractId,
2064
+ amountMinor: milestone.amountMinor,
2065
+ basis: revision.basis,
2066
+ approvedMoney: request.approvedMoney
2067
+ });
2068
+ issuePermissions.set(db, permissions);
2069
+ try {
2070
+ invoice.markSent();
2071
+ await invoice.save();
2072
+ } finally {
2073
+ permissions.delete(request.invoiceId);
2074
+ if (!permissions.size) issuePermissions.delete(db);
2075
+ }
2076
+ if (!invoice.moneySnapshotId) invalid$3("Milestone invoice was not frozen");
2077
+ const claim = {
2078
+ ...request,
2079
+ moneySnapshotId: invoice.moneySnapshotId
2080
+ };
2081
+ state.claims.push(claim);
2082
+ contract.billingScheduleJson = JSON.stringify(state);
2083
+ await contract.saveDerived(DOCUMENT_MONEY_WRITE);
2084
+ return claim;
2085
+ });
2086
+ }
2087
+ //#endregion
2088
+ //#region src/document-money.ts
2089
+ var document_money_exports = /* @__PURE__ */ __exportAll({
2090
+ deleteInvoice: () => deleteInvoice,
2091
+ deleteInvoiceLineItem: () => deleteInvoiceLineItem,
2092
+ getInvoiceMoneyFacts: () => getInvoiceMoneyFacts,
2093
+ lockAndReloadInvoice: () => lockAndReloadInvoice,
2094
+ previewInvoiceMoney: () => previewInvoiceMoney,
2095
+ refreshIssuedCreditBalance: () => refreshIssuedCreditBalance,
2096
+ saveInvoice: () => saveInvoice,
2097
+ saveInvoiceLineItem: () => saveInvoiceLineItem
2098
+ });
2099
+ var FROZEN_INVOICE_FIELDS = [
2100
+ "moneyAuthority",
2101
+ "historicalSalesMoneyJson",
2102
+ "historicalSourceKeyHash",
2103
+ "kind",
2104
+ "creditedInvoiceId",
2105
+ "tenantId",
2106
+ "customerId",
2107
+ "contractId",
2108
+ "invoiceNumber",
2109
+ "reference",
2110
+ "issueDate",
2111
+ "dueDate",
2112
+ "subtotal",
2113
+ "discountTotal",
2114
+ "taxAmount",
2115
+ "providerTaxAmount",
2116
+ "totalAmount",
2117
+ "currency",
2118
+ "lineAmountType",
2119
+ "taxRoundingScope",
2120
+ "sellerTaxRegistrationsJson",
2121
+ "partyTaxId",
2122
+ "moneySnapshotId",
2123
+ "notes",
2124
+ "customerNotes",
2125
+ "terms"
2126
+ ];
2127
+ var FROZEN_LINE_FIELDS = [
2128
+ "tenantId",
2129
+ "invoiceId",
2130
+ "creditedLineId",
2131
+ "creditCalculationBasisJson",
2132
+ "description",
2133
+ "sku",
2134
+ "productId",
2135
+ "quantity",
2136
+ "unitPrice",
2137
+ "discountType",
2138
+ "discountRate",
2139
+ "discount",
2140
+ "taxMode",
2141
+ "taxCodeId",
2142
+ "taxCodeCode",
2143
+ "taxCodeName",
2144
+ "taxComponentsJson",
2145
+ "taxAmount",
2146
+ "taxRate",
2147
+ "amount",
2148
+ "sourceType",
2149
+ "sourceId",
2150
+ "periodStart",
2151
+ "periodEnd",
2152
+ "revenueAccountId",
2153
+ "sortOrder",
2154
+ "moneySnapshotId"
2155
+ ];
2156
+ function comparable(value) {
2157
+ if (value instanceof Date) return value.toISOString();
2158
+ return value === "" || value === void 0 ? null : value;
2159
+ }
2160
+ function assertFieldsUnchanged(proposed, persisted, fields, label) {
2161
+ for (const field of fields) if (comparable(proposed[field]) !== comparable(persisted[field])) throw new MoneyValidationError("FROZEN_DOCUMENT", `${label} ${String(field)} is frozen`);
2162
+ }
2163
+ function assertTenantId(value) {
2164
+ if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money requires a tenant-owned invoice");
2165
+ }
2166
+ function requireId(value, label) {
2167
+ if (!value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} requires a persisted ID`);
2168
+ return value;
2169
+ }
2170
+ async function lockRuntimeSettings(db, tenantId) {
2171
+ if (!tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Commerce money documents require a tenant");
2172
+ return lockTenantMoneyRuntime(db, tenantId);
2173
+ }
2174
+ function isFrozen(invoice) {
2175
+ return invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId || !!invoice.moneySnapshotId;
2176
+ }
2177
+ function aggregateSignature(invoice) {
2178
+ return JSON.stringify([
2179
+ invoice.lineAmountType,
2180
+ invoice.taxRoundingScope,
2181
+ invoice.subtotal,
2182
+ invoice.discountTotal,
2183
+ invoice.taxAmount,
2184
+ invoice.totalAmount
2185
+ ]);
2186
+ }
2187
+ async function loadInvoice(db, id) {
2188
+ const collection = await InvoiceCollection.create({ db });
2189
+ const invoice = await collection.get({ id }, { cache: false });
2190
+ if (!invoice) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice ${id} was not found`);
2191
+ return {
2192
+ collection,
2193
+ invoice
2194
+ };
2195
+ }
2196
+ async function findInvoice(db, id) {
2197
+ return (await InvoiceCollection.create({ db })).get({ id }, { cache: false });
2198
+ }
2199
+ async function lockAndReloadInvoice(db, invoiceId, tenantId) {
2200
+ const first = await loadInvoice(db, invoiceId);
2201
+ await lockDocumentMoneyParents([{
2202
+ id: invoiceId,
2203
+ tenantId,
2204
+ collection: first.collection
2205
+ }]);
2206
+ const invoice = await first.collection.get({ id: invoiceId }, { cache: false });
2207
+ if (!invoice || invoice.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant identity changed while acquiring its money lock");
2208
+ return {
2209
+ collection: first.collection,
2210
+ invoice
2211
+ };
2212
+ }
2213
+ async function lockAndReloadMoneyInvoice(db, invoiceId, tenantId, creditedInvoiceId) {
2214
+ const collection = await InvoiceCollection.create({ db });
2215
+ const ids = [invoiceId, ...creditedInvoiceId ? [creditedInvoiceId] : []];
2216
+ 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");
2217
+ await lockDocumentMoneyParents(ids.map((id) => ({
2218
+ id,
2219
+ tenantId,
2220
+ collection
2221
+ })));
2222
+ const invoice = await collection.get({ id: invoiceId }, { cache: false });
2223
+ const source = creditedInvoiceId ? await collection.get({ id: creditedInvoiceId }, { cache: false }) : null;
2224
+ if (!invoice || invoice.tenantId !== tenantId || source && source.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money parent identity changed while acquiring its lock");
2225
+ return {
2226
+ collection,
2227
+ invoice,
2228
+ source
2229
+ };
2230
+ }
2231
+ function discountInput(line) {
2232
+ if (line.discountType === "percent") return {
2233
+ kind: "percent",
2234
+ rate: String(line.discountRate)
2235
+ };
2236
+ if (line.discountType === "amount" || line.discount !== 0) return {
2237
+ kind: "amount",
2238
+ minor: line.discount
2239
+ };
2240
+ if (line.discountType !== "none") throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown discount type: ${line.discountType}`);
2241
+ return { kind: "none" };
2242
+ }
2243
+ function issuedCredit(invoice) {
2244
+ return invoice.kind === "credit_note" && invoice.status !== InvoiceStatus.DRAFT && invoice.status !== InvoiceStatus.CANCELLED;
2245
+ }
2246
+ async function refreshIssuedCreditBalance(db, source) {
2247
+ const sourceId = requireId(source.id, "Credited invoice");
2248
+ const credits = await (await InvoiceCollection.create({ db })).list({
2249
+ where: {
2250
+ tenantId: source.tenantId,
2251
+ creditedInvoiceId: sourceId
2252
+ },
2253
+ limit: 100001
2254
+ });
2255
+ if (credits.length > 1e5) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Issued credit scope exceeds its bounded source limit");
2256
+ const issued = credits.filter(issuedCredit).sort((left, right) => {
2257
+ const date = left.issueDate.getTime() - right.issueDate.getTime();
2258
+ if (date) return date;
2259
+ return String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0;
2260
+ });
2261
+ const total = issued.reduce((sum, credit) => sum + credit.totalAmount, 0);
2262
+ if (!Number.isSafeInteger(total) || total < 0) throw new MoneyValidationError("UNSAFE_MONEY", "Issued credit total is not a nonnegative safe integer");
2263
+ let remainingSource = Math.max(0, source.totalAmount - source.amountPaid);
2264
+ for (const credit of credits) {
2265
+ const included = issued.includes(credit);
2266
+ const applied = included ? Math.min(credit.totalAmount, remainingSource) : 0;
2267
+ if (included) remainingSource -= applied;
2268
+ if (source.retentionAccountingStateJson) {
2269
+ const managedCredit = JSON.parse(source.retentionAccountingStateJson).credits.find((entry) => entry.creditId === credit.id);
2270
+ if (managedCredit && (applied !== managedCredit.retainedMinor + managedCredit.appliedMinor || !included)) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Provider-managed credit allocation cannot change without an explicit reversal contract");
2271
+ }
2272
+ if (credit.creditedAmount !== applied) {
2273
+ credit.creditedAmount = applied;
2274
+ credit.updatePaymentStatus(credit.amountPaid);
2275
+ await credit.saveDerived(DOCUMENT_MONEY_WRITE);
2276
+ }
2277
+ }
2278
+ if (source.retentionAccountingStateJson && total !== source.creditedAmount) {
2279
+ const state = JSON.parse(source.retentionAccountingStateJson);
2280
+ if (state.status !== "ready" || state.steps.some((step) => step.status !== "confirmed")) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Provider retention operation must reconcile before changing source credits");
2281
+ }
2282
+ source.creditedAmount = total;
2283
+ source.updatePaymentStatus(source.amountPaid);
2284
+ await source.saveDerived(DOCUMENT_MONEY_WRITE);
2285
+ }
2286
+ function lineNet(line, parent) {
2287
+ return parent.lineAmountType === "inclusive" ? line.amount - line.taxAmount : line.amount;
2288
+ }
2289
+ async function prepareCreditLines(db, invoice, lines) {
2290
+ if (!invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
2291
+ const sourceInvoice = await findInvoice(db, invoice.creditedInvoiceId);
2292
+ 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");
2293
+ if (sourceInvoice.moneyAuthority === "historical") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Historical source credits require exact original monetary facts and concrete historical source line lineage, not calculated quantity or inferred tax");
2294
+ if (sourceInvoice.currency !== invoice.currency) throw new MoneyValidationError("CURRENCY_MISMATCH", "Credit note currency must match its credited invoice");
2295
+ if (sourceInvoice.customerId !== invoice.customerId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit note customer must match its credited invoice");
2296
+ invoice.lineAmountType = sourceInvoice.lineAmountType;
2297
+ invoice.taxRoundingScope = sourceInvoice.taxRoundingScope;
2298
+ const lineCollection = await InvoiceLineItemCollection.create({ db });
2299
+ const sourceLines = await lineCollection.findByInvoice(requireId(sourceInvoice.id, "Credited invoice"));
2300
+ const sourceTaxes = await (await DocumentTaxCollection.create({ db })).findByDocument(requireId(sourceInvoice.tenantId, "Credited invoice tenant"), "invoice", requireId(sourceInvoice.id, "Credited invoice"));
2301
+ const sourceByLineId = new Map(sourceLines.map((line) => [requireId(line.id, "Credited line"), line]));
2302
+ const priorCredits = (await (await InvoiceCollection.create({ db })).list({ where: {
2303
+ tenantId: invoice.tenantId,
2304
+ creditedInvoiceId: invoice.creditedInvoiceId
2305
+ } })).filter((row) => row.id !== invoice.id && issuedCredit(row));
2306
+ const issuedLines = [];
2307
+ const issuedParentById = new Map(priorCredits.map((row) => [requireId(row.id, "Issued credit note"), row]));
2308
+ const consumptions = [];
2309
+ for (const credit of priorCredits) for (const line of await lineCollection.findByInvoice(requireId(credit.id, "Issued credit note"))) {
2310
+ if (!line.creditedLineId) continue;
2311
+ issuedLines.push(line);
2312
+ const basis = line.creditCalculationBasis;
2313
+ consumptions.push({
2314
+ creditedLineId: line.creditedLineId,
2315
+ quantity: basis?.kind === "quantity" ? basis.quantity : "0",
2316
+ netMinor: lineNet(line, credit),
2317
+ discountMinor: line.discount
2318
+ });
2319
+ }
2320
+ const targeted = lines.filter((line) => line.creditedLineId);
2321
+ const allocation = targeted.length ? allocateFrozenCredit(sourceLines.map((line) => ({
2322
+ id: requireId(line.id, "Credited line"),
2323
+ quantity: String(line.quantity),
2324
+ netMinor: lineNet(line, sourceInvoice),
2325
+ discountMinor: line.discount,
2326
+ unitPrice: line.unitPrice,
2327
+ discountType: line.discountType,
2328
+ discountRate: line.discountRate
2329
+ })), consumptions, targeted.map((line) => {
2330
+ const basis = line.creditCalculationBasis;
2331
+ return basis?.kind === "net_amount" ? {
2332
+ creditedLineId: requireId(line.creditedLineId, "Credited line"),
2333
+ netMinor: basis.netMinor
2334
+ } : {
2335
+ creditedLineId: requireId(line.creditedLineId, "Credited line"),
2336
+ quantity: basis?.kind === "quantity" ? basis.quantity : String(line.quantity)
2337
+ };
2338
+ })) : {
2339
+ allocations: [],
2340
+ exhaustsDocument: false
2341
+ };
2342
+ const allocationByLineId = /* @__PURE__ */ new Map();
2343
+ const targetBySource = new Map(targeted.map((line) => [requireId(line.creditedLineId, "Credited line"), line]));
2344
+ for (const row of allocation.allocations) {
2345
+ const line = targetBySource.get(row.creditedLineId);
2346
+ if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit allocation lost its target line");
2347
+ line.creditCalculationBasis = row.basis;
2348
+ allocationByLineId.set(requireId(line.id, "Credit line"), row);
2349
+ }
2350
+ return {
2351
+ allocationByLineId,
2352
+ sourceByLineId,
2353
+ issuedLines,
2354
+ issuedParentById,
2355
+ sourceInvoice,
2356
+ sourceTaxes,
2357
+ exhaustsDocument: allocation.exhaustsDocument
2358
+ };
2359
+ }
2360
+ async function recomputeInvoiceAggregate(db, invoice, settings, freezeSnapshotId = "", persist = true, proposedLines) {
2361
+ assertTenantId(invoice.tenantId);
2362
+ const tenantId = invoice.tenantId;
2363
+ const linesCollection = await InvoiceLineItemCollection.create({ db });
2364
+ const codesCollection = await TaxCodeCollection.create({ db });
2365
+ const componentsCollection = await TaxComponentCollection.create({ db });
2366
+ const invoiceId = requireId(invoice.id, "Invoice calculation");
2367
+ const lines = proposedLines ?? await linesCollection.findByInvoice(invoiceId);
2368
+ const credit = invoice.kind === "credit_note" ? await prepareCreditLines(db, invoice, lines) : null;
2369
+ const codes = await codesCollection.list({ where: { tenantId } });
2370
+ const components = await componentsCollection.list({ where: { tenantId } });
2371
+ const componentById = new Map(components.map((row) => [requireId(row.id, "Tax component"), row]));
2372
+ const codeById = new Map(codes.map((row) => [requireId(row.id, "Tax code"), row]));
2373
+ const codeInputs = {};
2374
+ for (const code of codes) {
2375
+ const codeId = requireId(code.id, "Tax code");
2376
+ codeInputs[codeId] = {
2377
+ id: codeId,
2378
+ kind: code.kind,
2379
+ active: code.active,
2380
+ appliesTo: code.appliesTo,
2381
+ components: code.components.map((part) => {
2382
+ const component = componentById.get(part.taxComponentId);
2383
+ if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
2384
+ return {
2385
+ taxComponentId: requireId(component.id, "Tax component"),
2386
+ percent: component.percent,
2387
+ compound: part.compound
2388
+ };
2389
+ })
2390
+ };
2391
+ }
2392
+ if (credit) for (const source of credit.sourceByLineId.values()) {
2393
+ if (!source.taxCodeId) continue;
2394
+ const frozenKind = credit.sourceTaxes.find((row) => row.taxCodeId === source.taxCodeId)?.kind;
2395
+ codeInputs[source.taxCodeId] = {
2396
+ id: source.taxCodeId,
2397
+ kind: frozenKind ?? (source.taxComponents.length ? "taxable" : "zero-rated"),
2398
+ active: true,
2399
+ appliesTo: "sales",
2400
+ components: source.taxComponents.map((component) => ({
2401
+ taxComponentId: component.taxComponentId,
2402
+ percent: component.percent,
2403
+ compound: component.compound ?? false
2404
+ }))
2405
+ };
2406
+ }
2407
+ const exempt = await codesCollection.findByCode("exempt", tenantId);
2408
+ const outOfScope = await codesCollection.findByCode("out-of-scope", tenantId);
2409
+ const customers = await CustomerCollection.create({ db });
2410
+ const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
2411
+ if (customer && customer.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice customer must belong to its tenant");
2412
+ const productIds = [...new Set(lines.filter((line) => !line.creditedLineId).map((line) => line.productId).filter((productId) => !!productId))];
2413
+ const productById = /* @__PURE__ */ new Map();
2414
+ if (productIds.length > 0) {
2415
+ const { ProductCollection } = await import("@happyvertical/smrt-products/collections");
2416
+ const products = await ProductCollection.create({ db });
2417
+ for (const productId of productIds) {
2418
+ const product = await products.get({ id: productId }, { cache: false });
2419
+ if (!product) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} was not found`);
2420
+ if (product.tenantId !== null && product.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} must be global or belong to its tenant`);
2421
+ productById.set(productId, product);
2422
+ }
2423
+ }
2424
+ const resolvedCodes = /* @__PURE__ */ new Map();
2425
+ const calculated = calculateMoneyDocument({
2426
+ currency: invoice.currency,
2427
+ lineAmountType: invoice.lineAmountType,
2428
+ roundingScope: invoice.taxRoundingScope,
2429
+ lines: lines.map((line) => {
2430
+ if (line.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice line ${line.id} must belong to its parent tenant`);
2431
+ const lineId = requireId(line.id, "Invoice line");
2432
+ if (line.creditedLineId) {
2433
+ if (!credit) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited line");
2434
+ const source = credit.sourceByLineId.get(line.creditedLineId);
2435
+ const allocation = credit.allocationByLineId.get(lineId);
2436
+ if (!source || !allocation) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credited line is not part of the frozen source invoice");
2437
+ const taxCode = source.taxCodeId ? codeInputs[source.taxCodeId] ?? null : null;
2438
+ resolvedCodes.set(lineId, source.taxCodeId ? {
2439
+ id: source.taxCodeId,
2440
+ code: source.taxCodeCode,
2441
+ name: source.taxCodeName,
2442
+ kind: taxCode?.kind ?? "zero-rated",
2443
+ components: structuredClone(source.taxComponents)
2444
+ } : null);
2445
+ line.description ||= source.description;
2446
+ line.sku ||= source.sku;
2447
+ line.productId = source.productId;
2448
+ line.revenueAccountId = source.revenueAccountId;
2449
+ if (allocation.basis.kind === "quantity") {
2450
+ line.quantity = Number(allocation.basis.quantity);
2451
+ line.unitPrice = allocation.basis.sourceUnitPrice;
2452
+ line.discountType = allocation.discountMinor ? "amount" : "none";
2453
+ line.discountRate = "0";
2454
+ } else {
2455
+ line.quantity = 1;
2456
+ line.unitPrice = allocation.netMinor ?? allocation.basis.netMinor;
2457
+ line.discountType = "none";
2458
+ line.discountRate = "0";
2459
+ }
2460
+ line.discount = allocation.discountMinor;
2461
+ return {
2462
+ id: lineId,
2463
+ quantity: line.quantity,
2464
+ unitPrice: line.unitPrice,
2465
+ netAmountMinor: allocation.basis.kind === "net_amount" ? allocation.netMinor ?? allocation.basis.netMinor : void 0,
2466
+ discount: allocation.discountMinor ? {
2467
+ kind: "amount",
2468
+ minor: allocation.discountMinor
2469
+ } : { kind: "none" },
2470
+ taxCode
2471
+ };
2472
+ }
2473
+ if (invoice.kind === "invoice" && line.creditCalculationBasis) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot carry a credit calculation basis");
2474
+ if (invoice.kind === "credit_note" && line.taxMode !== "override") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Free-form credit lines require an explicit tax-code override");
2475
+ const resolved = resolveLineTaxCode({
2476
+ lineAmountType: invoice.lineAmountType,
2477
+ lineOverride: line.taxMode === "override" ? line.taxCodeId : null
2478
+ }, {
2479
+ direction: "sales",
2480
+ codes: codeInputs,
2481
+ customerTaxExempt: customer?.taxExempt ?? false,
2482
+ exemptCodeId: exempt?.id,
2483
+ productCodeId: line.productId ? productById.get(line.productId)?.defaultSalesTaxCodeId || null : null,
2484
+ partyCodeId: customer?.defaultTaxCodeId,
2485
+ appDefaultCodeId: settings.defaultSalesTaxCodeId,
2486
+ outOfScopeCodeId: outOfScope?.id
2487
+ });
2488
+ resolvedCodes.set(lineId, resolved.taxCode ? (() => {
2489
+ const code = codeById.get(resolved.taxCode.id);
2490
+ if (!code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax code ${resolved.taxCode.id} was not loaded`);
2491
+ return {
2492
+ id: requireId(code.id, "Tax code"),
2493
+ code: code.code,
2494
+ name: code.name,
2495
+ kind: code.kind,
2496
+ components: code.components.map((part) => {
2497
+ const component = componentById.get(part.taxComponentId);
2498
+ if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
2499
+ return {
2500
+ taxComponentId: part.taxComponentId,
2501
+ name: component.name,
2502
+ percent: component.percent,
2503
+ ...part.compound ? { compound: true } : {},
2504
+ taxMinor: 0
2505
+ };
2506
+ })
2507
+ };
2508
+ })() : null);
2509
+ return {
2510
+ id: requireId(line.id, "Invoice line"),
2511
+ quantity: line.quantity,
2512
+ unitPrice: line.unitPrice,
2513
+ discount: discountInput(line),
2514
+ taxCode: resolved.taxCode
2515
+ };
2516
+ })
2517
+ });
2518
+ if (credit?.exhaustsDocument) {
2519
+ const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
2520
+ const targeted = lines.filter((line) => !!line.creditedLineId);
2521
+ for (const line of targeted) {
2522
+ const source = credit.sourceByLineId.get(requireId(line.creditedLineId, "Credited line"));
2523
+ const result = resultById.get(requireId(line.id, "Credit line"));
2524
+ if (!source || !result) continue;
2525
+ const prior = credit.issuedLines.filter((candidate) => candidate.creditedLineId === source.id);
2526
+ const priorNet = prior.reduce((sum, candidate) => {
2527
+ const parent = credit.issuedParentById.get(candidate.invoiceId);
2528
+ return sum + (parent ? lineNet(candidate, parent) : 0);
2529
+ }, 0);
2530
+ const desiredNet = lineNet(source, credit.sourceInvoice) - priorNet;
2531
+ const netDelta = desiredNet - result.netMinor;
2532
+ result.netMinor = desiredNet;
2533
+ result.amountMinor += netDelta;
2534
+ for (const sourceComponent of source.taxComponents) {
2535
+ const component = result.components.find((candidate) => candidate.taxComponentId === sourceComponent.taxComponentId);
2536
+ if (!component) continue;
2537
+ const priorTax = prior.reduce((sum, candidate) => sum + (candidate.taxComponents.find((part) => part.taxComponentId === sourceComponent.taxComponentId)?.taxMinor ?? 0), 0);
2538
+ const desiredTax = sourceComponent.taxMinor - priorTax;
2539
+ const taxDelta = desiredTax - component.taxMinor;
2540
+ component.taxMinor = desiredTax;
2541
+ if (sourceComponent.compound) {
2542
+ component.compound = true;
2543
+ component.taxableMinor = (sourceComponent.taxableMinor ?? 0) - prior.reduce((sum, candidate) => sum + (candidate.taxComponents.find((part) => part.taxComponentId === sourceComponent.taxComponentId)?.taxableMinor ?? 0), 0);
2544
+ }
2545
+ result.taxMinor += taxDelta;
2546
+ if (invoice.lineAmountType === "inclusive") result.amountMinor += taxDelta;
2547
+ }
2548
+ }
2549
+ const rebuilt = /* @__PURE__ */ new Map();
2550
+ for (const result of calculated.lines) {
2551
+ const metadata = resolvedCodes.get(result.id) ?? null;
2552
+ const parts = result.components.length ? result.components : [{
2553
+ taxComponentId: null,
2554
+ taxMinor: 0,
2555
+ compound: false,
2556
+ taxableMinor: void 0
2557
+ }];
2558
+ for (const part of parts) {
2559
+ const key = `${metadata?.id ?? ""}\0${part.taxComponentId ?? ""}`;
2560
+ const row = rebuilt.get(key) ?? {
2561
+ taxCodeId: metadata?.id ?? null,
2562
+ taxComponentId: part.taxComponentId,
2563
+ taxableMinor: 0,
2564
+ taxMinor: 0
2565
+ };
2566
+ row.taxableMinor += part.taxableMinor ?? result.netMinor;
2567
+ if (part.compound) row.netMinor = (row.netMinor ?? 0) + result.netMinor;
2568
+ row.taxMinor += part.taxMinor;
2569
+ rebuilt.set(key, row);
2570
+ }
2571
+ }
2572
+ calculated.taxes = [...rebuilt.values()].sort((left, right) => {
2573
+ const a = `${left.taxCodeId ?? ""}\0${left.taxComponentId ?? ""}`;
2574
+ const b = `${right.taxCodeId ?? ""}\0${right.taxComponentId ?? ""}`;
2575
+ return a < b ? -1 : a > b ? 1 : 0;
2576
+ });
2577
+ calculated.subtotalMinor = calculated.lines.reduce((sum, row) => sum + row.netMinor, 0);
2578
+ calculated.taxMinor = calculated.lines.reduce((sum, row) => sum + row.taxMinor, 0);
2579
+ calculated.totalMinor = calculated.subtotalMinor + calculated.taxMinor;
2580
+ }
2581
+ if (credit) {
2582
+ const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
2583
+ for (const [lineId, allocation] of credit.allocationByLineId) {
2584
+ if (allocation.basis.kind !== "quantity") continue;
2585
+ const result = resultById.get(lineId);
2586
+ const hasAmountBasisConsumption = credit.issuedLines.some((line) => line.creditedLineId === allocation.creditedLineId && line.creditCalculationBasis?.kind === "net_amount");
2587
+ 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");
2588
+ }
2589
+ }
2590
+ const lineById = new Map(lines.map((line) => [requireId(line.id, "Invoice line"), line]));
2591
+ for (const result of calculated.lines) {
2592
+ const line = lineById.get(result.id);
2593
+ if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Calculated line ${result.id} was not loaded`);
2594
+ const code = resolvedCodes.get(requireId(line.id, "Invoice line")) ?? null;
2595
+ line.discount = result.discountMinor;
2596
+ line.amount = result.amountMinor;
2597
+ line.taxAmount = result.taxMinor;
2598
+ line.taxCodeId = code?.id ?? null;
2599
+ line.taxCodeCode = code?.code ?? null;
2600
+ line.taxCodeName = code?.name ?? null;
2601
+ line.taxComponents = result.components.map((entry) => {
2602
+ const component = code?.components.find((candidate) => candidate.taxComponentId === entry.taxComponentId);
2603
+ if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Calculated component ${entry.taxComponentId} was not loaded`);
2604
+ return {
2605
+ taxComponentId: component.taxComponentId,
2606
+ name: component.name,
2607
+ percent: component.percent,
2608
+ taxMinor: entry.taxMinor,
2609
+ ...entry.compound ? {
2610
+ compound: true,
2611
+ taxableMinor: entry.taxableMinor
2612
+ } : {}
2613
+ };
2614
+ });
2615
+ line.taxRate = result.components.some((part) => part.compound) ? result.netMinor === 0 ? 0 : result.taxMinor / result.netMinor : code ? code.components.reduce((sum, part) => sum + Number(part.percent) / 100, 0) : 0;
2616
+ if (freezeSnapshotId) line.moneySnapshotId = freezeSnapshotId;
2617
+ if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE);
2618
+ }
2619
+ const taxRows = calculated.taxes.map((tax) => {
2620
+ const code = tax.taxCodeId ? [...resolvedCodes.values()].find((candidate) => candidate?.id === tax.taxCodeId) ?? null : null;
2621
+ const snapshotComponent = tax.taxComponentId ? code?.components.find((candidate) => candidate.taxComponentId === tax.taxComponentId) : null;
2622
+ const currentComponent = tax.taxComponentId ? componentById.get(tax.taxComponentId) : null;
2623
+ const sourceTax = credit?.sourceTaxes.find((row) => row.taxCodeId === tax.taxCodeId && row.taxComponentId === tax.taxComponentId);
2624
+ if (tax.taxCodeId && !code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax code ${tax.taxCodeId} was not loaded`);
2625
+ if (tax.taxComponentId && !snapshotComponent) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax component ${tax.taxComponentId} was not loaded`);
2626
+ return {
2627
+ tenantId,
2628
+ documentType: invoice.kind,
2629
+ documentId: invoiceId,
2630
+ currency: invoice.currency,
2631
+ taxCodeId: code ? requireId(code.id, "Tax code") : null,
2632
+ taxCodeCode: code?.code ?? null,
2633
+ kind: code?.kind ?? null,
2634
+ taxComponentId: snapshotComponent?.taxComponentId ?? null,
2635
+ taxComponentName: snapshotComponent?.name ?? null,
2636
+ percent: snapshotComponent?.percent ?? null,
2637
+ liabilityAccountId: sourceTax?.liabilityAccountId || currentComponent?.liabilityAccountId || null,
2638
+ taxableMinor: tax.taxableMinor,
2639
+ netMinor: tax.netMinor ?? null,
2640
+ taxMinor: tax.taxMinor,
2641
+ recoverableMinor: 0
2642
+ };
2643
+ });
2644
+ if (persist) await (await DocumentTaxCollection.create({ db })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
2645
+ tenantId,
2646
+ documentType: invoice.kind,
2647
+ documentId: invoiceId,
2648
+ rows: taxRows
2649
+ });
2650
+ invoice.subtotal = calculated.subtotalMinor;
2651
+ invoice.discountTotal = calculated.discountTotalMinor;
2652
+ invoice.taxAmount = calculated.taxMinor + invoice.providerTaxAmount;
2653
+ invoice.totalAmount = calculated.totalMinor + invoice.providerTaxAmount;
2654
+ return {
2655
+ lines,
2656
+ taxRows
2657
+ };
2658
+ }
2659
+ async function previewInvoiceMoneyOnScope(input, settings) {
2660
+ if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview requires its active principal tenant");
2661
+ const { invoice } = await loadInvoice(input.db, input.invoiceId);
2662
+ if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview cannot cross tenant boundaries");
2663
+ if (invoice.status !== InvoiceStatus.DRAFT || invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Frozen invoice money cannot be previewed as a proposed draft");
2664
+ const proposedHeader = input.draft?.header;
2665
+ if (proposedHeader) {
2666
+ for (const field of [
2667
+ "currency",
2668
+ "lineAmountType",
2669
+ "taxRoundingScope",
2670
+ "providerTaxAmount",
2671
+ "customerId"
2672
+ ]) if (proposedHeader[field] !== void 0) invoice[field] = proposedHeader[field];
2673
+ }
2674
+ const storedLines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
2675
+ const storedById = new Map(storedLines.map((line) => [line.id, line]));
2676
+ const providedIds = input.draft?.lines?.map((line) => line.id).filter((id) => !!id) ?? [];
2677
+ if (new Set(providedIds).size !== providedIds.length) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview line ids must be distinct");
2678
+ const unknownId = providedIds.find((id) => !storedById.has(id));
2679
+ if (unknownId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice preview line ${unknownId} was not found on the invoice`);
2680
+ const proposedLines = input.draft?.lines ? input.draft.lines.map((proposed) => {
2681
+ const result = (proposed.id ? storedById.get(proposed.id) : void 0) ?? new InvoiceLineItem();
2682
+ for (const field of [
2683
+ "description",
2684
+ "productId",
2685
+ "quantity",
2686
+ "unitPrice",
2687
+ "discountType",
2688
+ "discountRate",
2689
+ "discount",
2690
+ "taxMode",
2691
+ "taxCodeId"
2692
+ ]) if (proposed[field] !== void 0) result[field] = proposed[field];
2693
+ result.id ||= crypto.randomUUID();
2694
+ result.invoiceId = input.invoiceId;
2695
+ result.tenantId = input.tenantId;
2696
+ return result;
2697
+ }) : storedLines;
2698
+ const calculated = await recomputeInvoiceAggregate(input.db, invoice, settings, "", false, proposedLines);
2699
+ return {
2700
+ source: "calculated",
2701
+ header: {
2702
+ currency: invoice.currency,
2703
+ lineAmountType: invoice.lineAmountType,
2704
+ taxRoundingScope: invoice.taxRoundingScope,
2705
+ subtotalMinor: invoice.subtotal,
2706
+ discountTotalMinor: invoice.discountTotal,
2707
+ taxAmountMinor: invoice.taxAmount,
2708
+ providerTaxAmountMinor: invoice.providerTaxAmount,
2709
+ totalAmountMinor: invoice.totalAmount
2710
+ },
2711
+ lines: calculated.lines.map((line) => ({
2712
+ id: requireId(line.id, "Preview line"),
2713
+ grossMinor: line.amount + line.discount,
2714
+ discountMinor: line.discount,
2715
+ amountMinor: line.amount,
2716
+ taxMinor: line.taxAmount,
2717
+ taxCodeId: line.taxCodeId,
2718
+ taxCodeCode: line.taxCodeCode,
2719
+ taxCodeName: line.taxCodeName,
2720
+ taxComponents: structuredClone(line.taxComponents)
2721
+ })),
2722
+ taxes: calculated.taxRows
2723
+ };
2724
+ }
2725
+ async function previewInvoiceMoney(input) {
2726
+ return withTenantMoneyRuntime(input.db, input.tenantId, async (scope) => previewInvoiceMoneyOnScope({
2727
+ ...input,
2728
+ db: scope.db
2729
+ }, scope.settings));
2730
+ }
2731
+ async function getInvoiceMoneyFactsOnScope(input) {
2732
+ if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts require their active principal tenant");
2733
+ const { invoice } = await loadInvoice(input.db, input.invoiceId);
2734
+ if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts cannot cross tenant boundaries");
2735
+ const lines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
2736
+ const taxes = await (await DocumentTaxCollection.create({ db: input.db })).findByDocument(input.tenantId, invoice.kind, input.invoiceId);
2737
+ const historical = getHistoricalSalesMoney(invoice);
2738
+ return {
2739
+ source: "stored",
2740
+ ...historical ? { historical: {
2741
+ snapshot: historical,
2742
+ lines: (await readHistoricalSalesLines(input.db, invoice)).map((line) => ({
2743
+ sourceKey: line.sourceKey,
2744
+ lineId: String(line.id)
2745
+ }))
2746
+ } } : {},
2747
+ ...invoice.retentionStateJson ? { payable: invoice.getPayableFacts() } : {},
2748
+ invoiceId: input.invoiceId,
2749
+ moneySnapshotId: invoice.moneySnapshotId,
2750
+ kind: invoice.kind,
2751
+ status: invoice.status,
2752
+ header: {
2753
+ currency: invoice.currency,
2754
+ lineAmountType: invoice.lineAmountType,
2755
+ taxRoundingScope: invoice.taxRoundingScope,
2756
+ subtotalMinor: invoice.subtotal,
2757
+ discountTotalMinor: invoice.discountTotal,
2758
+ taxAmountMinor: invoice.taxAmount,
2759
+ providerTaxAmountMinor: invoice.providerTaxAmount,
2760
+ totalAmountMinor: invoice.totalAmount
2761
+ },
2762
+ lines: lines.map((line) => ({
2763
+ id: requireId(line.id, "Invoice line"),
2764
+ grossMinor: line.amount + line.discount,
2765
+ discountMinor: line.discount,
2766
+ amountMinor: line.amount,
2767
+ taxMinor: line.taxAmount,
2768
+ taxCodeId: line.taxCodeId,
2769
+ taxCodeCode: line.taxCodeCode,
2770
+ taxCodeName: line.taxCodeName,
2771
+ taxComponents: structuredClone(line.taxComponents)
2772
+ })),
2773
+ taxes: taxes.map((row) => ({
2774
+ tenantId: requireId(row.tenantId, "Document tax tenant"),
2775
+ documentType: row.documentType,
2776
+ documentId: row.documentId,
2777
+ currency: row.currency,
2778
+ taxCodeId: row.taxCodeId,
2779
+ taxCodeCode: row.taxCodeCode,
2780
+ kind: row.kind,
2781
+ taxComponentId: row.taxComponentId,
2782
+ taxComponentName: row.taxComponentName,
2783
+ percent: row.percent,
2784
+ liabilityAccountId: row.liabilityAccountId,
2785
+ taxableMinor: row.taxableMinor,
2786
+ ...row.netMinor !== null ? { netMinor: row.netMinor } : {},
2787
+ taxMinor: row.taxMinor,
2788
+ recoverableMinor: row.recoverableMinor
2789
+ }))
2790
+ };
2791
+ }
2792
+ async function getInvoiceMoneyFacts(input) {
2793
+ return withTenantMoneyRuntime(input.db, input.tenantId, async (scope) => getInvoiceMoneyFactsOnScope({
2794
+ ...input,
2795
+ db: scope.db
2796
+ }));
2797
+ }
2798
+ async function saveInvoice(input) {
2799
+ const priorRevision = input.updated_at;
2800
+ try {
2801
+ return await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
2802
+ if (invoice.kind !== "invoice" && invoice.kind !== "credit_note") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice kind must be invoice or credit_note");
2803
+ if (invoice.kind === "invoice" && invoice.creditedInvoiceId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited invoice");
2804
+ if (invoice.kind === "credit_note" && !invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
2805
+ const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
2806
+ const settings = await lockRuntimeSettings(tx, candidateTenantId);
2807
+ const existing = invoice.id ? await findInvoice(tx, invoice.id) : null;
2808
+ const tenantId = existing?.tenantId ?? candidateTenantId;
2809
+ if (invoice.retentionAccountingStateJson !== (existing?.retentionAccountingStateJson ?? "")) throw new MoneyValidationError("FROZEN_DOCUMENT", "Retention accounting state is managed by provider commands");
2810
+ if (invoice.moneyAuthority !== (existing?.moneyAuthority ?? "calculated") || invoice.historicalSalesMoneyJson !== (existing?.historicalSalesMoneyJson ?? "") || invoice.historicalSourceKeyHash !== (existing?.historicalSourceKeyHash ?? null)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Historical source authority is managed only by historical sales commands");
2811
+ if (existing?.moneyAuthority === "historical") getHistoricalSalesMoney(existing);
2812
+ if (invoice.retentionStateJson !== (existing?.retentionStateJson ?? "")) throw new MoneyValidationError("FROZEN_DOCUMENT", "Retention state is managed by retention commands");
2813
+ if (candidateTenantId && existing?.tenantId && existing.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
2814
+ assertTenantId(tenantId);
2815
+ if (!existing) {
2816
+ invoice.tenantId = tenantId;
2817
+ invoice.creditedAmount = 0;
2818
+ let source = null;
2819
+ if (invoice.kind === "credit_note") {
2820
+ source = await findInvoice(tx, requireId(invoice.creditedInvoiceId, "Credited invoice"));
2821
+ if (!source || source.tenantId !== tenantId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must exist in the active tenant");
2822
+ await lockDocumentMoneyParents([{
2823
+ id: requireId(source.id, "Credited invoice"),
2824
+ tenantId,
2825
+ collection: await InvoiceCollection.create({ db: tx })
2826
+ }]);
2827
+ 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");
2828
+ 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");
2829
+ }
2830
+ invoice.lineAmountType = invoice.kind === "credit_note" && source ? source.lineAmountType : settings.defaultLineAmountType;
2831
+ invoice.taxRoundingScope = invoice.kind === "credit_note" && source ? source.taxRoundingScope : settings.taxRoundingScope;
2832
+ invoice.subtotal = 0;
2833
+ invoice.discountTotal = 0;
2834
+ invoice.taxAmount = invoice.providerTaxAmount;
2835
+ invoice.totalAmount = invoice.providerTaxAmount;
2836
+ if (invoice.status !== InvoiceStatus.DRAFT || invoice.arJournalId) {
2837
+ await assertContractScheduleInvoiceFreeze(tx, invoice);
2838
+ const customers = await CustomerCollection.create({ db: tx });
2839
+ const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
2840
+ invoice.moneySnapshotId = crypto.randomUUID();
2841
+ invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
2842
+ invoice.partyTaxId = customer?.taxId ?? "";
2843
+ }
2844
+ return invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2845
+ }
2846
+ if (invoice.tenantId !== existing.tenantId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice tenant identity cannot change");
2847
+ if (invoice.kind !== existing.kind || invoice.creditedInvoiceId !== existing.creditedInvoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice kind and credited invoice identity are immutable");
2848
+ const locked = await lockAndReloadMoneyInvoice(tx, requireId(existing.id, "Invoice"), tenantId, existing.creditedInvoiceId);
2849
+ invoice.creditedAmount = locked.invoice.creditedAmount;
2850
+ if (isFrozen(locked.invoice)) {
2851
+ if (invoice.arReversalJournalId !== locked.invoice.arReversalJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice reversal journal identity is server-managed");
2852
+ if (locked.invoice.arJournalId && invoice.arJournalId !== locked.invoice.arJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice revenue journal identity is frozen once assigned");
2853
+ 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");
2854
+ 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");
2855
+ 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");
2856
+ assertFieldsUnchanged(invoice, locked.invoice, FROZEN_INVOICE_FIELDS, "Invoice");
2857
+ const saved2 = await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2858
+ if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
2859
+ return saved2;
2860
+ }
2861
+ const freezing = invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId;
2862
+ const snapshotId = freezing ? locked.invoice.moneySnapshotId || crypto.randomUUID() : "";
2863
+ await recomputeInvoiceAggregate(tx, invoice, settings, snapshotId);
2864
+ if (freezing) {
2865
+ await assertContractScheduleInvoiceFreeze(tx, invoice);
2866
+ const customers = await CustomerCollection.create({ db: tx });
2867
+ const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
2868
+ invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
2869
+ invoice.partyTaxId = customer?.taxId ?? "";
2870
+ invoice.moneySnapshotId = snapshotId;
2871
+ }
2872
+ const saved = await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2873
+ if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
2874
+ return saved;
2875
+ }));
2876
+ } catch (error) {
2877
+ input.updated_at = priorRevision;
2878
+ throw error;
2879
+ }
2880
+ }
2881
+ async function saveInvoiceLineItem(input) {
2882
+ return withDocumentMoneyObjectTransaction(input, async (line, tx) => {
2883
+ const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
2884
+ const settings = await lockRuntimeSettings(tx, candidateTenantId);
2885
+ const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
2886
+ const parentId = persisted?.invoiceId ?? line.invoiceId;
2887
+ const first = await loadInvoice(tx, parentId);
2888
+ if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
2889
+ assertTenantId(first.invoice.tenantId);
2890
+ const { invoice, source } = await lockAndReloadMoneyInvoice(tx, parentId, first.invoice.tenantId, first.invoice.creditedInvoiceId);
2891
+ if (persisted && line.invoiceId !== persisted.invoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice line cannot move between parents");
2892
+ line.invoiceId = requireId(invoice.id, "Invoice");
2893
+ line.tenantId = invoice.tenantId;
2894
+ if (invoice.kind === "invoice" && line.creditedLineId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot reference credited lines");
2895
+ 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");
2896
+ if (isFrozen(invoice)) {
2897
+ if (!persisted) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot add a line to a frozen invoice");
2898
+ assertFieldsUnchanged(line, persisted, FROZEN_LINE_FIELDS, "Invoice line");
2899
+ return line.saveDerived(DOCUMENT_MONEY_WRITE);
2900
+ }
2901
+ await line.saveDerived(DOCUMENT_MONEY_WRITE);
2902
+ const priorAggregate = aggregateSignature(invoice);
2903
+ await recomputeInvoiceAggregate(tx, invoice, settings);
2904
+ if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2905
+ const refreshed = await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: requireId(line.id, "Invoice line") }, { cache: false });
2906
+ if (refreshed) for (const field of [
2907
+ "discount",
2908
+ "amount",
2909
+ "taxAmount",
2910
+ "taxRate",
2911
+ "taxCodeId",
2912
+ "taxCodeCode",
2913
+ "taxCodeName",
2914
+ "taxComponentsJson",
2915
+ "moneySnapshotId",
2916
+ "updated_at"
2917
+ ]) line[field] = refreshed[field];
2918
+ return line;
2919
+ });
2920
+ }
2921
+ async function deleteInvoiceLineItem(input, options) {
2922
+ await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (line) => {
2923
+ const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
2924
+ const settings = await lockRuntimeSettings(tx, candidateTenantId);
2925
+ const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
2926
+ if (!persisted) return;
2927
+ const first = await loadInvoice(tx, persisted.invoiceId);
2928
+ if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
2929
+ assertTenantId(first.invoice.tenantId);
2930
+ const { invoice } = await lockAndReloadInvoice(tx, persisted.invoiceId, first.invoice.tenantId);
2931
+ if (isFrozen(invoice)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a line from a frozen invoice");
2932
+ await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);
2933
+ const priorAggregate = aggregateSignature(invoice);
2934
+ await recomputeInvoiceAggregate(tx, invoice, settings);
2935
+ if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2936
+ }));
2937
+ }
2938
+ async function deleteInvoice(input, options) {
2939
+ await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
2940
+ const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
2941
+ await lockRuntimeSettings(tx, candidateTenantId);
2942
+ const persisted = invoice.id ? await findInvoice(tx, invoice.id) : null;
2943
+ if (!persisted) return;
2944
+ if (candidateTenantId && persisted.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
2945
+ if (isFrozen(persisted)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a frozen invoice");
2946
+ assertTenantId(persisted.tenantId);
2947
+ const persistedId = requireId(persisted.id, "Invoice");
2948
+ await lockAndReloadInvoice(tx, persistedId, persisted.tenantId);
2949
+ await (await DocumentTaxCollection.create({ db: tx })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
2950
+ tenantId: persisted.tenantId,
2951
+ documentType: "invoice",
2952
+ documentId: persistedId,
2953
+ rows: []
2954
+ });
2955
+ await invoice.deleteDerived(DOCUMENT_MONEY_WRITE, options);
2956
+ }));
2957
+ }
2958
+ //#endregion
2959
+ //#region src/historical-sales-validation.ts
2960
+ function invalid$2() {
2961
+ throw new MoneyValidationError("INVALID_MONEY_INPUT", "Historical sales require exact supported source facts and explicit unknown values");
2962
+ }
2963
+ function historicalText(value, max = 2e3) {
2964
+ if (typeof value !== "string" || !value.trim() || value.trim() !== value || value.length > max) invalid$2();
2965
+ return value;
2966
+ }
2967
+ function historicalCalendarDate(value) {
2968
+ const date = historicalText(value);
2969
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date)) || new Date(date).toISOString().slice(0, 10) !== date) invalid$2();
2970
+ return date;
2971
+ }
2972
+ function record(value) {
2973
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid$2();
2974
+ return value;
2975
+ }
2976
+ function fields(input, allowed) {
2977
+ const value = record(input);
2978
+ if (Object.keys(value).some((key) => !allowed.includes(key))) invalid$2();
2979
+ return value;
2980
+ }
2981
+ function integer(value) {
2982
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) invalid$2();
2983
+ return value;
2984
+ }
2985
+ function nullableInteger(value) {
2986
+ return value === null ? null : integer(value);
2987
+ }
2988
+ function nullableText(value) {
2989
+ return value === null ? null : historicalText(value);
2990
+ }
2991
+ function sum(values) {
2992
+ const result = values.reduce((total, value) => total + BigInt(value), 0n);
2993
+ if (result > BigInt(Number.MAX_SAFE_INTEGER) || result < BigInt(Number.MIN_SAFE_INTEGER)) invalid$2();
2994
+ return Number(result);
2995
+ }
2996
+ function sameAmount(net, tax, gross) {
2997
+ if (sum([net, tax]) !== gross) invalid$2();
2998
+ }
2999
+ function historicalSalesMagnitudes(facts) {
3000
+ const sign = facts.kind === "credit_note" && facts.polarity === "signed" ? -1 : 1;
3001
+ const result = {
3002
+ netMinor: facts.netMinor * sign,
3003
+ taxMinor: facts.taxMinor * sign,
3004
+ grossMinor: facts.grossMinor * sign
3005
+ };
3006
+ if (Object.values(result).some((value) => value < 0)) invalid$2();
3007
+ return result;
3008
+ }
3009
+ function validateHistoricalSalesSource(input) {
3010
+ const value = fields(input, [
3011
+ "namespace",
3012
+ "key",
3013
+ "version",
3014
+ "issuedOn",
3015
+ "dueOn",
3016
+ "timeZone",
3017
+ "approval",
3018
+ "retention"
3019
+ ]);
3020
+ const retention = value.retention == null ? value.retention : (() => {
3021
+ const r = fields(value.retention, [
3022
+ "retainedMinor",
3023
+ "creditPolicy",
3024
+ "policyRef",
3025
+ "effectiveOn",
3026
+ "evidenceRef",
3027
+ "originalActorRef",
3028
+ "releases"
3029
+ ]);
3030
+ if (r.creditPolicy !== "retained-first" || !Array.isArray(r.releases) || r.releases.length > 1e4) invalid$2();
3031
+ const retainedMinor = integer(r.retainedMinor);
3032
+ if (retainedMinor <= 0) invalid$2();
3033
+ const effectiveOn = historicalCalendarDate(r.effectiveOn);
3034
+ const keys = /* @__PURE__ */ new Set();
3035
+ const releases = r.releases.map((input2) => {
3036
+ const event = fields(input2, [
3037
+ "sourceKey",
3038
+ "amountMinor",
3039
+ "effectiveOn",
3040
+ "evidenceRef",
3041
+ "originalActorRef",
3042
+ "sameDayCreditOrder"
3043
+ ]);
3044
+ const sourceKey = historicalText(event.sourceKey), amountMinor = integer(event.amountMinor), on = historicalCalendarDate(event.effectiveOn);
3045
+ if (keys.has(sourceKey) || amountMinor <= 0 || on < effectiveOn) invalid$2();
3046
+ keys.add(sourceKey);
3047
+ return {
3048
+ ...event.sameDayCreditOrder === void 0 ? {} : { sameDayCreditOrder: event.sameDayCreditOrder === null || event.sameDayCreditOrder === "credit-before-release" || event.sameDayCreditOrder === "release-before-credit" ? event.sameDayCreditOrder : invalid$2() },
3049
+ sourceKey,
3050
+ amountMinor,
3051
+ effectiveOn: on,
3052
+ evidenceRef: historicalText(event.evidenceRef),
3053
+ originalActorRef: nullableText(event.originalActorRef)
3054
+ };
3055
+ });
3056
+ if (sum(releases.map((event) => event.amountMinor)) > retainedMinor) invalid$2();
3057
+ return {
3058
+ retainedMinor,
3059
+ creditPolicy: "retained-first",
3060
+ policyRef: historicalText(r.policyRef),
3061
+ effectiveOn,
3062
+ evidenceRef: historicalText(r.evidenceRef),
3063
+ originalActorRef: nullableText(r.originalActorRef),
3064
+ releases
3065
+ };
3066
+ })();
3067
+ if (retention && retention.effectiveOn !== historicalCalendarDate(value.issuedOn)) invalid$2();
3068
+ const timeZone = historicalText(value.timeZone);
3069
+ try {
3070
+ new Intl.DateTimeFormat("en", { timeZone }).format(0);
3071
+ } catch {
3072
+ invalid$2();
3073
+ }
3074
+ const approval = value.approval === null ? null : fields(value.approval, ["actorRef", "approvedOn"]);
3075
+ return {
3076
+ ...retention === void 0 ? {} : { retention },
3077
+ namespace: historicalText(value.namespace),
3078
+ key: historicalText(value.key),
3079
+ version: historicalText(value.version),
3080
+ issuedOn: historicalCalendarDate(value.issuedOn),
3081
+ dueOn: value.dueOn === null ? null : historicalCalendarDate(value.dueOn),
3082
+ timeZone,
3083
+ approval: approval === null ? null : {
3084
+ actorRef: historicalText(approval.actorRef),
3085
+ approvedOn: approval.approvedOn === null ? null : historicalCalendarDate(approval.approvedOn)
3086
+ }
3087
+ };
3088
+ }
3089
+ function validateHistoricalSalesMoneyFacts(input) {
3090
+ const value = fields(input, [
3091
+ "currency",
3092
+ "kind",
3093
+ "polarity",
3094
+ "netMinor",
3095
+ "taxMinor",
3096
+ "grossMinor",
3097
+ "taxComponents",
3098
+ "detail"
3099
+ ]);
3100
+ const currency = historicalText(value.currency);
3101
+ if (!ISO_4217_MINOR_UNITS.has(currency) || ISO_4217_MINOR_UNITS.get(currency) === null || !["invoice", "credit_note"].includes(value.kind) || !["signed", "magnitudes"].includes(value.polarity)) invalid$2();
3102
+ const netMinor = integer(value.netMinor), taxMinor = integer(value.taxMinor), grossMinor = integer(value.grossMinor);
3103
+ sameAmount(netMinor, taxMinor, grossMinor);
3104
+ const seenComponents = /* @__PURE__ */ new Set();
3105
+ const taxComponents = value.taxComponents === null ? null : (() => {
3106
+ if (!Array.isArray(value.taxComponents) || value.taxComponents.length > 1e3) invalid$2();
3107
+ return value.taxComponents.map((inputComponent) => {
3108
+ const component = fields(inputComponent, [
3109
+ "sourceKey",
3110
+ "label",
3111
+ "amountMinor",
3112
+ "classification",
3113
+ "taxCodeId",
3114
+ "taxComponentId",
3115
+ "taxableMinor",
3116
+ "netMinor",
3117
+ "liabilityAccountId"
3118
+ ]), sourceKey = historicalText(component.sourceKey);
3119
+ if (seenComponents.has(sourceKey)) invalid$2();
3120
+ seenComponents.add(sourceKey);
3121
+ if (component.classification !== null && ![
3122
+ "taxable",
3123
+ "zero-rated",
3124
+ "exempt",
3125
+ "out-of-scope"
3126
+ ].includes(component.classification)) invalid$2();
3127
+ const classification = component.classification;
3128
+ const amountMinor = integer(component.amountMinor), taxableMinor = nullableInteger(component.taxableMinor);
3129
+ if (classification !== null && classification !== "taxable" && amountMinor !== 0) invalid$2();
3130
+ return {
3131
+ sourceKey,
3132
+ label: historicalText(component.label),
3133
+ amountMinor,
3134
+ classification,
3135
+ taxCodeId: nullableText(component.taxCodeId),
3136
+ taxComponentId: nullableText(component.taxComponentId),
3137
+ taxableMinor,
3138
+ netMinor: nullableInteger(component.netMinor),
3139
+ liabilityAccountId: nullableText(component.liabilityAccountId)
3140
+ };
3141
+ });
3142
+ })();
3143
+ if (taxComponents !== null && sum(taxComponents.map((component) => component.amountMinor)) !== taxMinor) invalid$2();
3144
+ const detailInput = fields(value.detail, record(value.detail).kind === "header-only" ? ["kind"] : ["kind", "lines"]);
3145
+ let detail;
3146
+ if (detailInput.kind === "header-only") detail = { kind: "header-only" };
3147
+ else if (detailInput.kind === "lines") {
3148
+ if (!Array.isArray(detailInput.lines) || !detailInput.lines.length || detailInput.lines.length > 1e5) invalid$2();
3149
+ const seen = /* @__PURE__ */ new Set();
3150
+ const lines = detailInput.lines.map((inputLine) => {
3151
+ const line = fields(inputLine, [
3152
+ "sourceKey",
3153
+ "creditedLineId",
3154
+ "description",
3155
+ "netMinor",
3156
+ "taxMinor",
3157
+ "grossMinor",
3158
+ "quantityText",
3159
+ "unitRateText",
3160
+ "sourceFields"
3161
+ ]), sourceKey = historicalText(line.sourceKey);
3162
+ if (seen.has(sourceKey)) invalid$2();
3163
+ seen.add(sourceKey);
3164
+ const sourceFields = record(line.sourceFields);
3165
+ if (Object.keys(sourceFields).length > 100 || Object.entries(sourceFields).some(([key, cell]) => key.length > 200 || typeof cell !== "string" || cell.length > 1e4)) invalid$2();
3166
+ const netMinor2 = integer(line.netMinor), taxMinor2 = nullableInteger(line.taxMinor), grossMinor2 = nullableInteger(line.grossMinor);
3167
+ if (taxMinor2 !== null && grossMinor2 !== null) sameAmount(netMinor2, taxMinor2, grossMinor2);
3168
+ if (typeof line.description !== "string" || line.description.length > 1e4) invalid$2();
3169
+ return {
3170
+ sourceKey,
3171
+ description: line.description,
3172
+ creditedLineId: nullableText(line.creditedLineId),
3173
+ netMinor: netMinor2,
3174
+ taxMinor: taxMinor2,
3175
+ grossMinor: grossMinor2,
3176
+ quantityText: nullableText(line.quantityText),
3177
+ unitRateText: nullableText(line.unitRateText),
3178
+ sourceFields: Object.fromEntries(Object.entries(sourceFields))
3179
+ };
3180
+ });
3181
+ if (sum(lines.map((line) => line.netMinor)) !== netMinor || lines.every((line) => line.taxMinor !== null) && sum(lines.map((line) => line.taxMinor)) !== taxMinor || lines.every((line) => line.grossMinor !== null) && sum(lines.map((line) => line.grossMinor)) !== grossMinor) invalid$2();
3182
+ detail = {
3183
+ kind: "lines",
3184
+ lines
3185
+ };
3186
+ } else invalid$2();
3187
+ const result = {
3188
+ currency,
3189
+ kind: value.kind,
3190
+ polarity: value.polarity,
3191
+ netMinor,
3192
+ taxMinor,
3193
+ grossMinor,
3194
+ taxComponents,
3195
+ detail
3196
+ };
3197
+ historicalSalesMagnitudes(result);
3198
+ return result;
3199
+ }
3200
+ //#endregion
3201
+ //#region src/historical-sales-money.ts
3202
+ function hash(value) {
3203
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
3204
+ }
3205
+ function identity$1(value) {
3206
+ const h = hash(value);
3207
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-5${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
3208
+ }
3209
+ function invalid$1(message) {
3210
+ throw new MoneyValidationError("INVALID_MONEY_INPUT", message);
3211
+ }
3212
+ function historicalSalesSourceHash(source) {
3213
+ return hash([source.namespace, source.key]);
3214
+ }
3215
+ function snapshotIdentity(invoice, snapshot) {
3216
+ return identity$1({
3217
+ tenantId: invoice.tenantId,
3218
+ invoiceId: invoice.id,
3219
+ customerId: invoice.customerId,
3220
+ invoiceNumber: invoice.invoiceNumber,
3221
+ snapshot
3222
+ });
3223
+ }
3224
+ function originalRetentionState(snapshot) {
3225
+ const r = snapshot.source.retention;
3226
+ if (!r) return null;
3227
+ return {
3228
+ version: 1,
3229
+ currency: snapshot.facts.currency,
3230
+ creditPolicy: r.creditPolicy,
3231
+ retainedMinor: r.retainedMinor,
3232
+ requestId: `historical-retention:${snapshot.requestId}`,
3233
+ policyVersion: r.policyRef,
3234
+ evidenceRef: r.evidenceRef,
3235
+ actorId: snapshot.review.actorId,
3236
+ reason: "Original source retention",
3237
+ recordedAt: snapshot.recordedAt,
3238
+ effectiveOn: r.effectiveOn,
3239
+ releases: r.releases.map((event) => ({
3240
+ requestId: `historical-release:${event.sourceKey}`,
3241
+ policyVersion: r.policyRef,
3242
+ evidenceRef: event.evidenceRef,
3243
+ actorId: snapshot.review.actorId,
3244
+ reason: "Original source release",
3245
+ recordedAt: snapshot.recordedAt,
3246
+ effectiveOn: event.effectiveOn,
3247
+ ...event.sameDayCreditOrder === void 0 ? {} : { sameDayCreditOrder: event.sameDayCreditOrder },
3248
+ amountMinor: event.amountMinor
3249
+ }))
3250
+ };
3251
+ }
3252
+ function getHistoricalSalesMoney(invoice) {
3253
+ if (invoice.moneyAuthority === "calculated" && !invoice.historicalSalesMoneyJson && invoice.historicalSourceKeyHash === null) return null;
3254
+ if (invoice.moneyAuthority !== "historical" || !invoice.historicalSalesMoneyJson) invalid$1("Historical invoice authority is incomplete");
3255
+ const value = JSON.parse(invoice.historicalSalesMoneyJson);
3256
+ if (value.version !== 1) invalid$1("Historical invoice snapshot version is unsupported");
3257
+ const snapshot = {
3258
+ version: 1,
3259
+ requestId: historicalText(value.requestId),
3260
+ evidenceRef: historicalText(value.evidenceRef),
3261
+ review: {
3262
+ actorId: historicalText(value.review?.actorId),
3263
+ policyRef: historicalText(value.review?.policyRef)
3264
+ },
3265
+ recordedAt: historicalText(value.recordedAt),
3266
+ source: validateHistoricalSalesSource(value.source),
3267
+ facts: validateHistoricalSalesMoneyFacts(value.facts),
3268
+ creditedSource: value.creditedSource === null ? null : {
3269
+ invoiceId: historicalText(value.creditedSource?.invoiceId),
3270
+ moneySnapshotId: historicalText(value.creditedSource?.moneySnapshotId)
3271
+ }
3272
+ };
3273
+ const amounts = historicalSalesMagnitudes(snapshot.facts);
3274
+ if (invoice.currency !== snapshot.facts.currency || invoice.kind !== snapshot.facts.kind || invoice.subtotal !== amounts.netMinor || invoice.taxAmount !== amounts.taxMinor || invoice.totalAmount !== amounts.grossMinor || invoice.discountTotal !== 0 || invoice.providerTaxAmount !== 0 || invoice.historicalSourceKeyHash !== historicalSalesSourceHash(snapshot.source) || invoice.moneySnapshotId !== snapshotIdentity(invoice, snapshot)) invalid$1("Historical invoice no longer matches its immutable original monetary authority");
3275
+ if (invoice.issueDate.toISOString().slice(0, 10) !== snapshot.source.issuedOn || (invoice.dueDate?.toISOString().slice(0, 10) ?? null) !== snapshot.source.dueOn) invalid$1("Historical source calendar dates changed");
3276
+ if (snapshot.facts.kind === "credit_note" ? invoice.creditedInvoiceId !== snapshot.creditedSource?.invoiceId : snapshot.creditedSource !== null) invalid$1("Historical credit source lineage changed");
3277
+ const expectedRetention = originalRetentionState(snapshot), retained = parseInvoiceRetention(invoice.retentionStateJson);
3278
+ if (expectedRetention) {
3279
+ if (!retained) invalid$1("Original retention authority is missing");
3280
+ for (const key of Object.keys(expectedRetention)) {
3281
+ if (key === "releases") continue;
3282
+ if (retained[key] !== expectedRetention[key]) invalid$1("Original retention source projection changed");
3283
+ }
3284
+ if (JSON.stringify(retained.releases.slice(0, expectedRetention.releases.length)) !== JSON.stringify(expectedRetention.releases)) invalid$1("Original retention release projection changed");
3285
+ } else if (retained) invalid$1("Historical invoice has no original retention authority");
3286
+ return structuredClone(snapshot);
3287
+ }
3288
+ function lineIdentity(invoiceId, sourceKey) {
3289
+ return identity$1({
3290
+ kind: "historical-sales-line:v1",
3291
+ invoiceId,
3292
+ sourceKey
3293
+ });
3294
+ }
3295
+ async function readHistoricalSalesLines(db, invoice) {
3296
+ const snapshot = getHistoricalSalesMoney(invoice);
3297
+ if (!snapshot) return [];
3298
+ const lines = await (await HistoricalSalesLineCollection.create({ db })).list({
3299
+ where: {
3300
+ tenantId: invoice.tenantId,
3301
+ invoiceId: invoice.id
3302
+ },
3303
+ limit: 100001
3304
+ });
3305
+ const expected = snapshot.facts.detail.kind === "lines" ? snapshot.facts.detail.lines : [];
3306
+ if (lines.length !== expected.length) invalid$1("Historical invoice source child identities are incomplete");
3307
+ const byKey = new Map(lines.map((line) => [line.sourceKey, line]));
3308
+ if (byKey.size !== lines.length) invalid$1("Historical source child identity is ambiguous");
3309
+ return expected.map((source) => {
3310
+ const line = byKey.get(source.sourceKey);
3311
+ if (!line || line.id !== lineIdentity(String(invoice.id), source.sourceKey) || line.sourceKeyHash !== hash(source.sourceKey) || line.moneySnapshotId !== invoice.moneySnapshotId || line.description !== source.description || line.creditedLineId !== source.creditedLineId || line.netMinor !== source.netMinor || line.taxMinor !== source.taxMinor || line.grossMinor !== source.grossMinor || line.quantityText !== source.quantityText || line.unitRateText !== source.unitRateText || line.sourceFieldsJson !== JSON.stringify(source.sourceFields)) invalid$1("Historical invoice source child no longer matches its original facts");
3312
+ return line;
3313
+ });
3314
+ }
3315
+ async function assertSourceLineCapacity(db, invoices, source, snapshot) {
3316
+ if (snapshot.facts.detail.kind !== "lines") return;
3317
+ const linked = snapshot.facts.detail.lines.filter((line) => line.creditedLineId !== null);
3318
+ if (!linked.length) return;
3319
+ if (!source || snapshot.facts.kind !== "credit_note") invalid$1("Only an exact historical credit may link original historical lines");
3320
+ const sourceLines = new Map((await readHistoricalSalesLines(db, source)).map((line) => [line.id, line]));
3321
+ const collection = await HistoricalSalesLineCollection.create({ db });
3322
+ const requestSign = snapshot.facts.polarity === "signed" ? -1 : 1;
3323
+ const pending = /* @__PURE__ */ new Map();
3324
+ for (const request of linked) {
3325
+ const original = sourceLines.get(request.creditedLineId ?? "");
3326
+ if (!original) invalid$1("Historical credit line does not belong to its exact original source invoice");
3327
+ const inRequest = pending.get(String(original.id)) ?? {
3328
+ netMinor: 0n,
3329
+ taxMinor: 0n,
3330
+ grossMinor: 0n
3331
+ };
3332
+ const previous = await collection.list({
3333
+ where: {
3334
+ tenantId: source.tenantId,
3335
+ creditedLineId: original.id
3336
+ },
3337
+ limit: 100001
3338
+ });
3339
+ if (previous.length > 1e5) invalid$1("Historical source line capacity exceeds the bounded credit scope");
3340
+ const consumed = {
3341
+ netMinor: 0n,
3342
+ taxMinor: 0n,
3343
+ grossMinor: 0n
3344
+ };
3345
+ for (const line of previous) {
3346
+ const parent = await invoices.get({
3347
+ id: line.invoiceId,
3348
+ tenantId: source.tenantId
3349
+ }, { cache: false });
3350
+ if (!parent || parent.kind !== "credit_note" || parent.creditedInvoiceId !== source.id) invalid$1("Historical source line has invalid credit lineage");
3351
+ if (parent.status === InvoiceStatus.DRAFT || parent.status === InvoiceStatus.CANCELLED) continue;
3352
+ const prior = getHistoricalSalesMoney(parent);
3353
+ if (!prior) invalid$1("Historical source line has no original monetary authority");
3354
+ const sign = prior.facts.polarity === "signed" ? -1 : 1;
3355
+ for (const field of [
3356
+ "netMinor",
3357
+ "taxMinor",
3358
+ "grossMinor"
3359
+ ]) if (line[field] !== null) consumed[field] += BigInt(line[field]) * BigInt(sign);
3360
+ }
3361
+ for (const field of [
3362
+ "netMinor",
3363
+ "taxMinor",
3364
+ "grossMinor"
3365
+ ]) {
3366
+ const originalAmount = original[field], requestedAmount = request[field];
3367
+ if (originalAmount === null || requestedAmount === null) continue;
3368
+ const magnitude = originalAmount < 0 ? -1n : 1n;
3369
+ const originalMagnitude = BigInt(originalAmount) * magnitude;
3370
+ const requestMagnitude = BigInt(requestedAmount) * BigInt(requestSign) * magnitude;
3371
+ if (requestMagnitude < 0n || (consumed[field] + inRequest[field]) * magnitude + requestMagnitude > originalMagnitude) invalid$1("Historical credit exceeds an exact original source line monetary capacity");
3372
+ }
3373
+ for (const field of [
3374
+ "netMinor",
3375
+ "taxMinor",
3376
+ "grossMinor"
3377
+ ]) if (request[field] !== null) inRequest[field] += BigInt(request[field]) * BigInt(requestSign);
3378
+ pending.set(String(original.id), inRequest);
3379
+ }
3380
+ }
3381
+ function stableRequest(snapshot) {
3382
+ return {
3383
+ source: snapshot.source,
3384
+ facts: snapshot.facts,
3385
+ evidenceRef: snapshot.evidenceRef,
3386
+ policyRef: snapshot.review.policyRef,
3387
+ creditedSource: snapshot.creditedSource
3388
+ };
3389
+ }
3390
+ async function recordHistoricalSalesMoney(input) {
3391
+ if (getTenantId() !== input.tenantId) invalid$1("Historical sales require the explicit active tenant");
3392
+ const facts = validateHistoricalSalesMoneyFacts(input.facts), source = validateHistoricalSalesSource(input.source);
3393
+ const snapshot = {
3394
+ version: 1,
3395
+ requestId: historicalText(input.requestId),
3396
+ evidenceRef: historicalText(input.evidenceRef),
3397
+ review: {
3398
+ actorId: historicalText(input.review?.actorId),
3399
+ policyRef: historicalText(input.review?.policyRef)
3400
+ },
3401
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
3402
+ source,
3403
+ facts,
3404
+ creditedSource: input.creditedSource ? {
3405
+ invoiceId: historicalText(input.creditedSource.invoiceId),
3406
+ moneySnapshotId: historicalText(input.creditedSource.moneySnapshotId)
3407
+ } : null
3408
+ };
3409
+ if (facts.kind === "credit_note" !== Boolean(snapshot.creditedSource)) invalid$1("Historical credits require the exact original issued invoice snapshot");
3410
+ return withTenantMoneyRuntime(input.db, input.tenantId, async ({ db }) => {
3411
+ const { invoice } = await lockAndReloadInvoice(db, historicalText(input.invoiceId), input.tenantId);
3412
+ if (invoice.tenantId !== input.tenantId) invalid$1("Historical invoice is not owned by the active tenant");
3413
+ const existing = getHistoricalSalesMoney(invoice);
3414
+ if (existing) {
3415
+ if (hash(stableRequest(existing)) !== hash(stableRequest(snapshot))) invalid$1("Historical invoice retry differs from its original facts or reviewed policy");
3416
+ return {
3417
+ snapshot: existing,
3418
+ invoiceId: String(invoice.id),
3419
+ moneySnapshotId: invoice.moneySnapshotId,
3420
+ lines: (await readHistoricalSalesLines(db, invoice)).map((line) => ({
3421
+ sourceKey: line.sourceKey,
3422
+ lineId: String(line.id)
3423
+ }))
3424
+ };
3425
+ }
3426
+ if (invoice.status !== InvoiceStatus.DRAFT || invoice.moneySnapshotId || invoice.arJournalId || invoice.amountPaid || invoice.creditedAmount || invoice.retentionStateJson || invoice.externalId || invoice.externalProvider || invoice.providerTaxAmount) invalid$1("Historical source money requires an empty unfrozen invoice without current financial effects");
3427
+ if (invoice.kind !== facts.kind || invoice.currency !== facts.currency) invalid$1("Historical source kind/currency must match its draft invoice");
3428
+ if (!(invoice.customerId ? await (await CustomerCollection.create({ db })).get({
3429
+ id: invoice.customerId,
3430
+ tenantId: input.tenantId
3431
+ }, { cache: false }) : null)) invalid$1("Historical sales require a customer owned by the active tenant");
3432
+ if ((await (await InvoiceLineItemCollection.create({ db })).findByInvoice(String(invoice.id))).length) invalid$1("Historical source money cannot mix with calculated invoice lines");
3433
+ const invoices = await InvoiceCollection.create({ db });
3434
+ const duplicate = (await invoices.list({
3435
+ where: {
3436
+ tenantId: input.tenantId,
3437
+ historicalSourceKeyHash: historicalSalesSourceHash(source)
3438
+ },
3439
+ limit: 1
3440
+ }))[0];
3441
+ if (duplicate && duplicate.id !== invoice.id) invalid$1("Historical source identity already belongs to another canonical invoice");
3442
+ for (const component of facts.taxComponents ?? []) {
3443
+ const code = component.taxCodeId ? await (await TaxCodeCollection.create({ db })).get({
3444
+ id: component.taxCodeId,
3445
+ tenantId: input.tenantId
3446
+ }, { cache: false }) : null;
3447
+ if (component.taxCodeId && (!code || component.classification !== null && code.kind !== component.classification)) invalid$1("Historical source tax code is not owned by its tenant with the reviewed classification");
3448
+ if (component.taxComponentId) {
3449
+ const tax = await (await TaxComponentCollection.create({ db })).get({
3450
+ id: component.taxComponentId,
3451
+ tenantId: input.tenantId
3452
+ }, { cache: false });
3453
+ if (!tax || code && !code.components.some((part) => part.taxComponentId === tax.id)) invalid$1("Historical source tax component is not owned by its declared tenant/code");
3454
+ }
3455
+ if (component.liabilityAccountId) {
3456
+ const { AccountCollection } = await import("@happyvertical/smrt-ledgers");
3457
+ const account = await (await AccountCollection.create({ db })).get({
3458
+ id: component.liabilityAccountId,
3459
+ tenantId: input.tenantId
3460
+ }, { cache: false });
3461
+ if (!account || account.type !== "liability") invalid$1("Historical source tax liability account is not owned by its tenant with the required type");
3462
+ }
3463
+ }
3464
+ let credited = null;
3465
+ const amounts = historicalSalesMagnitudes(facts);
3466
+ if (snapshot.creditedSource) {
3467
+ credited = (await lockAndReloadInvoice(db, snapshot.creditedSource.invoiceId, input.tenantId)).invoice;
3468
+ if (credited.kind !== "invoice" || credited.tenantId !== input.tenantId || credited.customerId !== invoice.customerId || credited.currency !== invoice.currency || !credited.moneySnapshotId || credited.moneySnapshotId !== snapshot.creditedSource.moneySnapshotId || credited.status === InvoiceStatus.DRAFT || credited.status === InvoiceStatus.CANCELLED || invoice.creditedInvoiceId !== credited.id) invalid$1("Historical credit original invoice/customer/currency/snapshot does not match");
3469
+ const prior = (await invoices.list({
3470
+ where: {
3471
+ tenantId: input.tenantId,
3472
+ creditedInvoiceId: credited.id
3473
+ },
3474
+ limit: 100001
3475
+ }).then((rows) => {
3476
+ if (rows.length > 1e5) invalid$1("Historical credit scope exceeds its bounded source limit");
3477
+ return rows;
3478
+ })).filter((row) => row.id !== invoice.id && row.status !== InvoiceStatus.DRAFT && row.status !== InvoiceStatus.CANCELLED);
3479
+ if (prior.length > 1e5) invalid$1("Historical credit scope exceeds its bounded source limit");
3480
+ const retention = parseInvoiceRetention(credited.retentionStateJson);
3481
+ if (retention) {
3482
+ const credits = [...prior.map((row) => ({
3483
+ on: getHistoricalSalesMoney(row)?.source.issuedOn ?? row.issueDate.toISOString().slice(0, 10),
3484
+ amount: row.totalAmount
3485
+ })), {
3486
+ on: source.issuedOn,
3487
+ amount: amounts.grossMinor
3488
+ }];
3489
+ let remaining = retention.retainedMinor;
3490
+ const events = [...credits.map((event) => ({
3491
+ ...event,
3492
+ kind: "credit",
3493
+ rank: 1
3494
+ })), ...retention.releases.map((release) => {
3495
+ const on = release.effectiveOn ?? release.recordedAt.slice(0, 10);
3496
+ if (credits.some((event) => event.on === on) && !release.sameDayCreditOrder) invalid$1("Same-day original retention release/credit requires explicit reviewed ordering");
3497
+ return {
3498
+ on,
3499
+ amount: release.amountMinor,
3500
+ kind: "release",
3501
+ rank: release.sameDayCreditOrder === "release-before-credit" ? 0 : 2
3502
+ };
3503
+ })].sort((left, right) => left.on.localeCompare(right.on) || left.rank - right.rank);
3504
+ for (const event of events) if (event.kind === "credit") remaining = Math.max(0, remaining - event.amount);
3505
+ else {
3506
+ if (event.amount > remaining) invalid$1("Original retention release exceeds retained-first capacity at its source date");
3507
+ remaining -= event.amount;
3508
+ }
3509
+ }
3510
+ for (const [field, value] of [
3511
+ ["subtotal", amounts.netMinor],
3512
+ ["taxAmount", amounts.taxMinor],
3513
+ ["totalAmount", amounts.grossMinor]
3514
+ ]) if (prior.reduce((sum, row) => sum + BigInt(row[field]), 0n) + BigInt(value) > BigInt(credited[field])) invalid$1("Historical credit exceeds the remaining exact original net/tax/gross capacity");
3515
+ }
3516
+ await assertSourceLineCapacity(db, invoices, credited, snapshot);
3517
+ if (source.retention && (facts.kind !== "invoice" || source.retention.retainedMinor > amounts.grossMinor)) invalid$1("Historical retention must match original invoice gross");
3518
+ const originalRetention = originalRetentionState(snapshot);
3519
+ invoice.retentionStateJson = originalRetention ? JSON.stringify(originalRetention) : "";
3520
+ invoice.moneyAuthority = "historical";
3521
+ invoice.historicalSalesMoneyJson = JSON.stringify(snapshot);
3522
+ invoice.historicalSourceKeyHash = historicalSalesSourceHash(source);
3523
+ invoice.subtotal = amounts.netMinor;
3524
+ invoice.discountTotal = 0;
3525
+ invoice.taxAmount = amounts.taxMinor;
3526
+ invoice.providerTaxAmount = 0;
3527
+ invoice.totalAmount = amounts.grossMinor;
3528
+ invoice.issueDate = /* @__PURE__ */ new Date(`${source.issuedOn}T00:00:00Z`);
3529
+ invoice.dueDate = source.dueOn === null ? null : /* @__PURE__ */ new Date(`${source.dueOn}T00:00:00Z`);
3530
+ invoice.moneySnapshotId = snapshotIdentity(invoice, snapshot);
3531
+ invoice.status = InvoiceStatus.SENT;
3532
+ await assertContractScheduleInvoiceFreeze(db, invoice);
3533
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
3534
+ if (facts.detail.kind === "lines") for (const line of facts.detail.lines) {
3535
+ const child = await new HistoricalSalesLine({
3536
+ db,
3537
+ id: lineIdentity(String(invoice.id), line.sourceKey),
3538
+ tenantId: input.tenantId,
3539
+ invoiceId: String(invoice.id),
3540
+ ...line,
3541
+ sourceKeyHash: hash(line.sourceKey),
3542
+ moneySnapshotId: invoice.moneySnapshotId
3543
+ }).initialize();
3544
+ await withHistoricalCommerceWrite(child, () => child.save());
3545
+ }
3546
+ if (credited) await refreshIssuedCreditBalance(db, credited);
3547
+ return {
3548
+ snapshot: structuredClone(snapshot),
3549
+ invoiceId: String(invoice.id),
3550
+ moneySnapshotId: invoice.moneySnapshotId,
3551
+ lines: (await readHistoricalSalesLines(db, invoice)).map((line) => ({
3552
+ sourceKey: line.sourceKey,
3553
+ lineId: String(line.id)
3554
+ }))
3555
+ };
3556
+ });
3557
+ }
3558
+ //#endregion
3559
+ //#region src/historical-sales-reports.ts
3560
+ var HistoricalSalesTaxClassificationRequired = class extends Error {
3561
+ constructor(invoiceId) {
3562
+ super(`Historical invoice ${invoiceId} retains exact header totals, but this tax breakdown requires explicit reviewed source classification, tax identity and taxable basis. Read the historical header facts or resolve the source assessment; never infer a rate or line allocation.`);
3563
+ this.invoiceId = invoiceId;
3564
+ this.name = "HistoricalSalesTaxClassificationRequired";
3565
+ }
3566
+ invoiceId;
3567
+ code = "HISTORICAL_SALES_TAX_CLASSIFICATION_REQUIRED";
3568
+ };
3569
+ function classified(invoice) {
3570
+ const snapshot = getHistoricalSalesMoney(invoice);
3571
+ if (!snapshot) return false;
3572
+ const components = snapshot.facts.taxComponents;
3573
+ return Boolean(components?.length && components.every((component) => component.classification !== null && component.taxCodeId && component.taxableMinor !== null && component.netMinor !== null && (component.classification !== "taxable" || component.taxComponentId)));
3574
+ }
3575
+ function historicalSalesTaxReportFacts(invoice) {
3576
+ const snapshot = getHistoricalSalesMoney(invoice);
3577
+ if (!snapshot) return [];
3578
+ if (snapshot.facts.netMinor === 0 && snapshot.facts.taxMinor === 0) return [];
3579
+ if (!classified(invoice)) throw new HistoricalSalesTaxClassificationRequired(String(invoice.id));
3580
+ const sign = snapshot.facts.kind === "credit_note" && snapshot.facts.polarity === "signed" ? -1 : 1;
3581
+ return (snapshot.facts.taxComponents ?? []).map((component) => {
3582
+ if (component.classification === null || !component.taxCodeId || component.taxableMinor === null) throw new HistoricalSalesTaxClassificationRequired(String(invoice.id));
3583
+ return {
3584
+ tenantId: String(invoice.tenantId),
3585
+ documentType: invoice.kind,
3586
+ documentId: String(invoice.id),
3587
+ documentNumber: invoice.invoiceNumber,
3588
+ partyId: invoice.customerId,
3589
+ documentKind: invoice.kind,
3590
+ issueDate: invoice.issueDate.toISOString(),
3591
+ status: invoice.status,
3592
+ direction: "sales",
3593
+ currency: invoice.currency,
3594
+ taxCodeId: component.taxCodeId,
3595
+ taxCodeKind: component.classification,
3596
+ taxableMinor: component.taxableMinor * sign,
3597
+ ...component.netMinor === null ? {} : { netMinor: component.netMinor * sign },
3598
+ taxMinor: component.amountMinor * sign,
3599
+ recoverableMinor: 0,
3600
+ taxComponentId: component.taxComponentId,
3601
+ componentName: component.label,
3602
+ liabilityAccountId: component.liabilityAccountId,
3603
+ commitmentId: null,
3604
+ commitmentLineId: null
3605
+ };
3606
+ });
3607
+ }
3608
+ async function historicalSalesReportInvoices(db, options) {
3609
+ const rows = await (await InvoiceCollection.create({ db })).list({
3610
+ where: {
3611
+ tenantId: options.tenantId,
3612
+ moneyAuthority: "historical",
3613
+ "issueDate >=": options.from.toISOString(),
3614
+ "issueDate <=": options.to.toISOString(),
3615
+ "status not in": ["draft", "cancelled"],
3616
+ ...options.currency ? { currency: options.currency } : {}
3617
+ },
3618
+ limit: 100001
3619
+ });
3620
+ if (rows.length > 1e5) throw new Error("Historical sales report scope exceeds the bounded document limit; select a smaller scope");
3621
+ return rows;
3622
+ }
3623
+ async function listHistoricalSalesHeaderFacts(input) {
3624
+ if (getTenantId() !== input.tenantId) throw new Error("Historical sales reporting requires the explicit active tenant");
3625
+ if (!(input.from instanceof Date) || !(input.to instanceof Date) || !Number.isFinite(input.from.getTime()) || !Number.isFinite(input.to.getTime()) || input.from > input.to) throw new Error("Historical sales report requires an explicit valid period");
3626
+ return withTenantMoneyRuntime(input.db, input.tenantId, async ({ db }) => (await historicalSalesReportInvoices(db, input)).map((invoice) => {
3627
+ const snapshot = getHistoricalSalesMoney(invoice);
3628
+ if (!snapshot) throw new Error("Historical source monetary authority is unavailable");
3629
+ return {
3630
+ invoiceId: String(invoice.id),
3631
+ moneySnapshotId: invoice.moneySnapshotId,
3632
+ customerId: invoice.customerId,
3633
+ invoiceNumber: invoice.invoiceNumber,
3634
+ kind: invoice.kind,
3635
+ issueDate: invoice.issueDate.toISOString(),
3636
+ currency: invoice.currency,
3637
+ ...historicalSalesMagnitudes(snapshot.facts),
3638
+ taxBreakdownKnown: classified(invoice)
3639
+ };
3640
+ }));
3641
+ }
3642
+ //#endregion
3643
+ //#region src/models/DocumentTax.ts
3644
+ var __defProp = Object.defineProperty;
3645
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3646
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
3647
+ enumerable: true,
3648
+ configurable: true,
3649
+ writable: true,
3650
+ value
3651
+ }) : obj[key] = value;
3652
+ var __decorateClass = (decorators, target, key, kind) => {
3653
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
3654
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
3655
+ if (kind && result) __defProp(target, key, result);
3656
+ return result;
3657
+ };
3658
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
3659
+ var DocumentTax = class extends SmrtObject {
3660
+ tenantId = "";
3661
+ documentType = "invoice";
3662
+ documentId = "";
3663
+ currency = "USD";
3664
+ taxCodeId = null;
3665
+ taxCodeCode = null;
3666
+ kind = null;
3667
+ taxComponentId = null;
3668
+ taxComponentName = null;
3669
+ percent = null;
3670
+ liabilityAccountId = null;
3671
+ taxableMinor = 0;
3672
+ netMinor = null;
3673
+ taxMinor = 0;
3674
+ recoverableMinor = 0;
3675
+ constructor(options = {}) {
3676
+ super(options);
3677
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
3678
+ if (options.documentType !== void 0) this.documentType = options.documentType;
3679
+ if (options.documentId !== void 0) this.documentId = options.documentId;
3680
+ if (options.currency !== void 0) this.currency = options.currency;
3681
+ if (options.taxCodeId !== void 0) this.taxCodeId = options.taxCodeId;
3682
+ if (options.taxCodeCode !== void 0) this.taxCodeCode = options.taxCodeCode;
3683
+ if (options.kind !== void 0) this.kind = options.kind;
3684
+ if (options.taxComponentId !== void 0) this.taxComponentId = options.taxComponentId;
3685
+ if (options.taxComponentName !== void 0) this.taxComponentName = options.taxComponentName;
3686
+ if (options.percent !== void 0) this.percent = options.percent;
3687
+ if (options.liabilityAccountId !== void 0) this.liabilityAccountId = options.liabilityAccountId;
3688
+ if (options.taxableMinor !== void 0) this.taxableMinor = options.taxableMinor;
3689
+ if (options.netMinor !== void 0) this.netMinor = options.netMinor;
3690
+ if (options.taxMinor !== void 0) this.taxMinor = options.taxMinor;
3691
+ if (options.recoverableMinor !== void 0) this.recoverableMinor = options.recoverableMinor;
3692
+ }
3693
+ assertValidDerivedRow() {
3694
+ if (![
3695
+ "invoice",
3696
+ "contract",
3697
+ "credit_note"
3698
+ ].includes(this.documentType)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown document tax type: ${this.documentType}`);
3699
+ for (const [label, value] of [
3700
+ ["tenantId", this.tenantId],
3701
+ ["documentId", this.documentId],
3702
+ ["currency", this.currency]
3703
+ ]) if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `DocumentTax ${label} must be a nonempty value`);
3704
+ for (const [label, value] of [
3705
+ ["taxableMinor", this.taxableMinor],
3706
+ ["netMinor", this.netMinor ?? this.taxableMinor],
3707
+ ["taxMinor", this.taxMinor],
3708
+ ["recoverableMinor", this.recoverableMinor]
3709
+ ]) if (!Number.isSafeInteger(value)) throw new MoneyValidationError("UNSAFE_MONEY", `DocumentTax ${label} must be safe integer minor units`);
3710
+ if (this.taxCodeId === null) {
3711
+ if (this.taxComponentId !== null || this.taxCodeCode !== null || this.kind !== null || this.taxComponentName !== null || this.percent !== null || this.liabilityAccountId !== null || this.taxMinor !== 0 || this.recoverableMinor !== 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", "An unassigned DocumentTax row cannot carry code/component snapshots or tax");
3712
+ return;
3713
+ }
3714
+ if (!this.taxCodeCode || !this.kind) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Assigned DocumentTax rows require code and kind snapshots");
3715
+ if (this.taxComponentId === null) {
3716
+ if (this.kind === "taxable" || this.taxComponentName !== null || this.percent !== null || this.liabilityAccountId !== null || this.taxMinor !== 0 || this.recoverableMinor !== 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Component-less DocumentTax rows must be non-taxable zero-tax snapshots");
3717
+ return;
3718
+ }
3719
+ if (this.kind !== "taxable" || !this.taxComponentName || !this.percent) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Component DocumentTax rows require taxable component snapshots");
3720
+ if (this.recoverableMinor !== 0 && this.recoverableMinor !== this.taxMinor) throw new MoneyValidationError("INVALID_MONEY_INPUT", "DocumentTax recoverableMinor must be zero or the component tax total");
3721
+ }
3722
+ async save() {
3723
+ throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax rows are derived and cannot be saved directly");
3724
+ }
3725
+ async delete(_options = {}) {
3726
+ throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax rows are derived and cannot be deleted directly");
3727
+ }
3728
+ /** @internal Aggregate persistence only. */
3729
+ async saveDerived(capability) {
3730
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
3731
+ this.assertValidDerivedRow();
3732
+ return await super.save();
3733
+ }
3734
+ /** @internal Aggregate persistence only. */
3735
+ async deleteDerived(capability, options = {}) {
3736
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
3737
+ await super.delete(options);
3738
+ }
3739
+ };
3740
+ __publicField(DocumentTax, "__smrtPackage__", "@happyvertical/smrt-commerce");
3741
+ __decorateClass([tenantId()], DocumentTax.prototype, "tenantId", 2);
3742
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "documentType", 2);
3743
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "documentId", 2);
3744
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "currency", 2);
3745
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxCodeId", 2);
3746
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxCodeCode", 2);
3747
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "kind", 2);
3748
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxComponentId", 2);
3749
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxComponentName", 2);
3750
+ __decorateClass([field({
3751
+ type: "decimal",
3752
+ nullable: true
3753
+ })], DocumentTax.prototype, "percent", 2);
3754
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "liabilityAccountId", 2);
3755
+ __decorateClass([field({
3756
+ description: "Actual component taxable basis, including prior compound taxes.",
3757
+ ui: { format: "money" }
3758
+ })], DocumentTax.prototype, "taxableMinor", 2);
3759
+ __decorateClass([field({
3760
+ type: "integer",
3761
+ nullable: true,
3762
+ description: "Net basis when different from the component taxable basis.",
3763
+ ui: { format: "money" }
3764
+ })], DocumentTax.prototype, "netMinor", 2);
3765
+ __decorateClass([field({
3766
+ description: "Tax total in integer minor units.",
3767
+ ui: { format: "money" }
3768
+ })], DocumentTax.prototype, "taxMinor", 2);
3769
+ __decorateClass([field({
3770
+ description: "Recoverable purchase tax in integer minor units.",
3771
+ ui: { format: "money" }
3772
+ })], DocumentTax.prototype, "recoverableMinor", 2);
3773
+ DocumentTax = __decorateClass([TenantScoped({ mode: "required" }), smrt({
3774
+ tableName: "document_taxes",
3775
+ conflictColumns: [
3776
+ "document_type",
3777
+ "document_id",
3778
+ "tax_code_id",
3779
+ "tax_component_id"
3780
+ ],
3781
+ api: { include: ["list", "get"] },
3782
+ mcp: { include: ["list", "get"] },
3783
+ cli: { include: ["list", "get"] }
3784
+ })], DocumentTax);
3785
+ //#endregion
3786
+ //#region src/collections/VendorCollection.ts
3787
+ var VendorCollection = class extends SmrtCollection {
3788
+ static _itemClass = Vendor;
3789
+ /**
3790
+ * Find vendors by profile ID
3791
+ *
3792
+ * @param profileId - Profile ID from smrt-profiles
3793
+ * @returns Array of vendors linked to this profile
3794
+ */
3795
+ async findByProfile(profileId) {
3796
+ return await this.list({
3797
+ where: { profileId },
3798
+ orderBy: "created_at DESC"
3799
+ });
3800
+ }
3801
+ /**
3802
+ * Find all active vendors
3803
+ *
3804
+ * @returns Array of active vendors
3805
+ */
3806
+ async findActive() {
3807
+ return await this.list({
3808
+ where: { status: VendorStatus.ACTIVE },
3809
+ orderBy: "created_at DESC"
3810
+ });
3811
+ }
3812
+ /**
3813
+ * Find vendors by status
3814
+ *
3815
+ * @param status - Vendor status
3816
+ * @returns Array of vendors
3817
+ */
3818
+ async findByStatus(status) {
3819
+ return await this.list({
3820
+ where: { status },
3821
+ orderBy: "created_at DESC"
3822
+ });
3823
+ }
3824
+ /**
3825
+ * Get or create a vendor for a profile
3826
+ *
3827
+ * @param profileId - Profile ID
3828
+ * @param defaults - Default values if creating
3829
+ * @returns Vendor
3830
+ */
3831
+ async getOrCreateForProfile(profileId, defaults = {}) {
3832
+ const existing = await this.findByProfile(profileId);
3833
+ if (existing.length > 0) return existing[0];
3834
+ const vendor = await this.create({
3835
+ profileId,
3836
+ leadTimeDays: defaults.leadTimeDays ?? 0,
3837
+ minimumOrderAmount: defaults.minimumOrderAmount ?? 0,
3838
+ paymentTerms: defaults.paymentTerms ?? "",
3839
+ status: VendorStatus.ACTIVE
3840
+ });
3841
+ await vendor.save();
3842
+ return vendor;
3843
+ }
3844
+ /**
3845
+ * Find all vendors belonging to a specific tenant
3846
+ *
3847
+ * @param tenantId - Tenant ID
3848
+ * @returns Array of vendors for the tenant
3849
+ */
3850
+ async findByTenant(tenantId) {
3851
+ return this.list({ where: { tenantId } });
3852
+ }
3853
+ /**
3854
+ * Find all global vendors (not associated with any tenant).
3855
+ *
3856
+ * Routes through the shared tenant-global helper so it does not throw under
3857
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
3858
+ * flagged as an isolation violation). (#1600)
3859
+ *
3860
+ * @returns Array of global vendors
3861
+ */
3862
+ async findGlobal() {
3863
+ return queryGlobal(this);
3864
+ }
3865
+ /**
3866
+ * Find vendors for a tenant including global vendors.
3867
+ *
3868
+ * Fails closed if an active tenant context requests a different tenant's
3869
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
3870
+ *
3871
+ * @param tenantId - Tenant ID
3872
+ * @returns Array of tenant-specific and global vendors
3873
+ */
3874
+ async findWithGlobals(tenantId) {
3875
+ return queryWithGlobals(this, tenantId, "Vendor.findWithGlobals");
3876
+ }
3877
+ };
3878
+ //#endregion
3879
+ //#region src/received-purchase-money.ts
3880
+ async function recordReceivedSalesContractMoney(input) {
3881
+ if (Object.hasOwn(input, "sourceQuote")) invalid("Sales agreement evidence cannot reference a supplier quote");
3882
+ if (input.facts.taxComponents?.some((component) => component.recoverableMinor !== null)) invalid("Purchase tax recovery is not applicable to sales agreement evidence; retain null");
3883
+ return recordReceivedContractMoney(input, "sales");
3884
+ }
3885
+ function getReceivedSalesContractMoney(contract) {
3886
+ if (!(contract instanceof Order || contract instanceof Agreement)) return null;
3887
+ return getReceivedPurchaseMoney(contract);
3888
+ }
3889
+ function invalid(message) {
3890
+ throw new MoneyValidationError("INVALID_MONEY_INPUT", message);
3891
+ }
3892
+ function object(value, label) {
3893
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(`${label} must be an object`);
3894
+ return value;
3895
+ }
3896
+ function text(value, label) {
3897
+ if (typeof value !== "string" || !value || value.trim() !== value) invalid(`${label} must be nonempty trimmed text`);
3898
+ return value;
3899
+ }
3900
+ function minor(value, label) {
3901
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) invalid(`${label} must be a nonnegative safe integer`);
3902
+ return value;
3903
+ }
3904
+ function amounts(value, label) {
3905
+ const netMinor = minor(value.netMinor, `${label} net`);
3906
+ const taxMinor = minor(value.taxMinor, `${label} tax`);
3907
+ const grossMinor = minor(value.grossMinor, `${label} gross`);
3908
+ if (BigInt(netMinor) + BigInt(taxMinor) !== BigInt(grossMinor)) invalid(`${label} net plus tax must equal gross`);
3909
+ return {
3910
+ netMinor,
3911
+ taxMinor,
3912
+ grossMinor
3913
+ };
3914
+ }
3915
+ function unique(keys, label) {
3916
+ if (new Set(keys).size !== keys.length) invalid(`${label} source keys must be unique`);
3917
+ }
3918
+ function validateReceivedPurchaseMoneyFacts(value) {
3919
+ const input = object(value, "Received purchase facts");
3920
+ const currency = text(input.currency, "Currency");
3921
+ if (!/^[A-Z]{3}$/.test(currency) || !ISO_4217_MINOR_UNITS.has(currency) || ISO_4217_MINOR_UNITS.get(currency) === null) invalid("Currency must be a supported uppercase ISO 4217 code");
3922
+ const totals = amounts(input, "Document");
3923
+ let taxComponents = null;
3924
+ if (input.taxComponents !== null) {
3925
+ if (!Array.isArray(input.taxComponents)) invalid("Tax components must be an array or explicit null");
3926
+ taxComponents = input.taxComponents.map((raw) => {
3927
+ const component = object(raw, "Tax component");
3928
+ const amountMinor = minor(component.amountMinor, "Component amount");
3929
+ const recoverableMinor = component.recoverableMinor === null ? null : minor(component.recoverableMinor, "Recoverable amount");
3930
+ if (recoverableMinor !== null && recoverableMinor > amountMinor) invalid("Recoverable tax cannot exceed component tax");
3931
+ const classification = component.classification;
3932
+ if (classification !== null && ![
3933
+ "taxable",
3934
+ "zero-rated",
3935
+ "exempt",
3936
+ "out-of-scope"
3937
+ ].includes(String(classification))) invalid("Unknown tax classification");
3938
+ const taxableMinor = component.taxableMinor == null ? null : minor(component.taxableMinor, "Taxable basis");
3939
+ const netMinor = component.netMinor == null ? null : minor(component.netMinor, "Component net basis");
3940
+ if (netMinor !== null && netMinor > totals.netMinor) invalid("Component net basis exceeds document net");
3941
+ if (classification !== null && classification !== "taxable" && amountMinor !== 0) invalid("A non-taxable classification cannot contain tax");
3942
+ return {
3943
+ sourceKey: text(component.sourceKey, "Component source key"),
3944
+ label: text(component.label, "Component label"),
3945
+ amountMinor,
3946
+ classification,
3947
+ taxCodeId: component.taxCodeId == null ? null : text(component.taxCodeId, "Tax code identity"),
3948
+ taxComponentId: component.taxComponentId == null ? null : text(component.taxComponentId, "Tax component identity"),
3949
+ taxableMinor,
3950
+ ...netMinor !== null ? { netMinor } : {},
3951
+ recoverableMinor
3952
+ };
3953
+ });
3954
+ unique(taxComponents.map((item) => item.sourceKey), "Tax component");
3955
+ if (taxComponents.reduce((sum, item) => sum + BigInt(item.amountMinor), 0n) !== BigInt(totals.taxMinor)) invalid("Component tax sum must equal document tax");
3956
+ }
3957
+ const rawDetail = object(input.detail, "Source detail");
3958
+ let detail;
3959
+ if (rawDetail.kind === "header-only") {
3960
+ if ("lines" in rawDetail) invalid("Header-only evidence cannot contain monetary lines");
3961
+ detail = { kind: "header-only" };
3962
+ } else if (rawDetail.kind === "lines") {
3963
+ if (!Array.isArray(rawDetail.lines) || !rawDetail.lines.length) invalid("Complete source lines are required");
3964
+ const lines = rawDetail.lines.map((raw) => {
3965
+ const line = object(raw, "Source line");
3966
+ const result = {
3967
+ sourceKey: text(line.sourceKey, "Line source key"),
3968
+ description: text(line.description, "Line description"),
3969
+ ...amounts(line, "Source line")
3970
+ };
3971
+ for (const key of ["quantityText", "unitRateText"]) {
3972
+ if (line[key] === void 0) continue;
3973
+ const decimal = text(line[key], key);
3974
+ if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(decimal)) invalid(`${key} must be nonnegative decimal source text`);
3975
+ result[key] = decimal;
3976
+ }
3977
+ return result;
3978
+ });
3979
+ unique(lines.map((item) => item.sourceKey), "Line");
3980
+ for (const key of [
3981
+ "netMinor",
3982
+ "taxMinor",
3983
+ "grossMinor"
3984
+ ]) if (lines.reduce((sum, line) => sum + BigInt(line[key]), 0n) !== BigInt(totals[key])) invalid(`Source line ${key} sum must equal document amount`);
3985
+ detail = {
3986
+ kind: "lines",
3987
+ lines
3988
+ };
3989
+ } else invalid("Unknown received source detail kind");
3990
+ return {
3991
+ currency,
3992
+ ...totals,
3993
+ taxComponents,
3994
+ detail
3995
+ };
3996
+ }
3997
+ function quoteSource(value) {
3998
+ if (value == null) return null;
3999
+ const input = object(value, "Source quote");
4000
+ if (input.relationship !== "copy" && input.relationship !== "negotiated") invalid("Unknown source quote relationship");
4001
+ return {
4002
+ contractId: text(input.contractId, "Source quote identity"),
4003
+ relationship: input.relationship
4004
+ };
4005
+ }
4006
+ function getReceivedPurchaseMoney(contract) {
4007
+ if (contract.moneyAuthority === "calculated") {
4008
+ if (contract.receivedPurchaseMoneyJson) invalid("Calculated money cannot contain received evidence");
4009
+ return null;
4010
+ }
4011
+ if (contract.moneyAuthority !== "received") invalid("Unknown contract money authority");
4012
+ let parsed;
4013
+ try {
4014
+ parsed = JSON.parse(contract.receivedPurchaseMoneyJson);
4015
+ } catch {
4016
+ invalid("Stored received money is invalid");
4017
+ }
4018
+ const input = object(parsed, "Received snapshot");
4019
+ if (input.version !== 1) invalid("Unknown received money version");
4020
+ return {
4021
+ version: 1,
4022
+ requestId: text(input.requestId, "Request identity"),
4023
+ evidenceRef: text(input.evidenceRef, "Evidence reference"),
4024
+ facts: validateReceivedPurchaseMoneyFacts(input.facts),
4025
+ sourceQuote: quoteSource(input.sourceQuote)
4026
+ };
4027
+ }
4028
+ async function recordReceivedPurchaseMoney(input) {
4029
+ return recordReceivedContractMoney(input, "purchase");
4030
+ }
4031
+ async function recordReceivedContractMoney(input, direction) {
4032
+ const tenantId = text(input.tenantId, "Tenant");
4033
+ if (getTenantId() !== tenantId) invalid("Received contract money requires its active tenant context");
4034
+ const contractId = text(input.contractId, "Contract");
4035
+ const snapshot = {
4036
+ version: 1,
4037
+ requestId: text(input.requestId, "Request identity"),
4038
+ evidenceRef: text(input.evidenceRef, "Evidence reference"),
4039
+ facts: validateReceivedPurchaseMoneyFacts(input.facts),
4040
+ sourceQuote: quoteSource(input.sourceQuote)
4041
+ };
4042
+ const serialized = JSON.stringify(snapshot);
4043
+ return withTenantMoneyRuntime(input.db, tenantId, async ({ db }) => {
4044
+ const contracts = await ContractCollection.create({ db });
4045
+ await lockDocumentMoneyParents([{
4046
+ id: contractId,
4047
+ tenantId,
4048
+ collection: contracts
4049
+ }, ...snapshot.sourceQuote ? [{
4050
+ id: snapshot.sourceQuote.contractId,
4051
+ tenantId,
4052
+ collection: contracts
4053
+ }] : []]);
4054
+ const contract = await contracts.get({ id: contractId }, { cache: false });
4055
+ const validParty = direction === "purchase" ? (contract instanceof SupplierQuote || contract instanceof PurchaseOrder) && contract.vendorId : (contract instanceof Order || contract instanceof Agreement) && contract.customerId;
4056
+ if (!contract || contract.tenantId !== tenantId || !validParty) invalid(direction === "purchase" ? "Received money requires a supplier quote or purchase order in the active tenant" : "Received money requires an Order or Agreement with a customer in the active tenant");
4057
+ const stored = getReceivedPurchaseMoney(contract);
4058
+ if (stored) {
4059
+ if (JSON.stringify(stored) !== serialized) invalid("Received money request changed; create a separate contract revision");
4060
+ return stored;
4061
+ }
4062
+ if (direction === "purchase" && (contract instanceof SupplierQuote || contract instanceof PurchaseOrder)) {
4063
+ const vendor = await (await VendorCollection.create({ db })).get({ id: contract.vendorId }, { cache: false });
4064
+ if (!vendor || vendor.tenantId !== tenantId) invalid("Supplier must belong to the active tenant");
4065
+ } else if (contract instanceof Order || contract instanceof Agreement) {
4066
+ const customer = await (await CustomerCollection.create({ db })).get({ id: contract.customerId }, { cache: false });
4067
+ if (!customer || customer.tenantId !== tenantId) invalid("Customer must belong to the active tenant");
4068
+ }
4069
+ if (snapshot.sourceQuote) {
4070
+ if (!(contract instanceof PurchaseOrder)) invalid("Only a purchase order can reference a source quote");
4071
+ const source = await contracts.get({ id: snapshot.sourceQuote.contractId }, { cache: false });
4072
+ if (!(source instanceof SupplierQuote) || source.tenantId !== tenantId || source.vendorId !== contract.vendorId || source.currency !== contract.currency || source.status !== ContractStatus.ACCEPTED || !source.moneySnapshotId) invalid("Source quote must be accepted with matching tenant, vendor and currency");
4073
+ const sourceMoney = getReceivedPurchaseMoney(source);
4074
+ if (!sourceMoney) invalid("Source quote must have received monetary evidence");
4075
+ if (snapshot.sourceQuote.relationship === "copy" && JSON.stringify(sourceMoney.facts) !== JSON.stringify(snapshot.facts)) invalid("Copied quote monetary facts must match exactly");
4076
+ if (snapshot.sourceQuote.relationship === "negotiated" && snapshot.evidenceRef === sourceMoney.evidenceRef) invalid("Negotiated award requires separate approved monetary evidence");
4077
+ }
4078
+ for (const component of snapshot.facts.taxComponents ?? []) {
4079
+ if (component.taxCodeId) {
4080
+ const code = await (await TaxCodeCollection.create({ db })).get({ id: component.taxCodeId }, { cache: false });
4081
+ if (!code || code.tenantId !== tenantId) invalid("Tax report code must belong to the active tenant");
4082
+ if (component.classification !== null && code.kind !== component.classification) invalid("Explicit tax classification must match its selected code");
4083
+ if (component.taxComponentId && !code.components.some((entry) => entry.taxComponentId === component.taxComponentId)) invalid("Tax component must belong to its selected tax code");
4084
+ }
4085
+ if (component.taxComponentId) {
4086
+ const tax = await (await TaxComponentCollection.create({ db })).get({ id: component.taxComponentId }, { cache: false });
4087
+ if (!tax || tax.tenantId !== tenantId) invalid("Tax report component must belong to the active tenant");
4088
+ }
4089
+ }
4090
+ if (contract.status !== ContractStatus.DRAFT || contract.moneySnapshotId) invalid("Received money requires an unfrozen draft");
4091
+ if (contract.currency !== snapshot.facts.currency) invalid("Received money currency must match the contract");
4092
+ if ((await (await ContractLineItemCollection.create({ db })).findByContract(contractId)).length) invalid("Received money cannot mix with calculated contract lines");
4093
+ contract.moneyAuthority = "received";
4094
+ contract.receivedPurchaseMoneyJson = serialized;
4095
+ contract.subtotal = snapshot.facts.netMinor;
4096
+ contract.discountTotal = 0;
4097
+ contract.taxAmount = snapshot.facts.taxMinor;
4098
+ contract.totalAmount = snapshot.facts.grossMinor;
4099
+ await contract.saveDerived(DOCUMENT_MONEY_WRITE);
4100
+ return snapshot;
4101
+ });
4102
+ }
4103
+ var ReceivedPurchaseTaxClassificationRequired = class extends Error {
4104
+ constructor(contractId) {
4105
+ super(`Purchase order ${contractId} needs explicit tax classification, tax-code/component identity, taxable basis and recoverability in its received evidence before tax reporting. Record a reviewed replacement revision; do not infer tax rates.`);
4106
+ this.contractId = contractId;
4107
+ this.name = "ReceivedPurchaseTaxClassificationRequired";
4108
+ }
4109
+ contractId;
4110
+ code = "RECEIVED_PURCHASE_TAX_CLASSIFICATION_REQUIRED";
4111
+ };
4112
+ function receivedPurchaseReportFacts(contract) {
4113
+ const snapshot = getReceivedPurchaseMoney(contract);
4114
+ if (!snapshot || !(contract instanceof PurchaseOrder)) return [];
4115
+ const id = text(contract.id, "Purchase order identity");
4116
+ const components = snapshot.facts.taxComponents;
4117
+ if (!components?.length) throw new ReceivedPurchaseTaxClassificationRequired(id);
4118
+ return components.map((component) => {
4119
+ if (component.classification === null || !component.taxCodeId || component.taxableMinor == null || component.recoverableMinor === null || component.taxableMinor > snapshot.facts.netMinor && component.netMinor == null || component.classification === "taxable" && !component.taxComponentId) throw new ReceivedPurchaseTaxClassificationRequired(id);
4120
+ return {
4121
+ tenantId: text(contract.tenantId, "Tenant"),
4122
+ documentType: "contract",
4123
+ documentId: id,
4124
+ documentNumber: contract.reference.trim() || id,
4125
+ partyId: contract.vendorId,
4126
+ documentKind: "contract",
4127
+ issueDate: contract.issueDate.toISOString(),
4128
+ status: contract.status,
4129
+ direction: "purchase",
4130
+ currency: snapshot.facts.currency,
4131
+ taxCodeId: component.taxCodeId,
4132
+ taxCodeKind: component.classification,
4133
+ taxableMinor: component.taxableMinor,
4134
+ ...component.netMinor != null ? { netMinor: component.netMinor } : {},
4135
+ taxMinor: component.amountMinor,
4136
+ recoverableMinor: component.recoverableMinor,
4137
+ taxComponentId: component.taxComponentId ?? null,
4138
+ componentName: component.label,
4139
+ liabilityAccountId: null,
4140
+ commitmentId: id,
4141
+ commitmentLineId: null
4142
+ };
4143
+ });
4144
+ }
4145
+ //#endregion
4146
+ //#region src/collections/DocumentTaxCollection.ts
4147
+ var DocumentTaxCollection_exports = /* @__PURE__ */ __exportAll({ DocumentTaxCollection: () => DocumentTaxCollection });
4148
+ var TAX_CODE_KINDS = /* @__PURE__ */ new Set([
4149
+ "taxable",
4150
+ "zero-rated",
4151
+ "exempt",
4152
+ "out-of-scope"
4153
+ ]);
4154
+ function requiredText(value, label) {
4155
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Invoice tax report ${label} must be a nonempty string.`);
4156
+ return value;
4157
+ }
4158
+ function storedMinor(value, label) {
4159
+ const parsed = typeof value === "bigint" ? Number(value) : typeof value === "number" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? Number(value) : NaN;
4160
+ if (!Number.isSafeInteger(parsed)) throw new RangeError(`Invoice tax report ${label} must be a safe integer minor-unit value.`);
4161
+ return parsed;
4162
+ }
4163
+ function reportDate(value, label) {
4164
+ const date = new Date(String(value));
4165
+ if (Number.isNaN(date.getTime())) throw new Error(`Invoice tax report ${label} must be a valid date.`);
4166
+ return date.toISOString();
4167
+ }
4168
+ function invoiceDocumentType(value) {
4169
+ if (value === "invoice" || value === "credit_note") return value;
4170
+ throw new Error("Invoice tax report document type must be invoice or credit_note.");
4171
+ }
4172
+ function invoiceKind(value) {
4173
+ if (value === "invoice" || value === "credit_note") return value;
4174
+ throw new Error("Invoice tax report kind must be invoice or credit_note.");
4175
+ }
4176
+ function contractDocumentType(value) {
4177
+ if (value === "contract") return value;
4178
+ throw new Error("Contract tax report document type must be contract.");
4179
+ }
4180
+ function taxCodeKind(value) {
4181
+ return typeof value === "string" && TAX_CODE_KINDS.has(value) ? value : null;
4182
+ }
4183
+ function assertReportFactOptions(options) {
4184
+ if (!options.tenantId?.trim()) throw new Error("Invoice tax report facts require a tenant.");
4185
+ if (!(options.from instanceof Date) || Number.isNaN(options.from.getTime())) throw new Error("Invoice tax report facts require a valid start date.");
4186
+ if (!(options.to instanceof Date) || Number.isNaN(options.to.getTime())) throw new Error("Invoice tax report facts require a valid end date.");
4187
+ if (options.from > options.to) throw new Error("Invoice tax report facts require an ordered date range.");
4188
+ if (options.currency !== void 0 && !options.currency.trim()) throw new Error("Invoice tax report currency must be nonempty when supplied.");
4189
+ }
4190
+ function compareCodeUnits(left, right) {
4191
+ return left < right ? -1 : left > right ? 1 : 0;
4192
+ }
4193
+ function identity(row) {
4194
+ return `${row.taxCodeId === null ? "0:" : `1:${row.taxCodeId}`}\0${row.taxComponentId === null ? "0:" : `1:${row.taxComponentId}`}`;
4195
+ }
4196
+ var BUSINESS_FIELDS = [
4197
+ "tenantId",
4198
+ "documentType",
4199
+ "documentId",
4200
+ "currency",
4201
+ "taxCodeId",
4202
+ "taxCodeCode",
4203
+ "kind",
4204
+ "taxComponentId",
4205
+ "taxComponentName",
4206
+ "percent",
4207
+ "liabilityAccountId",
4208
+ "taxableMinor",
4209
+ "netMinor",
4210
+ "taxMinor",
4211
+ "recoverableMinor"
4212
+ ];
4213
+ function unchanged(row, input) {
4214
+ return BUSINESS_FIELDS.every((field) => {
4215
+ const persisted = row[field];
4216
+ const proposed = input[field];
4217
+ if (field === "percent" && persisted !== null && proposed !== null) return String(persisted) === String(proposed);
4218
+ return (persisted ?? null) === (proposed ?? null);
4219
+ });
4220
+ }
4221
+ var DocumentTaxCollection = class extends SmrtCollection {
4222
+ static _itemClass = DocumentTax;
4223
+ /**
4224
+ * Read immutable commerce document-tax snapshots for one tenant.
4225
+ *
4226
+ * Invoice/credit rows join their shared storage by identity and tenant.
4227
+ * Purchase Contract rows first use the Contract owner's bounded hydrated
4228
+ * candidate API so custom PurchaseContract descendants retain their STI
4229
+ * classification. The query then joins only those candidate ids by identity
4230
+ * and tenant. Stored snapshots remain authoritative; this never recalculates
4231
+ * money or resolves present-day tax configuration.
4232
+ */
4233
+ async listReportFacts(options) {
4234
+ assertReportFactOptions(options);
4235
+ const [invoiceFacts, purchaseFacts] = await Promise.all([this.listInvoiceReportFacts(options), this.listPurchaseContractReportFacts(options)]);
4236
+ return [...invoiceFacts, ...purchaseFacts].sort((left, right) => left.issueDate.localeCompare(right.issueDate) || left.documentId.localeCompare(right.documentId) || String(left.taxCodeId).localeCompare(String(right.taxCodeId)) || String(left.taxComponentId).localeCompare(String(right.taxComponentId)));
4237
+ }
4238
+ async listInvoiceReportFacts(options) {
4239
+ const currencyClause = options.currency ? " AND d.currency = ?" : "";
4240
+ return [...(await this.db.query(`SELECT d.tenant_id, d.document_type, d.document_id, d.currency, d.tax_code_id, d.kind,
4241
+ d.tax_component_id, d.tax_component_name, d.liability_account_id,
4242
+ d.taxable_minor, d.net_minor, d.tax_minor, d.recoverable_minor,
4243
+ i.kind AS invoice_kind, i.invoice_number, i.customer_id, i.issue_date, i.status
4244
+ FROM document_taxes d
4245
+ JOIN invoices i
4246
+ ON CAST(i.id AS TEXT) = CAST(d.document_id AS TEXT)
4247
+ AND i.tenant_id = d.tenant_id
4248
+ WHERE CAST(d.tenant_id AS TEXT) = ?
4249
+ AND CAST(i.tenant_id AS TEXT) = ?
4250
+ AND (
4251
+ (d.document_type = 'invoice' AND i.kind = 'invoice')
4252
+ OR (d.document_type = 'credit_note' AND i.kind = 'credit_note')
4253
+ )
4254
+ AND i.status NOT IN ('draft', 'cancelled')
4255
+ AND i.issue_date >= ?
4256
+ AND i.issue_date <= ?${currencyClause}
4257
+ ORDER BY i.issue_date ASC, i.id ASC, d.tax_code_id ASC, d.tax_component_id ASC`, options.tenantId, options.tenantId, options.from.toISOString(), options.to.toISOString(), ...options.currency ? [options.currency] : [])).rows.map((row) => ({
4258
+ tenantId: requiredText(row.tenant_id, "tenant id"),
4259
+ documentType: invoiceDocumentType(row.document_type),
4260
+ documentId: requiredText(row.document_id, "document id"),
4261
+ documentNumber: requiredText(row.invoice_number, "invoice number"),
4262
+ partyId: typeof row.customer_id === "string" && row.customer_id.trim() ? row.customer_id : null,
4263
+ documentKind: invoiceKind(row.invoice_kind),
4264
+ issueDate: reportDate(row.issue_date, "issue date"),
4265
+ status: requiredText(row.status, "invoice status"),
4266
+ direction: "sales",
4267
+ currency: requiredText(row.currency, "currency"),
4268
+ taxCodeId: row.tax_code_id == null ? null : String(row.tax_code_id),
4269
+ taxCodeKind: taxCodeKind(row.kind),
4270
+ taxableMinor: storedMinor(row.taxable_minor, "taxable minor"),
4271
+ ...row.net_minor != null ? { netMinor: storedMinor(row.net_minor, "net minor") } : {},
4272
+ taxMinor: storedMinor(row.tax_minor, "tax minor"),
4273
+ recoverableMinor: storedMinor(row.recoverable_minor, "recoverable minor"),
4274
+ taxComponentId: row.tax_component_id == null ? null : String(row.tax_component_id),
4275
+ componentName: row.tax_component_name == null ? null : String(row.tax_component_name),
4276
+ liabilityAccountId: row.liability_account_id == null ? null : String(row.liability_account_id),
4277
+ commitmentId: null,
4278
+ commitmentLineId: null
4279
+ })), ...(await historicalSalesReportInvoices(this.db, options)).flatMap(historicalSalesTaxReportFacts)];
4280
+ }
4281
+ async listPurchaseContractReportFacts(options) {
4282
+ const candidates = (await withTenant({ tenantId: options.tenantId }, () => ContractCollection.create({ db: this.db }).then((collection) => collection.listSnapshotReportContracts({
4283
+ tenantId: options.tenantId,
4284
+ from: options.from,
4285
+ to: options.to
4286
+ })))).filter((contract) => classifyContractPurchase(contract) && (!options.currency || contract.currency === options.currency));
4287
+ const receivedFacts = candidates.filter((contract) => contract.moneyAuthority === "received").flatMap(receivedPurchaseReportFacts);
4288
+ const purchases = candidates.filter((contract) => contract.moneyAuthority !== "received");
4289
+ const ids = purchases.flatMap((contract) => contract.id ? [contract.id] : []);
4290
+ if (ids.length === 0) return receivedFacts;
4291
+ const byId = new Map(purchases.flatMap((contract) => contract.id ? [[contract.id, contract]] : []));
4292
+ const placeholders = ids.map(() => "?").join(", ");
4293
+ const currencyClause = options.currency ? " AND d.currency = ?" : "";
4294
+ return [...(await this.db.query(`SELECT d.tenant_id, d.document_type, d.document_id, d.currency, d.tax_code_id, d.kind,
4295
+ d.tax_component_id, d.tax_component_name, d.liability_account_id,
4296
+ d.taxable_minor, d.net_minor, d.tax_minor, d.recoverable_minor
4297
+ FROM document_taxes d
4298
+ JOIN contracts c
4299
+ ON CAST(c.id AS TEXT) = CAST(d.document_id AS TEXT)
4300
+ AND c.tenant_id = d.tenant_id
4301
+ WHERE CAST(d.tenant_id AS TEXT) = ?
4302
+ AND CAST(c.tenant_id AS TEXT) = ?
4303
+ AND d.document_type = 'contract'
4304
+ AND c.status NOT IN ('draft', 'cancelled')
4305
+ AND CAST(d.document_id AS TEXT) IN (${placeholders})${currencyClause}
4306
+ ORDER BY c.issue_date ASC, c.id ASC, d.tax_code_id ASC, d.tax_component_id ASC`, options.tenantId, options.tenantId, ...ids, ...options.currency ? [options.currency] : [])).rows.flatMap((row) => {
4307
+ const documentId = requiredText(row.document_id, "contract id");
4308
+ const contract = byId.get(documentId);
4309
+ if (!contract) return [];
4310
+ return [{
4311
+ tenantId: requiredText(row.tenant_id, "tenant id"),
4312
+ documentType: contractDocumentType(row.document_type),
4313
+ documentId,
4314
+ documentNumber: contract.reference.trim() || documentId,
4315
+ partyId: "vendorId" in contract && typeof contract.vendorId === "string" && contract.vendorId.trim() ? contract.vendorId : null,
4316
+ documentKind: "contract",
4317
+ issueDate: reportDate(contract.issueDate, "contract issue date"),
4318
+ status: requiredText(contract.status, "contract status"),
4319
+ direction: "purchase",
4320
+ currency: requiredText(row.currency, "currency"),
4321
+ taxCodeId: row.tax_code_id == null ? null : String(row.tax_code_id),
4322
+ taxCodeKind: taxCodeKind(row.kind),
4323
+ taxableMinor: storedMinor(row.taxable_minor, "taxable minor"),
4324
+ ...row.net_minor != null ? { netMinor: storedMinor(row.net_minor, "net minor") } : {},
4325
+ taxMinor: storedMinor(row.tax_minor, "tax minor"),
4326
+ recoverableMinor: storedMinor(row.recoverable_minor, "recoverable minor"),
4327
+ taxComponentId: row.tax_component_id == null ? null : String(row.tax_component_id),
4328
+ componentName: row.tax_component_name == null ? null : String(row.tax_component_name),
4329
+ liabilityAccountId: row.liability_account_id == null ? null : String(row.liability_account_id),
4330
+ commitmentId: documentId,
4331
+ commitmentLineId: null
4332
+ }];
4333
+ }), ...receivedFacts];
4334
+ }
4335
+ async findByDocument(tenantId, documentType, documentId) {
4336
+ return this.list({
4337
+ where: {
4338
+ tenantId,
4339
+ documentType,
4340
+ documentId
4341
+ },
4342
+ orderBy: ["taxCodeId ASC", "taxComponentId ASC"]
4343
+ });
4344
+ }
4345
+ /**
4346
+ * Replace one draft document's derived rows on the caller's transaction.
4347
+ * Unchanged rows are not saved, preserving their exact persisted bytes.
4348
+ *
4349
+ * @internal Aggregate persistence only.
4350
+ */
4351
+ async replaceDerivedForDocument(capability, input) {
4352
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
4353
+ const desired = /* @__PURE__ */ new Map();
4354
+ for (const row of input.rows) {
4355
+ if (row.tenantId !== input.tenantId || row.documentType !== input.documentType || row.documentId !== input.documentId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "DocumentTax replacement rows must share tenant and document identity");
4356
+ const key = identity(row);
4357
+ if (desired.has(key)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "DocumentTax replacement contains a duplicate code/component identity");
4358
+ desired.set(key, row);
4359
+ }
4360
+ const existing = await this.findByDocument(input.tenantId, input.documentType, input.documentId);
4361
+ const byIdentity = new Map(existing.map((row) => [identity(row), row]));
4362
+ const result = [];
4363
+ for (const [key, rowInput] of [...desired].sort(([left], [right]) => compareCodeUnits(left, right))) {
4364
+ const current = byIdentity.get(key);
4365
+ if (current) {
4366
+ byIdentity.delete(key);
4367
+ if (!unchanged(current, rowInput)) {
4368
+ Object.assign(current, rowInput);
4369
+ await current.saveDerived(capability);
4370
+ }
4371
+ result.push(current);
4372
+ continue;
4373
+ }
4374
+ const created = await this.createUnsaved(rowInput);
4375
+ await created.saveDerived(capability);
4376
+ result.push(created);
4377
+ }
4378
+ for (const stale of [...byIdentity.values()].sort((left, right) => compareCodeUnits(String(left.id), String(right.id)))) await stale.deleteDerived(capability, { expectedUpdatedAt: stale.updated_at ?? void 0 });
4379
+ return result;
4380
+ }
4381
+ };
4382
+ //#endregion
4383
+ export { DEFAULT_PARTY_CONTACT_FIELD_NAMES as $, PurchaseContractCollection as A, PurchaseOrder as B, refreshIssuedCreditBalance as C, issueContractMilestoneInvoice as D, getContractBillingSchedule as E, Lease as F, isContractSnapshotReportEligible as G, SupplierQuote as H, LicenseSale as I, HistoricalSalesLine as J, Vendor as K, Order as L, Cart as M, Contract as N, ContractCollection as O, Estimate as P, Customer as Q, ProductionOrder as R, previewInvoiceMoney as S, approveContractBillingSchedule as T, WholesaleOrder as U, SalesContract as V, classifyContractPurchase as W, InvoiceLineItem as X, InvoiceLineItemCollection as Y, CustomerCollection as Z, historicalText as _, getReceivedSalesContractMoney as a, normalizePartyContacts as at, getInvoiceMoneyFacts as b, validateReceivedPurchaseMoneyFacts as c, validatePartyContactRows as ct, HistoricalSalesTaxClassificationRequired as d, PARTY_CONTACT_MAX_LENGTHS as et, listHistoricalSalesHeaderFacts as f, historicalCalendarDate as g, recordHistoricalSalesMoney as h, getReceivedPurchaseMoney as i, applyPartyContactIntent as it, Agreement as j, SalesContractCollection as k, VendorCollection as l, readHistoricalSalesLines as m, DocumentTaxCollection_exports as n, PartyContactPayloadError as nt, recordReceivedPurchaseMoney as o, readPartyContactRows as ot, getHistoricalSalesMoney as p, HistoricalSalesLineCollection as q, ReceivedPurchaseTaxClassificationRequired as r, PartyContactValidationError as rt, recordReceivedSalesContractMoney as s, savePartyContacts as st, DocumentTaxCollection as t, PartyContactPartyNotFoundError as tt, DocumentTax as u, validateHistoricalSalesMoneyFacts as v, allocateContractMilestones as w, lockAndReloadInvoice as x, document_money_exports as y, PurchaseContract as z };
4384
+
4385
+ //# sourceMappingURL=DocumentTaxCollection-C42x0YLW.js.map