@happyvertical/smrt-commerce 0.56.0 → 0.56.2

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 (213) hide show
  1. package/AGENTS.md +9 -8
  2. package/README.md +98 -4
  3. package/agents/accounting-snapshot-mapper.md +111 -0
  4. package/agents/contract-money.md +24 -0
  5. package/agents/cost-billing.md +7 -2
  6. package/agents/customer-vendor-ui.md +7 -1
  7. package/agents/document-money.md +127 -0
  8. package/agents/invoice-calculations.md +14 -2
  9. package/agents/line-amount-migration.md +50 -0
  10. package/agents/minor-unit-migration.md +20 -0
  11. package/agents/money-calculator.md +45 -0
  12. package/agents/money-model.md +113 -0
  13. package/agents/revenue-journal.md +23 -0
  14. package/agents/tax-codes.md +48 -0
  15. package/agents/tax-summary.md +128 -0
  16. package/dist/accounting-snapshot.d.ts +79 -0
  17. package/dist/accounting-snapshot.d.ts.map +1 -0
  18. package/dist/billing/runtime.d.ts +2 -0
  19. package/dist/billing/runtime.d.ts.map +1 -1
  20. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js +265 -0
  21. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +1 -0
  22. package/dist/chunks/CustomerCollection-CjGaZSpf.js +99 -0
  23. package/dist/chunks/CustomerCollection-CjGaZSpf.js.map +1 -0
  24. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js +1659 -0
  25. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +1 -0
  26. package/dist/chunks/InvoiceCollection-BUe_8FT1.js +1047 -0
  27. package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +1 -0
  28. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +468 -0
  29. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +1 -0
  30. package/dist/chunks/TaxCodeCollection-DF2eaJed.js +149 -0
  31. package/dist/chunks/TaxCodeCollection-DF2eaJed.js.map +1 -0
  32. package/dist/chunks/TaxComponentCollection-3x24xSNh.js +129 -0
  33. package/dist/chunks/TaxComponentCollection-3x24xSNh.js.map +1 -0
  34. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +319 -0
  35. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +1 -0
  36. package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-Cq0g6CfO.js} +7 -12
  37. package/dist/chunks/allocation-integrity-Cq0g6CfO.js.map +1 -0
  38. package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
  39. package/dist/chunks/calculations-DaHR17il.js.map +1 -0
  40. package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
  41. package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
  42. package/dist/chunks/document-money-oUDvdjOf.js +860 -0
  43. package/dist/chunks/document-money-oUDvdjOf.js.map +1 -0
  44. package/dist/chunks/invoice-posting-CF3fE_Ec.js +229 -0
  45. package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +1 -0
  46. package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
  47. package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
  48. package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
  49. package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
  50. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  51. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +135 -0
  52. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +1 -0
  53. package/dist/chunks/{types-zVnIjSOk.js → types-Bbm0Xflc.js} +4 -14
  54. package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
  55. package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
  56. package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
  57. package/dist/collections/ContractCollection.d.ts +35 -1
  58. package/dist/collections/ContractCollection.d.ts.map +1 -1
  59. package/dist/collections/ContractLineItemCollection.d.ts +26 -1
  60. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  61. package/dist/collections/DocumentTaxCollection.d.ts +75 -0
  62. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
  63. package/dist/collections/InvoiceCollection.d.ts +26 -1
  64. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  65. package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
  66. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  67. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
  68. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
  69. package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
  70. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  71. package/dist/collections/TaxCodeCollection.d.ts +7 -0
  72. package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
  73. package/dist/collections/TaxComponentCollection.d.ts +6 -0
  74. package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
  75. package/dist/collections/TaxSettingsCollection.d.ts +16 -0
  76. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
  77. package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
  78. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
  79. package/dist/collections/index.d.ts +6 -0
  80. package/dist/collections/index.d.ts.map +1 -1
  81. package/dist/commerce-money-tools.d.ts +9 -0
  82. package/dist/commerce-money-tools.d.ts.map +1 -0
  83. package/dist/contract-money.d.ts +27 -0
  84. package/dist/contract-money.d.ts.map +1 -0
  85. package/dist/cost-billing.d.ts +0 -1
  86. package/dist/cost-billing.d.ts.map +1 -1
  87. package/dist/cost-billing.js +10 -15
  88. package/dist/cost-billing.js.map +1 -1
  89. package/dist/credit-allocation.d.ts +48 -0
  90. package/dist/credit-allocation.d.ts.map +1 -0
  91. package/dist/document-money-internal.d.ts +44 -0
  92. package/dist/document-money-internal.d.ts.map +1 -0
  93. package/dist/document-money.d.ts +85 -0
  94. package/dist/document-money.d.ts.map +1 -0
  95. package/dist/index.d.ts +15 -4
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +5411 -4249
  98. package/dist/index.js.map +1 -1
  99. package/dist/invoice-accounting-sync.d.ts +96 -0
  100. package/dist/invoice-accounting-sync.d.ts.map +1 -0
  101. package/dist/invoice-mcp.d.ts +16 -0
  102. package/dist/invoice-mcp.d.ts.map +1 -0
  103. package/dist/invoice-posting.d.ts +9 -0
  104. package/dist/invoice-posting.d.ts.map +1 -0
  105. package/dist/invoice-record-screen.d.ts +60 -0
  106. package/dist/invoice-record-screen.d.ts.map +1 -0
  107. package/dist/invoices.d.ts +2 -0
  108. package/dist/invoices.d.ts.map +1 -1
  109. package/dist/invoices.js +3 -2
  110. package/dist/manifest.json +12804 -6095
  111. package/dist/migrations/commerceLineAmounts.d.ts +84 -0
  112. package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
  113. package/dist/migrations/partyLinks.d.ts.map +1 -1
  114. package/dist/models/Contract.d.ts +34 -4
  115. package/dist/models/Contract.d.ts.map +1 -1
  116. package/dist/models/ContractLineItem.d.ts +43 -6
  117. package/dist/models/ContractLineItem.d.ts.map +1 -1
  118. package/dist/models/Customer.d.ts +7 -0
  119. package/dist/models/Customer.d.ts.map +1 -1
  120. package/dist/models/DocumentTax.d.ts +50 -0
  121. package/dist/models/DocumentTax.d.ts.map +1 -0
  122. package/dist/models/Invoice.d.ts +46 -2
  123. package/dist/models/Invoice.d.ts.map +1 -1
  124. package/dist/models/InvoiceLineItem.d.ts +46 -5
  125. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  126. package/dist/models/MoneyMigrationDocument.d.ts +28 -0
  127. package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
  128. package/dist/models/Payment.d.ts +5 -1
  129. package/dist/models/Payment.d.ts.map +1 -1
  130. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  131. package/dist/models/TaxCode.d.ts +22 -0
  132. package/dist/models/TaxCode.d.ts.map +1 -0
  133. package/dist/models/TaxComponent.d.ts +21 -0
  134. package/dist/models/TaxComponent.d.ts.map +1 -0
  135. package/dist/models/TaxSettings.d.ts +33 -0
  136. package/dist/models/TaxSettings.d.ts.map +1 -0
  137. package/dist/models/TaxSummaryReport.d.ts +35 -0
  138. package/dist/models/TaxSummaryReport.d.ts.map +1 -0
  139. package/dist/models/Vendor.d.ts +9 -0
  140. package/dist/models/Vendor.d.ts.map +1 -1
  141. package/dist/models/index.d.ts +7 -1
  142. package/dist/models/index.d.ts.map +1 -1
  143. package/dist/money-rational.d.ts +15 -0
  144. package/dist/money-rational.d.ts.map +1 -0
  145. package/dist/money.d.ts +106 -25
  146. package/dist/money.d.ts.map +1 -1
  147. package/dist/money.js +274 -0
  148. package/dist/money.js.map +1 -0
  149. package/dist/recipes.d.ts +15 -15
  150. package/dist/revenue-journal.d.ts +61 -0
  151. package/dist/revenue-journal.d.ts.map +1 -0
  152. package/dist/smrt-knowledge.json +9621 -7592
  153. package/dist/svelte/components/InvoiceActions.svelte +12 -0
  154. package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
  155. package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
  156. package/dist/svelte/components/InvoiceEditor.svelte +15 -1
  157. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  158. package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
  159. package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
  160. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
  161. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
  162. package/dist/svelte/components/InvoiceTotals.svelte +2 -2
  163. package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
  164. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
  165. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
  166. package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
  167. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
  168. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
  169. package/dist/svelte/components/QuoteEditor.svelte +54 -1
  170. package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
  171. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
  172. package/dist/svelte/i18n.d.ts +16 -0
  173. package/dist/svelte/i18n.d.ts.map +1 -1
  174. package/dist/svelte/i18n.js +16 -0
  175. package/dist/svelte/index.d.ts +7 -2
  176. package/dist/svelte/index.d.ts.map +1 -1
  177. package/dist/svelte/index.js +4 -1
  178. package/dist/svelte/invoices/calculations.d.ts +7 -2
  179. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  180. package/dist/svelte/invoices/calculations.js +141 -47
  181. package/dist/svelte/invoices/money-editor.d.ts +123 -0
  182. package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
  183. package/dist/svelte/invoices/money-editor.js +101 -0
  184. package/dist/svelte/invoices/types.d.ts +5 -1
  185. package/dist/svelte/invoices/types.d.ts.map +1 -1
  186. package/dist/svelte/purchasing-i18n.d.ts +5 -0
  187. package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
  188. package/dist/svelte/purchasing-i18n.js +5 -0
  189. package/dist/svelte/quote-i18n.d.ts +5 -0
  190. package/dist/svelte/quote-i18n.d.ts.map +1 -1
  191. package/dist/svelte/quote-i18n.js +5 -0
  192. package/dist/tax/cookbook.d.ts +22 -0
  193. package/dist/tax/cookbook.d.ts.map +1 -0
  194. package/dist/tax/example-pack.d.ts +25 -0
  195. package/dist/tax/example-pack.d.ts.map +1 -0
  196. package/dist/tax/identity.d.ts +5 -0
  197. package/dist/tax/identity.d.ts.map +1 -0
  198. package/dist/tax/index.d.ts +4 -0
  199. package/dist/tax/index.d.ts.map +1 -0
  200. package/dist/tax/legacy.d.ts +48 -0
  201. package/dist/tax/legacy.d.ts.map +1 -0
  202. package/dist/tax-summary-report-adapter.d.ts +37 -0
  203. package/dist/tax-summary-report-adapter.d.ts.map +1 -0
  204. package/dist/tax-summary.d.ts +129 -0
  205. package/dist/tax-summary.d.ts.map +1 -0
  206. package/dist/tenant-money-barrier.d.ts +66 -0
  207. package/dist/tenant-money-barrier.d.ts.map +1 -0
  208. package/dist/types/index.d.ts +138 -1
  209. package/dist/types/index.d.ts.map +1 -1
  210. package/package.json +19 -11
  211. package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
  212. package/dist/chunks/money-DfrRxrel.js.map +0 -1
  213. 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-Bbm0Xflc.js";
3
+ import { o as minorToMajorUnits } from "./units-CNcjH_eE.js";
4
+ import { i as claimAllocationParent, r as assertAllocationParent } from "./allocation-integrity-Cq0g6CfO.js";
5
+ import { MoneyValidationError, assertIntegerMinorUnits } from "../money.js";
6
+ import { a as DOCUMENT_MONEY_READ_REBIND, o as DOCUMENT_MONEY_WRITE } from "./TaxSettingsCollection-DYYF-In-.js";
7
+ import { i as withTenantMoneyCollectionRead } from "./tenant-money-barrier-y6EAa6vY.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
+ creditedAmount = 0;
91
+ tenantId = null;
92
+ customerId = "";
93
+ contractId = "";
94
+ invoiceNumber = "";
95
+ /**
96
+ * External reference (e.g., customer PO number)
97
+ */
98
+ reference = "";
99
+ issueDate = /* @__PURE__ */ new Date();
100
+ dueDate = /* @__PURE__ */ new Date();
101
+ paidDate = null;
102
+ subtotal = 0;
103
+ taxAmount = 0;
104
+ providerTaxAmount = 0;
105
+ totalAmount = 0;
106
+ amountPaid = 0;
107
+ currency = "CAD";
108
+ /** Pricing interpretation fixed for the document before issue. */
109
+ lineAmountType = "exclusive";
110
+ /** Component rounding scope fixed for the document before issue. */
111
+ taxRoundingScope = "line";
112
+ discountTotal = 0;
113
+ /** Immutable seller registration snapshot once the invoice leaves draft. */
114
+ sellerTaxRegistrationsJson = "[]";
115
+ get sellerTaxRegistrations() {
116
+ try {
117
+ const value = JSON.parse(this.sellerTaxRegistrationsJson);
118
+ if (!Array.isArray(value)) return [];
119
+ return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.number === "string" && typeof entry.label === "string");
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+ set sellerTaxRegistrations(value) {
125
+ this.sellerTaxRegistrationsJson = JSON.stringify(value);
126
+ }
127
+ /** Immutable customer tax identifier snapshot once issued. */
128
+ partyTaxId = "";
129
+ /** Stable issue/freeze identity, independent of lifecycle updated_at. */
130
+ moneySnapshotId = "";
131
+ status = InvoiceStatus.DRAFT;
132
+ arJournalId = "";
133
+ arReversalJournalId = null;
134
+ revenueJournalId = "";
135
+ /**
136
+ * External ID in accounting provider (e.g., QBO invoice ID)
137
+ */
138
+ externalId = "";
139
+ /**
140
+ * Customer's external ID in accounting provider.
141
+ * Used to link invoice to customer in external system.
142
+ */
143
+ customerExternalId = "";
144
+ /**
145
+ * Accounting provider name ('quickbooks' | 'stripe' | etc.)
146
+ */
147
+ externalProvider = "";
148
+ /**
149
+ * When invoice was last synced to provider
150
+ */
151
+ syncedAt = null;
152
+ accountingSyncStateJson = "";
153
+ /**
154
+ * How the provider collects this invoice (#3190), decided once by billing
155
+ * period close before its first push and reused on every retry, so it is
156
+ * what the provider was asked for. Server-managed. A host must not offer
157
+ * another payment rail on a `charge_automatically` invoice the provider is
158
+ * still collecting.
159
+ */
160
+ collectionMethod = "";
161
+ /**
162
+ * When invoice was sent to customer
163
+ */
164
+ sentAt = null;
165
+ /**
166
+ * When customer viewed the invoice
167
+ */
168
+ viewedAt = null;
169
+ /**
170
+ * Number of payment reminders sent
171
+ */
172
+ remindersSent = 0;
173
+ /**
174
+ * When last reminder was sent
175
+ */
176
+ lastReminderAt = null;
177
+ notes = "";
178
+ customerNotes = "";
179
+ terms = "";
180
+ constructor(options = {}) {
181
+ super(options);
182
+ if (options.kind !== void 0) this.kind = options.kind;
183
+ if (options.creditedInvoiceId !== void 0) this.creditedInvoiceId = options.creditedInvoiceId;
184
+ if (options.creditedAmount !== void 0) this.creditedAmount = options.creditedAmount;
185
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
186
+ if (options.customerId !== void 0) this.customerId = options.customerId;
187
+ if (options.contractId !== void 0) this.contractId = options.contractId;
188
+ if (options.invoiceNumber !== void 0) this.invoiceNumber = options.invoiceNumber;
189
+ if (options.reference !== void 0) this.reference = options.reference;
190
+ if (options.issueDate !== void 0) this.issueDate = options.issueDate;
191
+ if (options.dueDate !== void 0) this.dueDate = options.dueDate;
192
+ if (options.paidDate !== void 0) this.paidDate = options.paidDate;
193
+ if (options.subtotal !== void 0) this.subtotal = options.subtotal;
194
+ if (options.taxAmount !== void 0) this.taxAmount = options.taxAmount;
195
+ if (options.providerTaxAmount !== void 0) this.providerTaxAmount = options.providerTaxAmount;
196
+ if (options.totalAmount !== void 0) this.totalAmount = options.totalAmount;
197
+ if (options.amountPaid !== void 0) this.amountPaid = options.amountPaid;
198
+ if (options.currency !== void 0) this.currency = options.currency;
199
+ if (options.lineAmountType !== void 0) this.lineAmountType = options.lineAmountType;
200
+ if (options.taxRoundingScope !== void 0) this.taxRoundingScope = options.taxRoundingScope;
201
+ if (options.discountTotal !== void 0) this.discountTotal = options.discountTotal;
202
+ if (options.sellerTaxRegistrationsJson !== void 0) this.sellerTaxRegistrationsJson = options.sellerTaxRegistrationsJson;
203
+ if (options.sellerTaxRegistrations !== void 0) this.sellerTaxRegistrations = structuredClone(options.sellerTaxRegistrations);
204
+ if (options.partyTaxId !== void 0) this.partyTaxId = options.partyTaxId;
205
+ if (options.moneySnapshotId !== void 0) this.moneySnapshotId = options.moneySnapshotId;
206
+ if (options.status !== void 0) this.status = options.status;
207
+ if (options.arJournalId !== void 0) this.arJournalId = options.arJournalId;
208
+ if (options.arReversalJournalId !== void 0) this.arReversalJournalId = options.arReversalJournalId;
209
+ if (options.revenueJournalId !== void 0) this.revenueJournalId = options.revenueJournalId;
210
+ if (options.externalId !== void 0) this.externalId = options.externalId;
211
+ if (options.customerExternalId !== void 0) this.customerExternalId = options.customerExternalId;
212
+ if (options.externalProvider !== void 0) this.externalProvider = options.externalProvider;
213
+ if (options.syncedAt !== void 0) this.syncedAt = options.syncedAt;
214
+ if (options.accountingSyncStateJson !== void 0) this.accountingSyncStateJson = options.accountingSyncStateJson;
215
+ if (options.collectionMethod !== void 0) this.collectionMethod = options.collectionMethod;
216
+ if (options.sentAt !== void 0) this.sentAt = options.sentAt;
217
+ if (options.viewedAt !== void 0) this.viewedAt = options.viewedAt;
218
+ if (options.remindersSent !== void 0) this.remindersSent = options.remindersSent;
219
+ if (options.lastReminderAt !== void 0) this.lastReminderAt = options.lastReminderAt;
220
+ if (options.notes !== void 0) this.notes = options.notes;
221
+ if (options.customerNotes !== void 0) this.customerNotes = options.customerNotes;
222
+ if (options.terms !== void 0) this.terms = options.terms;
223
+ }
224
+ /**
225
+ * Capture the persisted status the row was loaded with, so the save-time
226
+ * transition guard can reject illegal status flips made via raw field
227
+ * assignment (mass-assignment on a generated update route, a stale caller,
228
+ * etc.). Only rows that already exist in the database carry a "prior"
229
+ * status — freshly-constructed (not-yet-saved) invoices have no prior and
230
+ * may start in any status.
231
+ */
232
+ async initialize() {
233
+ await super.initialize();
234
+ if (await this.isSaved()) loadedInvoiceStatus.set(this, this.status);
235
+ return this;
236
+ }
237
+ /**
238
+ * Recompute and validate financial fields before every write so forged
239
+ * totals, negative amounts, and illegal status flips can't be persisted via
240
+ * raw mass-assignment on the generated CRUD routes.
241
+ *
242
+ * Behaviour:
243
+ * - **Totals are authoritative from line items.** When the invoice is
244
+ * persisted and has line items, `subtotal` / `taxAmount` / `totalAmount`
245
+ * are recomputed from those items, overriding whatever the caller sent.
246
+ * This blocks "create real line items but claim a tiny total" forgery.
247
+ * - **Without line items**, the caller-supplied totals are accepted but the
248
+ * `totalAmount === subtotal + taxAmount` arithmetic invariant is enforced.
249
+ * - **amountPaid is derived/validated.** When persisted, it is recomputed
250
+ * from PaymentAllocations rather than trusted from the caller. It may
251
+ * never exceed `totalAmount`.
252
+ * - **Whole minor units and non-negativity** are enforced on all four
253
+ * amounts. The integer check runs FIRST (#2401), before the
254
+ * `totalAmount === subtotal + taxAmount` invariant, so a caller who wrote
255
+ * major units gets "money is minor units" rather than a confusing
256
+ * arithmetic-mismatch error from float addition.
257
+ * - **Status transitions** are validated against the prior persisted status.
258
+ */
259
+ async save() {
260
+ const { saveInvoice } = await import("./document-money-oUDvdjOf.js").then((n) => n.t);
261
+ return await saveInvoice(this);
262
+ }
263
+ /** @internal Aggregate persistence only. */
264
+ async saveDerived(capability, options) {
265
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice money write capability is required");
266
+ await claimAllocationParent(this, "invoice");
267
+ const persisted = await this.isSaved();
268
+ const prior = await this.resolvePriorStatus(persisted);
269
+ this.assertAmountsAreMinorUnits();
270
+ if (options.recomputeLegacy) await this.recomputeAmountsForSave(persisted);
271
+ else if (persisted && this.id) await this.recomputeAmountPaid();
272
+ else this.amountPaid = 0;
273
+ this.assertNonNegativeAmounts();
274
+ this.assertStatusTransition(prior);
275
+ this.assertPaymentStatusConsistent();
276
+ await this.assertDraftDeliveryHistory(persisted);
277
+ await assertAllocationParent(this, "invoice");
278
+ const result = await super.save();
279
+ loadedInvoiceStatus.set(this, this.status);
280
+ return result;
281
+ }
282
+ async delete(options = {}) {
283
+ const { deleteInvoice } = await import("./document-money-oUDvdjOf.js").then((n) => n.t);
284
+ await deleteInvoice(this, options);
285
+ }
286
+ /** @internal Aggregate persistence only. */
287
+ async deleteDerived(capability, options = {}) {
288
+ if (capability !== DOCUMENT_MONEY_WRITE) throw new Error("Invoice money write capability is required");
289
+ await super.delete(options);
290
+ }
291
+ /**
292
+ * Resolve the AUTHORITATIVE prior status (S5 audit #1390 round 4). The
293
+ * {@link loadedInvoiceStatus} WeakMap is only populated when {@link initialize}
294
+ * hydrated the row, so a `create({ id: <existing>, _skipLoad: true })` upsert
295
+ * yields an instance whose WeakMap entry is missing — trusting it would treat
296
+ * the write as a brand-new row and skip the transition guard entirely. Read
297
+ * the persisted row's status directly so a create-onto-existing is correctly
298
+ * treated as an update. `undefined` means no row exists (genuinely new).
299
+ */
300
+ async resolvePriorStatus(persisted) {
301
+ if (persisted && this.id) try {
302
+ const row = await this.db.get(this.tableName, { id: this.id });
303
+ if (row && row.status != null) return row.status;
304
+ } catch {}
305
+ return loadedInvoiceStatus.get(this);
306
+ }
307
+ /**
308
+ * Recompute subtotal/tax/total from line items (when present) and derive
309
+ * amountPaid from PaymentAllocations. Falls back to the arithmetic invariant
310
+ * when the invoice has no line items. Tolerant of smrt-ledgers being absent
311
+ * (the dynamic imports stay inside the package).
312
+ */
313
+ async recomputeAmountsForSave(persisted) {
314
+ if (persisted && this.id) {
315
+ const { InvoiceLineItemCollection } = await import("./InvoiceLineItemCollection-CPQGghyd.js").then((n) => n.n);
316
+ const lineItems = await (await InvoiceLineItemCollection.create({
317
+ ...this.options,
318
+ db: this.db
319
+ })).findByInvoice(this.id);
320
+ if (lineItems.length > 0) {
321
+ const subtotal = lineItems.reduce((sum, item) => sum + item.getSubtotal(), 0);
322
+ const taxAmount = lineItems.reduce((sum, item) => sum + item.getTaxAmount(), 0) + this.providerTaxAmount;
323
+ const computedTotal = subtotal + taxAmount;
324
+ this.subtotal = subtotal;
325
+ this.taxAmount = taxAmount;
326
+ this.totalAmount = computedTotal;
327
+ } else this.assertTotalArithmetic();
328
+ await this.recomputeAmountPaid();
329
+ } else {
330
+ this.assertTotalArithmetic();
331
+ this.amountPaid = 0;
332
+ 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.`);
333
+ }
334
+ }
335
+ async recomputeAmountPaid() {
336
+ const { PaymentAllocationCollection } = await import("../index.js").then((n) => n.r);
337
+ const allocationCollection = await PaymentAllocationCollection.create({
338
+ ...this.options,
339
+ db: this.db
340
+ });
341
+ if (!this.id) throw new Error("Persisted invoice is missing its ID");
342
+ this.amountPaid = await allocationCollection.getTotalAllocatedToInvoice(this.id);
343
+ }
344
+ /**
345
+ * Whether `amountPaid` covers `totalAmount`. This is the SINGLE source of
346
+ * truth for the **PAID** decision — both {@link updatePaymentStatus} (which
347
+ * decides PAID) and {@link assertPaymentStatusConsistent} (which validates
348
+ * PAID on save) call it, so the PAID-deciding comparison and the
349
+ * PAID-validating comparison can never drift apart (S5 audit #1390
350
+ * follow-up).
351
+ *
352
+ * With integer minor units this is a plain `>=` (#2401). The float-era
353
+ * hazard it used to guard against — a total summed from `0.1 × 3` line items
354
+ * reading as "not covered" by `>=` but "covered" by an epsilon-tolerant
355
+ * guard, leaving a genuinely-paid invoice unsaveable — cannot arise when both
356
+ * sides are exact integers. That also removes the documented PAID/PARTIAL
357
+ * asymmetry: {@link updatePaymentStatus} and {@link isPartiallyPaid} now
358
+ * agree that any `amountPaid > 0` is a partial payment, because there is no
359
+ * such thing as sub-cent dust.
360
+ */
361
+ isFullyPaid() {
362
+ return this.amountPaid + this.creditedAmount >= this.totalAmount;
363
+ }
364
+ /**
365
+ * Whether `amountPaid` is a non-trivial partial payment of `totalAmount`.
366
+ * Derived from {@link isFullyPaid} so the two stay mutually exclusive.
367
+ */
368
+ isPartiallyPaid() {
369
+ return this.amountPaid + this.creditedAmount > 0 && !this.isFullyPaid();
370
+ }
371
+ /**
372
+ * After amounts are recomputed and amountPaid is re-derived from allocations,
373
+ * assert the persisted `status` is consistent with the derived
374
+ * amountPaid-vs-totalAmount relationship. This blocks the raw
375
+ * `SENT → PAID with amountPaid=0` flip (and its inverse, claiming PARTIAL/SENT
376
+ * while fully allocated) that the status-transition map alone permits because
377
+ * SENT → PAID is a structurally legal edge.
378
+ *
379
+ * Only enforced for the payment-derived statuses (PAID / PARTIAL and the
380
+ * unpaid states DRAFT / SENT / VIEWED). Lifecycle statuses that aren't a
381
+ * function of amountPaid — OVERDUE, CANCELLED, WRITTEN_OFF — are left
382
+ * to their own transition rules.
383
+ */
384
+ assertPaymentStatusConsistent() {
385
+ const label = this.invoiceNumber || this.id || "<new>";
386
+ const fullyPaid = this.isFullyPaid();
387
+ const partiallyPaid = this.isPartiallyPaid();
388
+ if (this.status === InvoiceStatus.DRAFT && this.amountPaid !== 0) throw new Error(`Invoice ${label}: DRAFT requires zero derived payment allocations.`);
389
+ 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().`);
390
+ 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().`);
391
+ 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.`);
392
+ }
393
+ /** A payment reversal cannot erase an actual delivery event. */
394
+ async assertDraftDeliveryHistory(persisted) {
395
+ if (this.status !== InvoiceStatus.DRAFT) return;
396
+ if (this.sentAt || this.viewedAt) throw new Error("DRAFT requires an undelivered invoice");
397
+ if (persisted && this.id) {
398
+ const { InvoiceCollection } = await Promise.resolve().then(() => InvoiceCollection_exports);
399
+ const prior = await (await InvoiceCollection.create({ db: this.db })).get({ id: this.id }, { cache: false });
400
+ if (prior?.sentAt || prior?.viewedAt) throw new Error("DRAFT cannot erase persisted invoice delivery history");
401
+ }
402
+ }
403
+ /**
404
+ * Enforce `totalAmount === subtotal + taxAmount` exactly. Used when the
405
+ * invoice has no line items to recompute from.
406
+ *
407
+ * Exact because every term is integer minor units (#2401). The float era
408
+ * needed a `0.01` tolerance here and then had to SNAP `totalAmount` to the
409
+ * recomputed value afterwards, because a total that passed the invoice's
410
+ * loose tolerance could still fail smrt-ledgers' tighter `BALANCE_EPSILON`
411
+ * and void the journal `recognizeRevenue` builds from it. Integer arithmetic
412
+ * removes both halves of that workaround: what the caller supplied either is
413
+ * the sum or is rejected, and any journal built from it balances exactly.
414
+ */
415
+ assertTotalArithmetic() {
416
+ const expected = this.subtotal + this.taxAmount;
417
+ if (this.totalAmount !== expected) throw new Error(`Invoice ${this.invoiceNumber || this.id || "<new>"}: totalAmount ${this.totalAmount} must equal subtotal + taxAmount (${expected}).`);
418
+ }
419
+ /**
420
+ * Reject amounts that are not whole minor units, negative financial values,
421
+ * and an amountPaid that exceeds the total (overpayment is modelled
422
+ * elsewhere, not by letting amountPaid float above the invoice total).
423
+ */
424
+ assertAmountsAreMinorUnits() {
425
+ assertIntegerMinorUnits("Invoice", this.invoiceNumber || this.id || "<new>", [
426
+ ["subtotal", this.subtotal],
427
+ ["taxAmount", this.taxAmount],
428
+ ["providerTaxAmount", this.providerTaxAmount],
429
+ ["totalAmount", this.totalAmount],
430
+ ["amountPaid", this.amountPaid],
431
+ ["creditedAmount", this.creditedAmount]
432
+ ]);
433
+ }
434
+ assertNonNegativeAmounts() {
435
+ const label = this.invoiceNumber || this.id || "<new>";
436
+ const amounts = [
437
+ ["subtotal", this.subtotal],
438
+ ["taxAmount", this.taxAmount],
439
+ ["providerTaxAmount", this.providerTaxAmount],
440
+ ["totalAmount", this.totalAmount],
441
+ ["amountPaid", this.amountPaid],
442
+ ["creditedAmount", this.creditedAmount]
443
+ ];
444
+ assertIntegerMinorUnits("Invoice", label, amounts);
445
+ for (const [field2, value] of amounts) if (value < 0) throw new Error(`Invoice ${label}: ${field2} must be a non-negative number (got ${value}).`);
446
+ if (this.amountPaid > this.totalAmount) throw new Error(`Invoice ${label}: amountPaid ${this.amountPaid} cannot exceed totalAmount ${this.totalAmount}.`);
447
+ }
448
+ /**
449
+ * Reject an illegal status flip done via raw field assignment. Compares the
450
+ * about-to-be-written status against the status the row was loaded with.
451
+ * No-op transitions (status unchanged) and brand-new rows are always allowed.
452
+ */
453
+ assertStatusTransition(prior) {
454
+ if (prior === void 0) return;
455
+ if (prior === this.status) return;
456
+ 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).`);
457
+ }
458
+ /**
459
+ * Check if invoice is a draft
460
+ */
461
+ isDraft() {
462
+ return this.status === InvoiceStatus.DRAFT;
463
+ }
464
+ /**
465
+ * Check if invoice has been sent
466
+ */
467
+ isSent() {
468
+ return this.status === InvoiceStatus.SENT;
469
+ }
470
+ /**
471
+ * Check if invoice is fully paid
472
+ */
473
+ isPaid() {
474
+ return this.status === InvoiceStatus.PAID;
475
+ }
476
+ /**
477
+ * Check if invoice is partially paid
478
+ */
479
+ isPartial() {
480
+ return this.status === InvoiceStatus.PARTIAL;
481
+ }
482
+ /**
483
+ * Check if invoice is overdue
484
+ */
485
+ isOverdue() {
486
+ if (this.status === InvoiceStatus.OVERDUE) return true;
487
+ if (this.isPaid() || this.status === InvoiceStatus.CANCELLED) return false;
488
+ return /* @__PURE__ */ new Date() > this.dueDate;
489
+ }
490
+ /**
491
+ * Get remaining amount due
492
+ */
493
+ getAmountDue() {
494
+ return Math.max(0, this.getBalance());
495
+ }
496
+ /** Signed amount still owed after payments and issued credit notes. */
497
+ getBalance() {
498
+ return this.totalAmount - this.amountPaid - this.creditedAmount;
499
+ }
500
+ /**
501
+ * Mark invoice as sent
502
+ */
503
+ markSent() {
504
+ if (this.status === InvoiceStatus.PARTIAL || this.status === InvoiceStatus.PAID) {
505
+ this.sentAt ??= /* @__PURE__ */ new Date();
506
+ return;
507
+ }
508
+ if (this.status !== InvoiceStatus.DRAFT) throw new Error(`Cannot send invoice with status '${this.status}'. Only draft or payment-progress invoices can be sent.`);
509
+ this.status = InvoiceStatus.SENT;
510
+ this.sentAt = /* @__PURE__ */ new Date();
511
+ }
512
+ /**
513
+ * Record that the invoice was viewed by the customer.
514
+ *
515
+ * This method is idempotent - calling it multiple times will only
516
+ * record the first view time. Status only transitions to VIEWED if
517
+ * the current status is SENT (other statuses like PARTIAL or PAID
518
+ * are preserved).
519
+ */
520
+ markViewed() {
521
+ if (this.viewedAt) return;
522
+ this.viewedAt = /* @__PURE__ */ new Date();
523
+ if (this.status === InvoiceStatus.SENT) this.status = InvoiceStatus.VIEWED;
524
+ }
525
+ /**
526
+ * Update payment amount and status.
527
+ *
528
+ * Called when PaymentAllocations change. Handles both forward transitions
529
+ * (DRAFT/SENT → PARTIAL → PAID) and reversals (PAID → PARTIAL → original status).
530
+ *
531
+ * Terminal statuses (CANCELLED, WRITTEN_OFF) are not modified.
532
+ */
533
+ updatePaymentStatus(amountPaid) {
534
+ this.amountPaid = amountPaid;
535
+ if (this.status === InvoiceStatus.CANCELLED || this.status === InvoiceStatus.WRITTEN_OFF) return;
536
+ if (this.isFullyPaid()) {
537
+ if (this.status !== InvoiceStatus.PAID || !this.paidDate) this.paidDate = /* @__PURE__ */ new Date();
538
+ this.status = InvoiceStatus.PAID;
539
+ } else if (amountPaid + this.creditedAmount > 0) {
540
+ this.status = InvoiceStatus.PARTIAL;
541
+ this.paidDate = null;
542
+ } else {
543
+ this.paidDate = null;
544
+ if (this.viewedAt) this.status = InvoiceStatus.VIEWED;
545
+ else if (this.sentAt) this.status = InvoiceStatus.SENT;
546
+ else this.status = InvoiceStatus.DRAFT;
547
+ }
548
+ }
549
+ /**
550
+ * Cancel the invoice
551
+ */
552
+ cancel() {
553
+ 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}.`);
554
+ this.status = InvoiceStatus.CANCELLED;
555
+ }
556
+ /** Cancel a posted money-v2 invoice with an exact atomic journal reversal. */
557
+ async cancelWithRevenueReversal(options) {
558
+ const { cancelInvoiceWithRevenueReversal } = await import("./invoice-posting-CF3fE_Ec.js");
559
+ return cancelInvoiceWithRevenueReversal(this, options);
560
+ }
561
+ /**
562
+ * Write off the invoice (bad debt)
563
+ */
564
+ writeOff() {
565
+ this.status = InvoiceStatus.WRITTEN_OFF;
566
+ }
567
+ /**
568
+ * Recognize revenue and create AR journal entry.
569
+ *
570
+ * Creates a balanced journal entry in smrt-ledgers:
571
+ * - Debit: Accounts Receivable (assets increase)
572
+ * - Credit: Revenue (revenue increases)
573
+ * - Credit: Tax Payable (liability increases, if taxAmount > 0)
574
+ *
575
+ * **Note**: This method saves the invoice after setting arJournalId.
576
+ *
577
+ * @param options - Account IDs for the journal entry
578
+ * @returns The created journal
579
+ *
580
+ * @example
581
+ * ```typescript
582
+ * const journal = await invoice.recognizeRevenue({
583
+ * arAccountId: '1120',
584
+ * revenueAccountId: '4100',
585
+ * taxAccountId: '2130'
586
+ * });
587
+ * ```
588
+ */
589
+ async recognizeRevenue(options) {
590
+ const { recognizeInvoiceRevenueV2 } = await import("./invoice-posting-CF3fE_Ec.js");
591
+ const v2Journal = await recognizeInvoiceRevenueV2(this, options);
592
+ if (v2Journal) return v2Journal;
593
+ if (this.arJournalId) throw new Error(`Revenue already recognized for invoice ${this.invoiceNumber} (journal ID: ${this.arJournalId})`);
594
+ await this.save();
595
+ if (this.totalAmount <= 0) throw new Error(`Cannot recognize revenue for invoice ${this.invoiceNumber} with non-positive total: ${this.totalAmount}`);
596
+ if (this.taxAmount > 0 && !options.taxAccountId) throw new Error(`Invoice ${this.invoiceNumber} has taxAmount ${this.taxAmount} but no taxAccountId provided`);
597
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
598
+ const journalCollection = await JournalCollection.create(this.options);
599
+ const entries = [{
600
+ accountId: options.arAccountId,
601
+ debit: this.totalAmount,
602
+ memo: `Invoice ${this.invoiceNumber}`
603
+ }, {
604
+ accountId: options.revenueAccountId,
605
+ credit: this.subtotal,
606
+ memo: `Invoice ${this.invoiceNumber}`
607
+ }];
608
+ if (this.taxAmount > 0 && options.taxAccountId) entries.push({
609
+ accountId: options.taxAccountId,
610
+ credit: this.taxAmount,
611
+ memo: `Tax on Invoice ${this.invoiceNumber}`
612
+ });
613
+ const journal = await journalCollection.createWithEntries({
614
+ description: `Revenue: Invoice ${this.invoiceNumber}`,
615
+ sourceModule: "smrt-commerce",
616
+ sourceRef: this.id,
617
+ entries
618
+ });
619
+ await journal.post();
620
+ if (!journal.id) throw new Error("Invoice.recognizeRevenue: journal was posted but has no id");
621
+ this.arJournalId = journal.id;
622
+ try {
623
+ await this.save();
624
+ } catch (err) {
625
+ try {
626
+ await journal.void(`Reverted: invoice ${this.invoiceNumber} save failed during revenue recognition`);
627
+ } catch {}
628
+ this.arJournalId = "";
629
+ throw err;
630
+ }
631
+ return journal;
632
+ }
633
+ /**
634
+ * Get the AR journal entry (if revenue was recognized)
635
+ */
636
+ async getArJournal() {
637
+ if (!this.arJournalId) return null;
638
+ try {
639
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
640
+ return await (await JournalCollection.create(this.options)).get({ id: this.arJournalId });
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+ /**
646
+ * Convert to InvoiceInput for SDK accounting provider sync.
647
+ *
648
+ * Fetches line items and maps all fields to the format expected
649
+ * by @happyvertical/accounting providers. The SDK takes currency **major**
650
+ * units (`12.34` USD), so every amount is converted from this package's
651
+ * integer minor units with the currency's ISO 4217 exponent (#3060).
652
+ *
653
+ * @returns InvoiceInput compatible with @happyvertical/accounting
654
+ *
655
+ * @example
656
+ * ```typescript
657
+ * const provider = await getAccountingProvider({ type: 'quickbooks', ... });
658
+ * const input = await invoice.toAccountingInput();
659
+ * const result = await provider.invoices.push(input);
660
+ * invoice.externalId = result.externalId;
661
+ * invoice.syncedAt = result.syncedAt;
662
+ * await invoice.save();
663
+ * ```
664
+ */
665
+ async toAccountingInput(options) {
666
+ if (!this.tenantId) return this.toAccountingInputLocked(options, null);
667
+ const { withTenantMoneyRuntime } = await import("./tenant-money-barrier-y6EAa6vY.js").then((n) => n.r);
668
+ return withTenantMoneyRuntime(this.db, this.tenantId, "legacy-or-v2", (scope) => this.withDatabase(scope.db, (bound) => bound.toAccountingInputLocked(options, scope.barrier.settings)));
669
+ }
670
+ async toAccountingInputLocked(options, settings) {
671
+ if (settings?.moneyModel === "v2") {
672
+ if (!options) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Money-v2 accounting conversion requires explicit realm mapping");
673
+ if (!this.id || !this.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Money-v2 accounting conversion requires a persisted tenant invoice");
674
+ const { toFrozenInvoiceAccountingInput } = await import("../index.js").then((n) => n.n);
675
+ return toFrozenInvoiceAccountingInput({
676
+ db: this.db,
677
+ tenantId: this.tenantId,
678
+ invoiceId: this.id,
679
+ mapping: options.mapping
680
+ });
681
+ }
682
+ const { InvoiceLineItemCollection } = await import("./InvoiceLineItemCollection-CPQGghyd.js").then((n) => n.n);
683
+ const lineItemCollection = await InvoiceLineItemCollection.create(this.options);
684
+ if (!this.id) throw new Error("Invoice.toAccountingInput requires a persisted invoice (missing id)");
685
+ const invoiceId = this.id;
686
+ const lineItems = await lineItemCollection.findByInvoice(invoiceId);
687
+ return {
688
+ id: invoiceId,
689
+ externalId: this.externalId || void 0,
690
+ invoiceNumber: this.invoiceNumber,
691
+ customerId: this.customerId,
692
+ customerExternalId: this.customerExternalId || void 0,
693
+ issueDate: this.issueDate,
694
+ dueDate: this.dueDate,
695
+ lineItems: lineItems.map((item) => item.toAccountingLineItem(this.currency)),
696
+ subtotal: minorToMajorUnits(this.subtotal, this.currency),
697
+ taxAmount: minorToMajorUnits(this.taxAmount, this.currency),
698
+ totalAmount: minorToMajorUnits(this.totalAmount, this.currency),
699
+ currency: this.currency,
700
+ reference: this.reference || void 0,
701
+ memo: this.customerNotes || void 0
702
+ };
703
+ }
704
+ };
705
+ __publicField(Invoice, "__smrtPackage__", "@happyvertical/smrt-commerce");
706
+ __decorateClass([field({ description: "Invoice or customer credit note." })], Invoice.prototype, "kind", 2);
707
+ __decorateClass([foreignKey("Invoice", { nullable: true })], Invoice.prototype, "creditedInvoiceId", 2);
708
+ __decorateClass([field({ ui: { format: "money" } })], Invoice.prototype, "creditedAmount", 2);
709
+ __decorateClass([tenantId({ nullable: true })], Invoice.prototype, "tenantId", 2);
710
+ __decorateClass([field({ description: "The customer being billed." }), foreignKey("Customer")], Invoice.prototype, "customerId", 2);
711
+ __decorateClass([field({ description: "The order or agreement this invoice bills for, if any." }), foreignKey("SalesContract", { validateSubtype: true })], Invoice.prototype, "contractId", 2);
712
+ __decorateClass([field({ description: "The number printed on the invoice." })], Invoice.prototype, "invoiceNumber", 2);
713
+ __decorateClass([field({ description: "The date the invoice was issued." })], Invoice.prototype, "issueDate", 2);
714
+ __decorateClass([field({ description: "The date payment is expected." })], Invoice.prototype, "dueDate", 2);
715
+ __decorateClass([field({ description: "The date it was paid in full." })], Invoice.prototype, "paidDate", 2);
716
+ __decorateClass([field({
717
+ description: "The amount before tax.",
718
+ ui: { format: "money" }
719
+ })], Invoice.prototype, "subtotal", 2);
720
+ __decorateClass([field({
721
+ description: "The tax charged.",
722
+ ui: { format: "money" }
723
+ })], Invoice.prototype, "taxAmount", 2);
724
+ __decorateClass([field({ ui: { format: "money" } })], Invoice.prototype, "providerTaxAmount", 2);
725
+ __decorateClass([field({
726
+ description: "What the customer owes in all, tax included.",
727
+ ui: { format: "money" }
728
+ })], Invoice.prototype, "totalAmount", 2);
729
+ __decorateClass([field({
730
+ description: "How much of the total has been paid so far.",
731
+ ui: { format: "money" }
732
+ })], Invoice.prototype, "amountPaid", 2);
733
+ __decorateClass([field({
734
+ description: "The currency of the invoice.",
735
+ ui: { widget: "currency" }
736
+ })], Invoice.prototype, "currency", 2);
737
+ __decorateClass([field({ ui: { format: "money" } })], Invoice.prototype, "discountTotal", 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-BUe_8FT1.js.map