@happyvertical/smrt-commerce 0.56.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -8
- package/README.md +76 -0
- package/agents/accounting-snapshot-mapper.md +111 -0
- package/agents/contract-money.md +18 -0
- package/agents/cost-billing.md +7 -2
- package/agents/document-money.md +126 -0
- package/agents/invoice-calculations.md +14 -2
- package/agents/line-amount-migration.md +50 -0
- package/agents/minor-unit-migration.md +20 -0
- package/agents/money-calculator.md +45 -0
- package/agents/money-model.md +113 -0
- package/agents/revenue-journal.md +23 -0
- package/agents/tax-codes.md +48 -0
- package/agents/tax-summary.md +126 -0
- package/dist/accounting-snapshot.d.ts +79 -0
- package/dist/accounting-snapshot.d.ts.map +1 -0
- package/dist/billing/runtime.d.ts +2 -0
- package/dist/billing/runtime.d.ts.map +1 -1
- package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js +265 -0
- package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +1 -0
- package/dist/chunks/CustomerCollection-DQIujn4f.js +99 -0
- package/dist/chunks/CustomerCollection-DQIujn4f.js.map +1 -0
- package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js +1639 -0
- package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +1 -0
- package/dist/chunks/InvoiceCollection-CKQflFjc.js +1047 -0
- package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +1 -0
- package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +467 -0
- package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +1 -0
- package/dist/chunks/TaxCodeCollection-DOpzW4y8.js +149 -0
- package/dist/chunks/TaxCodeCollection-DOpzW4y8.js.map +1 -0
- package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js +129 -0
- package/dist/chunks/TaxComponentCollection-Cs2uXwEl.js.map +1 -0
- package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +319 -0
- package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +1 -0
- package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-DI3G16BN.js} +7 -12
- package/dist/chunks/allocation-integrity-DI3G16BN.js.map +1 -0
- package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
- package/dist/chunks/calculations-DaHR17il.js.map +1 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
- package/dist/chunks/document-money-DNVhDJyC.js +860 -0
- package/dist/chunks/document-money-DNVhDJyC.js.map +1 -0
- package/dist/chunks/invoice-posting-BwV8g7z0.js +229 -0
- package/dist/chunks/invoice-posting-BwV8g7z0.js.map +1 -0
- package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
- package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
- package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +135 -0
- package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +1 -0
- package/dist/chunks/{types-zVnIjSOk.js → types-LDETvQ1C.js} +3 -14
- package/dist/chunks/types-LDETvQ1C.js.map +1 -0
- package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
- package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
- package/dist/collections/ContractCollection.d.ts +35 -1
- package/dist/collections/ContractCollection.d.ts.map +1 -1
- package/dist/collections/ContractLineItemCollection.d.ts +26 -1
- package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
- package/dist/collections/DocumentTaxCollection.d.ts +75 -0
- package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
- package/dist/collections/InvoiceCollection.d.ts +26 -1
- package/dist/collections/InvoiceCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
- package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
- package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
- package/dist/collections/TaxCodeCollection.d.ts +7 -0
- package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
- package/dist/collections/TaxComponentCollection.d.ts +6 -0
- package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
- package/dist/collections/TaxSettingsCollection.d.ts +16 -0
- package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/commerce-money-tools.d.ts +9 -0
- package/dist/commerce-money-tools.d.ts.map +1 -0
- package/dist/contract-money.d.ts +27 -0
- package/dist/contract-money.d.ts.map +1 -0
- package/dist/cost-billing.d.ts +0 -1
- package/dist/cost-billing.d.ts.map +1 -1
- package/dist/cost-billing.js +10 -15
- package/dist/cost-billing.js.map +1 -1
- package/dist/credit-allocation.d.ts +48 -0
- package/dist/credit-allocation.d.ts.map +1 -0
- package/dist/document-money-internal.d.ts +44 -0
- package/dist/document-money-internal.d.ts.map +1 -0
- package/dist/document-money.d.ts +85 -0
- package/dist/document-money.d.ts.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5395 -4246
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts +96 -0
- package/dist/invoice-accounting-sync.d.ts.map +1 -0
- package/dist/invoice-mcp.d.ts +16 -0
- package/dist/invoice-mcp.d.ts.map +1 -0
- package/dist/invoice-posting.d.ts +9 -0
- package/dist/invoice-posting.d.ts.map +1 -0
- package/dist/invoice-record-screen.d.ts +60 -0
- package/dist/invoice-record-screen.d.ts.map +1 -0
- package/dist/invoices.d.ts +2 -0
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +3 -2
- package/dist/manifest.json +10365 -4562
- package/dist/migrations/commerceLineAmounts.d.ts +84 -0
- package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
- package/dist/models/Contract.d.ts +25 -1
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +30 -1
- package/dist/models/ContractLineItem.d.ts.map +1 -1
- package/dist/models/Customer.d.ts +7 -0
- package/dist/models/Customer.d.ts.map +1 -1
- package/dist/models/DocumentTax.d.ts +50 -0
- package/dist/models/DocumentTax.d.ts.map +1 -0
- package/dist/models/Invoice.d.ts +46 -2
- package/dist/models/Invoice.d.ts.map +1 -1
- package/dist/models/InvoiceLineItem.d.ts +33 -2
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/models/MoneyMigrationDocument.d.ts +28 -0
- package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
- package/dist/models/Payment.d.ts +5 -1
- package/dist/models/Payment.d.ts.map +1 -1
- package/dist/models/PaymentAllocation.d.ts.map +1 -1
- package/dist/models/TaxCode.d.ts +22 -0
- package/dist/models/TaxCode.d.ts.map +1 -0
- package/dist/models/TaxComponent.d.ts +21 -0
- package/dist/models/TaxComponent.d.ts.map +1 -0
- package/dist/models/TaxSettings.d.ts +33 -0
- package/dist/models/TaxSettings.d.ts.map +1 -0
- package/dist/models/TaxSummaryReport.d.ts +35 -0
- package/dist/models/TaxSummaryReport.d.ts.map +1 -0
- package/dist/models/Vendor.d.ts +9 -0
- package/dist/models/Vendor.d.ts.map +1 -1
- package/dist/models/index.d.ts +6 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/money-rational.d.ts +15 -0
- package/dist/money-rational.d.ts.map +1 -0
- package/dist/money.d.ts +106 -25
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +274 -0
- package/dist/money.js.map +1 -0
- package/dist/recipes.d.ts +15 -15
- package/dist/revenue-journal.d.ts +61 -0
- package/dist/revenue-journal.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +5711 -1029
- package/dist/svelte/components/InvoiceActions.svelte +12 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceEditor.svelte +15 -1
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
- package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceTotals.svelte +2 -2
- package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/QuoteEditor.svelte +54 -1
- package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +16 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +16 -0
- package/dist/svelte/index.d.ts +7 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/invoices/calculations.d.ts +7 -2
- package/dist/svelte/invoices/calculations.d.ts.map +1 -1
- package/dist/svelte/invoices/calculations.js +141 -47
- package/dist/svelte/invoices/money-editor.d.ts +123 -0
- package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
- package/dist/svelte/invoices/money-editor.js +101 -0
- package/dist/svelte/invoices/types.d.ts +5 -1
- package/dist/svelte/invoices/types.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.d.ts +5 -0
- package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.js +5 -0
- package/dist/svelte/quote-i18n.d.ts +5 -0
- package/dist/svelte/quote-i18n.d.ts.map +1 -1
- package/dist/svelte/quote-i18n.js +5 -0
- package/dist/tax/cookbook.d.ts +22 -0
- package/dist/tax/cookbook.d.ts.map +1 -0
- package/dist/tax/example-pack.d.ts +25 -0
- package/dist/tax/example-pack.d.ts.map +1 -0
- package/dist/tax/identity.d.ts +5 -0
- package/dist/tax/identity.d.ts.map +1 -0
- package/dist/tax/index.d.ts +4 -0
- package/dist/tax/index.d.ts.map +1 -0
- package/dist/tax/legacy.d.ts +48 -0
- package/dist/tax/legacy.d.ts.map +1 -0
- package/dist/tax-summary-report-adapter.d.ts +37 -0
- package/dist/tax-summary-report-adapter.d.ts.map +1 -0
- package/dist/tax-summary.d.ts +129 -0
- package/dist/tax-summary.d.ts.map +1 -0
- package/dist/tenant-money-barrier.d.ts +66 -0
- package/dist/tenant-money-barrier.d.ts.map +1 -0
- package/dist/types/index.d.ts +137 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +19 -11
- package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
- package/dist/chunks/money-DfrRxrel.js.map +0 -1
- 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
|