@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.
- package/AGENTS.md +9 -8
- package/README.md +98 -4
- package/agents/accounting-snapshot-mapper.md +111 -0
- package/agents/contract-money.md +24 -0
- package/agents/cost-billing.md +7 -2
- package/agents/customer-vendor-ui.md +7 -1
- package/agents/document-money.md +127 -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 +128 -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--BrGMtNz.js +265 -0
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +1 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js +99 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js.map +1 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js +1659 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +1 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js +1047 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +1 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +468 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +1 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js +149 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js.map +1 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js +129 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js.map +1 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +319 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +1 -0
- package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-Cq0g6CfO.js} +7 -12
- package/dist/chunks/allocation-integrity-Cq0g6CfO.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-oUDvdjOf.js +860 -0
- package/dist/chunks/document-money-oUDvdjOf.js.map +1 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.js +229 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.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-y6EAa6vY.js +135 -0
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +1 -0
- package/dist/chunks/{types-zVnIjSOk.js → types-Bbm0Xflc.js} +4 -14
- package/dist/chunks/types-Bbm0Xflc.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 +5411 -4249
- 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 +12804 -6095
- package/dist/migrations/commerceLineAmounts.d.ts +84 -0
- package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
- package/dist/migrations/partyLinks.d.ts.map +1 -1
- package/dist/models/Contract.d.ts +34 -4
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +43 -6
- 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 +46 -5
- 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 +7 -1
- 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 +9621 -7592
- 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 +138 -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,860 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { t as DocumentTaxCollection } from "./DocumentTaxCollection-KOkaQT2M.js";
|
|
3
|
+
import { s as InvoiceStatus } from "./types-Bbm0Xflc.js";
|
|
4
|
+
import { t as CustomerCollection } from "./CustomerCollection-CjGaZSpf.js";
|
|
5
|
+
import { MoneyValidationError, calculateMoneyDocument, resolveLineTaxCode } from "../money.js";
|
|
6
|
+
import { t as allocateFrozenCredit } from "./credit-allocation-yspz0Lab.js";
|
|
7
|
+
import { c as lockDocumentMoneyParents, l as withDocumentMoneyObjectTransaction, o as DOCUMENT_MONEY_WRITE, u as withDocumentMoneyTransaction } from "./TaxSettingsCollection-DYYF-In-.js";
|
|
8
|
+
import { t as InvoiceCollection } from "./InvoiceCollection-BUe_8FT1.js";
|
|
9
|
+
import { n as lockTenantMoneyRuntime, o as withTenantMoneyRuntime, t as lockTenantMoneyMigrationOwner } from "./tenant-money-barrier-y6EAa6vY.js";
|
|
10
|
+
import { r as InvoiceLineItem, t as InvoiceLineItemCollection } from "./InvoiceLineItemCollection-CPQGghyd.js";
|
|
11
|
+
import { t as TaxCodeCollection } from "./TaxCodeCollection-DF2eaJed.js";
|
|
12
|
+
import { t as TaxComponentCollection } from "./TaxComponentCollection-3x24xSNh.js";
|
|
13
|
+
import { getTenantId } from "@happyvertical/smrt-tenancy";
|
|
14
|
+
//#region src/document-money.ts
|
|
15
|
+
var document_money_exports = /* @__PURE__ */ __exportAll({
|
|
16
|
+
deleteInvoice: () => deleteInvoice,
|
|
17
|
+
deleteInvoiceLineItem: () => deleteInvoiceLineItem,
|
|
18
|
+
getInvoiceMoneyFacts: () => getInvoiceMoneyFacts,
|
|
19
|
+
lockAndReloadInvoice: () => lockAndReloadInvoice,
|
|
20
|
+
materializeInvoiceMoneyDraft: () => materializeInvoiceMoneyDraft,
|
|
21
|
+
previewInvoiceMoney: () => previewInvoiceMoney,
|
|
22
|
+
saveInvoice: () => saveInvoice,
|
|
23
|
+
saveInvoiceLineItem: () => saveInvoiceLineItem
|
|
24
|
+
});
|
|
25
|
+
var FROZEN_INVOICE_FIELDS = [
|
|
26
|
+
"kind",
|
|
27
|
+
"creditedInvoiceId",
|
|
28
|
+
"tenantId",
|
|
29
|
+
"customerId",
|
|
30
|
+
"contractId",
|
|
31
|
+
"invoiceNumber",
|
|
32
|
+
"reference",
|
|
33
|
+
"issueDate",
|
|
34
|
+
"dueDate",
|
|
35
|
+
"subtotal",
|
|
36
|
+
"discountTotal",
|
|
37
|
+
"taxAmount",
|
|
38
|
+
"providerTaxAmount",
|
|
39
|
+
"totalAmount",
|
|
40
|
+
"currency",
|
|
41
|
+
"lineAmountType",
|
|
42
|
+
"taxRoundingScope",
|
|
43
|
+
"sellerTaxRegistrationsJson",
|
|
44
|
+
"partyTaxId",
|
|
45
|
+
"moneySnapshotId",
|
|
46
|
+
"notes",
|
|
47
|
+
"customerNotes",
|
|
48
|
+
"terms"
|
|
49
|
+
];
|
|
50
|
+
var FROZEN_LINE_FIELDS = [
|
|
51
|
+
"tenantId",
|
|
52
|
+
"invoiceId",
|
|
53
|
+
"creditedLineId",
|
|
54
|
+
"creditCalculationBasisJson",
|
|
55
|
+
"description",
|
|
56
|
+
"sku",
|
|
57
|
+
"productId",
|
|
58
|
+
"quantity",
|
|
59
|
+
"unitPrice",
|
|
60
|
+
"discountType",
|
|
61
|
+
"discountRate",
|
|
62
|
+
"discount",
|
|
63
|
+
"taxMode",
|
|
64
|
+
"taxCodeId",
|
|
65
|
+
"taxCodeCode",
|
|
66
|
+
"taxCodeName",
|
|
67
|
+
"taxComponentsJson",
|
|
68
|
+
"taxAmount",
|
|
69
|
+
"taxRate",
|
|
70
|
+
"amount",
|
|
71
|
+
"sourceType",
|
|
72
|
+
"sourceId",
|
|
73
|
+
"periodStart",
|
|
74
|
+
"periodEnd",
|
|
75
|
+
"revenueAccountId",
|
|
76
|
+
"sortOrder",
|
|
77
|
+
"moneySnapshotId"
|
|
78
|
+
];
|
|
79
|
+
function comparable(value) {
|
|
80
|
+
return value instanceof Date ? value.toISOString() : value ?? null;
|
|
81
|
+
}
|
|
82
|
+
function assertFieldsUnchanged(proposed, persisted, fields, label) {
|
|
83
|
+
for (const field of fields) if (comparable(proposed[field]) !== comparable(persisted[field])) throw new MoneyValidationError("FROZEN_DOCUMENT", `${label} ${String(field)} is frozen`);
|
|
84
|
+
}
|
|
85
|
+
function assertTenantId(value) {
|
|
86
|
+
if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Money v2 requires a tenant-owned invoice");
|
|
87
|
+
}
|
|
88
|
+
function requireId(value, label) {
|
|
89
|
+
if (!value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} requires a persisted ID`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
async function lockRuntimeSettings(db, tenantId) {
|
|
93
|
+
if (!tenantId) return null;
|
|
94
|
+
return (await lockTenantMoneyRuntime(db, tenantId)).settings;
|
|
95
|
+
}
|
|
96
|
+
function isFrozen(invoice) {
|
|
97
|
+
return invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId || !!invoice.moneySnapshotId;
|
|
98
|
+
}
|
|
99
|
+
function aggregateSignature(invoice) {
|
|
100
|
+
return JSON.stringify([
|
|
101
|
+
invoice.lineAmountType,
|
|
102
|
+
invoice.taxRoundingScope,
|
|
103
|
+
invoice.subtotal,
|
|
104
|
+
invoice.discountTotal,
|
|
105
|
+
invoice.taxAmount,
|
|
106
|
+
invoice.totalAmount
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
async function loadInvoice(db, id) {
|
|
110
|
+
const collection = await InvoiceCollection.create({ db });
|
|
111
|
+
const invoice = await collection.get({ id }, { cache: false });
|
|
112
|
+
if (!invoice) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice ${id} was not found`);
|
|
113
|
+
return {
|
|
114
|
+
collection,
|
|
115
|
+
invoice
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function findInvoice(db, id) {
|
|
119
|
+
return (await InvoiceCollection.create({ db })).get({ id }, { cache: false });
|
|
120
|
+
}
|
|
121
|
+
async function lockAndReloadInvoice(db, invoiceId, tenantId) {
|
|
122
|
+
const first = await loadInvoice(db, invoiceId);
|
|
123
|
+
await lockDocumentMoneyParents([{
|
|
124
|
+
id: invoiceId,
|
|
125
|
+
tenantId,
|
|
126
|
+
collection: first.collection
|
|
127
|
+
}]);
|
|
128
|
+
const invoice = await first.collection.get({ id: invoiceId }, { cache: false });
|
|
129
|
+
if (!invoice || invoice.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant identity changed while acquiring its money lock");
|
|
130
|
+
return {
|
|
131
|
+
collection: first.collection,
|
|
132
|
+
invoice
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function lockAndReloadMoneyInvoice(db, invoiceId, tenantId, creditedInvoiceId) {
|
|
136
|
+
const collection = await InvoiceCollection.create({ db });
|
|
137
|
+
const ids = [invoiceId, ...creditedInvoiceId ? [creditedInvoiceId] : []];
|
|
138
|
+
if ((await Promise.all(ids.map((id) => collection.get({ id }, { cache: false })))).some((row) => !row || row.tenantId !== tenantId)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money parents must exist in the active tenant");
|
|
139
|
+
await lockDocumentMoneyParents(ids.map((id) => ({
|
|
140
|
+
id,
|
|
141
|
+
tenantId,
|
|
142
|
+
collection
|
|
143
|
+
})));
|
|
144
|
+
const invoice = await collection.get({ id: invoiceId }, { cache: false });
|
|
145
|
+
const source = creditedInvoiceId ? await collection.get({ id: creditedInvoiceId }, { cache: false }) : null;
|
|
146
|
+
if (!invoice || invoice.tenantId !== tenantId || source && source.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money parent identity changed while acquiring its lock");
|
|
147
|
+
return {
|
|
148
|
+
collection,
|
|
149
|
+
invoice,
|
|
150
|
+
source
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function discountInput(line) {
|
|
154
|
+
if (line.discountType === "percent") return {
|
|
155
|
+
kind: "percent",
|
|
156
|
+
rate: String(line.discountRate)
|
|
157
|
+
};
|
|
158
|
+
if (line.discountType === "amount" || line.discount !== 0) return {
|
|
159
|
+
kind: "amount",
|
|
160
|
+
minor: line.discount
|
|
161
|
+
};
|
|
162
|
+
if (line.discountType !== "none") throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown discount type: ${line.discountType}`);
|
|
163
|
+
return { kind: "none" };
|
|
164
|
+
}
|
|
165
|
+
function issuedCredit(invoice) {
|
|
166
|
+
return invoice.kind === "credit_note" && invoice.status !== InvoiceStatus.DRAFT && invoice.status !== InvoiceStatus.CANCELLED;
|
|
167
|
+
}
|
|
168
|
+
async function refreshIssuedCreditBalance(db, source) {
|
|
169
|
+
const sourceId = requireId(source.id, "Credited invoice");
|
|
170
|
+
const credits = await (await InvoiceCollection.create({ db })).list({ where: {
|
|
171
|
+
tenantId: source.tenantId,
|
|
172
|
+
creditedInvoiceId: sourceId
|
|
173
|
+
} });
|
|
174
|
+
const issued = credits.filter(issuedCredit).sort((left, right) => {
|
|
175
|
+
const date = left.issueDate.getTime() - right.issueDate.getTime();
|
|
176
|
+
if (date) return date;
|
|
177
|
+
return String(left.id) < String(right.id) ? -1 : String(left.id) > String(right.id) ? 1 : 0;
|
|
178
|
+
});
|
|
179
|
+
const total = issued.reduce((sum, credit) => sum + credit.totalAmount, 0);
|
|
180
|
+
if (!Number.isSafeInteger(total) || total < 0) throw new MoneyValidationError("UNSAFE_MONEY", "Issued credit total is not a nonnegative safe integer");
|
|
181
|
+
let remainingSource = Math.max(0, source.totalAmount - source.amountPaid);
|
|
182
|
+
for (const credit of credits) {
|
|
183
|
+
const included = issued.includes(credit);
|
|
184
|
+
const applied = included ? Math.min(credit.totalAmount, remainingSource) : 0;
|
|
185
|
+
if (included) remainingSource -= applied;
|
|
186
|
+
if (credit.creditedAmount !== applied) {
|
|
187
|
+
credit.creditedAmount = applied;
|
|
188
|
+
credit.updatePaymentStatus(credit.amountPaid);
|
|
189
|
+
await credit.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
source.creditedAmount = total;
|
|
193
|
+
source.updatePaymentStatus(source.amountPaid);
|
|
194
|
+
await source.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
195
|
+
}
|
|
196
|
+
function lineNet(line, parent) {
|
|
197
|
+
return parent.lineAmountType === "inclusive" ? line.amount - line.taxAmount : line.amount;
|
|
198
|
+
}
|
|
199
|
+
async function prepareCreditLines(db, invoice, lines) {
|
|
200
|
+
if (!invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
|
|
201
|
+
const sourceInvoice = await findInvoice(db, invoice.creditedInvoiceId);
|
|
202
|
+
if (sourceInvoice?.kind !== "invoice" || sourceInvoice.tenantId !== invoice.tenantId || sourceInvoice.status === InvoiceStatus.DRAFT || sourceInvoice.status === InvoiceStatus.CANCELLED || !sourceInvoice.moneySnapshotId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must be an issued, non-cancelled invoice in the same tenant");
|
|
203
|
+
if (sourceInvoice.currency !== invoice.currency) throw new MoneyValidationError("CURRENCY_MISMATCH", "Credit note currency must match its credited invoice");
|
|
204
|
+
if (sourceInvoice.customerId !== invoice.customerId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit note customer must match its credited invoice");
|
|
205
|
+
invoice.lineAmountType = sourceInvoice.lineAmountType;
|
|
206
|
+
invoice.taxRoundingScope = sourceInvoice.taxRoundingScope;
|
|
207
|
+
const lineCollection = await InvoiceLineItemCollection.create({ db });
|
|
208
|
+
const sourceLines = await lineCollection.findByInvoice(requireId(sourceInvoice.id, "Credited invoice"));
|
|
209
|
+
const sourceTaxes = await (await DocumentTaxCollection.create({ db })).findByDocument(requireId(sourceInvoice.tenantId, "Credited invoice tenant"), "invoice", requireId(sourceInvoice.id, "Credited invoice"));
|
|
210
|
+
const sourceByLineId = new Map(sourceLines.map((line) => [requireId(line.id, "Credited line"), line]));
|
|
211
|
+
const priorCredits = (await (await InvoiceCollection.create({ db })).list({ where: {
|
|
212
|
+
tenantId: invoice.tenantId,
|
|
213
|
+
creditedInvoiceId: invoice.creditedInvoiceId
|
|
214
|
+
} })).filter((row) => row.id !== invoice.id && issuedCredit(row));
|
|
215
|
+
const issuedLines = [];
|
|
216
|
+
const issuedParentById = new Map(priorCredits.map((row) => [requireId(row.id, "Issued credit note"), row]));
|
|
217
|
+
const consumptions = [];
|
|
218
|
+
for (const credit of priorCredits) for (const line of await lineCollection.findByInvoice(requireId(credit.id, "Issued credit note"))) {
|
|
219
|
+
if (!line.creditedLineId) continue;
|
|
220
|
+
issuedLines.push(line);
|
|
221
|
+
const basis = line.creditCalculationBasis;
|
|
222
|
+
consumptions.push({
|
|
223
|
+
creditedLineId: line.creditedLineId,
|
|
224
|
+
quantity: basis?.kind === "quantity" ? basis.quantity : "0",
|
|
225
|
+
netMinor: lineNet(line, credit),
|
|
226
|
+
discountMinor: line.discount
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const targeted = lines.filter((line) => line.creditedLineId);
|
|
230
|
+
const allocation = targeted.length ? allocateFrozenCredit(sourceLines.map((line) => ({
|
|
231
|
+
id: requireId(line.id, "Credited line"),
|
|
232
|
+
quantity: String(line.quantity),
|
|
233
|
+
netMinor: lineNet(line, sourceInvoice),
|
|
234
|
+
discountMinor: line.discount,
|
|
235
|
+
unitPrice: line.unitPrice,
|
|
236
|
+
discountType: line.discountType,
|
|
237
|
+
discountRate: line.discountRate
|
|
238
|
+
})), consumptions, targeted.map((line) => {
|
|
239
|
+
const basis = line.creditCalculationBasis;
|
|
240
|
+
return basis?.kind === "net_amount" ? {
|
|
241
|
+
creditedLineId: requireId(line.creditedLineId, "Credited line"),
|
|
242
|
+
netMinor: basis.netMinor
|
|
243
|
+
} : {
|
|
244
|
+
creditedLineId: requireId(line.creditedLineId, "Credited line"),
|
|
245
|
+
quantity: basis?.kind === "quantity" ? basis.quantity : String(line.quantity)
|
|
246
|
+
};
|
|
247
|
+
})) : {
|
|
248
|
+
allocations: [],
|
|
249
|
+
exhaustsDocument: false
|
|
250
|
+
};
|
|
251
|
+
const allocationByLineId = /* @__PURE__ */ new Map();
|
|
252
|
+
const targetBySource = new Map(targeted.map((line) => [requireId(line.creditedLineId, "Credited line"), line]));
|
|
253
|
+
for (const row of allocation.allocations) {
|
|
254
|
+
const line = targetBySource.get(row.creditedLineId);
|
|
255
|
+
if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit allocation lost its target line");
|
|
256
|
+
line.creditCalculationBasis = row.basis;
|
|
257
|
+
allocationByLineId.set(requireId(line.id, "Credit line"), row);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
allocationByLineId,
|
|
261
|
+
sourceByLineId,
|
|
262
|
+
issuedLines,
|
|
263
|
+
issuedParentById,
|
|
264
|
+
sourceInvoice,
|
|
265
|
+
sourceTaxes,
|
|
266
|
+
exhaustsDocument: allocation.exhaustsDocument
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function recomputeInvoiceAggregate(db, invoice, settings, freezeSnapshotId = "", persist = true, proposedLines) {
|
|
270
|
+
assertTenantId(invoice.tenantId);
|
|
271
|
+
const tenantId = invoice.tenantId;
|
|
272
|
+
const linesCollection = await InvoiceLineItemCollection.create({ db });
|
|
273
|
+
const codesCollection = await TaxCodeCollection.create({ db });
|
|
274
|
+
const componentsCollection = await TaxComponentCollection.create({ db });
|
|
275
|
+
const invoiceId = requireId(invoice.id, "Invoice calculation");
|
|
276
|
+
const lines = proposedLines ?? await linesCollection.findByInvoice(invoiceId);
|
|
277
|
+
const credit = invoice.kind === "credit_note" ? await prepareCreditLines(db, invoice, lines) : null;
|
|
278
|
+
const codes = await codesCollection.list({ where: { tenantId } });
|
|
279
|
+
const components = await componentsCollection.list({ where: { tenantId } });
|
|
280
|
+
const componentById = new Map(components.map((row) => [requireId(row.id, "Tax component"), row]));
|
|
281
|
+
const codeById = new Map(codes.map((row) => [requireId(row.id, "Tax code"), row]));
|
|
282
|
+
const codeInputs = {};
|
|
283
|
+
for (const code of codes) {
|
|
284
|
+
const codeId = requireId(code.id, "Tax code");
|
|
285
|
+
codeInputs[codeId] = {
|
|
286
|
+
id: codeId,
|
|
287
|
+
kind: code.kind,
|
|
288
|
+
active: code.active,
|
|
289
|
+
appliesTo: code.appliesTo,
|
|
290
|
+
components: code.components.map((part) => {
|
|
291
|
+
const component = componentById.get(part.taxComponentId);
|
|
292
|
+
if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
|
|
293
|
+
return {
|
|
294
|
+
taxComponentId: requireId(component.id, "Tax component"),
|
|
295
|
+
percent: component.percent,
|
|
296
|
+
compound: part.compound
|
|
297
|
+
};
|
|
298
|
+
})
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (credit) for (const source of credit.sourceByLineId.values()) {
|
|
302
|
+
if (!source.taxCodeId) continue;
|
|
303
|
+
const frozenKind = credit.sourceTaxes.find((row) => row.taxCodeId === source.taxCodeId)?.kind;
|
|
304
|
+
codeInputs[source.taxCodeId] = {
|
|
305
|
+
id: source.taxCodeId,
|
|
306
|
+
kind: frozenKind ?? (source.taxComponents.length ? "taxable" : "zero-rated"),
|
|
307
|
+
active: true,
|
|
308
|
+
appliesTo: "sales",
|
|
309
|
+
components: source.taxComponents.map((component) => ({
|
|
310
|
+
taxComponentId: component.taxComponentId,
|
|
311
|
+
percent: component.percent,
|
|
312
|
+
compound: false
|
|
313
|
+
}))
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const exempt = await codesCollection.findByCode("exempt");
|
|
317
|
+
const outOfScope = await codesCollection.findByCode("out-of-scope");
|
|
318
|
+
const customers = await CustomerCollection.create({ db });
|
|
319
|
+
const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
|
|
320
|
+
if (customer && customer.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice customer must belong to its tenant");
|
|
321
|
+
const productIds = [...new Set(lines.filter((line) => !line.creditedLineId).map((line) => line.productId).filter((productId) => !!productId))];
|
|
322
|
+
const productById = /* @__PURE__ */ new Map();
|
|
323
|
+
if (productIds.length > 0) {
|
|
324
|
+
const { ProductCollection } = await import("@happyvertical/smrt-products/collections");
|
|
325
|
+
const products = await ProductCollection.create({ db });
|
|
326
|
+
for (const productId of productIds) {
|
|
327
|
+
const product = await products.get({ id: productId }, { cache: false });
|
|
328
|
+
if (!product) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} was not found`);
|
|
329
|
+
if (product.tenantId !== null && product.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice product ${productId} must be global or belong to its tenant`);
|
|
330
|
+
productById.set(productId, product);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const resolvedCodes = /* @__PURE__ */ new Map();
|
|
334
|
+
const calculated = calculateMoneyDocument({
|
|
335
|
+
currency: invoice.currency,
|
|
336
|
+
lineAmountType: invoice.lineAmountType,
|
|
337
|
+
roundingScope: invoice.taxRoundingScope,
|
|
338
|
+
lines: lines.map((line) => {
|
|
339
|
+
if (line.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice line ${line.id} must belong to its parent tenant`);
|
|
340
|
+
const lineId = requireId(line.id, "Invoice line");
|
|
341
|
+
if (line.creditedLineId) {
|
|
342
|
+
if (!credit) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited line");
|
|
343
|
+
const source = credit.sourceByLineId.get(line.creditedLineId);
|
|
344
|
+
const allocation = credit.allocationByLineId.get(lineId);
|
|
345
|
+
if (!source || !allocation) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credited line is not part of the frozen source invoice");
|
|
346
|
+
const taxCode = source.taxCodeId ? codeInputs[source.taxCodeId] ?? null : null;
|
|
347
|
+
resolvedCodes.set(lineId, source.taxCodeId ? {
|
|
348
|
+
id: source.taxCodeId,
|
|
349
|
+
code: source.taxCodeCode,
|
|
350
|
+
name: source.taxCodeName,
|
|
351
|
+
kind: taxCode?.kind ?? "zero-rated",
|
|
352
|
+
components: structuredClone(source.taxComponents)
|
|
353
|
+
} : null);
|
|
354
|
+
line.description ||= source.description;
|
|
355
|
+
line.sku ||= source.sku;
|
|
356
|
+
line.productId = source.productId;
|
|
357
|
+
line.revenueAccountId = source.revenueAccountId;
|
|
358
|
+
if (allocation.basis.kind === "quantity") {
|
|
359
|
+
line.quantity = Number(allocation.basis.quantity);
|
|
360
|
+
line.unitPrice = allocation.basis.sourceUnitPrice;
|
|
361
|
+
line.discountType = allocation.discountMinor ? "amount" : "none";
|
|
362
|
+
line.discountRate = "0";
|
|
363
|
+
} else {
|
|
364
|
+
line.quantity = 1;
|
|
365
|
+
line.unitPrice = allocation.netMinor ?? allocation.basis.netMinor;
|
|
366
|
+
line.discountType = "none";
|
|
367
|
+
line.discountRate = "0";
|
|
368
|
+
}
|
|
369
|
+
line.discount = allocation.discountMinor;
|
|
370
|
+
return {
|
|
371
|
+
id: lineId,
|
|
372
|
+
quantity: line.quantity,
|
|
373
|
+
unitPrice: line.unitPrice,
|
|
374
|
+
netAmountMinor: allocation.basis.kind === "net_amount" ? allocation.netMinor ?? allocation.basis.netMinor : void 0,
|
|
375
|
+
discount: allocation.discountMinor ? {
|
|
376
|
+
kind: "amount",
|
|
377
|
+
minor: allocation.discountMinor
|
|
378
|
+
} : { kind: "none" },
|
|
379
|
+
taxCode
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (invoice.kind === "invoice" && line.creditCalculationBasis) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot carry a credit calculation basis");
|
|
383
|
+
if (invoice.kind === "credit_note" && line.taxMode !== "override") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Free-form credit lines require an explicit tax-code override");
|
|
384
|
+
const resolved = resolveLineTaxCode({
|
|
385
|
+
lineAmountType: invoice.lineAmountType,
|
|
386
|
+
lineOverride: line.taxMode === "override" ? line.taxCodeId : null
|
|
387
|
+
}, {
|
|
388
|
+
direction: "sales",
|
|
389
|
+
codes: codeInputs,
|
|
390
|
+
customerTaxExempt: customer?.taxExempt ?? false,
|
|
391
|
+
exemptCodeId: exempt?.id,
|
|
392
|
+
productCodeId: line.productId ? productById.get(line.productId)?.defaultSalesTaxCodeId || null : null,
|
|
393
|
+
partyCodeId: customer?.defaultTaxCodeId,
|
|
394
|
+
appDefaultCodeId: settings.defaultSalesTaxCodeId,
|
|
395
|
+
outOfScopeCodeId: outOfScope?.id
|
|
396
|
+
});
|
|
397
|
+
resolvedCodes.set(lineId, resolved.taxCode ? (() => {
|
|
398
|
+
const code = codeById.get(resolved.taxCode.id);
|
|
399
|
+
if (!code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax code ${resolved.taxCode.id} was not loaded`);
|
|
400
|
+
return {
|
|
401
|
+
id: requireId(code.id, "Tax code"),
|
|
402
|
+
code: code.code,
|
|
403
|
+
name: code.name,
|
|
404
|
+
kind: code.kind,
|
|
405
|
+
components: code.components.map((part) => {
|
|
406
|
+
const component = componentById.get(part.taxComponentId);
|
|
407
|
+
if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
|
|
408
|
+
return {
|
|
409
|
+
taxComponentId: part.taxComponentId,
|
|
410
|
+
name: component.name,
|
|
411
|
+
percent: component.percent,
|
|
412
|
+
taxMinor: 0
|
|
413
|
+
};
|
|
414
|
+
})
|
|
415
|
+
};
|
|
416
|
+
})() : null);
|
|
417
|
+
return {
|
|
418
|
+
id: requireId(line.id, "Invoice line"),
|
|
419
|
+
quantity: line.quantity,
|
|
420
|
+
unitPrice: line.unitPrice,
|
|
421
|
+
discount: discountInput(line),
|
|
422
|
+
taxCode: resolved.taxCode
|
|
423
|
+
};
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
if (credit?.exhaustsDocument) {
|
|
427
|
+
const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
|
|
428
|
+
const targeted = lines.filter((line) => !!line.creditedLineId);
|
|
429
|
+
for (const line of targeted) {
|
|
430
|
+
const source = credit.sourceByLineId.get(requireId(line.creditedLineId, "Credited line"));
|
|
431
|
+
const result = resultById.get(requireId(line.id, "Credit line"));
|
|
432
|
+
if (!source || !result) continue;
|
|
433
|
+
const prior = credit.issuedLines.filter((candidate) => candidate.creditedLineId === source.id);
|
|
434
|
+
const priorNet = prior.reduce((sum, candidate) => {
|
|
435
|
+
const parent = credit.issuedParentById.get(candidate.invoiceId);
|
|
436
|
+
return sum + (parent ? lineNet(candidate, parent) : 0);
|
|
437
|
+
}, 0);
|
|
438
|
+
const desiredNet = lineNet(source, credit.sourceInvoice) - priorNet;
|
|
439
|
+
const netDelta = desiredNet - result.netMinor;
|
|
440
|
+
result.netMinor = desiredNet;
|
|
441
|
+
result.amountMinor += netDelta;
|
|
442
|
+
for (const sourceComponent of source.taxComponents) {
|
|
443
|
+
const component = result.components.find((candidate) => candidate.taxComponentId === sourceComponent.taxComponentId);
|
|
444
|
+
if (!component) continue;
|
|
445
|
+
const priorTax = prior.reduce((sum, candidate) => sum + (candidate.taxComponents.find((part) => part.taxComponentId === sourceComponent.taxComponentId)?.taxMinor ?? 0), 0);
|
|
446
|
+
const desiredTax = sourceComponent.taxMinor - priorTax;
|
|
447
|
+
const taxDelta = desiredTax - component.taxMinor;
|
|
448
|
+
component.taxMinor = desiredTax;
|
|
449
|
+
result.taxMinor += taxDelta;
|
|
450
|
+
if (invoice.lineAmountType === "inclusive") result.amountMinor += taxDelta;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const rebuilt = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const result of calculated.lines) {
|
|
455
|
+
const metadata = resolvedCodes.get(result.id) ?? null;
|
|
456
|
+
const parts = result.components.length ? result.components : [{
|
|
457
|
+
taxComponentId: null,
|
|
458
|
+
taxMinor: 0
|
|
459
|
+
}];
|
|
460
|
+
for (const part of parts) {
|
|
461
|
+
const key = `${metadata?.id ?? ""}\0${part.taxComponentId ?? ""}`;
|
|
462
|
+
const row = rebuilt.get(key) ?? {
|
|
463
|
+
taxCodeId: metadata?.id ?? null,
|
|
464
|
+
taxComponentId: part.taxComponentId,
|
|
465
|
+
taxableMinor: 0,
|
|
466
|
+
taxMinor: 0
|
|
467
|
+
};
|
|
468
|
+
row.taxableMinor += result.netMinor;
|
|
469
|
+
row.taxMinor += part.taxMinor;
|
|
470
|
+
rebuilt.set(key, row);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
calculated.taxes = [...rebuilt.values()].sort((left, right) => {
|
|
474
|
+
const a = `${left.taxCodeId ?? ""}\0${left.taxComponentId ?? ""}`;
|
|
475
|
+
const b = `${right.taxCodeId ?? ""}\0${right.taxComponentId ?? ""}`;
|
|
476
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
477
|
+
});
|
|
478
|
+
calculated.subtotalMinor = calculated.lines.reduce((sum, row) => sum + row.netMinor, 0);
|
|
479
|
+
calculated.taxMinor = calculated.lines.reduce((sum, row) => sum + row.taxMinor, 0);
|
|
480
|
+
calculated.totalMinor = calculated.subtotalMinor + calculated.taxMinor;
|
|
481
|
+
}
|
|
482
|
+
if (credit) {
|
|
483
|
+
const resultById = new Map(calculated.lines.map((row) => [row.id, row]));
|
|
484
|
+
for (const [lineId, allocation] of credit.allocationByLineId) {
|
|
485
|
+
if (allocation.basis.kind !== "quantity") continue;
|
|
486
|
+
const result = resultById.get(lineId);
|
|
487
|
+
const hasAmountBasisConsumption = credit.issuedLines.some((line) => line.creditedLineId === allocation.creditedLineId && line.creditCalculationBasis?.kind === "net_amount");
|
|
488
|
+
if (result && Math.abs(result.netMinor) > Math.abs(allocation.remainingNetMinor) && (!allocation.exhaustsLine || hasAmountBasisConsumption)) throw new MoneyValidationError("CREDIT_EXCEEDS_LINE", "Credit quantity exceeds the remaining frozen net amount");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const lineById = new Map(lines.map((line) => [requireId(line.id, "Invoice line"), line]));
|
|
492
|
+
for (const result of calculated.lines) {
|
|
493
|
+
const line = lineById.get(result.id);
|
|
494
|
+
if (!line) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Calculated line ${result.id} was not loaded`);
|
|
495
|
+
const code = resolvedCodes.get(requireId(line.id, "Invoice line")) ?? null;
|
|
496
|
+
line.discount = result.discountMinor;
|
|
497
|
+
line.amount = result.amountMinor;
|
|
498
|
+
line.taxAmount = result.taxMinor;
|
|
499
|
+
line.taxCodeId = code?.id ?? null;
|
|
500
|
+
line.taxCodeCode = code?.code ?? null;
|
|
501
|
+
line.taxCodeName = code?.name ?? null;
|
|
502
|
+
line.taxComponents = result.components.map((entry) => {
|
|
503
|
+
const component = code?.components.find((candidate) => candidate.taxComponentId === entry.taxComponentId);
|
|
504
|
+
if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Calculated component ${entry.taxComponentId} was not loaded`);
|
|
505
|
+
return {
|
|
506
|
+
taxComponentId: component.taxComponentId,
|
|
507
|
+
name: component.name,
|
|
508
|
+
percent: component.percent,
|
|
509
|
+
taxMinor: entry.taxMinor
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
line.taxRate = code ? code.components.reduce((sum, part) => sum + Number(part.percent) / 100, 0) : 0;
|
|
513
|
+
if (freezeSnapshotId) line.moneySnapshotId = freezeSnapshotId;
|
|
514
|
+
if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE, false);
|
|
515
|
+
}
|
|
516
|
+
const taxRows = calculated.taxes.map((tax) => {
|
|
517
|
+
const code = tax.taxCodeId ? [...resolvedCodes.values()].find((candidate) => candidate?.id === tax.taxCodeId) ?? null : null;
|
|
518
|
+
const snapshotComponent = tax.taxComponentId ? code?.components.find((candidate) => candidate.taxComponentId === tax.taxComponentId) : null;
|
|
519
|
+
const currentComponent = tax.taxComponentId ? componentById.get(tax.taxComponentId) : null;
|
|
520
|
+
const sourceTax = credit?.sourceTaxes.find((row) => row.taxCodeId === tax.taxCodeId && row.taxComponentId === tax.taxComponentId);
|
|
521
|
+
if (tax.taxCodeId && !code) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax code ${tax.taxCodeId} was not loaded`);
|
|
522
|
+
if (tax.taxComponentId && !snapshotComponent) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Document tax component ${tax.taxComponentId} was not loaded`);
|
|
523
|
+
return {
|
|
524
|
+
tenantId,
|
|
525
|
+
documentType: invoice.kind,
|
|
526
|
+
documentId: invoiceId,
|
|
527
|
+
currency: invoice.currency,
|
|
528
|
+
taxCodeId: code ? requireId(code.id, "Tax code") : null,
|
|
529
|
+
taxCodeCode: code?.code ?? null,
|
|
530
|
+
kind: code?.kind ?? null,
|
|
531
|
+
taxComponentId: snapshotComponent?.taxComponentId ?? null,
|
|
532
|
+
taxComponentName: snapshotComponent?.name ?? null,
|
|
533
|
+
percent: snapshotComponent?.percent ?? null,
|
|
534
|
+
liabilityAccountId: sourceTax?.liabilityAccountId || currentComponent?.liabilityAccountId || null,
|
|
535
|
+
taxableMinor: tax.taxableMinor,
|
|
536
|
+
taxMinor: tax.taxMinor,
|
|
537
|
+
recoverableMinor: 0
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
if (persist) await (await DocumentTaxCollection.create({ db })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
|
|
541
|
+
tenantId,
|
|
542
|
+
documentType: invoice.kind,
|
|
543
|
+
documentId: invoiceId,
|
|
544
|
+
rows: taxRows
|
|
545
|
+
});
|
|
546
|
+
invoice.subtotal = calculated.subtotalMinor;
|
|
547
|
+
invoice.discountTotal = calculated.discountTotalMinor;
|
|
548
|
+
invoice.taxAmount = calculated.taxMinor + invoice.providerTaxAmount;
|
|
549
|
+
invoice.totalAmount = calculated.totalMinor + invoice.providerTaxAmount;
|
|
550
|
+
return {
|
|
551
|
+
lines,
|
|
552
|
+
taxRows
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async function previewInvoiceMoneyOnScope(input, settings) {
|
|
556
|
+
if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview requires its active principal tenant");
|
|
557
|
+
const { invoice } = await loadInvoice(input.db, input.invoiceId);
|
|
558
|
+
if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview cannot cross tenant boundaries");
|
|
559
|
+
if (invoice.status !== InvoiceStatus.DRAFT || invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Frozen invoice money cannot be previewed as a proposed draft");
|
|
560
|
+
const proposedHeader = input.draft?.header;
|
|
561
|
+
if (proposedHeader) {
|
|
562
|
+
for (const field of [
|
|
563
|
+
"currency",
|
|
564
|
+
"lineAmountType",
|
|
565
|
+
"taxRoundingScope",
|
|
566
|
+
"providerTaxAmount",
|
|
567
|
+
"customerId"
|
|
568
|
+
]) if (proposedHeader[field] !== void 0) invoice[field] = proposedHeader[field];
|
|
569
|
+
}
|
|
570
|
+
const storedLines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
|
|
571
|
+
const storedById = new Map(storedLines.map((line) => [line.id, line]));
|
|
572
|
+
const providedIds = input.draft?.lines?.map((line) => line.id).filter((id) => !!id) ?? [];
|
|
573
|
+
if (new Set(providedIds).size !== providedIds.length) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice preview line ids must be distinct");
|
|
574
|
+
const unknownId = providedIds.find((id) => !storedById.has(id));
|
|
575
|
+
if (unknownId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Invoice preview line ${unknownId} was not found on the invoice`);
|
|
576
|
+
const proposedLines = input.draft?.lines ? input.draft.lines.map((proposed) => {
|
|
577
|
+
const result = (proposed.id ? storedById.get(proposed.id) : void 0) ?? new InvoiceLineItem();
|
|
578
|
+
for (const field of [
|
|
579
|
+
"description",
|
|
580
|
+
"productId",
|
|
581
|
+
"quantity",
|
|
582
|
+
"unitPrice",
|
|
583
|
+
"discountType",
|
|
584
|
+
"discountRate",
|
|
585
|
+
"discount",
|
|
586
|
+
"taxMode",
|
|
587
|
+
"taxCodeId"
|
|
588
|
+
]) if (proposed[field] !== void 0) result[field] = proposed[field];
|
|
589
|
+
result.id ||= crypto.randomUUID();
|
|
590
|
+
result.invoiceId = input.invoiceId;
|
|
591
|
+
result.tenantId = input.tenantId;
|
|
592
|
+
return result;
|
|
593
|
+
}) : storedLines;
|
|
594
|
+
const calculated = await recomputeInvoiceAggregate(input.db, invoice, settings, "", false, proposedLines);
|
|
595
|
+
return {
|
|
596
|
+
source: "calculated",
|
|
597
|
+
header: {
|
|
598
|
+
currency: invoice.currency,
|
|
599
|
+
lineAmountType: invoice.lineAmountType,
|
|
600
|
+
taxRoundingScope: invoice.taxRoundingScope,
|
|
601
|
+
subtotalMinor: invoice.subtotal,
|
|
602
|
+
discountTotalMinor: invoice.discountTotal,
|
|
603
|
+
taxAmountMinor: invoice.taxAmount,
|
|
604
|
+
providerTaxAmountMinor: invoice.providerTaxAmount,
|
|
605
|
+
totalAmountMinor: invoice.totalAmount
|
|
606
|
+
},
|
|
607
|
+
lines: calculated.lines.map((line) => ({
|
|
608
|
+
id: requireId(line.id, "Preview line"),
|
|
609
|
+
grossMinor: line.amount + line.discount,
|
|
610
|
+
discountMinor: line.discount,
|
|
611
|
+
amountMinor: line.amount,
|
|
612
|
+
taxMinor: line.taxAmount,
|
|
613
|
+
taxCodeId: line.taxCodeId,
|
|
614
|
+
taxCodeCode: line.taxCodeCode,
|
|
615
|
+
taxCodeName: line.taxCodeName,
|
|
616
|
+
taxComponents: structuredClone(line.taxComponents)
|
|
617
|
+
})),
|
|
618
|
+
taxes: calculated.taxRows
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async function previewInvoiceMoney(input) {
|
|
622
|
+
return withTenantMoneyRuntime(input.db, input.tenantId, "v2", async (scope) => previewInvoiceMoneyOnScope({
|
|
623
|
+
...input,
|
|
624
|
+
db: scope.db
|
|
625
|
+
}, scope.barrier.settings));
|
|
626
|
+
}
|
|
627
|
+
async function getInvoiceMoneyFactsOnScope(input) {
|
|
628
|
+
if (!input.tenantId || getTenantId() !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts require their active principal tenant");
|
|
629
|
+
const { invoice } = await loadInvoice(input.db, input.invoiceId);
|
|
630
|
+
if (invoice.tenantId !== input.tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice money facts cannot cross tenant boundaries");
|
|
631
|
+
const lines = await (await InvoiceLineItemCollection.create({ db: input.db })).findByInvoice(input.invoiceId);
|
|
632
|
+
const taxes = await (await DocumentTaxCollection.create({ db: input.db })).findByDocument(input.tenantId, invoice.kind, input.invoiceId);
|
|
633
|
+
return {
|
|
634
|
+
source: "stored",
|
|
635
|
+
invoiceId: input.invoiceId,
|
|
636
|
+
moneySnapshotId: invoice.moneySnapshotId,
|
|
637
|
+
kind: invoice.kind,
|
|
638
|
+
status: invoice.status,
|
|
639
|
+
header: {
|
|
640
|
+
currency: invoice.currency,
|
|
641
|
+
lineAmountType: invoice.lineAmountType,
|
|
642
|
+
taxRoundingScope: invoice.taxRoundingScope,
|
|
643
|
+
subtotalMinor: invoice.subtotal,
|
|
644
|
+
discountTotalMinor: invoice.discountTotal,
|
|
645
|
+
taxAmountMinor: invoice.taxAmount,
|
|
646
|
+
providerTaxAmountMinor: invoice.providerTaxAmount,
|
|
647
|
+
totalAmountMinor: invoice.totalAmount
|
|
648
|
+
},
|
|
649
|
+
lines: lines.map((line) => ({
|
|
650
|
+
id: requireId(line.id, "Invoice line"),
|
|
651
|
+
grossMinor: line.amount + line.discount,
|
|
652
|
+
discountMinor: line.discount,
|
|
653
|
+
amountMinor: line.amount,
|
|
654
|
+
taxMinor: line.taxAmount,
|
|
655
|
+
taxCodeId: line.taxCodeId,
|
|
656
|
+
taxCodeCode: line.taxCodeCode,
|
|
657
|
+
taxCodeName: line.taxCodeName,
|
|
658
|
+
taxComponents: structuredClone(line.taxComponents)
|
|
659
|
+
})),
|
|
660
|
+
taxes: taxes.map((row) => ({
|
|
661
|
+
tenantId: requireId(row.tenantId, "Document tax tenant"),
|
|
662
|
+
documentType: row.documentType,
|
|
663
|
+
documentId: row.documentId,
|
|
664
|
+
currency: row.currency,
|
|
665
|
+
taxCodeId: row.taxCodeId,
|
|
666
|
+
taxCodeCode: row.taxCodeCode,
|
|
667
|
+
kind: row.kind,
|
|
668
|
+
taxComponentId: row.taxComponentId,
|
|
669
|
+
taxComponentName: row.taxComponentName,
|
|
670
|
+
percent: row.percent,
|
|
671
|
+
liabilityAccountId: row.liabilityAccountId,
|
|
672
|
+
taxableMinor: row.taxableMinor,
|
|
673
|
+
taxMinor: row.taxMinor,
|
|
674
|
+
recoverableMinor: row.recoverableMinor
|
|
675
|
+
}))
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
async function getInvoiceMoneyFacts(input) {
|
|
679
|
+
return withTenantMoneyRuntime(input.db, input.tenantId, "v2", async (scope) => getInvoiceMoneyFactsOnScope({
|
|
680
|
+
...input,
|
|
681
|
+
db: scope.db
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
async function saveInvoice(input) {
|
|
685
|
+
const priorRevision = input.updated_at;
|
|
686
|
+
try {
|
|
687
|
+
return await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
|
|
688
|
+
if (invoice.kind !== "invoice" && invoice.kind !== "credit_note") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice kind must be invoice or credit_note");
|
|
689
|
+
if (invoice.kind === "invoice" && invoice.creditedInvoiceId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Only a credit note may reference a credited invoice");
|
|
690
|
+
if (invoice.kind === "credit_note" && !invoice.creditedInvoiceId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "A credit note requires a credited invoice");
|
|
691
|
+
const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
|
|
692
|
+
const settings = await lockRuntimeSettings(tx, candidateTenantId);
|
|
693
|
+
const existing = invoice.id ? await findInvoice(tx, invoice.id) : null;
|
|
694
|
+
const tenantId = existing?.tenantId ?? candidateTenantId;
|
|
695
|
+
if (candidateTenantId && existing?.tenantId && existing.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
|
|
696
|
+
if (settings?.moneyModel !== "v2") return invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: true });
|
|
697
|
+
assertTenantId(tenantId);
|
|
698
|
+
if (!existing) {
|
|
699
|
+
invoice.tenantId = tenantId;
|
|
700
|
+
invoice.creditedAmount = 0;
|
|
701
|
+
let source = null;
|
|
702
|
+
if (invoice.kind === "credit_note") {
|
|
703
|
+
source = await findInvoice(tx, requireId(invoice.creditedInvoiceId, "Credited invoice"));
|
|
704
|
+
if (!source || source.tenantId !== tenantId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must exist in the active tenant");
|
|
705
|
+
await lockDocumentMoneyParents([{
|
|
706
|
+
id: requireId(source.id, "Credited invoice"),
|
|
707
|
+
tenantId,
|
|
708
|
+
collection: await InvoiceCollection.create({ db: tx })
|
|
709
|
+
}]);
|
|
710
|
+
if (source.kind !== "invoice" || source.status === InvoiceStatus.DRAFT || source.status === InvoiceStatus.CANCELLED || !source.moneySnapshotId) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit target must be an issued, non-cancelled invoice");
|
|
711
|
+
if (invoice.currency !== source.currency || invoice.customerId !== source.customerId) throw new MoneyValidationError(invoice.currency !== source.currency ? "CURRENCY_MISMATCH" : "INVALID_MONEY_INPUT", "Credit note currency and customer must match its source invoice");
|
|
712
|
+
}
|
|
713
|
+
invoice.lineAmountType = invoice.kind === "credit_note" && source ? source.lineAmountType : settings.defaultLineAmountType;
|
|
714
|
+
invoice.taxRoundingScope = invoice.kind === "credit_note" && source ? source.taxRoundingScope : settings.taxRoundingScope;
|
|
715
|
+
invoice.subtotal = 0;
|
|
716
|
+
invoice.discountTotal = 0;
|
|
717
|
+
invoice.taxAmount = invoice.providerTaxAmount;
|
|
718
|
+
invoice.totalAmount = invoice.providerTaxAmount;
|
|
719
|
+
if (invoice.status !== InvoiceStatus.DRAFT || invoice.arJournalId) {
|
|
720
|
+
const customers = await CustomerCollection.create({ db: tx });
|
|
721
|
+
const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
|
|
722
|
+
invoice.moneySnapshotId = crypto.randomUUID();
|
|
723
|
+
invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
|
|
724
|
+
invoice.partyTaxId = customer?.taxId ?? "";
|
|
725
|
+
}
|
|
726
|
+
return invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
727
|
+
}
|
|
728
|
+
if (invoice.tenantId !== existing.tenantId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice tenant identity cannot change");
|
|
729
|
+
if (invoice.kind !== existing.kind || invoice.creditedInvoiceId !== existing.creditedInvoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice kind and credited invoice identity are immutable");
|
|
730
|
+
const locked = await lockAndReloadMoneyInvoice(tx, requireId(existing.id, "Invoice"), tenantId, existing.creditedInvoiceId);
|
|
731
|
+
invoice.creditedAmount = locked.invoice.creditedAmount;
|
|
732
|
+
if (isFrozen(locked.invoice)) {
|
|
733
|
+
if (invoice.arReversalJournalId !== locked.invoice.arReversalJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice reversal journal identity is server-managed");
|
|
734
|
+
if (locked.invoice.arJournalId && invoice.arJournalId !== locked.invoice.arJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice revenue journal identity is frozen once assigned");
|
|
735
|
+
if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.kind === "invoice" && locked.invoice.isPaid()) throw new MoneyValidationError("FROZEN_DOCUMENT", "A paid invoice cannot be cancelled");
|
|
736
|
+
if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.kind === "credit_note" && locked.invoice.amountPaid > 0) throw new MoneyValidationError("FROZEN_DOCUMENT", "A refunded credit note cannot be cancelled");
|
|
737
|
+
if (invoice.status === InvoiceStatus.CANCELLED && locked.invoice.status !== InvoiceStatus.CANCELLED && locked.invoice.arJournalId && !locked.invoice.arReversalJournalId) throw new MoneyValidationError("FROZEN_DOCUMENT", "A posted money-v2 invoice requires an atomic reversal before cancellation");
|
|
738
|
+
assertFieldsUnchanged(invoice, locked.invoice, FROZEN_INVOICE_FIELDS, "Invoice");
|
|
739
|
+
const saved2 = await invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
740
|
+
if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
|
|
741
|
+
return saved2;
|
|
742
|
+
}
|
|
743
|
+
const freezing = invoice.status !== InvoiceStatus.DRAFT || !!invoice.arJournalId;
|
|
744
|
+
const snapshotId = freezing ? locked.invoice.moneySnapshotId || crypto.randomUUID() : "";
|
|
745
|
+
await recomputeInvoiceAggregate(tx, invoice, settings, snapshotId);
|
|
746
|
+
if (freezing) {
|
|
747
|
+
const customers = await CustomerCollection.create({ db: tx });
|
|
748
|
+
const customer = invoice.customerId ? await customers.get({ id: invoice.customerId }, { cache: false }) : null;
|
|
749
|
+
invoice.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
|
|
750
|
+
invoice.partyTaxId = customer?.taxId ?? "";
|
|
751
|
+
invoice.moneySnapshotId = snapshotId;
|
|
752
|
+
}
|
|
753
|
+
const saved = await invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
754
|
+
if (locked.source) await refreshIssuedCreditBalance(tx, locked.source);
|
|
755
|
+
return saved;
|
|
756
|
+
}));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
input.updated_at = priorRevision;
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function materializeInvoiceMoneyDraft(input) {
|
|
763
|
+
const settings = await lockTenantMoneyMigrationOwner(input.db, input.tenantId, input.migrationId);
|
|
764
|
+
const { invoice } = await lockAndReloadInvoice(input.db, input.invoiceId, input.tenantId);
|
|
765
|
+
if (isFrozen(invoice)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Only an unfrozen draft invoice can be migration-recomputed");
|
|
766
|
+
const result = await recomputeInvoiceAggregate(input.db, invoice, settings);
|
|
767
|
+
await invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
768
|
+
return {
|
|
769
|
+
invoiceId: input.invoiceId,
|
|
770
|
+
taxRows: result.taxRows
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
async function saveInvoiceLineItem(input) {
|
|
774
|
+
return withDocumentMoneyObjectTransaction(input, async (line, tx) => {
|
|
775
|
+
const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
|
|
776
|
+
const settings = await lockRuntimeSettings(tx, candidateTenantId);
|
|
777
|
+
if (settings?.moneyModel !== "v2") return line.saveDerived(DOCUMENT_MONEY_WRITE, true);
|
|
778
|
+
const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
|
|
779
|
+
const parentId = persisted?.invoiceId ?? line.invoiceId;
|
|
780
|
+
const first = await loadInvoice(tx, parentId);
|
|
781
|
+
if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
|
|
782
|
+
assertTenantId(first.invoice.tenantId);
|
|
783
|
+
const { invoice, source } = await lockAndReloadMoneyInvoice(tx, parentId, first.invoice.tenantId, first.invoice.creditedInvoiceId);
|
|
784
|
+
if (persisted && line.invoiceId !== persisted.invoiceId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Invoice line cannot move between parents");
|
|
785
|
+
line.invoiceId = requireId(invoice.id, "Invoice");
|
|
786
|
+
line.tenantId = invoice.tenantId;
|
|
787
|
+
if (invoice.kind === "invoice" && line.creditedLineId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Ordinary invoice lines cannot reference credited lines");
|
|
788
|
+
if (invoice.kind === "credit_note" && line.creditedLineId && (!source || source.id !== invoice.creditedInvoiceId)) throw new MoneyValidationError("CREDIT_TARGET_NOT_ISSUED", "Credit source changed while saving its line");
|
|
789
|
+
if (isFrozen(invoice)) {
|
|
790
|
+
if (!persisted) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot add a line to a frozen invoice");
|
|
791
|
+
assertFieldsUnchanged(line, persisted, FROZEN_LINE_FIELDS, "Invoice line");
|
|
792
|
+
return line.saveDerived(DOCUMENT_MONEY_WRITE, false);
|
|
793
|
+
}
|
|
794
|
+
await line.saveDerived(DOCUMENT_MONEY_WRITE, false);
|
|
795
|
+
const priorAggregate = aggregateSignature(invoice);
|
|
796
|
+
await recomputeInvoiceAggregate(tx, invoice, settings);
|
|
797
|
+
if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
798
|
+
const refreshed = await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: requireId(line.id, "Invoice line") }, { cache: false });
|
|
799
|
+
if (refreshed) for (const field of [
|
|
800
|
+
"discount",
|
|
801
|
+
"amount",
|
|
802
|
+
"taxAmount",
|
|
803
|
+
"taxRate",
|
|
804
|
+
"taxCodeId",
|
|
805
|
+
"taxCodeCode",
|
|
806
|
+
"taxCodeName",
|
|
807
|
+
"taxComponentsJson",
|
|
808
|
+
"moneySnapshotId",
|
|
809
|
+
"updated_at"
|
|
810
|
+
]) line[field] = refreshed[field];
|
|
811
|
+
return line;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function deleteInvoiceLineItem(input, options) {
|
|
815
|
+
await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (line) => {
|
|
816
|
+
const candidateTenantId = getTenantId() ?? line.tenantId ?? null;
|
|
817
|
+
const settings = await lockRuntimeSettings(tx, candidateTenantId);
|
|
818
|
+
const persisted = line.id ? await (await InvoiceLineItemCollection.create({ db: tx })).get({ id: line.id }, { cache: false }) : null;
|
|
819
|
+
if (!persisted) return;
|
|
820
|
+
const first = await loadInvoice(tx, persisted.invoiceId);
|
|
821
|
+
if (candidateTenantId && first.invoice.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice line tenant does not match its active money barrier");
|
|
822
|
+
if (settings?.moneyModel !== "v2") {
|
|
823
|
+
await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
assertTenantId(first.invoice.tenantId);
|
|
827
|
+
const { invoice } = await lockAndReloadInvoice(tx, persisted.invoiceId, first.invoice.tenantId);
|
|
828
|
+
if (isFrozen(invoice)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a line from a frozen invoice");
|
|
829
|
+
await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);
|
|
830
|
+
const priorAggregate = aggregateSignature(invoice);
|
|
831
|
+
await recomputeInvoiceAggregate(tx, invoice, settings);
|
|
832
|
+
if (aggregateSignature(invoice) !== priorAggregate) await invoice.saveDerived(DOCUMENT_MONEY_WRITE, { recomputeLegacy: false });
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
async function deleteInvoice(input, options) {
|
|
836
|
+
await withDocumentMoneyTransaction(input.db, async (tx) => input.withDatabase(tx, async (invoice) => {
|
|
837
|
+
const candidateTenantId = getTenantId() ?? invoice.tenantId ?? null;
|
|
838
|
+
const settings = await lockRuntimeSettings(tx, candidateTenantId);
|
|
839
|
+
const persisted = invoice.id ? await findInvoice(tx, invoice.id) : null;
|
|
840
|
+
if (!persisted) return;
|
|
841
|
+
if (candidateTenantId && persisted.tenantId !== candidateTenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Invoice tenant does not match its active money barrier");
|
|
842
|
+
if (settings?.moneyModel === "v2" && isFrozen(persisted)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a frozen invoice");
|
|
843
|
+
if (settings?.moneyModel === "v2") {
|
|
844
|
+
assertTenantId(persisted.tenantId);
|
|
845
|
+
const persistedId = requireId(persisted.id, "Invoice");
|
|
846
|
+
await lockAndReloadInvoice(tx, persistedId, persisted.tenantId);
|
|
847
|
+
await (await DocumentTaxCollection.create({ db: tx })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
|
|
848
|
+
tenantId: persisted.tenantId,
|
|
849
|
+
documentType: "invoice",
|
|
850
|
+
documentId: persistedId,
|
|
851
|
+
rows: []
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
await invoice.deleteDerived(DOCUMENT_MONEY_WRITE, options);
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
//#endregion
|
|
858
|
+
export { previewInvoiceMoney as a, materializeInvoiceMoneyDraft as i, getInvoiceMoneyFacts as n, lockAndReloadInvoice as r, document_money_exports as t };
|
|
859
|
+
|
|
860
|
+
//# sourceMappingURL=document-money-oUDvdjOf.js.map
|