@happyvertical/smrt-commerce 0.55.24 → 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 (220) hide show
  1. package/AGENTS.md +11 -9
  2. package/README.md +114 -2
  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/purchasing-ui.md +4 -3
  13. package/agents/quote-ui.md +3 -2
  14. package/agents/revenue-journal.md +23 -0
  15. package/agents/tax-codes.md +48 -0
  16. package/agents/tax-summary.md +126 -0
  17. package/dist/accounting-snapshot.d.ts +79 -0
  18. package/dist/accounting-snapshot.d.ts.map +1 -0
  19. package/dist/billing/runtime.d.ts +2 -0
  20. package/dist/billing/runtime.d.ts.map +1 -1
  21. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js +265 -0
  22. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +1 -0
  23. package/dist/chunks/CustomerCollection-DQIujn4f.js +99 -0
  24. package/dist/chunks/CustomerCollection-DQIujn4f.js.map +1 -0
  25. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js +1639 -0
  26. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +1 -0
  27. package/dist/chunks/InvoiceCollection-CKQflFjc.js +1047 -0
  28. package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +1 -0
  29. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +467 -0
  30. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +1 -0
  31. package/dist/chunks/TaxCodeCollection-DOpzW4y8.js +149 -0
  32. package/dist/chunks/TaxCodeCollection-DOpzW4y8.js.map +1 -0
  33. package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js +129 -0
  34. package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js.map +1 -0
  35. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +319 -0
  36. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +1 -0
  37. package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-DI3G16BN.js} +7 -12
  38. package/dist/chunks/allocation-integrity-DI3G16BN.js.map +1 -0
  39. package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
  40. package/dist/chunks/calculations-DaHR17il.js.map +1 -0
  41. package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
  42. package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
  43. package/dist/chunks/document-money-DNVhDJyC.js +860 -0
  44. package/dist/chunks/document-money-DNVhDJyC.js.map +1 -0
  45. package/dist/chunks/invoice-posting-BwV8g7z0.js +229 -0
  46. package/dist/chunks/invoice-posting-BwV8g7z0.js.map +1 -0
  47. package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
  48. package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
  49. package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
  50. package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
  51. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  52. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +135 -0
  53. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +1 -0
  54. package/dist/chunks/{types-zVnIjSOk.js → types-LDETvQ1C.js} +3 -14
  55. package/dist/chunks/types-LDETvQ1C.js.map +1 -0
  56. package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
  57. package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
  58. package/dist/collections/ContractCollection.d.ts +37 -1
  59. package/dist/collections/ContractCollection.d.ts.map +1 -1
  60. package/dist/collections/ContractLineItemCollection.d.ts +26 -1
  61. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  62. package/dist/collections/DocumentTaxCollection.d.ts +75 -0
  63. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
  64. package/dist/collections/InvoiceCollection.d.ts +26 -1
  65. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  66. package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
  67. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  68. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
  69. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
  70. package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
  71. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  72. package/dist/collections/PurchaseContractCollection.d.ts +8 -0
  73. package/dist/collections/PurchaseContractCollection.d.ts.map +1 -0
  74. package/dist/collections/SalesContractCollection.d.ts +8 -0
  75. package/dist/collections/SalesContractCollection.d.ts.map +1 -0
  76. package/dist/collections/TaxCodeCollection.d.ts +7 -0
  77. package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
  78. package/dist/collections/TaxComponentCollection.d.ts +6 -0
  79. package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
  80. package/dist/collections/TaxSettingsCollection.d.ts +16 -0
  81. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
  82. package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
  83. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
  84. package/dist/collections/index.d.ts +8 -0
  85. package/dist/collections/index.d.ts.map +1 -1
  86. package/dist/commerce-money-tools.d.ts +9 -0
  87. package/dist/commerce-money-tools.d.ts.map +1 -0
  88. package/dist/contract-money.d.ts +27 -0
  89. package/dist/contract-money.d.ts.map +1 -0
  90. package/dist/cost-billing.d.ts +0 -1
  91. package/dist/cost-billing.d.ts.map +1 -1
  92. package/dist/cost-billing.js +10 -15
  93. package/dist/cost-billing.js.map +1 -1
  94. package/dist/credit-allocation.d.ts +48 -0
  95. package/dist/credit-allocation.d.ts.map +1 -0
  96. package/dist/document-money-internal.d.ts +44 -0
  97. package/dist/document-money-internal.d.ts.map +1 -0
  98. package/dist/document-money.d.ts +85 -0
  99. package/dist/document-money.d.ts.map +1 -0
  100. package/dist/index.d.ts +16 -4
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +5563 -4243
  103. package/dist/index.js.map +1 -1
  104. package/dist/invoice-accounting-sync.d.ts +96 -0
  105. package/dist/invoice-accounting-sync.d.ts.map +1 -0
  106. package/dist/invoice-mcp.d.ts +16 -0
  107. package/dist/invoice-mcp.d.ts.map +1 -0
  108. package/dist/invoice-posting.d.ts +9 -0
  109. package/dist/invoice-posting.d.ts.map +1 -0
  110. package/dist/invoice-record-screen.d.ts +60 -0
  111. package/dist/invoice-record-screen.d.ts.map +1 -0
  112. package/dist/invoices.d.ts +2 -0
  113. package/dist/invoices.d.ts.map +1 -1
  114. package/dist/invoices.js +3 -2
  115. package/dist/manifest.json +11605 -4598
  116. package/dist/migrations/commerceLineAmounts.d.ts +84 -0
  117. package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
  118. package/dist/migrations/partyLinks.d.ts +39 -0
  119. package/dist/migrations/partyLinks.d.ts.map +1 -0
  120. package/dist/models/Contract.d.ts +61 -27
  121. package/dist/models/Contract.d.ts.map +1 -1
  122. package/dist/models/ContractLineItem.d.ts +30 -1
  123. package/dist/models/ContractLineItem.d.ts.map +1 -1
  124. package/dist/models/Customer.d.ts +7 -0
  125. package/dist/models/Customer.d.ts.map +1 -1
  126. package/dist/models/DocumentTax.d.ts +50 -0
  127. package/dist/models/DocumentTax.d.ts.map +1 -0
  128. package/dist/models/Invoice.d.ts +46 -2
  129. package/dist/models/Invoice.d.ts.map +1 -1
  130. package/dist/models/InvoiceLineItem.d.ts +33 -2
  131. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  132. package/dist/models/MoneyMigrationDocument.d.ts +28 -0
  133. package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
  134. package/dist/models/Payment.d.ts +5 -1
  135. package/dist/models/Payment.d.ts.map +1 -1
  136. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  137. package/dist/models/TaxCode.d.ts +22 -0
  138. package/dist/models/TaxCode.d.ts.map +1 -0
  139. package/dist/models/TaxComponent.d.ts +21 -0
  140. package/dist/models/TaxComponent.d.ts.map +1 -0
  141. package/dist/models/TaxSettings.d.ts +33 -0
  142. package/dist/models/TaxSettings.d.ts.map +1 -0
  143. package/dist/models/TaxSummaryReport.d.ts +35 -0
  144. package/dist/models/TaxSummaryReport.d.ts.map +1 -0
  145. package/dist/models/Vendor.d.ts +9 -0
  146. package/dist/models/Vendor.d.ts.map +1 -1
  147. package/dist/models/index.d.ts +7 -1
  148. package/dist/models/index.d.ts.map +1 -1
  149. package/dist/money-rational.d.ts +15 -0
  150. package/dist/money-rational.d.ts.map +1 -0
  151. package/dist/money.d.ts +106 -25
  152. package/dist/money.d.ts.map +1 -1
  153. package/dist/money.js +274 -0
  154. package/dist/money.js.map +1 -0
  155. package/dist/recipes.d.ts +17 -47
  156. package/dist/recipes.d.ts.map +1 -1
  157. package/dist/revenue-journal.d.ts +61 -0
  158. package/dist/revenue-journal.d.ts.map +1 -0
  159. package/dist/smrt-knowledge.json +6558 -1316
  160. package/dist/svelte/components/InvoiceActions.svelte +12 -0
  161. package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
  162. package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
  163. package/dist/svelte/components/InvoiceEditor.svelte +15 -1
  164. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  165. package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
  166. package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
  167. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
  168. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
  169. package/dist/svelte/components/InvoiceTotals.svelte +2 -2
  170. package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
  171. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
  172. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
  173. package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
  174. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
  175. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
  176. package/dist/svelte/components/QuoteEditor.svelte +54 -1
  177. package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
  178. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
  179. package/dist/svelte/i18n.d.ts +16 -0
  180. package/dist/svelte/i18n.d.ts.map +1 -1
  181. package/dist/svelte/i18n.js +16 -0
  182. package/dist/svelte/index.d.ts +7 -2
  183. package/dist/svelte/index.d.ts.map +1 -1
  184. package/dist/svelte/index.js +4 -1
  185. package/dist/svelte/invoices/calculations.d.ts +7 -2
  186. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  187. package/dist/svelte/invoices/calculations.js +141 -47
  188. package/dist/svelte/invoices/money-editor.d.ts +123 -0
  189. package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
  190. package/dist/svelte/invoices/money-editor.js +101 -0
  191. package/dist/svelte/invoices/types.d.ts +5 -1
  192. package/dist/svelte/invoices/types.d.ts.map +1 -1
  193. package/dist/svelte/purchasing-i18n.d.ts +5 -0
  194. package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
  195. package/dist/svelte/purchasing-i18n.js +5 -0
  196. package/dist/svelte/quote-i18n.d.ts +5 -0
  197. package/dist/svelte/quote-i18n.d.ts.map +1 -1
  198. package/dist/svelte/quote-i18n.js +5 -0
  199. package/dist/tax/cookbook.d.ts +22 -0
  200. package/dist/tax/cookbook.d.ts.map +1 -0
  201. package/dist/tax/example-pack.d.ts +25 -0
  202. package/dist/tax/example-pack.d.ts.map +1 -0
  203. package/dist/tax/identity.d.ts +5 -0
  204. package/dist/tax/identity.d.ts.map +1 -0
  205. package/dist/tax/index.d.ts +4 -0
  206. package/dist/tax/index.d.ts.map +1 -0
  207. package/dist/tax/legacy.d.ts +48 -0
  208. package/dist/tax/legacy.d.ts.map +1 -0
  209. package/dist/tax-summary-report-adapter.d.ts +37 -0
  210. package/dist/tax-summary-report-adapter.d.ts.map +1 -0
  211. package/dist/tax-summary.d.ts +129 -0
  212. package/dist/tax-summary.d.ts.map +1 -0
  213. package/dist/tenant-money-barrier.d.ts +66 -0
  214. package/dist/tenant-money-barrier.d.ts.map +1 -0
  215. package/dist/types/index.d.ts +145 -5
  216. package/dist/types/index.d.ts.map +1 -1
  217. package/package.json +19 -11
  218. package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
  219. package/dist/chunks/money-DfrRxrel.js.map +0 -1
  220. package/dist/chunks/types-zVnIjSOk.js.map +0 -1
@@ -0,0 +1,1047 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { s as InvoiceStatus } from "./types-LDETvQ1C.js";
3
+ import { o as minorToMajorUnits } from "./units-CNcjH_eE.js";
4
+ import { i as claimAllocationParent, r as assertAllocationParent } from "./allocation-integrity-DI3G16BN.js";
5
+ import { MoneyValidationError, assertIntegerMinorUnits } from "../money.js";
6
+ import { a as DOCUMENT_MONEY_READ_REBIND, o as DOCUMENT_MONEY_WRITE } from "./TaxSettingsCollection-BBTZDH1S.js";
7
+ import { i as withTenantMoneyCollectionRead } from "./tenant-money-barrier-DjRXgMnX.js";
8
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
9
+ import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
10
+ //#region src/models/Invoice.ts
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value
18
+ }) : obj[key] = value;
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
22
+ if (kind && result) __defProp(target, key, result);
23
+ return result;
24
+ };
25
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
26
+ var INVOICE_STATUS_TRANSITIONS = {
27
+ [InvoiceStatus.DRAFT]: [
28
+ InvoiceStatus.SENT,
29
+ InvoiceStatus.PARTIAL,
30
+ InvoiceStatus.PAID,
31
+ InvoiceStatus.CANCELLED,
32
+ InvoiceStatus.WRITTEN_OFF
33
+ ],
34
+ [InvoiceStatus.SENT]: [
35
+ InvoiceStatus.VIEWED,
36
+ InvoiceStatus.PARTIAL,
37
+ InvoiceStatus.PAID,
38
+ InvoiceStatus.OVERDUE,
39
+ InvoiceStatus.CANCELLED,
40
+ InvoiceStatus.WRITTEN_OFF
41
+ ],
42
+ [InvoiceStatus.VIEWED]: [
43
+ InvoiceStatus.SENT,
44
+ InvoiceStatus.PARTIAL,
45
+ InvoiceStatus.PAID,
46
+ InvoiceStatus.OVERDUE,
47
+ InvoiceStatus.CANCELLED,
48
+ InvoiceStatus.WRITTEN_OFF
49
+ ],
50
+ [InvoiceStatus.PARTIAL]: [
51
+ InvoiceStatus.DRAFT,
52
+ InvoiceStatus.SENT,
53
+ InvoiceStatus.VIEWED,
54
+ InvoiceStatus.PAID,
55
+ InvoiceStatus.OVERDUE,
56
+ InvoiceStatus.CANCELLED,
57
+ InvoiceStatus.WRITTEN_OFF
58
+ ],
59
+ [InvoiceStatus.OVERDUE]: [
60
+ InvoiceStatus.SENT,
61
+ InvoiceStatus.VIEWED,
62
+ InvoiceStatus.PARTIAL,
63
+ InvoiceStatus.PAID,
64
+ InvoiceStatus.CANCELLED,
65
+ InvoiceStatus.WRITTEN_OFF
66
+ ],
67
+ [InvoiceStatus.PAID]: [
68
+ InvoiceStatus.DRAFT,
69
+ InvoiceStatus.PARTIAL,
70
+ InvoiceStatus.VIEWED,
71
+ InvoiceStatus.SENT,
72
+ InvoiceStatus.OVERDUE,
73
+ InvoiceStatus.CANCELLED,
74
+ InvoiceStatus.WRITTEN_OFF
75
+ ],
76
+ [InvoiceStatus.CANCELLED]: [],
77
+ [InvoiceStatus.WRITTEN_OFF]: []
78
+ };
79
+ var loadedInvoiceStatus = /* @__PURE__ */ new WeakMap();
80
+ var Invoice = class extends SmrtObject {
81
+ /** @internal Collection read guard returns this snapshot on its root DB. */
82
+ rebindMoneyRead(capability, db) {
83
+ if (capability !== DOCUMENT_MONEY_READ_REBIND) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice read rebinding requires the owning capability");
84
+ this._db = db;
85
+ this.options.db = db;
86
+ return this;
87
+ }
88
+ kind = "invoice";
89
+ creditedInvoiceId = null;
90
+ /** Issued credit-note total cached under the source invoice lock. */
91
+ creditedAmount = 0;
92
+ tenantId = null;
93
+ customerId = "";
94
+ contractId = "";
95
+ invoiceNumber = "";
96
+ /**
97
+ * External reference (e.g., customer PO number)
98
+ */
99
+ reference = "";
100
+ issueDate = /* @__PURE__ */ new Date();
101
+ dueDate = /* @__PURE__ */ new Date();
102
+ paidDate = null;
103
+ subtotal = 0;
104
+ taxAmount = 0;
105
+ providerTaxAmount = 0;
106
+ totalAmount = 0;
107
+ amountPaid = 0;
108
+ currency = "CAD";
109
+ /** Pricing interpretation fixed for the document before issue. */
110
+ lineAmountType = "exclusive";
111
+ /** Component rounding scope fixed for the document before issue. */
112
+ taxRoundingScope = "line";
113
+ /** Informational sum of resolved line discounts, in minor units. */
114
+ discountTotal = 0;
115
+ /** Immutable seller registration snapshot once the invoice leaves draft. */
116
+ sellerTaxRegistrationsJson = "[]";
117
+ get sellerTaxRegistrations() {
118
+ try {
119
+ const value = JSON.parse(this.sellerTaxRegistrationsJson);
120
+ if (!Array.isArray(value)) return [];
121
+ return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.number === "string" && typeof entry.label === "string");
122
+ } catch {
123
+ return [];
124
+ }
125
+ }
126
+ set sellerTaxRegistrations(value) {
127
+ this.sellerTaxRegistrationsJson = JSON.stringify(value);
128
+ }
129
+ /** Immutable customer tax identifier snapshot once issued. */
130
+ partyTaxId = "";
131
+ /** Stable issue/freeze identity, independent of lifecycle updated_at. */
132
+ moneySnapshotId = "";
133
+ status = InvoiceStatus.DRAFT;
134
+ arJournalId = "";
135
+ arReversalJournalId = null;
136
+ revenueJournalId = "";
137
+ /**
138
+ * External ID in accounting provider (e.g., QBO invoice ID)
139
+ */
140
+ externalId = "";
141
+ /**
142
+ * Customer's external ID in accounting provider.
143
+ * Used to link invoice to customer in external system.
144
+ */
145
+ customerExternalId = "";
146
+ /**
147
+ * Accounting provider name ('quickbooks' | 'stripe' | etc.)
148
+ */
149
+ externalProvider = "";
150
+ /**
151
+ * When invoice was last synced to provider
152
+ */
153
+ syncedAt = null;
154
+ accountingSyncStateJson = "";
155
+ /**
156
+ * How the provider collects this invoice (#3190), decided once by billing
157
+ * period close before its first push and reused on every retry, so it is
158
+ * what the provider was asked for. Server-managed. A host must not offer
159
+ * another payment rail on a `charge_automatically` invoice the provider is
160
+ * still collecting.
161
+ */
162
+ collectionMethod = "";
163
+ /**
164
+ * When invoice was sent to customer
165
+ */
166
+ sentAt = null;
167
+ /**
168
+ * When customer viewed the invoice
169
+ */
170
+ viewedAt = null;
171
+ /**
172
+ * Number of payment reminders sent
173
+ */
174
+ remindersSent = 0;
175
+ /**
176
+ * When last reminder was sent
177
+ */
178
+ lastReminderAt = null;
179
+ notes = "";
180
+ customerNotes = "";
181
+ terms = "";
182
+ constructor(options = {}) {
183
+ super(options);
184
+ if (options.kind !== void 0) this.kind = options.kind;
185
+ if (options.creditedInvoiceId !== void 0) this.creditedInvoiceId = options.creditedInvoiceId;
186
+ if (options.creditedAmount !== void 0) this.creditedAmount = options.creditedAmount;
187
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
188
+ if (options.customerId !== void 0) this.customerId = options.customerId;
189
+ if (options.contractId !== void 0) this.contractId = options.contractId;
190
+ if (options.invoiceNumber !== void 0) this.invoiceNumber = options.invoiceNumber;
191
+ if (options.reference !== void 0) this.reference = options.reference;
192
+ if (options.issueDate !== void 0) this.issueDate = options.issueDate;
193
+ if (options.dueDate !== void 0) this.dueDate = options.dueDate;
194
+ if (options.paidDate !== void 0) this.paidDate = options.paidDate;
195
+ if (options.subtotal !== void 0) this.subtotal = options.subtotal;
196
+ if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
197
+ if (options.providerTaxAmount !== void 0) this.providerTaxAmount = options.providerTaxAmount;
198
+ if (options.totalAmount !== void 0) this.totalAmount = options.totalAmount;
199
+ if (options.amountPaid !== void 0) this.amountPaid = options.amountPaid;
200
+ if (options.currency !== void 0) this.currency = options.currency;
201
+ if (options.lineAmountType !== void 0) this.lineAmountType = options.lineAmountType;
202
+ if (options.taxRoundingScope !== void 0) this.taxRoundingScope = options.taxRoundingScope;
203
+ if (options.discountTotal !== void 0) this.discountTotal = options.discountTotal;
204
+ if (options.sellerTaxRegistrationsJson !== void 0) this.sellerTaxRegistrationsJson = options.sellerTaxRegistrationsJson;
205
+ if (options.sellerTaxRegistrations !== void 0) this.sellerTaxRegistrations = structuredClone(options.sellerTaxRegistrations);
206
+ if (options.partyTaxId !== void 0) this.partyTaxId = options.partyTaxId;
207
+ if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
208
+ if (options.status !== void 0) this.status = options.status;
209
+ if (options.arJournalId !== void 0) this.arJournalId = options.arJournalId;
210
+ if (options.arReversalJournalId !== void 0) this.arReversalJournalId = options.arReversalJournalId;
211
+ if (options.revenueJournalId !== void 0) this.revenueJournalId = options.revenueJournalId;
212
+ if (options.externalId !== void 0) this.externalId = options.externalId;
213
+ if (options.customerExternalId !== void 0) this.customerExternalId = options.customerExternalId;
214
+ if (options.externalProvider !== void 0) this.externalProvider = options.externalProvider;
215
+ if (options.syncedAt !== void 0) this.syncedAt = options.syncedAt;
216
+ if (options.accountingSyncStateJson !== void 0) this.accountingSyncStateJson = options.accountingSyncStateJson;
217
+ if (options.collectionMethod !== void 0) this.collectionMethod = options.collectionMethod;
218
+ if (options.sentAt !== void 0) this.sentAt = options.sentAt;
219
+ if (options.viewedAt !== void 0) this.viewedAt = options.viewedAt;
220
+ if (options.remindersSent !== void 0) this.remindersSent = options.remindersSent;
221
+ if (options.lastReminderAt !== void 0) this.lastReminderAt = options.lastReminderAt;
222
+ if (options.notes !== void 0) this.notes = options.notes;
223
+ if (options.customerNotes !== void 0) this.customerNotes = options.customerNotes;
224
+ if (options.terms !== void 0) this.terms = options.terms;
225
+ }
226
+ /**
227
+ * Capture the persisted status the row was loaded with, so the save-time
228
+ * transition guard can reject illegal status flips made via raw field
229
+ * assignment (mass-assignment on a generated update route, a stale caller,
230
+ * etc.). Only rows that already exist in the database carry a "prior"
231
+ * status — freshly-constructed (not-yet-saved) invoices have no prior and
232
+ * may start in any status.
233
+ */
234
+ async initialize() {
235
+ await super.initialize();
236
+ if (await this.isSaved()) loadedInvoiceStatus.set(this, this.status);
237
+ return this;
238
+ }
239
+ /**
240
+ * Recompute and validate financial fields before every write so forged
241
+ * totals, negative amounts, and illegal status flips can't be persisted via
242
+ * raw mass-assignment on the generated CRUD routes.
243
+ *
244
+ * Behaviour:
245
+ * - **Totals are authoritative from line items.** When the invoice is
246
+ * persisted and has line items, `subtotal` / `taxAmount` / `totalAmount`
247
+ * are recomputed from those items, overriding whatever the caller sent.
248
+ * This blocks "create real line items but claim a tiny total" forgery.
249
+ * - **Without line items**, the caller-supplied totals are accepted but the
250
+ * `totalAmount === subtotal + taxAmount` arithmetic invariant is enforced.
251
+ * - **amountPaid is derived/validated.** When persisted, it is recomputed
252
+ * from PaymentAllocations rather than trusted from the caller. It may
253
+ * never exceed `totalAmount`.
254
+ * - **Whole minor units and non-negativity** are enforced on all four
255
+ * amounts. The integer check runs FIRST (#2401), before the
256
+ * `totalAmount === subtotal + taxAmount` invariant, so a caller who wrote
257
+ * major units gets "money is minor units" rather than a confusing
258
+ * arithmetic-mismatch error from float addition.
259
+ * - **Status transitions** are validated against the prior persisted status.
260
+ */
261
+ async save() {
262
+ const { saveInvoice } = await import("./document-money-DNVhDJyC.js").then((n) => n.t);
263
+ return await saveInvoice(this);
264
+ }
265
+ /** @internal Aggregate persistence only. */
266
+ async saveDerived(capability, options) {
267
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice money write capability is required");
268
+ await claimAllocationParent(this, "invoice");
269
+ const persisted = await this.isSaved();
270
+ const prior = await this.resolvePriorStatus(persisted);
271
+ this.assertAmountsAreMinorUnits();
272
+ if (options.recomputeLegacy) await this.recomputeAmountsForSave(persisted);
273
+ else if (persisted && this.id) await this.recomputeAmountPaid();
274
+ else this.amountPaid = 0;
275
+ this.assertNonNegativeAmounts();
276
+ this.assertStatusTransition(prior);
277
+ this.assertPaymentStatusConsistent();
278
+ await this.assertDraftDeliveryHistory(persisted);
279
+ await assertAllocationParent(this, "invoice");
280
+ const result = await super.save();
281
+ loadedInvoiceStatus.set(this, this.status);
282
+ return result;
283
+ }
284
+ async delete(options = {}) {
285
+ const { deleteInvoice } = await import("./document-money-DNVhDJyC.js").then((n) => n.t);
286
+ await deleteInvoice(this, options);
287
+ }
288
+ /** @internal Aggregate persistence only. */
289
+ async deleteDerived(capability, options = {}) {
290
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice money write capability is required");
291
+ await super.delete(options);
292
+ }
293
+ /**
294
+ * Resolve the AUTHORITATIVE prior status (S5 audit #1390 round 4). The
295
+ * {@link loadedInvoiceStatus} WeakMap is only populated when {@link initialize}
296
+ * hydrated the row, so a `create({ id: <existing>, _skipLoad: true })` upsert
297
+ * yields an instance whose WeakMap entry is missing — trusting it would treat
298
+ * the write as a brand-new row and skip the transition guard entirely. Read
299
+ * the persisted row's status directly so a create-onto-existing is correctly
300
+ * treated as an update. `undefined` means no row exists (genuinely new).
301
+ */
302
+ async resolvePriorStatus(persisted) {
303
+ if (persisted && this.id) try {
304
+ const row = await this.db.get(this.tableName, { id: this.id });
305
+ if (row && row.status != null) return row.status;
306
+ } catch {}
307
+ return loadedInvoiceStatus.get(this);
308
+ }
309
+ /**
310
+ * Recompute subtotal/tax/total from line items (when present) and derive
311
+ * amountPaid from PaymentAllocations. Falls back to the arithmetic invariant
312
+ * when the invoice has no line items. Tolerant of smrt-ledgers being absent
313
+ * (the dynamic imports stay inside the package).
314
+ */
315
+ async recomputeAmountsForSave(persisted) {
316
+ if (persisted && this.id) {
317
+ const { InvoiceLineItemCollection } = await import("./InvoiceLineItemCollection-CHm9zdqh.js").then((n) => n.n);
318
+ const lineItems = await (await InvoiceLineItemCollection.create({
319
+ ...this.options,
320
+ db: this.db
321
+ })).findByInvoice(this.id);
322
+ if (lineItems.length > 0) {
323
+ const subtotal = lineItems.reduce((sum, item) => sum + item.getSubtotal(), 0);
324
+ const taxAmount = lineItems.reduce((sum, item) => sum + item.getTaxAmount(), 0) + this.providerTaxAmount;
325
+ const computedTotal = subtotal + taxAmount;
326
+ this.subtotal = subtotal;
327
+ this.taxAmount = taxAmount;
328
+ this.totalAmount = computedTotal;
329
+ } else this.assertTotalArithmetic();
330
+ await this.recomputeAmountPaid();
331
+ } else {
332
+ this.assertTotalArithmetic();
333
+ this.amountPaid = 0;
334
+ if (this.status === InvoiceStatus.PAID || this.status === InvoiceStatus.PARTIAL) throw new Error(`Invoice ${this.invoiceNumber || "<new>"}: a new invoice cannot start in status '${this.status}' \u2014 payment status is derived from PaymentAllocations, not set at creation. Create the invoice unpaid (DRAFT / SENT) and record payments via allocations.`);
335
+ }
336
+ }
337
+ async recomputeAmountPaid() {
338
+ const { PaymentAllocationCollection } = await import("../index.js").then((n) => n.r);
339
+ const allocationCollection = await PaymentAllocationCollection.create({
340
+ ...this.options,
341
+ db: this.db
342
+ });
343
+ if (!this.id) throw new Error("Persisted invoice is missing its ID");
344
+ this.amountPaid = await allocationCollection.getTotalAllocatedToInvoice(this.id);
345
+ }
346
+ /**
347
+ * Whether `amountPaid` covers `totalAmount`. This is the SINGLE source of
348
+ * truth for the **PAID** decision — both {@link updatePaymentStatus} (which
349
+ * decides PAID) and {@link assertPaymentStatusConsistent} (which validates
350
+ * PAID on save) call it, so the PAID-deciding comparison and the
351
+ * PAID-validating comparison can never drift apart (S5 audit #1390
352
+ * follow-up).
353
+ *
354
+ * With integer minor units this is a plain `>=` (#2401). The float-era
355
+ * hazard it used to guard against — a total summed from `0.1 × 3` line items
356
+ * reading as "not covered" by `>=` but "covered" by an epsilon-tolerant
357
+ * guard, leaving a genuinely-paid invoice unsaveable — cannot arise when both
358
+ * sides are exact integers. That also removes the documented PAID/PARTIAL
359
+ * asymmetry: {@link updatePaymentStatus} and {@link isPartiallyPaid} now
360
+ * agree that any `amountPaid > 0` is a partial payment, because there is no
361
+ * such thing as sub-cent dust.
362
+ */
363
+ isFullyPaid() {
364
+ return this.amountPaid + this.creditedAmount >= this.totalAmount;
365
+ }
366
+ /**
367
+ * Whether `amountPaid` is a non-trivial partial payment of `totalAmount`.
368
+ * Derived from {@link isFullyPaid} so the two stay mutually exclusive.
369
+ */
370
+ isPartiallyPaid() {
371
+ return this.amountPaid + this.creditedAmount > 0 && !this.isFullyPaid();
372
+ }
373
+ /**
374
+ * After amounts are recomputed and amountPaid is re-derived from allocations,
375
+ * assert the persisted `status` is consistent with the derived
376
+ * amountPaid-vs-totalAmount relationship. This blocks the raw
377
+ * `SENT → PAID with amountPaid=0` flip (and its inverse, claiming PARTIAL/SENT
378
+ * while fully allocated) that the status-transition map alone permits because
379
+ * SENT → PAID is a structurally legal edge.
380
+ *
381
+ * Only enforced for the payment-derived statuses (PAID / PARTIAL and the
382
+ * unpaid states DRAFT / SENT / VIEWED). Lifecycle statuses that aren't a
383
+ * function of amountPaid — OVERDUE, CANCELLED, WRITTEN_OFF — are left
384
+ * to their own transition rules.
385
+ */
386
+ assertPaymentStatusConsistent() {
387
+ const label = this.invoiceNumber || this.id || "<new>";
388
+ const fullyPaid = this.isFullyPaid();
389
+ const partiallyPaid = this.isPartiallyPaid();
390
+ if (this.status === InvoiceStatus.DRAFT && this.amountPaid !== 0) throw new Error(`Invoice ${label}: DRAFT requires zero derived payment allocations.`);
391
+ if (this.status === InvoiceStatus.PAID && !fullyPaid) throw new Error(`Invoice ${label}: status is PAID but derived amountPaid ${this.amountPaid} does not cover totalAmount ${this.totalAmount}. Payment status is derived from PaymentAllocations \u2014 use updatePaymentStatus().`);
392
+ if (this.status === InvoiceStatus.PARTIAL && !partiallyPaid) throw new Error(`Invoice ${label}: status is PARTIAL but derived amountPaid ${this.amountPaid} is not a partial payment of totalAmount ${this.totalAmount}. Payment status is derived from PaymentAllocations \u2014 use updatePaymentStatus().`);
393
+ if ((this.status === InvoiceStatus.SENT || this.status === InvoiceStatus.VIEWED) && this.totalAmount > 0 && fullyPaid) throw new Error(`Invoice ${label}: status is '${this.status}' but derived amountPaid ${this.amountPaid} fully covers totalAmount ${this.totalAmount}. Use updatePaymentStatus() so the status reflects the allocations.`);
394
+ }
395
+ /** A payment reversal cannot erase an actual delivery event. */
396
+ async assertDraftDeliveryHistory(persisted) {
397
+ if (this.status !== InvoiceStatus.DRAFT) return;
398
+ if (this.sentAt || this.viewedAt) throw new Error("DRAFT requires an undelivered invoice");
399
+ if (persisted && this.id) {
400
+ const { InvoiceCollection } = await Promise.resolve().then(() => InvoiceCollection_exports);
401
+ const prior = await (await InvoiceCollection.create({ db: this.db })).get({ id: this.id }, { cache: false });
402
+ if (prior?.sentAt || prior?.viewedAt) throw new Error("DRAFT cannot erase persisted invoice delivery history");
403
+ }
404
+ }
405
+ /**
406
+ * Enforce `totalAmount === subtotal + taxAmount` exactly. Used when the
407
+ * invoice has no line items to recompute from.
408
+ *
409
+ * Exact because every term is integer minor units (#2401). The float era
410
+ * needed a `0.01` tolerance here and then had to SNAP `totalAmount` to the
411
+ * recomputed value afterwards, because a total that passed the invoice's
412
+ * loose tolerance could still fail smrt-ledgers' tighter `BALANCE_EPSILON`
413
+ * and void the journal `recognizeRevenue` builds from it. Integer arithmetic
414
+ * removes both halves of that workaround: what the caller supplied either is
415
+ * the sum or is rejected, and any journal built from it balances exactly.
416
+ */
417
+ assertTotalArithmetic() {
418
+ const expected = this.subtotal + this.taxAmount;
419
+ if (this.totalAmount !== expected) throw new Error(`Invoice ${this.invoiceNumber || this.id || "<new>"}: totalAmount ${this.totalAmount} must equal subtotal + taxAmount (${expected}).`);
420
+ }
421
+ /**
422
+ * Reject amounts that are not whole minor units, negative financial values,
423
+ * and an amountPaid that exceeds the total (overpayment is modelled
424
+ * elsewhere, not by letting amountPaid float above the invoice total).
425
+ */
426
+ assertAmountsAreMinorUnits() {
427
+ assertIntegerMinorUnits("Invoice", this.invoiceNumber || this.id || "<new>", [
428
+ ["subtotal", this.subtotal],
429
+ ["taxAmount", this.taxAmount],
430
+ ["providerTaxAmount", this.providerTaxAmount],
431
+ ["totalAmount", this.totalAmount],
432
+ ["amountPaid", this.amountPaid],
433
+ ["creditedAmount", this.creditedAmount]
434
+ ]);
435
+ }
436
+ assertNonNegativeAmounts() {
437
+ const label = this.invoiceNumber || this.id || "<new>";
438
+ const amounts = [
439
+ ["subtotal", this.subtotal],
440
+ ["taxAmount", this.taxAmount],
441
+ ["providerTaxAmount", this.providerTaxAmount],
442
+ ["totalAmount", this.totalAmount],
443
+ ["amountPaid", this.amountPaid],
444
+ ["creditedAmount", this.creditedAmount]
445
+ ];
446
+ assertIntegerMinorUnits("Invoice", label, amounts);
447
+ for (const [field2, value] of amounts) if (value < 0) throw new Error(`Invoice ${label}: ${field2} must be a non-negative number (got ${value}).`);
448
+ if (this.amountPaid > this.totalAmount) throw new Error(`Invoice ${label}: amountPaid ${this.amountPaid} cannot exceed totalAmount ${this.totalAmount}.`);
449
+ }
450
+ /**
451
+ * Reject an illegal status flip done via raw field assignment. Compares the
452
+ * about-to-be-written status against the status the row was loaded with.
453
+ * No-op transitions (status unchanged) and brand-new rows are always allowed.
454
+ */
455
+ assertStatusTransition(prior) {
456
+ if (prior === void 0) return;
457
+ if (prior === this.status) return;
458
+ if (!(INVOICE_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Invoice ${this.invoiceNumber || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'. Use the guarded transition helpers (markSent / markViewed / updatePaymentStatus / cancel / writeOff).`);
459
+ }
460
+ /**
461
+ * Check if invoice is a draft
462
+ */
463
+ isDraft() {
464
+ return this.status === InvoiceStatus.DRAFT;
465
+ }
466
+ /**
467
+ * Check if invoice has been sent
468
+ */
469
+ isSent() {
470
+ return this.status === InvoiceStatus.SENT;
471
+ }
472
+ /**
473
+ * Check if invoice is fully paid
474
+ */
475
+ isPaid() {
476
+ return this.status === InvoiceStatus.PAID;
477
+ }
478
+ /**
479
+ * Check if invoice is partially paid
480
+ */
481
+ isPartial() {
482
+ return this.status === InvoiceStatus.PARTIAL;
483
+ }
484
+ /**
485
+ * Check if invoice is overdue
486
+ */
487
+ isOverdue() {
488
+ if (this.status === InvoiceStatus.OVERDUE) return true;
489
+ if (this.isPaid() || this.status === InvoiceStatus.CANCELLED) return false;
490
+ return /* @__PURE__ */ new Date() > this.dueDate;
491
+ }
492
+ /**
493
+ * Get remaining amount due
494
+ */
495
+ getAmountDue() {
496
+ return Math.max(0, this.getBalance());
497
+ }
498
+ /** Signed amount still owed after payments and issued credit notes. */
499
+ getBalance() {
500
+ return this.totalAmount - this.amountPaid - this.creditedAmount;
501
+ }
502
+ /**
503
+ * Mark invoice as sent
504
+ */
505
+ markSent() {
506
+ if (this.status === InvoiceStatus.PARTIAL || this.status === InvoiceStatus.PAID) {
507
+ this.sentAt ??= /* @__PURE__ */ new Date();
508
+ return;
509
+ }
510
+ if (this.status !== InvoiceStatus.DRAFT) throw new Error(`Cannot send invoice with status '${this.status}'. Only draft or payment-progress invoices can be sent.`);
511
+ this.status = InvoiceStatus.SENT;
512
+ this.sentAt = /* @__PURE__ */ new Date();
513
+ }
514
+ /**
515
+ * Record that the invoice was viewed by the customer.
516
+ *
517
+ * This method is idempotent - calling it multiple times will only
518
+ * record the first view time. Status only transitions to VIEWED if
519
+ * the current status is SENT (other statuses like PARTIAL or PAID
520
+ * are preserved).
521
+ */
522
+ markViewed() {
523
+ if (this.viewedAt) return;
524
+ this.viewedAt = /* @__PURE__ */ new Date();
525
+ if (this.status === InvoiceStatus.SENT) this.status = InvoiceStatus.VIEWED;
526
+ }
527
+ /**
528
+ * Update payment amount and status.
529
+ *
530
+ * Called when PaymentAllocations change. Handles both forward transitions
531
+ * (DRAFT/SENT → PARTIAL → PAID) and reversals (PAID → PARTIAL → original status).
532
+ *
533
+ * Terminal statuses (CANCELLED, WRITTEN_OFF) are not modified.
534
+ */
535
+ updatePaymentStatus(amountPaid) {
536
+ this.amountPaid = amountPaid;
537
+ if (this.status === InvoiceStatus.CANCELLED || this.status === InvoiceStatus.WRITTEN_OFF) return;
538
+ if (this.isFullyPaid()) {
539
+ if (this.status !== InvoiceStatus.PAID || !this.paidDate) this.paidDate = /* @__PURE__ */ new Date();
540
+ this.status = InvoiceStatus.PAID;
541
+ } else if (amountPaid + this.creditedAmount > 0) {
542
+ this.status = InvoiceStatus.PARTIAL;
543
+ this.paidDate = null;
544
+ } else {
545
+ this.paidDate = null;
546
+ if (this.viewedAt) this.status = InvoiceStatus.VIEWED;
547
+ else if (this.sentAt) this.status = InvoiceStatus.SENT;
548
+ else this.status = InvoiceStatus.DRAFT;
549
+ }
550
+ }
551
+ /**
552
+ * Cancel the invoice
553
+ */
554
+ cancel() {
555
+ if (this.kind === "invoice" && this.isPaid() || this.kind === "credit_note" && this.amountPaid > 0) throw new Error(`Cannot cancel ${this.kind} with status '${this.status}' and refunded/paid amount ${this.amountPaid}.`);
556
+ this.status = InvoiceStatus.CANCELLED;
557
+ }
558
+ /** Cancel a posted money-v2 invoice with an exact atomic journal reversal. */
559
+ async cancelWithRevenueReversal(options) {
560
+ const { cancelInvoiceWithRevenueReversal } = await import("./invoice-posting-BwV8g7z0.js");
561
+ return cancelInvoiceWithRevenueReversal(this, options);
562
+ }
563
+ /**
564
+ * Write off the invoice (bad debt)
565
+ */
566
+ writeOff() {
567
+ this.status = InvoiceStatus.WRITTEN_OFF;
568
+ }
569
+ /**
570
+ * Recognize revenue and create AR journal entry.
571
+ *
572
+ * Creates a balanced journal entry in smrt-ledgers:
573
+ * - Debit: Accounts Receivable (assets increase)
574
+ * - Credit: Revenue (revenue increases)
575
+ * - Credit: Tax Payable (liability increases, if taxAmount > 0)
576
+ *
577
+ * **Note**: This method saves the invoice after setting arJournalId.
578
+ *
579
+ * @param options - Account IDs for the journal entry
580
+ * @returns The created journal
581
+ *
582
+ * @example
583
+ * ```typescript
584
+ * const journal = await invoice.recognizeRevenue({
585
+ * arAccountId: '1120',
586
+ * revenueAccountId: '4100',
587
+ * taxAccountId: '2130'
588
+ * });
589
+ * ```
590
+ */
591
+ async recognizeRevenue(options) {
592
+ const { recognizeInvoiceRevenueV2 } = await import("./invoice-posting-BwV8g7z0.js");
593
+ const v2Journal = await recognizeInvoiceRevenueV2(this, options);
594
+ if (v2Journal) return v2Journal;
595
+ if (this.arJournalId) throw new Error(`Revenue already recognized for invoice ${this.invoiceNumber} (journal ID: ${this.arJournalId})`);
596
+ await this.save();
597
+ if (this.totalAmount <= 0) throw new Error(`Cannot recognize revenue for invoice ${this.invoiceNumber} with non-positive total: ${this.totalAmount}`);
598
+ if (this.taxAmount > 0 && !options.taxAccountId) throw new Error(`Invoice ${this.invoiceNumber} has taxAmount ${this.taxAmount} but no taxAccountId provided`);
599
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
600
+ const journalCollection = await JournalCollection.create(this.options);
601
+ const entries = [{
602
+ accountId: options.arAccountId,
603
+ debit: this.totalAmount,
604
+ memo: `Invoice ${this.invoiceNumber}`
605
+ }, {
606
+ accountId: options.revenueAccountId,
607
+ credit: this.subtotal,
608
+ memo: `Invoice ${this.invoiceNumber}`
609
+ }];
610
+ if (this.taxAmount > 0 && options.taxAccountId) entries.push({
611
+ accountId: options.taxAccountId,
612
+ credit: this.taxAmount,
613
+ memo: `Tax on Invoice ${this.invoiceNumber}`
614
+ });
615
+ const journal = await journalCollection.createWithEntries({
616
+ description: `Revenue: Invoice ${this.invoiceNumber}`,
617
+ sourceModule: "smrt-commerce",
618
+ sourceRef: this.id,
619
+ entries
620
+ });
621
+ await journal.post();
622
+ if (!journal.id) throw new Error("Invoice.recognizeRevenue: journal was posted but has no id");
623
+ this.arJournalId = journal.id;
624
+ try {
625
+ await this.save();
626
+ } catch (err) {
627
+ try {
628
+ await journal.void(`Reverted: invoice ${this.invoiceNumber} save failed during revenue recognition`);
629
+ } catch {}
630
+ this.arJournalId = "";
631
+ throw err;
632
+ }
633
+ return journal;
634
+ }
635
+ /**
636
+ * Get the AR journal entry (if revenue was recognized)
637
+ */
638
+ async getArJournal() {
639
+ if (!this.arJournalId) return null;
640
+ try {
641
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
642
+ return await (await JournalCollection.create(this.options)).get({ id: this.arJournalId });
643
+ } catch {
644
+ return null;
645
+ }
646
+ }
647
+ /**
648
+ * Convert to InvoiceInput for SDK accounting provider sync.
649
+ *
650
+ * Fetches line items and maps all fields to the format expected
651
+ * by @happyvertical/accounting providers. The SDK takes currency **major**
652
+ * units (`12.34` USD), so every amount is converted from this package's
653
+ * integer minor units with the currency's ISO 4217 exponent (#3060).
654
+ *
655
+ * @returns InvoiceInput compatible with @happyvertical/accounting
656
+ *
657
+ * @example
658
+ * ```typescript
659
+ * const provider = await getAccountingProvider({ type: 'quickbooks', ... });
660
+ * const input = await invoice.toAccountingInput();
661
+ * const result = await provider.invoices.push(input);
662
+ * invoice.externalId = result.externalId;
663
+ * invoice.syncedAt = result.syncedAt;
664
+ * await invoice.save();
665
+ * ```
666
+ */
667
+ async toAccountingInput(options) {
668
+ if (!this.tenantId) return this.toAccountingInputLocked(options, null);
669
+ const { withTenantMoneyRuntime } = await import("./tenant-money-barrier-DjRXgMnX.js").then((n) => n.r);
670
+ return withTenantMoneyRuntime(this.db, this.tenantId, "legacy-or-v2", (scope) => this.withDatabase(scope.db, (bound) => bound.toAccountingInputLocked(options, scope.barrier.settings)));
671
+ }
672
+ async toAccountingInputLocked(options, settings) {
673
+ if (settings?.moneyModel === "v2") {
674
+ if (!options) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Money-v2 accounting conversion requires explicit realm mapping");
675
+ if (!this.id || !this.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Money-v2 accounting conversion requires a persisted tenant invoice");
676
+ const { toFrozenInvoiceAccountingInput } = await import("../index.js").then((n) => n.n);
677
+ return toFrozenInvoiceAccountingInput({
678
+ db: this.db,
679
+ tenantId: this.tenantId,
680
+ invoiceId: this.id,
681
+ mapping: options.mapping
682
+ });
683
+ }
684
+ const { InvoiceLineItemCollection } = await import("./InvoiceLineItemCollection-CHm9zdqh.js").then((n) => n.n);
685
+ const lineItemCollection = await InvoiceLineItemCollection.create(this.options);
686
+ if (!this.id) throw new Error("Invoice.toAccountingInput requires a persisted invoice (missing id)");
687
+ const invoiceId = this.id;
688
+ const lineItems = await lineItemCollection.findByInvoice(invoiceId);
689
+ return {
690
+ id: invoiceId,
691
+ externalId: this.externalId || void 0,
692
+ invoiceNumber: this.invoiceNumber,
693
+ customerId: this.customerId,
694
+ customerExternalId: this.customerExternalId || void 0,
695
+ issueDate: this.issueDate,
696
+ dueDate: this.dueDate,
697
+ lineItems: lineItems.map((item) => item.toAccountingLineItem(this.currency)),
698
+ subtotal: minorToMajorUnits(this.subtotal, this.currency),
699
+ taxAmount: minorToMajorUnits(this.taxAmount, this.currency),
700
+ totalAmount: minorToMajorUnits(this.totalAmount, this.currency),
701
+ currency: this.currency,
702
+ reference: this.reference || void 0,
703
+ memo: this.customerNotes || void 0
704
+ };
705
+ }
706
+ };
707
+ __publicField(Invoice, "__smrtPackage__", "@happyvertical/smrt-commerce");
708
+ __decorateClass([field({ description: "Invoice or customer credit note." })], Invoice.prototype, "kind", 2);
709
+ __decorateClass([foreignKey("Invoice", { nullable: true })], Invoice.prototype, "creditedInvoiceId", 2);
710
+ __decorateClass([tenantId({ nullable: true })], Invoice.prototype, "tenantId", 2);
711
+ __decorateClass([field({ description: "The customer being billed." }), foreignKey("Customer")], Invoice.prototype, "customerId", 2);
712
+ __decorateClass([field({ description: "The order or agreement this invoice bills for, if any." }), foreignKey("SalesContract", { validateSubtype: true })], Invoice.prototype, "contractId", 2);
713
+ __decorateClass([field({ description: "The number printed on the invoice." })], Invoice.prototype, "invoiceNumber", 2);
714
+ __decorateClass([field({ description: "The date the invoice was issued." })], Invoice.prototype, "issueDate", 2);
715
+ __decorateClass([field({ description: "The date payment is expected." })], Invoice.prototype, "dueDate", 2);
716
+ __decorateClass([field({ description: "The date it was paid in full." })], Invoice.prototype, "paidDate", 2);
717
+ __decorateClass([field({
718
+ description: "The amount before tax.",
719
+ ui: { format: "money" }
720
+ })], Invoice.prototype, "subtotal", 2);
721
+ __decorateClass([field({
722
+ description: "The tax charged.",
723
+ ui: { format: "money" }
724
+ })], Invoice.prototype, "taxAmount", 2);
725
+ __decorateClass([field({ ui: { format: "money" } })], Invoice.prototype, "providerTaxAmount", 2);
726
+ __decorateClass([field({
727
+ description: "What the customer owes in all, tax included.",
728
+ ui: { format: "money" }
729
+ })], Invoice.prototype, "totalAmount", 2);
730
+ __decorateClass([field({
731
+ description: "How much of the total has been paid so far.",
732
+ ui: { format: "money" }
733
+ })], Invoice.prototype, "amountPaid", 2);
734
+ __decorateClass([field({
735
+ description: "The currency of the invoice.",
736
+ ui: { widget: "currency" }
737
+ })], Invoice.prototype, "currency", 2);
738
+ __decorateClass([field({ description: "Where the invoice stands: draft, sent, viewed, paid, overdue or cancelled." })], Invoice.prototype, "status", 2);
739
+ __decorateClass([crossPackageRef("@happyvertical/smrt-ledgers:Journal")], Invoice.prototype, "arJournalId", 2);
740
+ __decorateClass([crossPackageRef("@happyvertical/smrt-ledgers:Journal", { nullable: true })], Invoice.prototype, "arReversalJournalId", 2);
741
+ __decorateClass([crossPackageRef("@happyvertical/smrt-ledgers:Journal")], Invoice.prototype, "revenueJournalId", 2);
742
+ __decorateClass([field({ sensitive: true })], Invoice.prototype, "accountingSyncStateJson", 2);
743
+ __decorateClass([field({
744
+ description: "Private notes, not shown to the customer.",
745
+ ui: { widget: "textarea" }
746
+ })], Invoice.prototype, "notes", 2);
747
+ __decorateClass([field({
748
+ description: "A message shown to the customer on the invoice.",
749
+ ui: { widget: "textarea" }
750
+ })], Invoice.prototype, "customerNotes", 2);
751
+ __decorateClass([field({
752
+ description: "The payment conditions, such as Net 30.",
753
+ ui: { widget: "textarea" }
754
+ })], Invoice.prototype, "terms", 2);
755
+ Invoice = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
756
+ api: {
757
+ include: [
758
+ "list",
759
+ "get",
760
+ "create",
761
+ "update"
762
+ ],
763
+ writable: [
764
+ "customerId",
765
+ "contractId",
766
+ "invoiceNumber",
767
+ "reference",
768
+ "issueDate",
769
+ "dueDate",
770
+ "currency",
771
+ "lineAmountType",
772
+ "taxRoundingScope",
773
+ "notes",
774
+ "customerNotes",
775
+ "terms",
776
+ "externalId",
777
+ "customerExternalId",
778
+ "externalProvider"
779
+ ]
780
+ },
781
+ mcp: {
782
+ include: ["list", "get"],
783
+ serializers: { get: {
784
+ importPath: "@happyvertical/smrt-commerce",
785
+ exportName: "serializeInvoiceMcpGet"
786
+ } }
787
+ },
788
+ cli: { skipApiCheck: true }
789
+ })], Invoice);
790
+ //#endregion
791
+ //#region src/collections/InvoiceCollection.ts
792
+ var InvoiceCollection_exports = /* @__PURE__ */ __exportAll({
793
+ InvoiceCollection: () => InvoiceCollection,
794
+ UNPAID_STATUSES: () => UNPAID_STATUSES
795
+ });
796
+ var UNPAID_STATUSES = [
797
+ InvoiceStatus.SENT,
798
+ InvoiceStatus.VIEWED,
799
+ InvoiceStatus.PARTIAL,
800
+ InvoiceStatus.OVERDUE
801
+ ];
802
+ var InvoiceCollection = class extends SmrtCollection {
803
+ static _itemClass = Invoice;
804
+ async withMoneyRead(operation) {
805
+ const rootDb = this.db;
806
+ const tenantId = getTenantId();
807
+ if (!tenantId) return operation();
808
+ const result = await withTenantMoneyCollectionRead(rootDb, tenantId, (boundDb) => this.withDatabase(boundDb, operation));
809
+ if (result instanceof Invoice) result.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
810
+ else if (Array.isArray(result)) {
811
+ for (const item of result) if (item instanceof Invoice) item.rebindMoneyRead(DOCUMENT_MONEY_READ_REBIND, rootDb);
812
+ }
813
+ return result;
814
+ }
815
+ async get(filter, options = {}) {
816
+ return this.withMoneyRead(() => super.get(filter, options));
817
+ }
818
+ async list(options = {}) {
819
+ return this.withMoneyRead(() => super.list(options));
820
+ }
821
+ /**
822
+ * Find invoices by customer
823
+ *
824
+ * @param customerId - Customer ID
825
+ * @returns Array of invoices
826
+ */
827
+ async findByCustomer(customerId) {
828
+ return await this.list({
829
+ where: { customerId },
830
+ orderBy: "issueDate DESC"
831
+ });
832
+ }
833
+ /**
834
+ * Find invoices by contract
835
+ *
836
+ * @param contractId - Contract ID
837
+ * @returns Array of invoices
838
+ */
839
+ async findByContract(contractId) {
840
+ return await this.list({
841
+ where: { contractId },
842
+ orderBy: "issueDate DESC"
843
+ });
844
+ }
845
+ /**
846
+ * Find invoices by status
847
+ *
848
+ * @param status - Invoice status
849
+ * @returns Array of invoices
850
+ */
851
+ async findByStatus(status) {
852
+ return await this.list({
853
+ where: { status },
854
+ orderBy: "issueDate DESC"
855
+ });
856
+ }
857
+ /**
858
+ * Find all draft invoices
859
+ *
860
+ * @returns Array of draft invoices
861
+ */
862
+ async findDrafts() {
863
+ return await this.findByStatus(InvoiceStatus.DRAFT);
864
+ }
865
+ /**
866
+ * Find all sent invoices
867
+ *
868
+ * @returns Array of sent invoices
869
+ */
870
+ async findSent() {
871
+ return await this.findByStatus(InvoiceStatus.SENT);
872
+ }
873
+ /**
874
+ * Find all paid invoices
875
+ *
876
+ * @returns Array of paid invoices
877
+ */
878
+ async findPaid() {
879
+ return await this.findByStatus(InvoiceStatus.PAID);
880
+ }
881
+ /**
882
+ * Find all partial invoices
883
+ *
884
+ * @returns Array of partially paid invoices
885
+ */
886
+ async findPartial() {
887
+ return await this.findByStatus(InvoiceStatus.PARTIAL);
888
+ }
889
+ /**
890
+ * Find overdue invoices (past due date, not paid).
891
+ *
892
+ * **Note**: This method finds invoices by date, but does not automatically
893
+ * update their status to OVERDUE. Use a background job or scheduled task
894
+ * to periodically update invoice statuses based on due dates.
895
+ *
896
+ * @returns Array of overdue invoices
897
+ */
898
+ async findOverdue() {
899
+ const now = (/* @__PURE__ */ new Date()).toISOString();
900
+ return (await this.list({
901
+ where: { "dueDate <": now },
902
+ orderBy: "dueDate ASC"
903
+ })).filter((inv) => UNPAID_STATUSES.includes(inv.status));
904
+ }
905
+ /**
906
+ * Find invoices in date range (by issue date)
907
+ *
908
+ * @param startDate - Start date
909
+ * @param endDate - End date
910
+ * @returns Array of invoices
911
+ */
912
+ async findByDateRange(startDate, endDate) {
913
+ return await this.list({
914
+ where: {
915
+ "issueDate >=": startDate.toISOString(),
916
+ "issueDate <=": endDate.toISOString()
917
+ },
918
+ orderBy: "issueDate DESC"
919
+ });
920
+ }
921
+ /**
922
+ * Find invoice by external ID (from accounting provider)
923
+ *
924
+ * @param externalId - External ID
925
+ * @returns Invoice or null
926
+ */
927
+ async findByExternalId(externalId) {
928
+ return (await this.list({
929
+ where: { externalId },
930
+ limit: 1
931
+ }))[0] || null;
932
+ }
933
+ /**
934
+ * Find invoices needing sync to external provider
935
+ * (no externalId or stale syncedAt)
936
+ *
937
+ * @returns Array of invoices needing sync
938
+ */
939
+ async findNeedingSync() {
940
+ return (await this.list({ orderBy: "issueDate DESC" })).filter((inv) => inv.status !== InvoiceStatus.DRAFT && inv.status !== InvoiceStatus.CANCELLED && !inv.externalId);
941
+ }
942
+ /**
943
+ * Find invoice by invoice number
944
+ *
945
+ * @param invoiceNumber - Invoice number
946
+ * @returns Invoice or null
947
+ */
948
+ async findByNumber(invoiceNumber) {
949
+ return (await this.list({
950
+ where: { invoiceNumber },
951
+ limit: 1
952
+ }))[0] || null;
953
+ }
954
+ /**
955
+ * Count invoices for a given year
956
+ *
957
+ * @param year - Year to count
958
+ * @returns Number of invoices
959
+ */
960
+ async countForYear(year) {
961
+ const startDate = new Date(year, 0, 1).toISOString();
962
+ const endDate = new Date(year + 1, 0, 1).toISOString();
963
+ return (await this.list({ where: {
964
+ "issueDate >=": startDate,
965
+ "issueDate <": endDate
966
+ } })).length;
967
+ }
968
+ /**
969
+ * Generate the next invoice number.
970
+ *
971
+ * **Warning**: This method has a potential race condition if multiple
972
+ * invoices are created concurrently. For high-concurrency environments,
973
+ * consider using a database sequence or atomic counter instead.
974
+ *
975
+ * @param options - Optional configuration for number format
976
+ * @returns Generated invoice number (e.g., 'INV-2025-0001')
977
+ *
978
+ * @example
979
+ * ```typescript
980
+ * // Default format: INV-2025-0001
981
+ * const num = await invoices.generateInvoiceNumber();
982
+ *
983
+ * // Custom prefix: BILL-2025-0001
984
+ * const num = await invoices.generateInvoiceNumber({ prefix: 'BILL' });
985
+ *
986
+ * // Simple sequential: INV-000001
987
+ * const num = await invoices.generateInvoiceNumber({ format: 'prefix-seq' });
988
+ * ```
989
+ */
990
+ async generateInvoiceNumber(options = {}) {
991
+ const prefix = options.prefix || "INV";
992
+ const format = options.format || "prefix-year-seq";
993
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
994
+ if (format === "prefix-year-seq") {
995
+ const count = await this.countForYear(year);
996
+ return `${prefix}-${year}-${String(count + 1).padStart(4, "0")}`;
997
+ }
998
+ const allInvoices = await this.list({});
999
+ return `${prefix}-${String(allInvoices.length + 1).padStart(6, "0")}`;
1000
+ }
1001
+ /**
1002
+ * Get total outstanding (unpaid) amount for a customer
1003
+ *
1004
+ * @param customerId - Customer ID
1005
+ * @returns Total amount due
1006
+ */
1007
+ async getTotalOutstandingForCustomer(customerId) {
1008
+ return (await this.list({ where: { customerId } })).filter((inv) => UNPAID_STATUSES.includes(inv.status)).reduce((sum, inv) => sum + inv.getAmountDue(), 0);
1009
+ }
1010
+ /**
1011
+ * Find all invoices belonging to a specific tenant
1012
+ *
1013
+ * @param tenantId - Tenant ID
1014
+ * @returns Array of invoices for the tenant
1015
+ */
1016
+ async findByTenant(tenantId) {
1017
+ return this.list({ where: { tenantId } });
1018
+ }
1019
+ /**
1020
+ * Find all global invoices (not associated with any tenant).
1021
+ *
1022
+ * Routes through the shared tenant-global helper so it does not throw under
1023
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1024
+ * flagged as an isolation violation). (#1600)
1025
+ *
1026
+ * @returns Array of global invoices
1027
+ */
1028
+ async findGlobal() {
1029
+ return queryGlobal(this);
1030
+ }
1031
+ /**
1032
+ * Find invoices for a tenant including global invoices.
1033
+ *
1034
+ * Fails closed if an active tenant context requests a different tenant's
1035
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1036
+ *
1037
+ * @param tenantId - Tenant ID
1038
+ * @returns Array of tenant-specific and global invoices
1039
+ */
1040
+ async findWithGlobals(tenantId) {
1041
+ return queryWithGlobals(this, tenantId, "Invoice.findWithGlobals");
1042
+ }
1043
+ };
1044
+ //#endregion
1045
+ export { Invoice as i, InvoiceCollection_exports as n, UNPAID_STATUSES as r, InvoiceCollection as t };
1046
+
1047
+ //# sourceMappingURL=InvoiceCollection-CKQflFjc.js.map