@happyvertical/smrt-commerce 0.56.0 → 0.56.1

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 (211) hide show
  1. package/AGENTS.md +9 -8
  2. package/README.md +76 -0
  3. package/agents/accounting-snapshot-mapper.md +111 -0
  4. package/agents/contract-money.md +18 -0
  5. package/agents/cost-billing.md +7 -2
  6. package/agents/document-money.md +126 -0
  7. package/agents/invoice-calculations.md +14 -2
  8. package/agents/line-amount-migration.md +50 -0
  9. package/agents/minor-unit-migration.md +20 -0
  10. package/agents/money-calculator.md +45 -0
  11. package/agents/money-model.md +113 -0
  12. package/agents/revenue-journal.md +23 -0
  13. package/agents/tax-codes.md +48 -0
  14. package/agents/tax-summary.md +126 -0
  15. package/dist/accounting-snapshot.d.ts +79 -0
  16. package/dist/accounting-snapshot.d.ts.map +1 -0
  17. package/dist/billing/runtime.d.ts +2 -0
  18. package/dist/billing/runtime.d.ts.map +1 -1
  19. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js +265 -0
  20. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +1 -0
  21. package/dist/chunks/CustomerCollection-DQIujn4f.js +99 -0
  22. package/dist/chunks/CustomerCollection-DQIujn4f.js.map +1 -0
  23. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js +1639 -0
  24. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +1 -0
  25. package/dist/chunks/InvoiceCollection-CKQflFjc.js +1047 -0
  26. package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +1 -0
  27. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +467 -0
  28. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +1 -0
  29. package/dist/chunks/TaxCodeCollection-DOpzW4y8.js +149 -0
  30. package/dist/chunks/TaxCodeCollection-DOpzW4y8.js.map +1 -0
  31. package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js +129 -0
  32. package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js.map +1 -0
  33. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +319 -0
  34. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +1 -0
  35. package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-DI3G16BN.js} +7 -12
  36. package/dist/chunks/allocation-integrity-DI3G16BN.js.map +1 -0
  37. package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
  38. package/dist/chunks/calculations-DaHR17il.js.map +1 -0
  39. package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
  40. package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
  41. package/dist/chunks/document-money-DNVhDJyC.js +860 -0
  42. package/dist/chunks/document-money-DNVhDJyC.js.map +1 -0
  43. package/dist/chunks/invoice-posting-BwV8g7z0.js +229 -0
  44. package/dist/chunks/invoice-posting-BwV8g7z0.js.map +1 -0
  45. package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
  46. package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
  47. package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
  48. package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
  49. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  50. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +135 -0
  51. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +1 -0
  52. package/dist/chunks/{types-zVnIjSOk.js → types-LDETvQ1C.js} +3 -14
  53. package/dist/chunks/types-LDETvQ1C.js.map +1 -0
  54. package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
  55. package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
  56. package/dist/collections/ContractCollection.d.ts +35 -1
  57. package/dist/collections/ContractCollection.d.ts.map +1 -1
  58. package/dist/collections/ContractLineItemCollection.d.ts +26 -1
  59. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  60. package/dist/collections/DocumentTaxCollection.d.ts +75 -0
  61. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
  62. package/dist/collections/InvoiceCollection.d.ts +26 -1
  63. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  64. package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
  65. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  66. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
  67. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
  68. package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
  69. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  70. package/dist/collections/TaxCodeCollection.d.ts +7 -0
  71. package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
  72. package/dist/collections/TaxComponentCollection.d.ts +6 -0
  73. package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
  74. package/dist/collections/TaxSettingsCollection.d.ts +16 -0
  75. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
  76. package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
  77. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
  78. package/dist/collections/index.d.ts +6 -0
  79. package/dist/collections/index.d.ts.map +1 -1
  80. package/dist/commerce-money-tools.d.ts +9 -0
  81. package/dist/commerce-money-tools.d.ts.map +1 -0
  82. package/dist/contract-money.d.ts +27 -0
  83. package/dist/contract-money.d.ts.map +1 -0
  84. package/dist/cost-billing.d.ts +0 -1
  85. package/dist/cost-billing.d.ts.map +1 -1
  86. package/dist/cost-billing.js +10 -15
  87. package/dist/cost-billing.js.map +1 -1
  88. package/dist/credit-allocation.d.ts +48 -0
  89. package/dist/credit-allocation.d.ts.map +1 -0
  90. package/dist/document-money-internal.d.ts +44 -0
  91. package/dist/document-money-internal.d.ts.map +1 -0
  92. package/dist/document-money.d.ts +85 -0
  93. package/dist/document-money.d.ts.map +1 -0
  94. package/dist/index.d.ts +15 -4
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +5395 -4246
  97. package/dist/index.js.map +1 -1
  98. package/dist/invoice-accounting-sync.d.ts +96 -0
  99. package/dist/invoice-accounting-sync.d.ts.map +1 -0
  100. package/dist/invoice-mcp.d.ts +16 -0
  101. package/dist/invoice-mcp.d.ts.map +1 -0
  102. package/dist/invoice-posting.d.ts +9 -0
  103. package/dist/invoice-posting.d.ts.map +1 -0
  104. package/dist/invoice-record-screen.d.ts +60 -0
  105. package/dist/invoice-record-screen.d.ts.map +1 -0
  106. package/dist/invoices.d.ts +2 -0
  107. package/dist/invoices.d.ts.map +1 -1
  108. package/dist/invoices.js +3 -2
  109. package/dist/manifest.json +10365 -4562
  110. package/dist/migrations/commerceLineAmounts.d.ts +84 -0
  111. package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
  112. package/dist/models/Contract.d.ts +25 -1
  113. package/dist/models/Contract.d.ts.map +1 -1
  114. package/dist/models/ContractLineItem.d.ts +30 -1
  115. package/dist/models/ContractLineItem.d.ts.map +1 -1
  116. package/dist/models/Customer.d.ts +7 -0
  117. package/dist/models/Customer.d.ts.map +1 -1
  118. package/dist/models/DocumentTax.d.ts +50 -0
  119. package/dist/models/DocumentTax.d.ts.map +1 -0
  120. package/dist/models/Invoice.d.ts +46 -2
  121. package/dist/models/Invoice.d.ts.map +1 -1
  122. package/dist/models/InvoiceLineItem.d.ts +33 -2
  123. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  124. package/dist/models/MoneyMigrationDocument.d.ts +28 -0
  125. package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
  126. package/dist/models/Payment.d.ts +5 -1
  127. package/dist/models/Payment.d.ts.map +1 -1
  128. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  129. package/dist/models/TaxCode.d.ts +22 -0
  130. package/dist/models/TaxCode.d.ts.map +1 -0
  131. package/dist/models/TaxComponent.d.ts +21 -0
  132. package/dist/models/TaxComponent.d.ts.map +1 -0
  133. package/dist/models/TaxSettings.d.ts +33 -0
  134. package/dist/models/TaxSettings.d.ts.map +1 -0
  135. package/dist/models/TaxSummaryReport.d.ts +35 -0
  136. package/dist/models/TaxSummaryReport.d.ts.map +1 -0
  137. package/dist/models/Vendor.d.ts +9 -0
  138. package/dist/models/Vendor.d.ts.map +1 -1
  139. package/dist/models/index.d.ts +6 -0
  140. package/dist/models/index.d.ts.map +1 -1
  141. package/dist/money-rational.d.ts +15 -0
  142. package/dist/money-rational.d.ts.map +1 -0
  143. package/dist/money.d.ts +106 -25
  144. package/dist/money.d.ts.map +1 -1
  145. package/dist/money.js +274 -0
  146. package/dist/money.js.map +1 -0
  147. package/dist/recipes.d.ts +15 -15
  148. package/dist/revenue-journal.d.ts +61 -0
  149. package/dist/revenue-journal.d.ts.map +1 -0
  150. package/dist/smrt-knowledge.json +5711 -1029
  151. package/dist/svelte/components/InvoiceActions.svelte +12 -0
  152. package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
  153. package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
  154. package/dist/svelte/components/InvoiceEditor.svelte +15 -1
  155. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  156. package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
  157. package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
  158. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
  159. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
  160. package/dist/svelte/components/InvoiceTotals.svelte +2 -2
  161. package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
  162. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
  163. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
  164. package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
  165. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
  166. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
  167. package/dist/svelte/components/QuoteEditor.svelte +54 -1
  168. package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
  169. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
  170. package/dist/svelte/i18n.d.ts +16 -0
  171. package/dist/svelte/i18n.d.ts.map +1 -1
  172. package/dist/svelte/i18n.js +16 -0
  173. package/dist/svelte/index.d.ts +7 -2
  174. package/dist/svelte/index.d.ts.map +1 -1
  175. package/dist/svelte/index.js +4 -1
  176. package/dist/svelte/invoices/calculations.d.ts +7 -2
  177. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  178. package/dist/svelte/invoices/calculations.js +141 -47
  179. package/dist/svelte/invoices/money-editor.d.ts +123 -0
  180. package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
  181. package/dist/svelte/invoices/money-editor.js +101 -0
  182. package/dist/svelte/invoices/types.d.ts +5 -1
  183. package/dist/svelte/invoices/types.d.ts.map +1 -1
  184. package/dist/svelte/purchasing-i18n.d.ts +5 -0
  185. package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
  186. package/dist/svelte/purchasing-i18n.js +5 -0
  187. package/dist/svelte/quote-i18n.d.ts +5 -0
  188. package/dist/svelte/quote-i18n.d.ts.map +1 -1
  189. package/dist/svelte/quote-i18n.js +5 -0
  190. package/dist/tax/cookbook.d.ts +22 -0
  191. package/dist/tax/cookbook.d.ts.map +1 -0
  192. package/dist/tax/example-pack.d.ts +25 -0
  193. package/dist/tax/example-pack.d.ts.map +1 -0
  194. package/dist/tax/identity.d.ts +5 -0
  195. package/dist/tax/identity.d.ts.map +1 -0
  196. package/dist/tax/index.d.ts +4 -0
  197. package/dist/tax/index.d.ts.map +1 -0
  198. package/dist/tax/legacy.d.ts +48 -0
  199. package/dist/tax/legacy.d.ts.map +1 -0
  200. package/dist/tax-summary-report-adapter.d.ts +37 -0
  201. package/dist/tax-summary-report-adapter.d.ts.map +1 -0
  202. package/dist/tax-summary.d.ts +129 -0
  203. package/dist/tax-summary.d.ts.map +1 -0
  204. package/dist/tenant-money-barrier.d.ts +66 -0
  205. package/dist/tenant-money-barrier.d.ts.map +1 -0
  206. package/dist/types/index.d.ts +137 -1
  207. package/dist/types/index.d.ts.map +1 -1
  208. package/package.json +19 -11
  209. package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
  210. package/dist/chunks/money-DfrRxrel.js.map +0 -1
  211. package/dist/chunks/types-zVnIjSOk.js.map +0 -1
@@ -0,0 +1,1639 @@
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, t as ContractStatus } from "./types-LDETvQ1C.js";
3
+ import { MoneyValidationError, assertIntegerMinorUnits } from "../money.js";
4
+ import { a as DOCUMENT_MONEY_READ_REBIND, o as DOCUMENT_MONEY_WRITE } from "./TaxSettingsCollection-BBTZDH1S.js";
5
+ import { i as withTenantMoneyCollectionRead } from "./tenant-money-barrier-DjRXgMnX.js";
6
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
7
+ import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
8
+ //#region src/party-contacts.ts
9
+ var DEFAULT_PARTY_CONTACT_FIELD_NAMES = {
10
+ contactId: "contactId",
11
+ contactName: "contactName",
12
+ contactLabel: "contactLabel",
13
+ contactEmail: "contactEmail",
14
+ contactPhone: "contactPhone",
15
+ contactAddress: "contactAddress",
16
+ contactCount: "contactCount"
17
+ };
18
+ var PARTY_CONTACT_MAX_LENGTHS = {
19
+ name: 160,
20
+ label: 80,
21
+ email: 320,
22
+ phone: 80,
23
+ address: 500
24
+ };
25
+ var FIELDS = [
26
+ "name",
27
+ "label",
28
+ "email",
29
+ "phone",
30
+ "address"
31
+ ];
32
+ var PartyContactValidationError = class extends Error {
33
+ /** Errors aligned with the submitted rows; `undefined` for a valid row. */
34
+ errors;
35
+ constructor(errors) {
36
+ super("One or more contact rows are invalid");
37
+ this.name = "PartyContactValidationError";
38
+ this.errors = errors;
39
+ }
40
+ };
41
+ var PartyContactPayloadError = class extends Error {
42
+ reason;
43
+ constructor(reason, message) {
44
+ super(message);
45
+ this.name = "PartyContactPayloadError";
46
+ this.reason = reason;
47
+ }
48
+ };
49
+ var PartyContactPartyNotFoundError = class extends Error {
50
+ constructor(partyId) {
51
+ super(`Customer or vendor ${partyId} was not found`);
52
+ this.name = "PartyContactPartyNotFoundError";
53
+ }
54
+ };
55
+ function text(value) {
56
+ return typeof value === "string" ? value : "";
57
+ }
58
+ function normalizePartyContacts(value) {
59
+ let input = value;
60
+ if (typeof input === "string") try {
61
+ input = JSON.parse(input);
62
+ } catch {
63
+ return [];
64
+ }
65
+ if (!Array.isArray(input)) return [];
66
+ const contacts = [];
67
+ for (const entry of input) {
68
+ if (!entry || typeof entry !== "object") continue;
69
+ const row = entry;
70
+ contacts.push({
71
+ id: text(row.id) || globalThis.crypto.randomUUID(),
72
+ name: text(row.name),
73
+ label: text(row.label),
74
+ email: text(row.email),
75
+ phone: text(row.phone),
76
+ address: text(row.address)
77
+ });
78
+ }
79
+ return contacts;
80
+ }
81
+ function readPartyContactRows(form, fieldNames = {}) {
82
+ const names = {
83
+ ...DEFAULT_PARTY_CONTACT_FIELD_NAMES,
84
+ ...fieldNames
85
+ };
86
+ const column = (name) => form.getAll(name).map(text);
87
+ const columns = {
88
+ id: column(names.contactId),
89
+ name: column(names.contactName),
90
+ label: column(names.contactLabel),
91
+ email: column(names.contactEmail),
92
+ phone: column(names.contactPhone),
93
+ address: column(names.contactAddress)
94
+ };
95
+ const markers = column(names.contactCount);
96
+ 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`);
97
+ const count = columns.id.length;
98
+ if (markers[0] !== String(count)) throw new PartyContactPayloadError("misaligned", `Contact payload is misaligned: ${names.contactCount} is ${markers[0]} but ${count} contact ids were posted`);
99
+ 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`);
100
+ return columns.id.map((id, index) => ({
101
+ id,
102
+ name: columns.name[index],
103
+ label: columns.label[index],
104
+ email: columns.email[index],
105
+ phone: columns.phone[index],
106
+ address: columns.address[index]
107
+ }));
108
+ }
109
+ function defaultParseRemoveIntent(intent) {
110
+ const match = /^removeContact:(\d+)$/.exec(intent);
111
+ return match ? Number(match[1]) : void 0;
112
+ }
113
+ function applyPartyContactIntent(rows, intent, intents = {}) {
114
+ if (intent === (intents.addIntent ?? "addContact")) return {
115
+ contacts: [...rows, {}],
116
+ handled: true
117
+ };
118
+ const index = (intents.parseRemoveIntent ?? defaultParseRemoveIntent)(intent);
119
+ if (index !== void 0) return {
120
+ contacts: rows.filter((_, position) => position !== index),
121
+ handled: true
122
+ };
123
+ return {
124
+ contacts: [...rows],
125
+ handled: false
126
+ };
127
+ }
128
+ var EMAIL = /^[^\s@]+@[^\s@]+$/;
129
+ function validatePartyContactRows(rows) {
130
+ const contacts = [];
131
+ const errors = [];
132
+ let invalid = false;
133
+ rows.forEach((row, index) => {
134
+ const trimmed = { id: text(row.id).trim() };
135
+ let rowErrors;
136
+ for (const field of FIELDS) {
137
+ const value = text(row[field]).trim();
138
+ trimmed[field] = value;
139
+ if (value.length > PARTY_CONTACT_MAX_LENGTHS[field]) rowErrors = {
140
+ ...rowErrors,
141
+ [field]: "too_long"
142
+ };
143
+ }
144
+ if (trimmed.email && !rowErrors?.email && !EMAIL.test(trimmed.email)) rowErrors = {
145
+ ...rowErrors,
146
+ email: "invalid_email"
147
+ };
148
+ errors[index] = rowErrors;
149
+ if (rowErrors) invalid = true;
150
+ if (FIELDS.some((field) => trimmed[field])) contacts.push(trimmed);
151
+ });
152
+ return invalid ? {
153
+ contacts,
154
+ errors
155
+ } : { contacts };
156
+ }
157
+ async function savePartyContacts(collection, partyId, posted, options = {}) {
158
+ const rows = Array.isArray(posted) ? posted : readPartyContactRows(posted, options.fieldNames);
159
+ const party = await collection.get({ id: partyId });
160
+ if (!party || options.expectedTenantId !== void 0 && party.tenantId !== options.expectedTenantId) throw new PartyContactPartyNotFoundError(partyId);
161
+ const validation = validatePartyContactRows(rows);
162
+ const { contacts } = validation;
163
+ const errors = validation.errors ?? rows.map(() => void 0);
164
+ const saved = new Set(normalizePartyContacts(party.contacts).map((c) => c.id));
165
+ let invalid = validation.errors !== void 0;
166
+ rows.forEach((row, index) => {
167
+ const id = text(row.id).trim();
168
+ if (id && !saved.has(id)) {
169
+ errors[index] = {
170
+ ...errors[index],
171
+ id: "unknown_contact"
172
+ };
173
+ invalid = true;
174
+ }
175
+ });
176
+ if (invalid) throw new PartyContactValidationError(errors);
177
+ const seen = /* @__PURE__ */ new Set();
178
+ party.contacts = contacts.map((row) => {
179
+ const id = row.id && !seen.has(row.id) ? row.id : globalThis.crypto.randomUUID();
180
+ seen.add(id);
181
+ return {
182
+ id,
183
+ name: row.name ?? "",
184
+ label: row.label ?? "",
185
+ email: row.email ?? "",
186
+ phone: row.phone ?? "",
187
+ address: row.address ?? ""
188
+ };
189
+ });
190
+ await party.save();
191
+ return party.contacts;
192
+ }
193
+ //#endregion
194
+ //#region src/models/Customer.ts
195
+ var __defProp$3 = Object.defineProperty;
196
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
197
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, {
198
+ enumerable: true,
199
+ configurable: true,
200
+ writable: true,
201
+ value
202
+ }) : obj[key] = value;
203
+ var __decorateClass$3 = (decorators, target, key, kind) => {
204
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
205
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
206
+ if (kind && result) __defProp$3(target, key, result);
207
+ return result;
208
+ };
209
+ var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
210
+ var Customer = class extends SmrtObject {
211
+ tenantId = null;
212
+ profileId = "";
213
+ creditLimit = 0;
214
+ paymentTerms = "";
215
+ taxExempt = false;
216
+ taxId = "";
217
+ defaultTaxCodeId = "";
218
+ defaultShippingAddress = {};
219
+ defaultBillingAddress = {};
220
+ status = CustomerStatus.ACTIVE;
221
+ customerType = CustomerType.DTC;
222
+ notes = "";
223
+ contacts = [];
224
+ constructor(options = {}) {
225
+ super(options);
226
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
227
+ if (options.profileId !== void 0) this.profileId = options.profileId;
228
+ if (options.creditLimit !== void 0) this.creditLimit = options.creditLimit;
229
+ if (options.paymentTerms !== void 0) this.paymentTerms = options.paymentTerms;
230
+ if (options.taxExempt !== void 0) this.taxExempt = options.taxExempt;
231
+ if (options.taxId !== void 0) this.taxId = options.taxId;
232
+ if (options.defaultTaxCodeId !== void 0) this.defaultTaxCodeId = options.defaultTaxCodeId;
233
+ if (options.defaultShippingAddress !== void 0) this.defaultShippingAddress = options.defaultShippingAddress;
234
+ if (options.defaultBillingAddress !== void 0) this.defaultBillingAddress = options.defaultBillingAddress;
235
+ if (options.status !== void 0) this.status = options.status;
236
+ if (options.customerType !== void 0 && options.customerType !== null) this.customerType = options.customerType;
237
+ if (options.notes !== void 0) this.notes = options.notes;
238
+ if (options.contacts !== void 0) this.contacts = normalizePartyContacts(options.contacts);
239
+ }
240
+ /**
241
+ * Re-normalize contacts after the framework's option-override pass, which
242
+ * re-applies raw option values (including a stored JSON string).
243
+ */
244
+ async initialize() {
245
+ await super.initialize();
246
+ this.contacts = normalizePartyContacts(this.contacts);
247
+ return this;
248
+ }
249
+ /** Save with a final contacts normalization against direct assignment. */
250
+ async save() {
251
+ this.contacts = normalizePartyContacts(this.contacts);
252
+ return super.save();
253
+ }
254
+ /**
255
+ * Convenience predicate: is this a wholesale customer?
256
+ */
257
+ isWholesale() {
258
+ return this.customerType === CustomerType.WHOLESALE;
259
+ }
260
+ /**
261
+ * Check if customer is active
262
+ */
263
+ isActive() {
264
+ return this.status === CustomerStatus.ACTIVE;
265
+ }
266
+ /**
267
+ * Check if customer has available credit
268
+ */
269
+ hasAvailableCredit(amount) {
270
+ return this.creditLimit >= amount;
271
+ }
272
+ /**
273
+ * Get the profile ID for external lookup.
274
+ *
275
+ * To get the actual Profile object, use the ProfileCollection from smrt-profiles:
276
+ * ```typescript
277
+ * const profiles = await ProfileCollection.create(options);
278
+ * const profile = await profiles.get({ id: customer.profileId });
279
+ * ```
280
+ *
281
+ * @returns The profile ID or empty string if not set
282
+ */
283
+ getProfileId() {
284
+ return this.profileId;
285
+ }
286
+ };
287
+ __publicField$3(Customer, "__smrtPackage__", "@happyvertical/smrt-commerce");
288
+ __decorateClass$3([tenantId({ nullable: true })], Customer.prototype, "tenantId", 2);
289
+ __decorateClass$3([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);
290
+ __decorateClass$3([field({
291
+ description: "The most this customer may owe you at one time. Leave it at zero if you do not set limits.",
292
+ ui: { format: "money" }
293
+ })], Customer.prototype, "creditLimit", 2);
294
+ __decorateClass$3([field({ description: "How long the customer has to pay, written the way you would say it, for example \"Net 30\"." })], Customer.prototype, "paymentTerms", 2);
295
+ __decorateClass$3([field({ description: "Turn this on if you do not charge this customer sales tax." })], Customer.prototype, "taxExempt", 2);
296
+ __decorateClass$3([field({
297
+ sensitive: true,
298
+ description: "The tax or VAT number of the customer, kept on file for tax-exempt sales."
299
+ })], Customer.prototype, "taxId", 2);
300
+ __decorateClass$3([foreignKey("TaxCode", { description: "The tax code normally used when selling to this customer." })], Customer.prototype, "defaultTaxCodeId", 2);
301
+ __decorateClass$3([field({ description: "Where you usually send goods to this customer, so you do not retype it on every order." })], Customer.prototype, "defaultShippingAddress", 2);
302
+ __decorateClass$3([field({ description: "Where you send invoices to this customer." })], Customer.prototype, "defaultBillingAddress", 2);
303
+ __decorateClass$3([field({ description: "Whether you are still dealing with this customer: active, inactive or suspended." })], Customer.prototype, "status", 2);
304
+ __decorateClass$3([field({ description: "The kind of customer: a direct shopper, a wholesale buyer or a retail customer." })], Customer.prototype, "customerType", 2);
305
+ __decorateClass$3([field({
306
+ description: "Anything worth remembering about this customer.",
307
+ ui: { widget: "textarea" }
308
+ })], Customer.prototype, "notes", 2);
309
+ __decorateClass$3([field({ description: "The people you deal with at this customer, with their role and how to reach them." })], Customer.prototype, "contacts", 2);
310
+ Customer = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
311
+ api: { include: [
312
+ "list",
313
+ "get",
314
+ "create",
315
+ "update"
316
+ ] },
317
+ mcp: { include: ["list", "get"] },
318
+ cli: { skipApiCheck: true }
319
+ })], Customer);
320
+ //#endregion
321
+ //#region src/models/Vendor.ts
322
+ var __defProp$2 = Object.defineProperty;
323
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
324
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
325
+ enumerable: true,
326
+ configurable: true,
327
+ writable: true,
328
+ value
329
+ }) : obj[key] = value;
330
+ var __decorateClass$2 = (decorators, target, key, kind) => {
331
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
332
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
333
+ if (kind && result) __defProp$2(target, key, result);
334
+ return result;
335
+ };
336
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
337
+ var Vendor = class extends SmrtObject {
338
+ tenantId = null;
339
+ profileId = "";
340
+ leadTimeDays = 0;
341
+ minimumOrderAmount = 0;
342
+ paymentTerms = "";
343
+ currency = "USD";
344
+ taxId = "";
345
+ defaultTaxCodeId = "";
346
+ defaultContactEmail = "";
347
+ defaultContactPhone = "";
348
+ status = VendorStatus.ACTIVE;
349
+ notes = "";
350
+ payoutAddresses = {};
351
+ contacts = [];
352
+ constructor(options = {}) {
353
+ super(options);
354
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
355
+ if (options.profileId !== void 0) this.profileId = options.profileId;
356
+ if (options.leadTimeDays !== void 0) this.leadTimeDays = options.leadTimeDays;
357
+ if (options.minimumOrderAmount !== void 0) this.minimumOrderAmount = options.minimumOrderAmount;
358
+ if (options.paymentTerms !== void 0) this.paymentTerms = options.paymentTerms;
359
+ if (options.currency !== void 0) this.currency = options.currency;
360
+ if (options.taxId !== void 0) this.taxId = options.taxId;
361
+ if (options.defaultTaxCodeId !== void 0) this.defaultTaxCodeId = options.defaultTaxCodeId;
362
+ if (options.defaultContactEmail !== void 0) this.defaultContactEmail = options.defaultContactEmail;
363
+ if (options.defaultContactPhone !== void 0) this.defaultContactPhone = options.defaultContactPhone;
364
+ if (options.status !== void 0) this.status = options.status;
365
+ if (options.notes !== void 0) this.notes = options.notes;
366
+ if (options.payoutAddresses !== void 0) this.payoutAddresses = Vendor.normalizePayoutAddresses(options.payoutAddresses);
367
+ if (options.contacts !== void 0) this.contacts = normalizePartyContacts(options.contacts);
368
+ }
369
+ /**
370
+ * The framework's {@link SmrtObject.initialize} re-applies `options` keys on
371
+ * top of constructor-set values so option data always wins over field
372
+ * initializers. That means our constructor's normalization runs *before*
373
+ * `initializePropertiesFromOptions` overwrites `payoutAddresses` with the
374
+ * raw cloned value. Re-normalize once super has finished so consumers
375
+ * who pass a string, a partially-typed map, or `null` end up with a
376
+ * clean `Record<string, string>` before any save / read happens.
377
+ */
378
+ async initialize() {
379
+ await super.initialize();
380
+ this.payoutAddresses = Vendor.normalizePayoutAddresses(this.payoutAddresses);
381
+ this.contacts = normalizePartyContacts(this.contacts);
382
+ return this;
383
+ }
384
+ /**
385
+ * Save with a final normalization pass so direct field assignments
386
+ * (`vendor.payoutAddresses = somethingFunny`) can't smuggle non-string
387
+ * values into the persisted row.
388
+ */
389
+ async save() {
390
+ this.payoutAddresses = Vendor.normalizePayoutAddresses(this.payoutAddresses);
391
+ this.contacts = normalizePartyContacts(this.contacts);
392
+ return super.save();
393
+ }
394
+ /**
395
+ * Check if vendor is active
396
+ */
397
+ isActive() {
398
+ return this.status === VendorStatus.ACTIVE;
399
+ }
400
+ /**
401
+ * Check if order meets minimum requirements
402
+ */
403
+ meetsMinimumOrder(amount) {
404
+ return amount >= this.minimumOrderAmount;
405
+ }
406
+ /**
407
+ * Get the profile ID for external lookup.
408
+ *
409
+ * To get the actual Profile object, use the ProfileCollection from smrt-profiles:
410
+ * ```typescript
411
+ * const profiles = await ProfileCollection.create(options);
412
+ * const profile = await profiles.get({ id: vendor.profileId });
413
+ * ```
414
+ *
415
+ * @returns The profile ID or empty string if not set
416
+ */
417
+ getProfileId() {
418
+ return this.profileId;
419
+ }
420
+ /**
421
+ * Look up the payout destination for a single currency. Returns
422
+ * `undefined` if the vendor has no entry for that currency — caller's
423
+ * job to decide whether that means "skip the option" or "raise a
424
+ * configuration error". The lookup is case-sensitive; the contract is
425
+ * that the currency code passed in matches exactly what was stored
426
+ * (`USDC-base`, not `usdc-base`).
427
+ */
428
+ getPayoutAddress(currency) {
429
+ const map = this.payoutAddresses;
430
+ if (!map || typeof map !== "object") return void 0;
431
+ const value = map[currency];
432
+ return typeof value === "string" ? value : void 0;
433
+ }
434
+ /**
435
+ * Set the payout destination for a single currency. Mutates the live
436
+ * map and returns `this` for chaining. Use {@link clearPayoutAddress}
437
+ * to remove a single entry.
438
+ */
439
+ setPayoutAddress(currency, destination) {
440
+ this.payoutAddresses = {
441
+ ...this.payoutAddresses ?? {},
442
+ [currency]: destination
443
+ };
444
+ return this;
445
+ }
446
+ /**
447
+ * Remove a single currency entry from the payout map. No-op if the
448
+ * currency was already absent.
449
+ */
450
+ clearPayoutAddress(currency) {
451
+ if (!this.payoutAddresses || !(currency in this.payoutAddresses)) return this;
452
+ const next = { ...this.payoutAddresses };
453
+ delete next[currency];
454
+ this.payoutAddresses = next;
455
+ return this;
456
+ }
457
+ /**
458
+ * Normalize an input value (object, pre-serialized JSON string, or
459
+ * `null`/`undefined`) into a plain `Record<string, string>`. Used by
460
+ * the constructor so consumers can pass either shape into
461
+ * `VendorCollection.create({ payoutAddresses: ... })`.
462
+ *
463
+ * Non-string values inside an input object are silently dropped — a
464
+ * `Record<string, string>` invariant is what consumers downstream
465
+ * (`PaymentIntent` builders, payout SDK adapters) rely on.
466
+ */
467
+ static normalizePayoutAddresses(value) {
468
+ if (value == null) return {};
469
+ let parsed = value;
470
+ if (typeof value === "string") try {
471
+ parsed = JSON.parse(value);
472
+ } catch {
473
+ return {};
474
+ }
475
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
476
+ const out = {};
477
+ for (const [key, v] of Object.entries(parsed)) if (typeof v === "string") out[key] = v;
478
+ return out;
479
+ }
480
+ };
481
+ __publicField$2(Vendor, "__smrtPackage__", "@happyvertical/smrt-commerce");
482
+ __decorateClass$2([tenantId({ nullable: true })], Vendor.prototype, "tenantId", 2);
483
+ __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);
484
+ __decorateClass$2([field({ description: "How many days the vendor usually takes between your order and delivery." })], Vendor.prototype, "leadTimeDays", 2);
485
+ __decorateClass$2([field({
486
+ description: "The smallest order the vendor will accept.",
487
+ ui: { format: "money" }
488
+ })], Vendor.prototype, "minimumOrderAmount", 2);
489
+ __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);
490
+ __decorateClass$2([field({
491
+ description: "The currency this vendor bills you in.",
492
+ ui: { widget: "currency" }
493
+ })], Vendor.prototype, "currency", 2);
494
+ __decorateClass$2([field({
495
+ sensitive: true,
496
+ description: "The vendor tax or VAT registration number kept on file."
497
+ })], Vendor.prototype, "taxId", 2);
498
+ __decorateClass$2([foreignKey("TaxCode", { description: "The tax code normally used when buying from this vendor." })], Vendor.prototype, "defaultTaxCodeId", 2);
499
+ __decorateClass$2([field({
500
+ description: "The email address you normally use to reach this vendor.",
501
+ ui: { widget: "email" }
502
+ })], Vendor.prototype, "defaultContactEmail", 2);
503
+ __decorateClass$2([field({
504
+ description: "The phone number you normally use to reach this vendor.",
505
+ ui: { widget: "phone" }
506
+ })], Vendor.prototype, "defaultContactPhone", 2);
507
+ __decorateClass$2([field({ description: "Whether you still buy from this vendor: active, inactive or suspended." })], Vendor.prototype, "status", 2);
508
+ __decorateClass$2([field({
509
+ description: "Anything worth remembering about this vendor.",
510
+ ui: { widget: "textarea" }
511
+ })], Vendor.prototype, "notes", 2);
512
+ __decorateClass$2([field({
513
+ sensitive: true,
514
+ description: "Where payments to this vendor should be sent."
515
+ })], Vendor.prototype, "payoutAddresses", 2);
516
+ __decorateClass$2([field({ description: "The people you deal with at this vendor, with their role and how to reach them." })], Vendor.prototype, "contacts", 2);
517
+ Vendor = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
518
+ api: { include: [
519
+ "list",
520
+ "get",
521
+ "create",
522
+ "update"
523
+ ] },
524
+ mcp: { include: ["list", "get"] },
525
+ cli: { skipApiCheck: true }
526
+ })], Vendor);
527
+ //#endregion
528
+ //#region src/models/Contract.ts
529
+ var __defProp$1 = Object.defineProperty;
530
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
531
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
532
+ enumerable: true,
533
+ configurable: true,
534
+ writable: true,
535
+ value
536
+ }) : obj[key] = value;
537
+ var __decorateClass$1 = (decorators, target, key, kind) => {
538
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
539
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
540
+ if (kind && result) __defProp$1(target, key, result);
541
+ return result;
542
+ };
543
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
544
+ var CONTRACT_STATUS_TRANSITIONS = {
545
+ [ContractStatus.DRAFT]: [
546
+ ContractStatus.SENT,
547
+ ContractStatus.ACCEPTED,
548
+ ContractStatus.DECLINED,
549
+ ContractStatus.CANCELLED
550
+ ],
551
+ [ContractStatus.SENT]: [
552
+ ContractStatus.ACCEPTED,
553
+ ContractStatus.DECLINED,
554
+ ContractStatus.CANCELLED
555
+ ],
556
+ [ContractStatus.ACCEPTED]: [ContractStatus.COMPLETED, ContractStatus.CANCELLED],
557
+ [ContractStatus.DECLINED]: [],
558
+ [ContractStatus.COMPLETED]: [],
559
+ [ContractStatus.CANCELLED]: []
560
+ };
561
+ var loadedContractStatus = /* @__PURE__ */ new WeakMap();
562
+ function classifyContractPurchase(contract) {
563
+ return "vendorId" in contract;
564
+ }
565
+ function isContractSnapshotReportEligible(contract) {
566
+ return contract.status !== ContractStatus.DRAFT && contract.status !== ContractStatus.CANCELLED;
567
+ }
568
+ var Contract = class extends SmrtObject {
569
+ /** @internal Collection read guard returns this snapshot on its root DB. */
570
+ rebindMoneyRead(capability, db) {
571
+ if (capability !== DOCUMENT_MONEY_READ_REBIND) throw new Error("Contract read rebinding requires the owning capability");
572
+ this._db = db;
573
+ this.options.db = db;
574
+ return this;
575
+ }
576
+ tenantId = null;
577
+ contractType = ContractType.ORDER;
578
+ status = ContractStatus.DRAFT;
579
+ subtotal = 0;
580
+ /** Informational sum of resolved line discounts, in integer minor units. */
581
+ discountTotal = 0;
582
+ taxAmount = 0;
583
+ totalAmount = 0;
584
+ currency = "USD";
585
+ /** Pricing interpretation fixed before the Contract leaves draft. */
586
+ lineAmountType = "exclusive";
587
+ /** Component rounding scope fixed before the Contract leaves draft. */
588
+ taxRoundingScope = "line";
589
+ /** Immutable seller registration snapshot once the Contract leaves draft. */
590
+ sellerTaxRegistrationsJson = "[]";
591
+ get sellerTaxRegistrations() {
592
+ try {
593
+ const value = JSON.parse(this.sellerTaxRegistrationsJson);
594
+ if (!Array.isArray(value)) return [];
595
+ return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.number === "string" && typeof entry.label === "string");
596
+ } catch {
597
+ return [];
598
+ }
599
+ }
600
+ set sellerTaxRegistrations(value) {
601
+ this.sellerTaxRegistrationsJson = JSON.stringify(value);
602
+ }
603
+ /** Immutable customer or vendor tax identifier snapshot after issue. */
604
+ partyTaxId = "";
605
+ /** Stable issue/freeze identity, independent of lifecycle updated_at. */
606
+ moneySnapshotId = "";
607
+ issueDate = /* @__PURE__ */ new Date();
608
+ dueDate = null;
609
+ expiryDate = null;
610
+ reference = "";
611
+ notes = "";
612
+ terms = "";
613
+ channelId = "";
614
+ constructor(options = {}) {
615
+ super(options);
616
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
617
+ if (options.contractType !== void 0) this.contractType = options.contractType;
618
+ if (options.status !== void 0) this.status = options.status;
619
+ if (options.subtotal !== void 0) this.subtotal = options.subtotal;
620
+ if (options.discountTotal !== void 0) this.discountTotal = options.discountTotal;
621
+ if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
622
+ if (options.totalAmount !== void 0) this.totalAmount = options.totalAmount;
623
+ if (options.currency !== void 0) this.currency = options.currency;
624
+ if (options.lineAmountType !== void 0) this.lineAmountType = options.lineAmountType;
625
+ if (options.taxRoundingScope !== void 0) this.taxRoundingScope = options.taxRoundingScope;
626
+ if (options.sellerTaxRegistrationsJson !== void 0) this.sellerTaxRegistrationsJson = options.sellerTaxRegistrationsJson;
627
+ if (options.sellerTaxRegistrations !== void 0) this.sellerTaxRegistrations = structuredClone(options.sellerTaxRegistrations);
628
+ if (options.partyTaxId !== void 0) this.partyTaxId = options.partyTaxId;
629
+ if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
630
+ if (options.issueDate !== void 0) this.issueDate = options.issueDate;
631
+ if (options.dueDate !== void 0) this.dueDate = options.dueDate;
632
+ if (options.expiryDate !== void 0) this.expiryDate = options.expiryDate;
633
+ if (options.reference !== void 0) this.reference = options.reference;
634
+ if (options.notes !== void 0) this.notes = options.notes;
635
+ if (options.terms !== void 0) this.terms = options.terms;
636
+ if (options.channelId !== void 0) this.channelId = options.channelId;
637
+ }
638
+ /**
639
+ * Check if contract is in draft state
640
+ */
641
+ isDraft() {
642
+ return this.status === ContractStatus.DRAFT;
643
+ }
644
+ /**
645
+ * Check if contract is accepted
646
+ */
647
+ isAccepted() {
648
+ return this.status === ContractStatus.ACCEPTED;
649
+ }
650
+ /**
651
+ * Check if contract is completed
652
+ */
653
+ isCompleted() {
654
+ return this.status === ContractStatus.COMPLETED;
655
+ }
656
+ /**
657
+ * Check if contract is expired (for estimates)
658
+ */
659
+ isExpired() {
660
+ if (!this.expiryDate) return false;
661
+ return /* @__PURE__ */ new Date() > this.expiryDate;
662
+ }
663
+ /**
664
+ * Check if contract is overdue
665
+ */
666
+ isOverdue() {
667
+ if (!this.dueDate) return false;
668
+ if (this.status === ContractStatus.COMPLETED) return false;
669
+ return /* @__PURE__ */ new Date() > this.dueDate;
670
+ }
671
+ /**
672
+ * Calculate and update totals from line items
673
+ */
674
+ async recalculateTotals() {
675
+ const { recalculateContractTotals } = await import("../index.js").then((n) => n.t);
676
+ await recalculateContractTotals(this);
677
+ }
678
+ /**
679
+ * Capture the status the row was loaded with so the save-time transition
680
+ * guard can reject illegal status flips made via raw field assignment
681
+ * (mass-assignment on the generated update route, a stale caller, etc.).
682
+ * Only persisted rows carry a prior status.
683
+ */
684
+ async initialize() {
685
+ await super.initialize();
686
+ if (await this.isSaved()) loadedContractStatus.set(this, this.status);
687
+ return this;
688
+ }
689
+ /**
690
+ * Validate the amounts and the status transition before persisting, then
691
+ * save. Inherited by every STI subtype (Estimate/Order/Lease/.../LicenseSale),
692
+ * so a forged `status` on any of them is caught here. LicenseSale layers its
693
+ * own rights-immutability guard on top via `super.save()`. See S5 audit
694
+ * #1390.
695
+ *
696
+ * `subtotal` / `taxAmount` / `totalAmount` are mass-assignable on the
697
+ * generated create/update routes, so the minor-units check runs on every
698
+ * write surface (#2401).
699
+ */
700
+ async save() {
701
+ const { saveContract } = await import("../index.js").then((n) => n.t);
702
+ return await saveContract(this);
703
+ }
704
+ /** @internal Contract aggregate persistence only. */
705
+ async saveDerived(capability) {
706
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Contract money write capability is required");
707
+ assertIntegerMinorUnits("Contract", this.reference || this.id || "<new>", [
708
+ ["subtotal", this.subtotal],
709
+ ["discountTotal", this.discountTotal],
710
+ ["taxAmount", this.taxAmount],
711
+ ["totalAmount", this.totalAmount]
712
+ ]);
713
+ const prior = await this.resolvePriorStatus();
714
+ this.assertContractStatusTransition(prior);
715
+ const result = await super.save();
716
+ loadedContractStatus.set(this, this.status);
717
+ return result;
718
+ }
719
+ /**
720
+ * Reject an illegal status flip done via raw assignment. No-op transitions
721
+ * and brand-new rows are always allowed.
722
+ */
723
+ assertContractStatusTransition(prior) {
724
+ if (prior === void 0) return;
725
+ if (prior === this.status) return;
726
+ if (!(CONTRACT_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Contract ${this.reference || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
727
+ }
728
+ /**
729
+ * Resolve the AUTHORITATIVE prior status (S5 audit #1390 round 5, codex
730
+ * MEDIUM#2). The WeakMap is only populated when {@link initialize} loaded the
731
+ * row from the DB; it is empty for an instance built via
732
+ * `collection.create({ id: <existing>, _skipLoad: true })` — the upsert path
733
+ * that lets a caller write onto an existing row without hydrating it. Trusting
734
+ * an empty WeakMap there would treat the write as a brand-new row and skip the
735
+ * transition guard entirely (a poisonable prior-state). Mirrors the
736
+ * authoritative-prior-load applied to Payment/PaymentIntent/Invoice/Payout.
737
+ *
738
+ * So when this instance carries an `id`, read the persisted row straight from
739
+ * the database and treat its `status` as the prior — a create-onto-existing is
740
+ * an update. Only when no row exists in the DB (truly new) do we fall back to
741
+ * the WeakMap (which is also empty then), i.e. `undefined` = genuinely new.
742
+ * The raw row's `status` column is single-word (no snake_case transform).
743
+ */
744
+ async resolvePriorStatus() {
745
+ if (this.id) try {
746
+ const row = await this.db.get(this.tableName, { id: this.id });
747
+ if (row && row.status != null) return row.status;
748
+ } catch {}
749
+ return loadedContractStatus.get(this);
750
+ }
751
+ };
752
+ __publicField$1(Contract, "__smrtPackage__", "@happyvertical/smrt-commerce");
753
+ /**
754
+ * STI discriminator field
755
+ */
756
+ __publicField$1(Contract, "_stiField", "contractType");
757
+ __decorateClass$1([tenantId({ nullable: true })], Contract.prototype, "tenantId", 2);
758
+ __decorateClass$1([field({ description: "The kind of contract: order, estimate, lease, agreement and so on." })], Contract.prototype, "contractType", 2);
759
+ __decorateClass$1([field({ description: "Where the order or agreement stands, for example draft, sent, accepted or completed." })], Contract.prototype, "status", 2);
760
+ __decorateClass$1([field({
761
+ description: "The amount before tax.",
762
+ ui: { format: "money" }
763
+ })], Contract.prototype, "subtotal", 2);
764
+ __decorateClass$1([field({
765
+ description: "The tax charged.",
766
+ ui: { format: "money" }
767
+ })], Contract.prototype, "taxAmount", 2);
768
+ __decorateClass$1([field({
769
+ description: "The full amount, tax included.",
770
+ ui: { format: "money" }
771
+ })], Contract.prototype, "totalAmount", 2);
772
+ __decorateClass$1([field({
773
+ description: "The currency of the amounts.",
774
+ ui: { widget: "currency" }
775
+ })], Contract.prototype, "currency", 2);
776
+ __decorateClass$1([field({ description: "The date it was issued or placed." })], Contract.prototype, "issueDate", 2);
777
+ __decorateClass$1([field({ description: "The date payment or delivery is due." })], Contract.prototype, "dueDate", 2);
778
+ __decorateClass$1([field({ description: "The last day the price or offer is good for." })], Contract.prototype, "expiryDate", 2);
779
+ __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);
780
+ __decorateClass$1([field({
781
+ description: "Anything worth remembering about this.",
782
+ ui: { widget: "textarea" }
783
+ })], Contract.prototype, "notes", 2);
784
+ __decorateClass$1([field({
785
+ description: "The conditions agreed, such as deposits, payment or delivery conditions.",
786
+ ui: { widget: "textarea" }
787
+ })], Contract.prototype, "terms", 2);
788
+ __decorateClass$1([field({ description: "Where it came from, for example your shop, a marketplace or a phone call." })], Contract.prototype, "channelId", 2);
789
+ Contract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
790
+ tableStrategy: "sti",
791
+ api: { include: [
792
+ "list",
793
+ "get",
794
+ "create",
795
+ "update"
796
+ ] },
797
+ mcp: { include: ["list", "get"] },
798
+ cli: { skipApiCheck: true }
799
+ })], Contract);
800
+ var SalesContract = class extends Contract {
801
+ customerId = null;
802
+ constructor(options = {}) {
803
+ super(options);
804
+ if (options.customerId !== void 0) this.customerId = options.customerId;
805
+ }
806
+ };
807
+ __publicField$1(SalesContract, "__smrtPackage__", "@happyvertical/smrt-commerce");
808
+ __decorateClass$1([foreignKey(Customer, { description: "The customer a sales order or agreement is for." })], SalesContract.prototype, "customerId", 2);
809
+ SalesContract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({ abstract: true })], SalesContract);
810
+ var PurchaseContract = class extends Contract {
811
+ vendorId = null;
812
+ constructor(options = {}) {
813
+ super(options);
814
+ if (options.vendorId !== void 0) this.vendorId = options.vendorId;
815
+ }
816
+ };
817
+ __publicField$1(PurchaseContract, "__smrtPackage__", "@happyvertical/smrt-commerce");
818
+ __decorateClass$1([foreignKey(Vendor, { description: "The vendor a purchase order is placed with." })], PurchaseContract.prototype, "vendorId", 2);
819
+ PurchaseContract = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({ abstract: true })], PurchaseContract);
820
+ var Estimate = class extends SalesContract {
821
+ customerId = null;
822
+ contractType = ContractType.ESTIMATE;
823
+ constructor(options = {}) {
824
+ super(options);
825
+ if (options.customerId !== void 0) this.customerId = options.customerId;
826
+ }
827
+ };
828
+ __publicField$1(Estimate, "__smrtPackage__", "@happyvertical/smrt-commerce");
829
+ __decorateClass$1([foreignKey(Customer, { required: true })], Estimate.prototype, "customerId", 2);
830
+ Estimate = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Estimate);
831
+ var Order = class extends SalesContract {
832
+ customerId = null;
833
+ contractType = ContractType.ORDER;
834
+ constructor(options = {}) {
835
+ super(options);
836
+ if (options.customerId !== void 0) this.customerId = options.customerId;
837
+ }
838
+ };
839
+ __publicField$1(Order, "__smrtPackage__", "@happyvertical/smrt-commerce");
840
+ __decorateClass$1([foreignKey(Customer, { required: true })], Order.prototype, "customerId", 2);
841
+ Order = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Order);
842
+ var Lease = class extends SalesContract {
843
+ customerId = null;
844
+ contractType = ContractType.LEASE;
845
+ constructor(options = {}) {
846
+ super(options);
847
+ if (options.customerId !== void 0) this.customerId = options.customerId;
848
+ }
849
+ };
850
+ __publicField$1(Lease, "__smrtPackage__", "@happyvertical/smrt-commerce");
851
+ __decorateClass$1([foreignKey(Customer, { required: true })], Lease.prototype, "customerId", 2);
852
+ Lease = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Lease);
853
+ var Agreement = class extends SalesContract {
854
+ customerId = null;
855
+ contractType = ContractType.AGREEMENT;
856
+ constructor(options = {}) {
857
+ super(options);
858
+ if (options.customerId !== void 0) this.customerId = options.customerId;
859
+ }
860
+ };
861
+ __publicField$1(Agreement, "__smrtPackage__", "@happyvertical/smrt-commerce");
862
+ __decorateClass$1([foreignKey(Customer, { required: true })], Agreement.prototype, "customerId", 2);
863
+ Agreement = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Agreement);
864
+ var PurchaseOrder = class extends PurchaseContract {
865
+ vendorId = null;
866
+ contractType = ContractType.PURCHASE_ORDER;
867
+ expectedDate = "";
868
+ shipTo = "";
869
+ /** Construct a purchase order with its optional delivery header. */
870
+ constructor(options = {}) {
871
+ super(options);
872
+ if (options.vendorId !== void 0) this.vendorId = options.vendorId;
873
+ if (options.expectedDate !== void 0) this.expectedDate = options.expectedDate;
874
+ if (options.shipTo !== void 0) this.shipTo = options.shipTo;
875
+ }
876
+ };
877
+ __publicField$1(PurchaseOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
878
+ __decorateClass$1([foreignKey(Vendor, { required: true })], PurchaseOrder.prototype, "vendorId", 2);
879
+ __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);
880
+ __decorateClass$1([field({ description: "Where this order should be delivered, including any delivery instructions." })], PurchaseOrder.prototype, "shipTo", 2);
881
+ PurchaseOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], PurchaseOrder);
882
+ var WholesaleOrder = class extends SalesContract {
883
+ customerId = null;
884
+ contractType = ContractType.WHOLESALE_ORDER;
885
+ constructor(options = {}) {
886
+ super(options);
887
+ if (options.customerId !== void 0) this.customerId = options.customerId;
888
+ }
889
+ };
890
+ __publicField$1(WholesaleOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
891
+ __decorateClass$1([foreignKey(Customer, { required: true })], WholesaleOrder.prototype, "customerId", 2);
892
+ WholesaleOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], WholesaleOrder);
893
+ var ProductionOrder = class extends PurchaseContract {
894
+ contractType = ContractType.PRODUCTION_ORDER;
895
+ /**
896
+ * Plain string reference to the upstream product the order is asked
897
+ * to make. Cross-package id; never `@foreignKey()`. Required by
898
+ * `ProductionService.consumeMaterials` unless `bomId` is supplied.
899
+ */
900
+ productId = "";
901
+ /**
902
+ * Optional explicit BOM revision the order locked to at posting
903
+ * time. When set, manufacturing uses this exact row even if the
904
+ * product's active BOM has since changed. When empty, falls back
905
+ * to the active BOM for `productId`.
906
+ */
907
+ bomId = "";
908
+ };
909
+ __publicField$1(ProductionOrder, "__smrtPackage__", "@happyvertical/smrt-commerce");
910
+ ProductionOrder = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], ProductionOrder);
911
+ var Cart = class extends SalesContract {
912
+ contractType = ContractType.CART;
913
+ };
914
+ __publicField$1(Cart, "__smrtPackage__", "@happyvertical/smrt-commerce");
915
+ Cart = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], Cart);
916
+ var issuedRightsSnapshot = /* @__PURE__ */ new WeakMap();
917
+ var LicenseSale = class extends SalesContract {
918
+ contractType = ContractType.LICENSE_SALE;
919
+ /**
920
+ * Plain string reference to the upstream `Sku` (from
921
+ * `@happyvertical/smrt-products`) that this license was sold against.
922
+ * Cross-package id; never `@foreignKey()`.
923
+ */
924
+ skuId = "";
925
+ /**
926
+ * Plain string reference to the `Payment` that satisfied the
927
+ * purchase. Cross-model reference matches the rest of the
928
+ * package's convention.
929
+ */
930
+ paymentId = "";
931
+ /**
932
+ * The licensee's email address — the primary identity carried
933
+ * forward into PDF generation and any downstream hash-registry
934
+ * publication. Required; without it, downstream rights cannot be
935
+ * attributed.
936
+ */
937
+ licenseeEmail = "";
938
+ /**
939
+ * Optional legal-entity name of the licensee for purchases that
940
+ * warrant it (typically high-tier B2B licenses). Empty string for
941
+ * individual-buyer licenses where the email *is* the legal identity.
942
+ */
943
+ licenseeLegalEntity = "";
944
+ /**
945
+ * Optional ISO-3166 jurisdiction code of the licensee, captured for
946
+ * tax / disclosure / governing-law purposes. Empty when not
947
+ * required.
948
+ */
949
+ licenseeJurisdiction = "";
950
+ /** Permitted media — `web,print,social`, `broadcast`, etc. */
951
+ rightsMedium = "";
952
+ /** Distribution scope — `worldwide`, `territorial`, `private`, etc. */
953
+ rightsDistributionScope = "";
954
+ /** Exclusivity — `exclusive` / `non-exclusive` / `co-exclusive`. */
955
+ rightsExclusivity = "";
956
+ /** Duration — `perpetual` / `12-months` / `event-only`, etc. */
957
+ rightsDuration = "";
958
+ /** Territory — ISO-3166 list, `worldwide`, region name, etc. */
959
+ rightsTerritory = "";
960
+ /** Whether the licensee may sublicense the rights. */
961
+ rightsSublicensing = false;
962
+ /** Whether the licensee may create derivative works. */
963
+ rightsDerivatives = false;
964
+ /** URL of the signed PDF (typically a cloud-storage public URL). */
965
+ pdfUrl = "";
966
+ /**
967
+ * SHA-256 of the signed PDF bytes (hex string, no `0x` prefix).
968
+ * Lets downstream consumers verify the PDF the licensee holds is
969
+ * the one this row was issued for, without re-fetching.
970
+ */
971
+ pdfHash = "";
972
+ /**
973
+ * Optional reference to an on-chain hash-registry entry — set when
974
+ * the LicenseSale's `pdfHash` has been anchored on a public chain
975
+ * for tamper-evidence. Format is whatever the hash-registry
976
+ * service emits (e.g. `chain:tx-hash:log-index`). Empty for
977
+ * deployments that don't publish hashes on-chain.
978
+ */
979
+ onChainHashRegistryRef = "";
980
+ constructor(options = {}) {
981
+ super(options);
982
+ if (options.skuId !== void 0) this.skuId = options.skuId;
983
+ if (options.paymentId !== void 0) this.paymentId = options.paymentId;
984
+ if (options.licenseeEmail !== void 0) this.licenseeEmail = options.licenseeEmail;
985
+ if (options.licenseeLegalEntity !== void 0) this.licenseeLegalEntity = options.licenseeLegalEntity;
986
+ if (options.licenseeJurisdiction !== void 0) this.licenseeJurisdiction = options.licenseeJurisdiction;
987
+ if (options.rightsMedium !== void 0) this.rightsMedium = options.rightsMedium;
988
+ if (options.rightsDistributionScope !== void 0) this.rightsDistributionScope = options.rightsDistributionScope;
989
+ if (options.rightsExclusivity !== void 0) this.rightsExclusivity = options.rightsExclusivity;
990
+ if (options.rightsDuration !== void 0) this.rightsDuration = options.rightsDuration;
991
+ if (options.rightsTerritory !== void 0) this.rightsTerritory = options.rightsTerritory;
992
+ if (options.rightsSublicensing !== void 0) this.rightsSublicensing = options.rightsSublicensing;
993
+ if (options.rightsDerivatives !== void 0) this.rightsDerivatives = options.rightsDerivatives;
994
+ if (options.pdfUrl !== void 0) this.pdfUrl = options.pdfUrl;
995
+ if (options.pdfHash !== void 0) this.pdfHash = options.pdfHash;
996
+ if (options.onChainHashRegistryRef !== void 0) this.onChainHashRegistryRef = options.onChainHashRegistryRef;
997
+ }
998
+ /**
999
+ * Once `super.initialize()` has applied the framework's option-
1000
+ * override pass, capture the rights snapshot if (and only if) the
1001
+ * row arrived already issued. That's how the immutability guard
1002
+ * tells "freshly drafting a license" (snapshot still mutable) apart
1003
+ * from "loaded an already-issued row" (snapshot frozen).
1004
+ */
1005
+ async initialize() {
1006
+ await super.initialize();
1007
+ if (this.status === ContractStatus.ACCEPTED && await this.isSaved()) issuedRightsSnapshot.set(this, this.serializeRightsSnapshot());
1008
+ return this;
1009
+ }
1010
+ /**
1011
+ * Return the current rights snapshot as a typed object. Read-only
1012
+ * for callers — mutation should go through assignment to the
1013
+ * individual `rights*` fields, gated by the issued-immutability
1014
+ * guard.
1015
+ */
1016
+ getRightsSnapshot() {
1017
+ return {
1018
+ medium: this.rightsMedium,
1019
+ distributionScope: this.rightsDistributionScope,
1020
+ exclusivity: this.rightsExclusivity,
1021
+ duration: this.rightsDuration,
1022
+ territory: this.rightsTerritory,
1023
+ sublicensing: this.rightsSublicensing,
1024
+ derivatives: this.rightsDerivatives
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Revoke an issued license — moves status to `CANCELLED` without
1029
+ * touching the rights snapshot (which stays frozen). Throws if
1030
+ * called on a non-issued license; a draft license should be
1031
+ * deleted, not revoked.
1032
+ */
1033
+ revoke() {
1034
+ 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`);
1035
+ this.status = ContractStatus.CANCELLED;
1036
+ }
1037
+ /**
1038
+ * Save with the immutability check: if the row was loaded with
1039
+ * `ACCEPTED` status, the rights snapshot at save time must match
1040
+ * the snapshot at load time. Anything else means an out-of-band
1041
+ * mutation crept in and we surface it instead of letting the row
1042
+ * drift silently.
1043
+ *
1044
+ * Newly-drafted licenses (where the row was constructed without
1045
+ * `ACCEPTED` status) capture their snapshot at the moment the
1046
+ * caller transitions to `ACCEPTED` and saves — `validateImmutable`
1047
+ * picks that up post-save on the next call.
1048
+ */
1049
+ async save() {
1050
+ this.validateImmutable();
1051
+ const result = await super.save();
1052
+ if (this.status === ContractStatus.ACCEPTED && !issuedRightsSnapshot.has(this)) issuedRightsSnapshot.set(this, this.serializeRightsSnapshot());
1053
+ return result;
1054
+ }
1055
+ /**
1056
+ * Compare the current rights against the captured snapshot
1057
+ * (populated either at load time by `initialize` or at first
1058
+ * issuance by `save`). Throws on mismatch.
1059
+ */
1060
+ validateImmutable() {
1061
+ const captured = issuedRightsSnapshot.get(this);
1062
+ if (!captured) return;
1063
+ 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.`);
1064
+ }
1065
+ serializeRightsSnapshot() {
1066
+ return JSON.stringify({
1067
+ medium: this.rightsMedium,
1068
+ distributionScope: this.rightsDistributionScope,
1069
+ exclusivity: this.rightsExclusivity,
1070
+ duration: this.rightsDuration,
1071
+ territory: this.rightsTerritory,
1072
+ sublicensing: this.rightsSublicensing,
1073
+ derivatives: this.rightsDerivatives
1074
+ });
1075
+ }
1076
+ };
1077
+ __publicField$1(LicenseSale, "__smrtPackage__", "@happyvertical/smrt-commerce");
1078
+ LicenseSale = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt()], LicenseSale);
1079
+ //#endregion
1080
+ //#region src/collections/PurchaseContractCollection.ts
1081
+ var PurchaseContractCollection = class extends SmrtCollection {
1082
+ static _itemClass = PurchaseContract;
1083
+ async findByVendor(vendorId) {
1084
+ return this.list({
1085
+ where: { vendorId },
1086
+ orderBy: "created_at DESC"
1087
+ });
1088
+ }
1089
+ };
1090
+ //#endregion
1091
+ //#region src/collections/SalesContractCollection.ts
1092
+ var SalesContractCollection = class extends SmrtCollection {
1093
+ static _itemClass = SalesContract;
1094
+ async findByCustomer(customerId) {
1095
+ return this.list({
1096
+ where: { customerId },
1097
+ orderBy: "created_at DESC"
1098
+ });
1099
+ }
1100
+ };
1101
+ //#endregion
1102
+ //#region src/collections/ContractCollection.ts
1103
+ var ContractCollection = class extends SmrtCollection {
1104
+ static _itemClass = Contract;
1105
+ async withMoneyRead(operation) {
1106
+ const rootDb = this.db;
1107
+ const tenantId = getTenantId();
1108
+ if (!tenantId) return operation();
1109
+ const result = await withTenantMoneyCollectionRead(rootDb, tenantId, (boundDb) => this.withDatabase(boundDb, operation));
1110
+ if (result instanceof Contract) result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
1111
+ else if (Array.isArray(result)) {
1112
+ for (const item of result) if (item instanceof Contract) item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
1113
+ }
1114
+ return result;
1115
+ }
1116
+ async get(filter, options = {}) {
1117
+ return this.withMoneyRead(() => super.get(filter, options));
1118
+ }
1119
+ async list(options = {}) {
1120
+ return this.withMoneyRead(() => super.list(options));
1121
+ }
1122
+ /**
1123
+ * Find contracts by customer
1124
+ *
1125
+ * @param customerId - Customer ID
1126
+ * @returns Array of contracts
1127
+ * @deprecated Use SalesContractCollection.findByCustomer.
1128
+ */
1129
+ async findByCustomer(customerId) {
1130
+ return (await SalesContractCollection.create({
1131
+ ...this.options,
1132
+ db: this.db
1133
+ })).findByCustomer(customerId);
1134
+ }
1135
+ /**
1136
+ * Find contracts by vendor
1137
+ *
1138
+ * @param vendorId - Vendor ID
1139
+ * @returns Array of contracts
1140
+ * @deprecated Use PurchaseContractCollection.findByVendor.
1141
+ */
1142
+ async findByVendor(vendorId) {
1143
+ return (await PurchaseContractCollection.create({
1144
+ ...this.options,
1145
+ db: this.db
1146
+ })).findByVendor(vendorId);
1147
+ }
1148
+ /**
1149
+ * Hydrate a bounded tenant/date report candidate set once, retaining exact
1150
+ * STI descendants so callers can classify custom purchase types safely.
1151
+ */
1152
+ async listSnapshotReportContracts(options) {
1153
+ return (await this.list({
1154
+ where: {
1155
+ tenantId: options.tenantId,
1156
+ "issueDate >=": options.from.toISOString(),
1157
+ "issueDate <=": options.to.toISOString()
1158
+ },
1159
+ orderBy: "issueDate ASC"
1160
+ })).filter(isContractSnapshotReportEligible);
1161
+ }
1162
+ /**
1163
+ * Find contracts by type (STI)
1164
+ *
1165
+ * @param contractType - Contract type
1166
+ * @returns Array of contracts
1167
+ */
1168
+ async findByType(contractType) {
1169
+ return await this.list({
1170
+ where: { contractType },
1171
+ orderBy: "created_at DESC"
1172
+ });
1173
+ }
1174
+ /**
1175
+ * Find contracts by status
1176
+ *
1177
+ * @param status - Contract status
1178
+ * @returns Array of contracts
1179
+ */
1180
+ async findByStatus(status) {
1181
+ return await this.list({
1182
+ where: { status },
1183
+ orderBy: "created_at DESC"
1184
+ });
1185
+ }
1186
+ /**
1187
+ * Find all draft contracts
1188
+ *
1189
+ * @returns Array of draft contracts
1190
+ */
1191
+ async findDrafts() {
1192
+ return await this.findByStatus(ContractStatus.DRAFT);
1193
+ }
1194
+ /**
1195
+ * Find all accepted contracts
1196
+ *
1197
+ * @returns Array of accepted contracts
1198
+ */
1199
+ async findAccepted() {
1200
+ return await this.findByStatus(ContractStatus.ACCEPTED);
1201
+ }
1202
+ /**
1203
+ * Find overdue contracts
1204
+ *
1205
+ * @returns Array of overdue contracts
1206
+ */
1207
+ async findOverdue() {
1208
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1209
+ return (await this.list({
1210
+ where: { "dueDate <": now },
1211
+ orderBy: "dueDate ASC"
1212
+ })).filter((c) => c.status !== ContractStatus.COMPLETED && c.status !== ContractStatus.CANCELLED);
1213
+ }
1214
+ /**
1215
+ * Find expired estimates/quotes
1216
+ *
1217
+ * @returns Array of expired estimates
1218
+ */
1219
+ async findExpired() {
1220
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1221
+ return (await this.list({
1222
+ where: {
1223
+ contractType: ContractType.ESTIMATE,
1224
+ "expiryDate <": now
1225
+ },
1226
+ orderBy: "expiryDate ASC"
1227
+ })).filter((c) => c.status !== ContractStatus.ACCEPTED && c.status !== ContractStatus.DECLINED && c.status !== ContractStatus.CANCELLED);
1228
+ }
1229
+ /**
1230
+ * Find contracts in date range
1231
+ *
1232
+ * @param startDate - Start date
1233
+ * @param endDate - End date
1234
+ * @returns Array of contracts
1235
+ */
1236
+ async findByDateRange(startDate, endDate) {
1237
+ return await this.list({
1238
+ where: {
1239
+ "issueDate >=": startDate.toISOString(),
1240
+ "issueDate <=": endDate.toISOString()
1241
+ },
1242
+ orderBy: "issueDate DESC"
1243
+ });
1244
+ }
1245
+ /**
1246
+ * Find all contracts belonging to a specific tenant
1247
+ *
1248
+ * @param tenantId - Tenant ID
1249
+ * @returns Array of contracts for the tenant
1250
+ */
1251
+ async findByTenant(tenantId) {
1252
+ return this.list({ where: { tenantId } });
1253
+ }
1254
+ /**
1255
+ * Find all global contracts (not associated with any tenant).
1256
+ *
1257
+ * Routes through the shared tenant-global helper so it does not throw under
1258
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1259
+ * flagged as an isolation violation). (#1600)
1260
+ *
1261
+ * @returns Array of global contracts
1262
+ */
1263
+ async findGlobal() {
1264
+ return queryGlobal(this);
1265
+ }
1266
+ /**
1267
+ * Find contracts for a tenant including global contracts.
1268
+ *
1269
+ * Fails closed if an active tenant context requests a different tenant's
1270
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1271
+ *
1272
+ * @param tenantId - Tenant ID
1273
+ * @returns Array of tenant-specific and global contracts
1274
+ */
1275
+ async findWithGlobals(tenantId) {
1276
+ return queryWithGlobals(this, tenantId, "Contract.findWithGlobals");
1277
+ }
1278
+ };
1279
+ //#endregion
1280
+ //#region src/models/DocumentTax.ts
1281
+ var __defProp = Object.defineProperty;
1282
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1283
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
1284
+ enumerable: true,
1285
+ configurable: true,
1286
+ writable: true,
1287
+ value
1288
+ }) : obj[key] = value;
1289
+ var __decorateClass = (decorators, target, key, kind) => {
1290
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1291
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1292
+ if (kind && result) __defProp(target, key, result);
1293
+ return result;
1294
+ };
1295
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1296
+ var DocumentTax = class extends SmrtObject {
1297
+ tenantId = "";
1298
+ documentType = "invoice";
1299
+ documentId = "";
1300
+ currency = "USD";
1301
+ taxCodeId = null;
1302
+ taxCodeCode = null;
1303
+ kind = null;
1304
+ taxComponentId = null;
1305
+ taxComponentName = null;
1306
+ percent = null;
1307
+ liabilityAccountId = null;
1308
+ taxableMinor = 0;
1309
+ taxMinor = 0;
1310
+ recoverableMinor = 0;
1311
+ constructor(options = {}) {
1312
+ super(options);
1313
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1314
+ if (options.documentType !== void 0) this.documentType = options.documentType;
1315
+ if (options.documentId !== void 0) this.documentId = options.documentId;
1316
+ if (options.currency !== void 0) this.currency = options.currency;
1317
+ if (options.taxCodeId !== void 0) this.taxCodeId = options.taxCodeId;
1318
+ if (options.taxCodeCode !== void 0) this.taxCodeCode = options.taxCodeCode;
1319
+ if (options.kind !== void 0) this.kind = options.kind;
1320
+ if (options.taxComponentId !== void 0) this.taxComponentId = options.taxComponentId;
1321
+ if (options.taxComponentName !== void 0) this.taxComponentName = options.taxComponentName;
1322
+ if (options.percent !== void 0) this.percent = options.percent;
1323
+ if (options.liabilityAccountId !== void 0) this.liabilityAccountId = options.liabilityAccountId;
1324
+ if (options.taxableMinor !== void 0) this.taxableMinor = options.taxableMinor;
1325
+ if (options.taxMinor !== void 0) this.taxMinor = options.taxMinor;
1326
+ if (options.recoverableMinor !== void 0) this.recoverableMinor = options.recoverableMinor;
1327
+ }
1328
+ assertValidDerivedRow() {
1329
+ if (![
1330
+ "invoice",
1331
+ "contract",
1332
+ "credit_note"
1333
+ ].includes(this.documentType)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown document tax type: ${this.documentType}`);
1334
+ for (const [label, value] of [
1335
+ ["tenantId", this.tenantId],
1336
+ ["documentId", this.documentId],
1337
+ ["currency", this.currency]
1338
+ ]) if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `DocumentTax ${label} must be a nonempty value`);
1339
+ for (const [label, value] of [
1340
+ ["taxableMinor", this.taxableMinor],
1341
+ ["taxMinor", this.taxMinor],
1342
+ ["recoverableMinor", this.recoverableMinor]
1343
+ ]) if (!Number.isSafeInteger(value)) throw new MoneyValidationError("UNSAFE_MONEY", `DocumentTax ${label} must be safe integer minor units`);
1344
+ if (this.taxCodeId === null) {
1345
+ 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");
1346
+ return;
1347
+ }
1348
+ if (!this.taxCodeCode || !this.kind) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Assigned DocumentTax rows require code and kind snapshots");
1349
+ if (this.taxComponentId === null) {
1350
+ 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");
1351
+ return;
1352
+ }
1353
+ if (this.kind !== "taxable" || !this.taxComponentName || !this.percent) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Component DocumentTax rows require taxable component snapshots");
1354
+ if (this.recoverableMinor !== 0 && this.recoverableMinor !== this.taxMinor) throw new MoneyValidationError("INVALID_MONEY_INPUT", "DocumentTax recoverableMinor must be zero or the component tax total");
1355
+ }
1356
+ async save() {
1357
+ throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax rows are derived and cannot be saved directly");
1358
+ }
1359
+ async delete(_options = {}) {
1360
+ throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax rows are derived and cannot be deleted directly");
1361
+ }
1362
+ /** @internal Aggregate persistence only. */
1363
+ async saveDerived(capability) {
1364
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
1365
+ this.assertValidDerivedRow();
1366
+ return await super.save();
1367
+ }
1368
+ /** @internal Aggregate persistence only. */
1369
+ async deleteDerived(capability, options = {}) {
1370
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
1371
+ await super.delete(options);
1372
+ }
1373
+ };
1374
+ __publicField(DocumentTax, "__smrtPackage__", "@happyvertical/smrt-commerce");
1375
+ __decorateClass([tenantId()], DocumentTax.prototype, "tenantId", 2);
1376
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "documentType", 2);
1377
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "documentId", 2);
1378
+ __decorateClass([field({ required: true })], DocumentTax.prototype, "currency", 2);
1379
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxCodeId", 2);
1380
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxCodeCode", 2);
1381
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "kind", 2);
1382
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxComponentId", 2);
1383
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "taxComponentName", 2);
1384
+ __decorateClass([field({
1385
+ type: "decimal",
1386
+ nullable: true
1387
+ })], DocumentTax.prototype, "percent", 2);
1388
+ __decorateClass([field({ nullable: true })], DocumentTax.prototype, "liabilityAccountId", 2);
1389
+ __decorateClass([field({ description: "Tax-exclusive base in integer minor units." })], DocumentTax.prototype, "taxableMinor", 2);
1390
+ __decorateClass([field({ description: "Tax total in integer minor units." })], DocumentTax.prototype, "taxMinor", 2);
1391
+ __decorateClass([field({ description: "Recoverable purchase tax in integer minor units." })], DocumentTax.prototype, "recoverableMinor", 2);
1392
+ DocumentTax = __decorateClass([TenantScoped({ mode: "required" }), smrt({
1393
+ tableName: "document_taxes",
1394
+ conflictColumns: [
1395
+ "document_type",
1396
+ "document_id",
1397
+ "tax_code_id",
1398
+ "tax_component_id"
1399
+ ],
1400
+ api: { include: ["list", "get"] },
1401
+ mcp: { include: ["list", "get"] },
1402
+ cli: { include: ["list", "get"] }
1403
+ })], DocumentTax);
1404
+ //#endregion
1405
+ //#region src/collections/DocumentTaxCollection.ts
1406
+ var DocumentTaxCollection_exports = /* @__PURE__ */ __exportAll({ DocumentTaxCollection: () => DocumentTaxCollection });
1407
+ var TAX_CODE_KINDS = /* @__PURE__ */ new Set([
1408
+ "taxable",
1409
+ "zero-rated",
1410
+ "exempt",
1411
+ "out-of-scope"
1412
+ ]);
1413
+ function requiredText(value, label) {
1414
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Invoice tax report ${label} must be a nonempty string.`);
1415
+ return value;
1416
+ }
1417
+ function storedMinor(value, label) {
1418
+ const parsed = typeof value === "bigint" ? Number(value) : typeof value === "number" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? Number(value) : NaN;
1419
+ if (!Number.isSafeInteger(parsed)) throw new RangeError(`Invoice tax report ${label} must be a safe integer minor-unit value.`);
1420
+ return parsed;
1421
+ }
1422
+ function reportDate(value, label) {
1423
+ const date = new Date(String(value));
1424
+ if (Number.isNaN(date.getTime())) throw new Error(`Invoice tax report ${label} must be a valid date.`);
1425
+ return date.toISOString();
1426
+ }
1427
+ function invoiceDocumentType(value) {
1428
+ if (value === "invoice" || value === "credit_note") return value;
1429
+ throw new Error("Invoice tax report document type must be invoice or credit_note.");
1430
+ }
1431
+ function invoiceKind(value) {
1432
+ if (value === "invoice" || value === "credit_note") return value;
1433
+ throw new Error("Invoice tax report kind must be invoice or credit_note.");
1434
+ }
1435
+ function contractDocumentType(value) {
1436
+ if (value === "contract") return value;
1437
+ throw new Error("Contract tax report document type must be contract.");
1438
+ }
1439
+ function taxCodeKind(value) {
1440
+ return typeof value === "string" && TAX_CODE_KINDS.has(value) ? value : null;
1441
+ }
1442
+ function assertReportFactOptions(options) {
1443
+ if (!options.tenantId?.trim()) throw new Error("Invoice tax report facts require a tenant.");
1444
+ if (!(options.from instanceof Date) || Number.isNaN(options.from.getTime())) throw new Error("Invoice tax report facts require a valid start date.");
1445
+ if (!(options.to instanceof Date) || Number.isNaN(options.to.getTime())) throw new Error("Invoice tax report facts require a valid end date.");
1446
+ if (options.from > options.to) throw new Error("Invoice tax report facts require an ordered date range.");
1447
+ if (options.currency !== void 0 && !options.currency.trim()) throw new Error("Invoice tax report currency must be nonempty when supplied.");
1448
+ }
1449
+ function compareCodeUnits(left, right) {
1450
+ return left < right ? -1 : left > right ? 1 : 0;
1451
+ }
1452
+ function identity(row) {
1453
+ return `${row.taxCodeId === null ? "0:" : `1:${row.taxCodeId}`}\0${row.taxComponentId === null ? "0:" : `1:${row.taxComponentId}`}`;
1454
+ }
1455
+ var BUSINESS_FIELDS = [
1456
+ "tenantId",
1457
+ "documentType",
1458
+ "documentId",
1459
+ "currency",
1460
+ "taxCodeId",
1461
+ "taxCodeCode",
1462
+ "kind",
1463
+ "taxComponentId",
1464
+ "taxComponentName",
1465
+ "percent",
1466
+ "liabilityAccountId",
1467
+ "taxableMinor",
1468
+ "taxMinor",
1469
+ "recoverableMinor"
1470
+ ];
1471
+ function unchanged(row, input) {
1472
+ return BUSINESS_FIELDS.every((field) => {
1473
+ const persisted = row[field];
1474
+ const proposed = input[field];
1475
+ if (field === "percent" && persisted !== null && proposed !== null) return String(persisted) === String(proposed);
1476
+ return (persisted ?? null) === (proposed ?? null);
1477
+ });
1478
+ }
1479
+ var DocumentTaxCollection = class extends SmrtCollection {
1480
+ static _itemClass = DocumentTax;
1481
+ /**
1482
+ * Read immutable commerce document-tax snapshots for one tenant.
1483
+ *
1484
+ * Invoice/credit rows join their shared storage by identity and tenant.
1485
+ * Purchase Contract rows first use the Contract owner's bounded hydrated
1486
+ * candidate API so custom PurchaseContract descendants retain their STI
1487
+ * classification. The query then joins only those candidate ids by identity
1488
+ * and tenant. Stored snapshots remain authoritative; this never recalculates
1489
+ * money or resolves present-day tax configuration.
1490
+ */
1491
+ async listReportFacts(options) {
1492
+ assertReportFactOptions(options);
1493
+ const [invoiceFacts, purchaseFacts] = await Promise.all([this.listInvoiceReportFacts(options), this.listPurchaseContractReportFacts(options)]);
1494
+ 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)));
1495
+ }
1496
+ async listInvoiceReportFacts(options) {
1497
+ const currencyClause = options.currency ? " AND d.currency = ?" : "";
1498
+ return (await this.db.query(`SELECT d.tenant_id, d.document_type, d.document_id, d.currency, d.tax_code_id, d.kind,
1499
+ d.tax_component_id, d.tax_component_name, d.liability_account_id,
1500
+ d.taxable_minor, d.tax_minor, d.recoverable_minor,
1501
+ i.kind AS invoice_kind, i.invoice_number, i.customer_id, i.issue_date, i.status
1502
+ FROM document_taxes d
1503
+ JOIN invoices i
1504
+ ON CAST(i.id AS TEXT) = CAST(d.document_id AS TEXT)
1505
+ AND i.tenant_id = d.tenant_id
1506
+ WHERE CAST(d.tenant_id AS TEXT) = ?
1507
+ AND CAST(i.tenant_id AS TEXT) = ?
1508
+ AND (
1509
+ (d.document_type = 'invoice' AND i.kind = 'invoice')
1510
+ OR (d.document_type = 'credit_note' AND i.kind = 'credit_note')
1511
+ )
1512
+ AND i.status NOT IN ('draft', 'cancelled')
1513
+ AND i.issue_date >= ?
1514
+ AND i.issue_date <= ?${currencyClause}
1515
+ 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) => ({
1516
+ tenantId: requiredText(row.tenant_id, "tenant id"),
1517
+ documentType: invoiceDocumentType(row.document_type),
1518
+ documentId: requiredText(row.document_id, "document id"),
1519
+ documentNumber: requiredText(row.invoice_number, "invoice number"),
1520
+ partyId: typeof row.customer_id === "string" && row.customer_id.trim() ? row.customer_id : null,
1521
+ documentKind: invoiceKind(row.invoice_kind),
1522
+ issueDate: reportDate(row.issue_date, "issue date"),
1523
+ status: requiredText(row.status, "invoice status"),
1524
+ direction: "sales",
1525
+ currency: requiredText(row.currency, "currency"),
1526
+ taxCodeId: row.tax_code_id == null ? null : String(row.tax_code_id),
1527
+ taxCodeKind: taxCodeKind(row.kind),
1528
+ taxableMinor: storedMinor(row.taxable_minor, "taxable minor"),
1529
+ taxMinor: storedMinor(row.tax_minor, "tax minor"),
1530
+ recoverableMinor: storedMinor(row.recoverable_minor, "recoverable minor"),
1531
+ taxComponentId: row.tax_component_id == null ? null : String(row.tax_component_id),
1532
+ componentName: row.tax_component_name == null ? null : String(row.tax_component_name),
1533
+ liabilityAccountId: row.liability_account_id == null ? null : String(row.liability_account_id),
1534
+ commitmentId: null,
1535
+ commitmentLineId: null
1536
+ }));
1537
+ }
1538
+ async listPurchaseContractReportFacts(options) {
1539
+ const purchases = (await withTenant({ tenantId: options.tenantId }, () => ContractCollection.create({ db: this.db }).then((collection) => collection.listSnapshotReportContracts({
1540
+ tenantId: options.tenantId,
1541
+ from: options.from,
1542
+ to: options.to
1543
+ })))).filter(classifyContractPurchase);
1544
+ const ids = purchases.flatMap((contract) => contract.id ? [contract.id] : []);
1545
+ if (ids.length === 0) return [];
1546
+ const byId = new Map(purchases.flatMap((contract) => contract.id ? [[contract.id, contract]] : []));
1547
+ const placeholders = ids.map(() => "?").join(", ");
1548
+ const currencyClause = options.currency ? " AND d.currency = ?" : "";
1549
+ return (await this.db.query(`SELECT d.tenant_id, d.document_type, d.document_id, d.currency, d.tax_code_id, d.kind,
1550
+ d.tax_component_id, d.tax_component_name, d.liability_account_id,
1551
+ d.taxable_minor, d.tax_minor, d.recoverable_minor
1552
+ FROM document_taxes d
1553
+ JOIN contracts c
1554
+ ON CAST(c.id AS TEXT) = CAST(d.document_id AS TEXT)
1555
+ AND c.tenant_id = d.tenant_id
1556
+ WHERE CAST(d.tenant_id AS TEXT) = ?
1557
+ AND CAST(c.tenant_id AS TEXT) = ?
1558
+ AND d.document_type = 'contract'
1559
+ AND c.status NOT IN ('draft', 'cancelled')
1560
+ AND CAST(d.document_id AS TEXT) IN (${placeholders})${currencyClause}
1561
+ 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) => {
1562
+ const documentId = requiredText(row.document_id, "contract id");
1563
+ const contract = byId.get(documentId);
1564
+ if (!contract) return [];
1565
+ return [{
1566
+ tenantId: requiredText(row.tenant_id, "tenant id"),
1567
+ documentType: contractDocumentType(row.document_type),
1568
+ documentId,
1569
+ documentNumber: contract.reference.trim() || documentId,
1570
+ partyId: "vendorId" in contract && typeof contract.vendorId === "string" && contract.vendorId.trim() ? contract.vendorId : null,
1571
+ documentKind: "contract",
1572
+ issueDate: reportDate(contract.issueDate, "contract issue date"),
1573
+ status: requiredText(contract.status, "contract status"),
1574
+ direction: "purchase",
1575
+ currency: requiredText(row.currency, "currency"),
1576
+ taxCodeId: row.tax_code_id == null ? null : String(row.tax_code_id),
1577
+ taxCodeKind: taxCodeKind(row.kind),
1578
+ taxableMinor: storedMinor(row.taxable_minor, "taxable minor"),
1579
+ taxMinor: storedMinor(row.tax_minor, "tax minor"),
1580
+ recoverableMinor: storedMinor(row.recoverable_minor, "recoverable minor"),
1581
+ taxComponentId: row.tax_component_id == null ? null : String(row.tax_component_id),
1582
+ componentName: row.tax_component_name == null ? null : String(row.tax_component_name),
1583
+ liabilityAccountId: row.liability_account_id == null ? null : String(row.liability_account_id),
1584
+ commitmentId: documentId,
1585
+ commitmentLineId: null
1586
+ }];
1587
+ });
1588
+ }
1589
+ async findByDocument(tenantId, documentType, documentId) {
1590
+ return this.list({
1591
+ where: {
1592
+ tenantId,
1593
+ documentType,
1594
+ documentId
1595
+ },
1596
+ orderBy: ["taxCodeId ASC", "taxComponentId ASC"]
1597
+ });
1598
+ }
1599
+ /**
1600
+ * Replace one draft document's derived rows on the caller's transaction.
1601
+ * Unchanged rows are not saved, preserving their exact persisted bytes.
1602
+ *
1603
+ * @internal Aggregate persistence only.
1604
+ */
1605
+ async replaceDerivedForDocument(capability, input) {
1606
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "DocumentTax write capability is required");
1607
+ const desired = /* @__PURE__ */ new Map();
1608
+ for (const row of input.rows) {
1609
+ 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");
1610
+ const key = identity(row);
1611
+ if (desired.has(key)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "DocumentTax replacement contains a duplicate code/component identity");
1612
+ desired.set(key, row);
1613
+ }
1614
+ const existing = await this.findByDocument(input.tenantId, input.documentType, input.documentId);
1615
+ const byIdentity = new Map(existing.map((row) => [identity(row), row]));
1616
+ const result = [];
1617
+ for (const [key, rowInput] of [...desired].sort(([left], [right]) => compareCodeUnits(left, right))) {
1618
+ const current = byIdentity.get(key);
1619
+ if (current) {
1620
+ byIdentity.delete(key);
1621
+ if (!unchanged(current, rowInput)) {
1622
+ Object.assign(current, rowInput);
1623
+ await current.saveDerived(capability);
1624
+ }
1625
+ result.push(current);
1626
+ continue;
1627
+ }
1628
+ const created = await this.createUnsaved(rowInput);
1629
+ await created.saveDerived(capability);
1630
+ result.push(created);
1631
+ }
1632
+ 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 });
1633
+ return result;
1634
+ }
1635
+ };
1636
+ //#endregion
1637
+ export { savePartyContacts as A, PARTY_CONTACT_MAX_LENGTHS as C, applyPartyContactIntent as D, PartyContactValidationError as E, normalizePartyContacts as O, DEFAULT_PARTY_CONTACT_FIELD_NAMES as S, PartyContactPayloadError as T, SalesContract as _, SalesContractCollection as a, Vendor as b, Cart as c, Lease as d, LicenseSale as f, PurchaseOrder as g, PurchaseContract as h, ContractCollection as i, validatePartyContactRows as j, readPartyContactRows as k, Contract as l, ProductionOrder as m, DocumentTaxCollection_exports as n, PurchaseContractCollection as o, Order as p, DocumentTax as r, Agreement as s, DocumentTaxCollection as t, Estimate as u, WholesaleOrder as v, PartyContactPartyNotFoundError as w, Customer as x, classifyContractPurchase as y };
1638
+
1639
+ //# sourceMappingURL=DocumentTaxCollection-BEpoqXAQ.js.map