@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
package/dist/money.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { n as roundSignedRational, t as allocateSignedRationals } from "./chunks/money-rational-CcW7gzG5.js";
|
|
2
|
+
import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
|
|
3
|
+
//#region src/money.ts
|
|
4
|
+
function assertIntegerMinorUnits(context, label, entries) {
|
|
5
|
+
for (const [field, value] of entries) if (!Number.isFinite(value) || !Number.isInteger(value)) throw new Error(`${context} ${label}: ${field} must be an integer number of minor units (cents, satoshis) \u2014 got ${value}. Money is exact: $19.99 is 1999, not 19.99.`);
|
|
6
|
+
}
|
|
7
|
+
var MoneyValidationError = class extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.publicMessage = message;
|
|
12
|
+
}
|
|
13
|
+
code;
|
|
14
|
+
name = "MoneyValidationError";
|
|
15
|
+
/** Explicit marker consumed by public transports that preserve typed errors. */
|
|
16
|
+
publicMessage;
|
|
17
|
+
};
|
|
18
|
+
var PERCENT_SCALE = 1000000n;
|
|
19
|
+
var PERCENT_DENOMINATOR = 100n * PERCENT_SCALE;
|
|
20
|
+
var MAX_SAFE_MONEY = BigInt(Number.MAX_SAFE_INTEGER);
|
|
21
|
+
var IDENTIFIER = /^\S(?:.*\S)?$/;
|
|
22
|
+
function validationError(code, message) {
|
|
23
|
+
throw new MoneyValidationError(code, message);
|
|
24
|
+
}
|
|
25
|
+
function assertIdentifier(value, label) {
|
|
26
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) validationError("INVALID_MONEY_INPUT", `${label} must be a nonempty string`);
|
|
27
|
+
}
|
|
28
|
+
function safeMoney(value) {
|
|
29
|
+
if (value > MAX_SAFE_MONEY || value < -MAX_SAFE_MONEY) validationError("UNSAFE_MONEY", "Money exceeds safe integer minor units");
|
|
30
|
+
return Number(value);
|
|
31
|
+
}
|
|
32
|
+
function money(value) {
|
|
33
|
+
if (!Number.isSafeInteger(value)) validationError("UNSAFE_MONEY", "Money must be safe integer minor units");
|
|
34
|
+
return BigInt(value);
|
|
35
|
+
}
|
|
36
|
+
function numberDecimal(value, precision) {
|
|
37
|
+
if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) validationError("INVALID_MONEY_INPUT", "Quantity is outside the supported range");
|
|
38
|
+
const [mantissa, exponentText = "0"] = value.toString().split("e");
|
|
39
|
+
const [whole, fraction = ""] = mantissa.split(".");
|
|
40
|
+
const places = fraction.length - Number(exponentText);
|
|
41
|
+
if (places > precision) validationError("INVALID_MONEY_INPUT", "Quantity exceeds the supported decimal precision");
|
|
42
|
+
const coefficient = BigInt(whole + fraction);
|
|
43
|
+
return places < 0 ? {
|
|
44
|
+
coefficient: coefficient * 10n ** BigInt(-places),
|
|
45
|
+
scale: 1n
|
|
46
|
+
} : {
|
|
47
|
+
coefficient,
|
|
48
|
+
scale: 10n ** BigInt(places)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function percentUnits(value) {
|
|
52
|
+
if (typeof value !== "string" || !/^(?:0|[1-9]\d{0,2})(?:\.\d{1,6})?$/.test(value)) validationError("PERCENT_OUT_OF_RANGE", "Percent must be an exact decimal from 0 through 100 with at most six fractional digits");
|
|
53
|
+
const [whole, fraction = ""] = value.split(".");
|
|
54
|
+
const units = BigInt(whole) * PERCENT_SCALE + BigInt(fraction.padEnd(6, "0"));
|
|
55
|
+
if (units > PERCENT_DENOMINATOR) validationError("PERCENT_OUT_OF_RANGE", "Percent exceeds 100");
|
|
56
|
+
return units;
|
|
57
|
+
}
|
|
58
|
+
function validateTaxCode(code, rates) {
|
|
59
|
+
if (!code || typeof code !== "object") validationError("INVALID_MONEY_INPUT", "Tax code must be an object");
|
|
60
|
+
assertIdentifier(code.id, "Tax code ID");
|
|
61
|
+
if (code.kind !== "taxable" && code.kind !== "zero-rated" && code.kind !== "exempt" && code.kind !== "out-of-scope") validationError("TAX_CODE_KIND", "Unknown tax code kind");
|
|
62
|
+
if (!Array.isArray(code.components)) validationError("INVALID_MONEY_INPUT", "Tax code components must be an array");
|
|
63
|
+
if (code.kind === "taxable" && code.components.length === 0) validationError("TAX_CODE_TAXABLE_EMPTY", "Taxable codes require at least one component");
|
|
64
|
+
if (code.kind !== "taxable" && code.components.length !== 0) validationError("TAX_CODE_NON_TAXABLE_COMPONENTS", "Non-taxable codes cannot contain components");
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
return code.components.map((component) => {
|
|
67
|
+
if (!component || typeof component !== "object") validationError("INVALID_MONEY_INPUT", "Tax component must be an object");
|
|
68
|
+
assertIdentifier(component.taxComponentId, "Tax component ID");
|
|
69
|
+
if (seen.has(component.taxComponentId)) validationError("TAX_CODE_DUPLICATE_COMPONENT", "Duplicate tax component within code");
|
|
70
|
+
seen.add(component.taxComponentId);
|
|
71
|
+
if (typeof component.compound !== "boolean") validationError("INVALID_MONEY_INPUT", "Tax component compound must be boolean");
|
|
72
|
+
if (component.compound) validationError("COMPOUND_UNSUPPORTED", "Compound tax components are not supported");
|
|
73
|
+
const rate = percentUnits(component.percent);
|
|
74
|
+
const existing = rates.get(component.taxComponentId);
|
|
75
|
+
if (existing !== void 0 && existing !== rate) validationError("TAX_COMPONENT_RATE_CONFLICT", "Shared tax component has conflicting percentages");
|
|
76
|
+
rates.set(component.taxComponentId, rate);
|
|
77
|
+
return {
|
|
78
|
+
taxComponentId: component.taxComponentId,
|
|
79
|
+
rate
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function discountFor(gross, discount) {
|
|
84
|
+
if (!discount || typeof discount !== "object") validationError("INVALID_MONEY_INPUT", "Discount must be a tagged object");
|
|
85
|
+
const runtime = discount;
|
|
86
|
+
if (runtime.rate !== void 0 && runtime.minor !== void 0) validationError("DISCOUNT_BOTH_FORMS", "A discount cannot include both percent and amount forms");
|
|
87
|
+
let result;
|
|
88
|
+
if (discount.kind === "none") {
|
|
89
|
+
if (runtime.rate !== void 0 || runtime.minor !== void 0) validationError("DISCOUNT_BOTH_FORMS", "A none discount cannot include percent or amount values");
|
|
90
|
+
result = 0n;
|
|
91
|
+
} else if (discount.kind === "percent") result = roundSignedRational(gross * percentUnits(discount.rate), PERCENT_DENOMINATOR);
|
|
92
|
+
else if (discount.kind === "amount") result = money(discount.minor);
|
|
93
|
+
else validationError("INVALID_MONEY_INPUT", "Unknown discount kind");
|
|
94
|
+
if (result !== 0n && gross !== 0n && result < 0n !== gross < 0n) validationError("DISCOUNT_SIGN", "Discount amount must carry the sign of gross");
|
|
95
|
+
if (gross === 0n && result !== 0n) validationError("DISCOUNT_SIGN", "A zero gross cannot carry a discount");
|
|
96
|
+
const magnitude = (value) => value < 0n ? -value : value;
|
|
97
|
+
if (magnitude(result) > magnitude(gross)) validationError("DISCOUNT_EXCEEDS_GROSS", "Discount cannot exceed gross");
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function validateDocumentInput(input) {
|
|
101
|
+
if (!input || typeof input !== "object") validationError("INVALID_MONEY_INPUT", "Money input must be an object");
|
|
102
|
+
if (typeof input.currency !== "string" || !/^[A-Z]{3}$/.test(input.currency) || !ISO_4217_MINOR_UNITS.has(input.currency) || ISO_4217_MINOR_UNITS.get(input.currency) === null) validationError("CURRENCY_MISMATCH", "Currency must be a supported uppercase ISO 4217 code with minor units");
|
|
103
|
+
if (input.lineAmountType !== "exclusive" && input.lineAmountType !== "inclusive" && input.lineAmountType !== "none") validationError("INVALID_MONEY_INPUT", "Unknown line amount type");
|
|
104
|
+
if (input.roundingScope !== "line" && input.roundingScope !== "document") validationError("INVALID_MONEY_INPUT", "Unknown tax rounding scope");
|
|
105
|
+
if (!Array.isArray(input.lines)) validationError("INVALID_MONEY_INPUT", "Lines must be an array");
|
|
106
|
+
}
|
|
107
|
+
function calculateMoneyDocument(input) {
|
|
108
|
+
validateDocumentInput(input);
|
|
109
|
+
const ids = /* @__PURE__ */ new Set();
|
|
110
|
+
const rates = /* @__PURE__ */ new Map();
|
|
111
|
+
const working = input.lines.map((line) => {
|
|
112
|
+
if (!line || typeof line !== "object") validationError("INVALID_MONEY_INPUT", "Money line must be an object");
|
|
113
|
+
assertIdentifier(line.id, "Line ID");
|
|
114
|
+
if (ids.has(line.id)) validationError("INVALID_MONEY_INPUT", "Duplicate money line ID");
|
|
115
|
+
ids.add(line.id);
|
|
116
|
+
const explicitNet = line.netAmountMinor === void 0 ? null : money(line.netAmountMinor);
|
|
117
|
+
const quantity = numberDecimal(line.quantity, 6);
|
|
118
|
+
const gross = explicitNet ?? roundSignedRational(quantity.coefficient * money(line.unitPrice), quantity.scale);
|
|
119
|
+
safeMoney(gross);
|
|
120
|
+
const discount = explicitNet === null ? discountFor(gross, line.discount) : 0n;
|
|
121
|
+
const amount = gross - discount;
|
|
122
|
+
safeMoney(amount);
|
|
123
|
+
if (input.lineAmountType === "none" && !line.taxCode) validationError("TAX_CODE_NOT_FOUND", "None pricing requires an out-of-scope tax code");
|
|
124
|
+
if (input.lineAmountType === "none" && line.taxCode?.kind !== "out-of-scope") validationError("TAX_CODE_KIND", "None pricing requires an out-of-scope tax code");
|
|
125
|
+
const components = line.taxCode ? validateTaxCode(line.taxCode, rates) : [];
|
|
126
|
+
const totalRate = components.reduce((sum, component) => sum + component.rate, 0n);
|
|
127
|
+
const denominator = explicitNet === null && input.lineAmountType === "inclusive" ? PERCENT_DENOMINATOR + totalRate : PERCENT_DENOMINATOR;
|
|
128
|
+
return {
|
|
129
|
+
input: line,
|
|
130
|
+
gross,
|
|
131
|
+
discount,
|
|
132
|
+
amount,
|
|
133
|
+
explicitNet,
|
|
134
|
+
exact: input.lineAmountType === "none" ? [] : components.map((component) => ({
|
|
135
|
+
taxComponentId: component.taxComponentId,
|
|
136
|
+
numerator: amount * component.rate,
|
|
137
|
+
denominator
|
|
138
|
+
})),
|
|
139
|
+
taxes: /* @__PURE__ */ new Map()
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
if (input.roundingScope === "line") for (const line of working) for (const component of line.exact) line.taxes.set(component.taxComponentId, roundSignedRational(component.numerator, component.denominator));
|
|
143
|
+
else for (const componentId of [...rates.keys()].sort()) {
|
|
144
|
+
const eligible = working.flatMap((line) => {
|
|
145
|
+
const exact = line.exact.find((component) => component.taxComponentId === componentId);
|
|
146
|
+
return exact ? [{
|
|
147
|
+
line,
|
|
148
|
+
exact
|
|
149
|
+
}] : [];
|
|
150
|
+
});
|
|
151
|
+
const allocated = allocateSignedRationals(eligible.map(({ line, exact }) => ({
|
|
152
|
+
id: line.input.id,
|
|
153
|
+
numerator: exact.numerator,
|
|
154
|
+
denominator: exact.denominator
|
|
155
|
+
})));
|
|
156
|
+
for (const allocation of allocated.allocations) eligible.find(({ line }) => line.input.id === allocation.id)?.line.taxes.set(componentId, allocation.value);
|
|
157
|
+
}
|
|
158
|
+
const lines = working.map((line) => {
|
|
159
|
+
const components = line.exact.map((component) => ({
|
|
160
|
+
taxComponentId: component.taxComponentId,
|
|
161
|
+
taxMinor: safeMoney(line.taxes.get(component.taxComponentId) ?? 0n)
|
|
162
|
+
}));
|
|
163
|
+
const tax2 = components.reduce((sum, component) => sum + BigInt(component.taxMinor), 0n);
|
|
164
|
+
const net = line.explicitNet ?? (input.lineAmountType === "inclusive" ? line.amount - tax2 : line.amount);
|
|
165
|
+
const amount = line.explicitNet !== null && input.lineAmountType === "inclusive" ? net + tax2 : line.amount;
|
|
166
|
+
return {
|
|
167
|
+
id: line.input.id,
|
|
168
|
+
grossMinor: safeMoney(line.gross),
|
|
169
|
+
discountMinor: safeMoney(line.discount),
|
|
170
|
+
amountMinor: safeMoney(amount),
|
|
171
|
+
components,
|
|
172
|
+
taxMinor: safeMoney(tax2),
|
|
173
|
+
netMinor: safeMoney(net)
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
const taxRows = /* @__PURE__ */ new Map();
|
|
177
|
+
for (let index = 0; index < working.length; index++) {
|
|
178
|
+
const source = working[index].input;
|
|
179
|
+
const result = lines[index];
|
|
180
|
+
if (!source.taxCode || source.taxCode.components.length === 0 || input.lineAmountType === "none") {
|
|
181
|
+
const key = `${source.taxCode?.id ?? ""}\0`;
|
|
182
|
+
const row = taxRows.get(key) ?? {
|
|
183
|
+
taxCodeId: source.taxCode?.id ?? null,
|
|
184
|
+
taxComponentId: null,
|
|
185
|
+
taxableMinor: 0,
|
|
186
|
+
taxMinor: 0
|
|
187
|
+
};
|
|
188
|
+
row.taxableMinor = safeMoney(BigInt(row.taxableMinor) + BigInt(result.netMinor));
|
|
189
|
+
taxRows.set(key, row);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const component of result.components) {
|
|
193
|
+
if (!source.taxCode) validationError("INVALID_MONEY_INPUT", "Tax component requires a tax code");
|
|
194
|
+
const key = `${source.taxCode.id}\0${component.taxComponentId}`;
|
|
195
|
+
const row = taxRows.get(key) ?? {
|
|
196
|
+
taxCodeId: source.taxCode.id,
|
|
197
|
+
taxComponentId: component.taxComponentId,
|
|
198
|
+
taxableMinor: 0,
|
|
199
|
+
taxMinor: 0
|
|
200
|
+
};
|
|
201
|
+
row.taxableMinor = safeMoney(BigInt(row.taxableMinor) + BigInt(result.netMinor));
|
|
202
|
+
row.taxMinor = safeMoney(BigInt(row.taxMinor) + BigInt(component.taxMinor));
|
|
203
|
+
taxRows.set(key, row);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const taxes = [...taxRows.values()].sort((left, right) => {
|
|
207
|
+
const leftCode = left.taxCodeId ?? "";
|
|
208
|
+
const rightCode = right.taxCodeId ?? "";
|
|
209
|
+
if (leftCode !== rightCode) return leftCode < rightCode ? -1 : 1;
|
|
210
|
+
const leftComponent = left.taxComponentId ?? "";
|
|
211
|
+
const rightComponent = right.taxComponentId ?? "";
|
|
212
|
+
return leftComponent < rightComponent ? -1 : leftComponent > rightComponent ? 1 : 0;
|
|
213
|
+
});
|
|
214
|
+
const subtotal = lines.reduce((sum, line) => sum + BigInt(line.netMinor), 0n);
|
|
215
|
+
const discountTotal = lines.reduce((sum, line) => sum + BigInt(line.discountMinor), 0n);
|
|
216
|
+
const tax = lines.reduce((sum, line) => sum + BigInt(line.taxMinor), 0n);
|
|
217
|
+
return {
|
|
218
|
+
lines,
|
|
219
|
+
subtotalMinor: safeMoney(subtotal),
|
|
220
|
+
discountTotalMinor: safeMoney(discountTotal),
|
|
221
|
+
taxMinor: safeMoney(tax),
|
|
222
|
+
totalMinor: safeMoney(subtotal + tax),
|
|
223
|
+
taxes
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function selectedCode(id, source, context) {
|
|
227
|
+
if (!id) return null;
|
|
228
|
+
const taxCode = context.codes[id];
|
|
229
|
+
if (!taxCode) validationError("TAX_CODE_NOT_FOUND", `Unknown tax code: ${id}`);
|
|
230
|
+
if (typeof taxCode.active !== "boolean") validationError("INVALID_MONEY_INPUT", `Tax code ${id} has invalid state`);
|
|
231
|
+
if (!taxCode.active) validationError("TAX_CODE_INACTIVE", `Tax code ${id} is inactive`);
|
|
232
|
+
if (taxCode.appliesTo !== "sales" && taxCode.appliesTo !== "purchases" && taxCode.appliesTo !== "both") validationError("INVALID_MONEY_INPUT", `Tax code ${id} has invalid applicability`);
|
|
233
|
+
if (taxCode.appliesTo !== "both" && taxCode.appliesTo !== context.direction) validationError("TAX_CODE_SCOPE", `Tax code ${id} does not apply to ${context.direction}`);
|
|
234
|
+
return {
|
|
235
|
+
taxCode,
|
|
236
|
+
source
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function resolveLineTaxCode(line, context) {
|
|
240
|
+
if (!line || typeof line !== "object" || !context || typeof context !== "object") validationError("INVALID_MONEY_INPUT", "Tax-code resolution inputs must be objects");
|
|
241
|
+
if (!context.codes || typeof context.codes !== "object") validationError("INVALID_MONEY_INPUT", "Tax-code map must be an object");
|
|
242
|
+
if (line.lineAmountType !== "exclusive" && line.lineAmountType !== "inclusive" && line.lineAmountType !== "none") validationError("INVALID_MONEY_INPUT", "Unknown line amount type");
|
|
243
|
+
if (context.direction !== "sales" && context.direction !== "purchases") validationError("INVALID_MONEY_INPUT", "Unknown tax-code direction");
|
|
244
|
+
if (line.lineAmountType === "none") {
|
|
245
|
+
if (!context.outOfScopeCodeId) validationError("TAX_CODE_NOT_FOUND", "None mode requires an out-of-scope tax code");
|
|
246
|
+
const resolved = selectedCode(context.outOfScopeCodeId, "none", context);
|
|
247
|
+
if (resolved?.taxCode?.kind !== "out-of-scope") validationError("TAX_CODE_KIND", "None mode requires an out-of-scope tax code");
|
|
248
|
+
return resolved;
|
|
249
|
+
}
|
|
250
|
+
if (context.customerTaxExempt && !line.lineOverride) {
|
|
251
|
+
if (!context.exemptCodeId) validationError("TAX_CODE_NOT_FOUND", "An exempt customer requires an exempt tax code");
|
|
252
|
+
const exempt = selectedCode(context.exemptCodeId, "exempt-customer", context);
|
|
253
|
+
if (exempt?.taxCode?.kind !== "exempt") validationError("TAX_CODE_KIND", "Customer exemption requires an exempt tax code");
|
|
254
|
+
return exempt;
|
|
255
|
+
}
|
|
256
|
+
const candidates = [
|
|
257
|
+
[line.lineOverride, "line"],
|
|
258
|
+
[context.productCodeId, "product"],
|
|
259
|
+
[context.partyCodeId, "customer"],
|
|
260
|
+
[context.appDefaultCodeId, "app"]
|
|
261
|
+
];
|
|
262
|
+
for (const [id, source] of candidates) {
|
|
263
|
+
const resolved = selectedCode(id, source, context);
|
|
264
|
+
if (resolved) return resolved;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
taxCode: null,
|
|
268
|
+
source: "none"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
export { MoneyValidationError, assertIntegerMinorUnits, calculateMoneyDocument, resolveLineTaxCode };
|
|
273
|
+
|
|
274
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","names":["tax"],"sources":["../src/money.ts"],"sourcesContent":["/** Exact, transport-neutral commercial document money calculation. */\n\nimport { ISO_4217_MINOR_UNITS } from '@happyvertical/smrt-ui/currency';\nimport {\n allocateSignedRationals,\n roundSignedRational,\n} from './money-rational.js';\n\n/** Throw unless every named model value is a finite integer minor-unit amount. */\nexport function assertIntegerMinorUnits(\n context: string,\n label: string,\n entries: ReadonlyArray<readonly [string, number]>,\n): void {\n for (const [field, value] of entries) {\n if (!Number.isFinite(value) || !Number.isInteger(value)) {\n throw new Error(\n `${context} ${label}: ${field} must be an integer number of minor ` +\n `units (cents, satoshis) — got ${value}. Money is exact: $19.99 is ` +\n '1999, not 19.99.',\n );\n }\n }\n}\n\nexport type LineAmountType = 'exclusive' | 'inclusive' | 'none';\nexport type TaxRoundingScope = 'line' | 'document';\nexport type TaxCodeKind = 'taxable' | 'zero-rated' | 'exempt' | 'out-of-scope';\nexport type TaxCodeAppliesTo = 'sales' | 'purchases' | 'both';\n\nexport type MoneyValidationCode =\n | 'DISCOUNT_BOTH_FORMS'\n | 'DISCOUNT_EXCEEDS_GROSS'\n | 'DISCOUNT_SIGN'\n | 'PERCENT_OUT_OF_RANGE'\n | 'COMPOUND_UNSUPPORTED'\n | 'INVALID_MONEY_INPUT'\n | 'TAX_CODE_DUPLICATE_COMPONENT'\n | 'TAX_CODE_KIND'\n | 'TAX_CODE_NON_TAXABLE_COMPONENTS'\n | 'TAX_CODE_NOT_FOUND'\n | 'TAX_CODE_TAXABLE_EMPTY'\n | 'TAX_COMPONENT_RATE_CONFLICT'\n | 'TAX_CODE_INACTIVE'\n | 'TAX_CODE_SCOPE'\n | 'UNSAFE_MONEY'\n | 'FROZEN_DOCUMENT'\n | 'CURRENCY_MISMATCH'\n | 'ACCOUNTING_SYNC_IN_PROGRESS'\n | 'ACCOUNTING_SYNC_RECONCILIATION_REQUIRED'\n | 'ACCOUNTING_SYNC_UNSUPPORTED'\n | 'CREDIT_EXCEEDS_LINE'\n | 'CREDIT_TARGET_NOT_ISSUED';\n\n/** Stable validation error shared by model, editor, and transport adapters. */\nexport class MoneyValidationError extends Error {\n override readonly name = 'MoneyValidationError';\n /** Explicit marker consumed by public transports that preserve typed errors. */\n readonly publicMessage: string;\n\n constructor(\n readonly code: MoneyValidationCode,\n message: string,\n ) {\n super(message);\n this.publicMessage = message;\n }\n}\n\nexport interface TaxComponentInput {\n taxComponentId: string;\n /** Exact percent: \"7\" means 7%; at most six fractional digits. */\n percent: string;\n compound: boolean;\n}\n\nexport interface MoneyTaxCodeInput {\n id: string;\n kind: TaxCodeKind;\n components: readonly TaxComponentInput[];\n}\n\nexport interface ResolvableTaxCodeInput extends MoneyTaxCodeInput {\n active: boolean;\n appliesTo: TaxCodeAppliesTo;\n}\n\nexport type MoneyDiscountInput =\n | { kind: 'none' }\n | { kind: 'percent'; rate: string }\n | { kind: 'amount'; minor: number };\n\nexport interface MoneyLineInput {\n id: string;\n /** Nonnegative decimal quantity with at most six fractional digits. */\n quantity: number;\n /** Signed safe integer minor units. */\n unitPrice: number;\n discount: MoneyDiscountInput;\n /** Explicit tax-exclusive basis for credit-note amount allocations. */\n netAmountMinor?: number;\n /** Null means no code resolved; `none` pricing requires an out-of-scope code. */\n taxCode: MoneyTaxCodeInput | null;\n}\n\nexport interface MoneyLineResult {\n id: string;\n grossMinor: number;\n discountMinor: number;\n amountMinor: number;\n components: { taxComponentId: string; taxMinor: number }[];\n taxMinor: number;\n netMinor: number;\n}\n\nexport interface MoneyDocumentTaxResult {\n /** Null records an unassigned line without inventing a persisted tax-code ID. */\n taxCodeId: string | null;\n taxComponentId: string | null;\n taxableMinor: number;\n taxMinor: number;\n}\n\nexport interface MoneyDocumentResult {\n lines: MoneyLineResult[];\n subtotalMinor: number;\n discountTotalMinor: number;\n taxMinor: number;\n totalMinor: number;\n taxes: MoneyDocumentTaxResult[];\n}\n\nexport type TaxCodeResolutionSource =\n | 'line'\n | 'exempt-customer'\n | 'product'\n | 'customer'\n | 'app'\n | 'none';\n\nexport interface ResolveLineTaxCodeInput {\n lineAmountType: LineAmountType;\n lineOverride?: string | null;\n}\n\nexport interface ResolveLineTaxCodeContext {\n direction: 'sales' | 'purchases';\n codes: Readonly<Record<string, ResolvableTaxCodeInput>>;\n customerTaxExempt: boolean;\n exemptCodeId?: string | null;\n productCodeId?: string | null;\n partyCodeId?: string | null;\n appDefaultCodeId?: string | null;\n outOfScopeCodeId?: string | null;\n}\n\nexport interface ResolvedLineTaxCode {\n taxCode: ResolvableTaxCodeInput | null;\n source: TaxCodeResolutionSource;\n}\n\nconst PERCENT_SCALE = 1_000_000n;\nconst PERCENT_DENOMINATOR = 100n * PERCENT_SCALE;\nconst MAX_SAFE_MONEY = BigInt(Number.MAX_SAFE_INTEGER);\nconst IDENTIFIER = /^\\S(?:.*\\S)?$/;\n\nfunction validationError(code: MoneyValidationCode, message: string): never {\n throw new MoneyValidationError(code, message);\n}\n\nfunction assertIdentifier(value: string, label: string): void {\n if (typeof value !== 'string' || !IDENTIFIER.test(value))\n validationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonempty string`,\n );\n}\n\nfunction safeMoney(value: bigint): number {\n if (value > MAX_SAFE_MONEY || value < -MAX_SAFE_MONEY)\n validationError('UNSAFE_MONEY', 'Money exceeds safe integer minor units');\n return Number(value);\n}\n\nfunction money(value: number): bigint {\n if (!Number.isSafeInteger(value))\n validationError('UNSAFE_MONEY', 'Money must be safe integer minor units');\n return BigInt(value);\n}\n\ninterface ExactDecimal {\n coefficient: bigint;\n scale: bigint;\n}\n\nfunction numberDecimal(value: number, precision: number): ExactDecimal {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER)\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Quantity is outside the supported range',\n );\n const [mantissa, exponentText = '0'] = value.toString().split('e');\n const [whole, fraction = ''] = mantissa.split('.');\n const places = fraction.length - Number(exponentText);\n if (places > precision)\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Quantity exceeds the supported decimal precision',\n );\n const coefficient = BigInt(whole + fraction);\n return places < 0\n ? { coefficient: coefficient * 10n ** BigInt(-places), scale: 1n }\n : { coefficient, scale: 10n ** BigInt(places) };\n}\n\nfunction percentUnits(value: string): bigint {\n if (\n typeof value !== 'string' ||\n !/^(?:0|[1-9]\\d{0,2})(?:\\.\\d{1,6})?$/.test(value)\n )\n validationError(\n 'PERCENT_OUT_OF_RANGE',\n 'Percent must be an exact decimal from 0 through 100 with at most six fractional digits',\n );\n const [whole, fraction = ''] = value.split('.');\n const units = BigInt(whole) * PERCENT_SCALE + BigInt(fraction.padEnd(6, '0'));\n if (units > PERCENT_DENOMINATOR)\n validationError('PERCENT_OUT_OF_RANGE', 'Percent exceeds 100');\n return units;\n}\n\nfunction validateTaxCode(\n code: MoneyTaxCodeInput,\n rates: Map<string, bigint>,\n): { taxComponentId: string; rate: bigint }[] {\n if (!code || typeof code !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Tax code must be an object');\n assertIdentifier(code.id, 'Tax code ID');\n if (\n code.kind !== 'taxable' &&\n code.kind !== 'zero-rated' &&\n code.kind !== 'exempt' &&\n code.kind !== 'out-of-scope'\n )\n validationError('TAX_CODE_KIND', 'Unknown tax code kind');\n if (!Array.isArray(code.components))\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Tax code components must be an array',\n );\n if (code.kind === 'taxable' && code.components.length === 0)\n validationError(\n 'TAX_CODE_TAXABLE_EMPTY',\n 'Taxable codes require at least one component',\n );\n if (code.kind !== 'taxable' && code.components.length !== 0)\n validationError(\n 'TAX_CODE_NON_TAXABLE_COMPONENTS',\n 'Non-taxable codes cannot contain components',\n );\n const seen = new Set<string>();\n return code.components.map((component) => {\n if (!component || typeof component !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Tax component must be an object');\n assertIdentifier(component.taxComponentId, 'Tax component ID');\n if (seen.has(component.taxComponentId))\n validationError(\n 'TAX_CODE_DUPLICATE_COMPONENT',\n 'Duplicate tax component within code',\n );\n seen.add(component.taxComponentId);\n if (typeof component.compound !== 'boolean')\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Tax component compound must be boolean',\n );\n if (component.compound)\n validationError(\n 'COMPOUND_UNSUPPORTED',\n 'Compound tax components are not supported',\n );\n const rate = percentUnits(component.percent);\n const existing = rates.get(component.taxComponentId);\n if (existing !== undefined && existing !== rate)\n validationError(\n 'TAX_COMPONENT_RATE_CONFLICT',\n 'Shared tax component has conflicting percentages',\n );\n rates.set(component.taxComponentId, rate);\n return { taxComponentId: component.taxComponentId, rate };\n });\n}\n\nfunction discountFor(gross: bigint, discount: MoneyDiscountInput): bigint {\n if (!discount || typeof discount !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Discount must be a tagged object');\n const runtime = discount as MoneyDiscountInput & {\n rate?: unknown;\n minor?: unknown;\n };\n if (runtime.rate !== undefined && runtime.minor !== undefined)\n validationError(\n 'DISCOUNT_BOTH_FORMS',\n 'A discount cannot include both percent and amount forms',\n );\n let result: bigint;\n if (discount.kind === 'none') {\n if (runtime.rate !== undefined || runtime.minor !== undefined)\n validationError(\n 'DISCOUNT_BOTH_FORMS',\n 'A none discount cannot include percent or amount values',\n );\n result = 0n;\n } else if (discount.kind === 'percent') {\n result = roundSignedRational(\n gross * percentUnits(discount.rate),\n PERCENT_DENOMINATOR,\n );\n } else if (discount.kind === 'amount') {\n result = money(discount.minor);\n } else {\n validationError('INVALID_MONEY_INPUT', 'Unknown discount kind');\n }\n if (result !== 0n && gross !== 0n && result < 0n !== gross < 0n)\n validationError(\n 'DISCOUNT_SIGN',\n 'Discount amount must carry the sign of gross',\n );\n if (gross === 0n && result !== 0n)\n validationError('DISCOUNT_SIGN', 'A zero gross cannot carry a discount');\n const magnitude = (value: bigint) => (value < 0n ? -value : value);\n if (magnitude(result) > magnitude(gross))\n validationError('DISCOUNT_EXCEEDS_GROSS', 'Discount cannot exceed gross');\n return result;\n}\n\nfunction validateDocumentInput(input: {\n currency: string;\n lineAmountType: LineAmountType;\n roundingScope: TaxRoundingScope;\n lines: readonly MoneyLineInput[];\n}): void {\n if (!input || typeof input !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Money input must be an object');\n if (\n typeof input.currency !== 'string' ||\n !/^[A-Z]{3}$/.test(input.currency) ||\n !ISO_4217_MINOR_UNITS.has(input.currency) ||\n ISO_4217_MINOR_UNITS.get(input.currency) === null\n )\n validationError(\n 'CURRENCY_MISMATCH',\n 'Currency must be a supported uppercase ISO 4217 code with minor units',\n );\n if (\n input.lineAmountType !== 'exclusive' &&\n input.lineAmountType !== 'inclusive' &&\n input.lineAmountType !== 'none'\n )\n validationError('INVALID_MONEY_INPUT', 'Unknown line amount type');\n if (input.roundingScope !== 'line' && input.roundingScope !== 'document')\n validationError('INVALID_MONEY_INPUT', 'Unknown tax rounding scope');\n if (!Array.isArray(input.lines))\n validationError('INVALID_MONEY_INPUT', 'Lines must be an array');\n}\n\n/** Calculate exact line amounts, per-component tax, and document totals. */\nexport function calculateMoneyDocument(input: {\n currency: string;\n lineAmountType: LineAmountType;\n roundingScope: TaxRoundingScope;\n lines: readonly MoneyLineInput[];\n}): MoneyDocumentResult {\n validateDocumentInput(input);\n const ids = new Set<string>();\n const rates = new Map<string, bigint>();\n const working = input.lines.map((line) => {\n if (!line || typeof line !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Money line must be an object');\n assertIdentifier(line.id, 'Line ID');\n if (ids.has(line.id))\n validationError('INVALID_MONEY_INPUT', 'Duplicate money line ID');\n ids.add(line.id);\n const explicitNet =\n line.netAmountMinor === undefined ? null : money(line.netAmountMinor);\n const quantity = numberDecimal(line.quantity, 6);\n const gross =\n explicitNet ??\n roundSignedRational(\n quantity.coefficient * money(line.unitPrice),\n quantity.scale,\n );\n safeMoney(gross);\n const discount =\n explicitNet === null ? discountFor(gross, line.discount) : 0n;\n const amount = gross - discount;\n safeMoney(amount);\n if (input.lineAmountType === 'none' && !line.taxCode)\n validationError(\n 'TAX_CODE_NOT_FOUND',\n 'None pricing requires an out-of-scope tax code',\n );\n if (\n input.lineAmountType === 'none' &&\n line.taxCode?.kind !== 'out-of-scope'\n )\n validationError(\n 'TAX_CODE_KIND',\n 'None pricing requires an out-of-scope tax code',\n );\n const components = line.taxCode ? validateTaxCode(line.taxCode, rates) : [];\n const totalRate = components.reduce(\n (sum, component) => sum + component.rate,\n 0n,\n );\n const denominator =\n explicitNet === null && input.lineAmountType === 'inclusive'\n ? PERCENT_DENOMINATOR + totalRate\n : PERCENT_DENOMINATOR;\n return {\n input: line,\n gross,\n discount,\n amount,\n explicitNet,\n exact:\n input.lineAmountType === 'none'\n ? []\n : components.map((component) => ({\n taxComponentId: component.taxComponentId,\n numerator: amount * component.rate,\n denominator,\n })),\n taxes: new Map<string, bigint>(),\n };\n });\n\n if (input.roundingScope === 'line') {\n for (const line of working)\n for (const component of line.exact)\n line.taxes.set(\n component.taxComponentId,\n roundSignedRational(component.numerator, component.denominator),\n );\n } else {\n for (const componentId of [...rates.keys()].sort()) {\n const eligible = working.flatMap((line) => {\n const exact = line.exact.find(\n (component) => component.taxComponentId === componentId,\n );\n return exact ? [{ line, exact }] : [];\n });\n const allocated = allocateSignedRationals(\n eligible.map(({ line, exact }) => ({\n id: line.input.id,\n numerator: exact.numerator,\n denominator: exact.denominator,\n })),\n );\n for (const allocation of allocated.allocations)\n eligible\n .find(({ line }) => line.input.id === allocation.id)\n ?.line.taxes.set(componentId, allocation.value);\n }\n }\n\n const lines = working.map((line): MoneyLineResult => {\n const components = line.exact.map((component) => ({\n taxComponentId: component.taxComponentId,\n taxMinor: safeMoney(line.taxes.get(component.taxComponentId) ?? 0n),\n }));\n const tax = components.reduce(\n (sum, component) => sum + BigInt(component.taxMinor),\n 0n,\n );\n const net =\n line.explicitNet ??\n (input.lineAmountType === 'inclusive' ? line.amount - tax : line.amount);\n const amount =\n line.explicitNet !== null && input.lineAmountType === 'inclusive'\n ? net + tax\n : line.amount;\n return {\n id: line.input.id,\n grossMinor: safeMoney(line.gross),\n discountMinor: safeMoney(line.discount),\n amountMinor: safeMoney(amount),\n components,\n taxMinor: safeMoney(tax),\n netMinor: safeMoney(net),\n };\n });\n\n const taxRows = new Map<string, MoneyDocumentTaxResult>();\n for (let index = 0; index < working.length; index++) {\n const source = working[index].input;\n const result = lines[index];\n if (\n !source.taxCode ||\n source.taxCode.components.length === 0 ||\n input.lineAmountType === 'none'\n ) {\n const key = `${source.taxCode?.id ?? ''}\\u0000`;\n const row = taxRows.get(key) ?? {\n taxCodeId: source.taxCode?.id ?? null,\n taxComponentId: null,\n taxableMinor: 0,\n taxMinor: 0,\n };\n row.taxableMinor = safeMoney(\n BigInt(row.taxableMinor) + BigInt(result.netMinor),\n );\n taxRows.set(key, row);\n continue;\n }\n for (const component of result.components) {\n if (!source.taxCode)\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Tax component requires a tax code',\n );\n const key = `${source.taxCode.id}\\u0000${component.taxComponentId}`;\n const row = taxRows.get(key) ?? {\n taxCodeId: source.taxCode.id,\n taxComponentId: component.taxComponentId,\n taxableMinor: 0,\n taxMinor: 0,\n };\n row.taxableMinor = safeMoney(\n BigInt(row.taxableMinor) + BigInt(result.netMinor),\n );\n row.taxMinor = safeMoney(\n BigInt(row.taxMinor) + BigInt(component.taxMinor),\n );\n taxRows.set(key, row);\n }\n }\n const taxes = [...taxRows.values()].sort((left, right) => {\n const leftCode = left.taxCodeId ?? '';\n const rightCode = right.taxCodeId ?? '';\n if (leftCode !== rightCode) return leftCode < rightCode ? -1 : 1;\n const leftComponent = left.taxComponentId ?? '';\n const rightComponent = right.taxComponentId ?? '';\n return leftComponent < rightComponent\n ? -1\n : leftComponent > rightComponent\n ? 1\n : 0;\n });\n const subtotal = lines.reduce((sum, line) => sum + BigInt(line.netMinor), 0n);\n const discountTotal = lines.reduce(\n (sum, line) => sum + BigInt(line.discountMinor),\n 0n,\n );\n const tax = lines.reduce((sum, line) => sum + BigInt(line.taxMinor), 0n);\n return {\n lines,\n subtotalMinor: safeMoney(subtotal),\n discountTotalMinor: safeMoney(discountTotal),\n taxMinor: safeMoney(tax),\n totalMinor: safeMoney(subtotal + tax),\n taxes,\n };\n}\n\nfunction selectedCode(\n id: string | null | undefined,\n source: TaxCodeResolutionSource,\n context: ResolveLineTaxCodeContext,\n): ResolvedLineTaxCode | null {\n if (!id) return null;\n const taxCode = context.codes[id];\n if (!taxCode)\n validationError('TAX_CODE_NOT_FOUND', `Unknown tax code: ${id}`);\n if (typeof taxCode.active !== 'boolean')\n validationError('INVALID_MONEY_INPUT', `Tax code ${id} has invalid state`);\n if (!taxCode.active)\n validationError('TAX_CODE_INACTIVE', `Tax code ${id} is inactive`);\n if (\n taxCode.appliesTo !== 'sales' &&\n taxCode.appliesTo !== 'purchases' &&\n taxCode.appliesTo !== 'both'\n )\n validationError(\n 'INVALID_MONEY_INPUT',\n `Tax code ${id} has invalid applicability`,\n );\n if (taxCode.appliesTo !== 'both' && taxCode.appliesTo !== context.direction)\n validationError(\n 'TAX_CODE_SCOPE',\n `Tax code ${id} does not apply to ${context.direction}`,\n );\n return { taxCode, source };\n}\n\n/** Resolve a line tax code from already-loaded values, without model or I/O access. */\nexport function resolveLineTaxCode(\n line: ResolveLineTaxCodeInput,\n context: ResolveLineTaxCodeContext,\n): ResolvedLineTaxCode {\n if (\n !line ||\n typeof line !== 'object' ||\n !context ||\n typeof context !== 'object'\n )\n validationError(\n 'INVALID_MONEY_INPUT',\n 'Tax-code resolution inputs must be objects',\n );\n if (!context.codes || typeof context.codes !== 'object')\n validationError('INVALID_MONEY_INPUT', 'Tax-code map must be an object');\n if (\n line.lineAmountType !== 'exclusive' &&\n line.lineAmountType !== 'inclusive' &&\n line.lineAmountType !== 'none'\n )\n validationError('INVALID_MONEY_INPUT', 'Unknown line amount type');\n if (context.direction !== 'sales' && context.direction !== 'purchases')\n validationError('INVALID_MONEY_INPUT', 'Unknown tax-code direction');\n if (line.lineAmountType === 'none') {\n if (!context.outOfScopeCodeId)\n validationError(\n 'TAX_CODE_NOT_FOUND',\n 'None mode requires an out-of-scope tax code',\n );\n const resolved = selectedCode(context.outOfScopeCodeId, 'none', context);\n if (resolved?.taxCode?.kind !== 'out-of-scope')\n validationError(\n 'TAX_CODE_KIND',\n 'None mode requires an out-of-scope tax code',\n );\n return resolved;\n }\n if (context.customerTaxExempt && !line.lineOverride) {\n if (!context.exemptCodeId)\n validationError(\n 'TAX_CODE_NOT_FOUND',\n 'An exempt customer requires an exempt tax code',\n );\n const exempt = selectedCode(\n context.exemptCodeId,\n 'exempt-customer',\n context,\n );\n if (exempt?.taxCode?.kind !== 'exempt')\n validationError(\n 'TAX_CODE_KIND',\n 'Customer exemption requires an exempt tax code',\n );\n return exempt;\n }\n const candidates: readonly [\n string | null | undefined,\n TaxCodeResolutionSource,\n ][] = [\n [line.lineOverride, 'line'],\n [context.productCodeId, 'product'],\n [context.partyCodeId, 'customer'],\n [context.appDefaultCodeId, 'app'],\n ];\n for (const [id, source] of candidates) {\n const resolved = selectedCode(id, source, context);\n if (resolved) return resolved;\n }\n return { taxCode: null, source: 'none' };\n}\n"],"mappings":"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"}
|
package/dist/recipes.d.ts
CHANGED
|
@@ -61,7 +61,7 @@ export declare class PurchasesRecipe extends SmrtRecipe {
|
|
|
61
61
|
icon: string;
|
|
62
62
|
description: string;
|
|
63
63
|
};
|
|
64
|
-
static models: (typeof
|
|
64
|
+
static models: (typeof ContractLineItem | typeof PurchaseOrder)[];
|
|
65
65
|
static nav: {
|
|
66
66
|
label: string;
|
|
67
67
|
model: typeof PurchaseOrder;
|
|
@@ -110,7 +110,7 @@ export declare class PurchasesRecipe extends SmrtRecipe {
|
|
|
110
110
|
readonly visibility: "hidden";
|
|
111
111
|
};
|
|
112
112
|
readonly taxRate: {
|
|
113
|
-
readonly help: "
|
|
113
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
114
114
|
};
|
|
115
115
|
};
|
|
116
116
|
};
|
|
@@ -129,7 +129,7 @@ export declare class SalesRecipe extends SmrtRecipe {
|
|
|
129
129
|
icon: string;
|
|
130
130
|
description: string;
|
|
131
131
|
};
|
|
132
|
-
static models: (typeof
|
|
132
|
+
static models: (typeof ContractLineItem | typeof Order)[];
|
|
133
133
|
static nav: {
|
|
134
134
|
label: string;
|
|
135
135
|
model: typeof Order;
|
|
@@ -178,7 +178,7 @@ export declare class SalesRecipe extends SmrtRecipe {
|
|
|
178
178
|
readonly visibility: "hidden";
|
|
179
179
|
};
|
|
180
180
|
readonly taxRate: {
|
|
181
|
-
readonly help: "
|
|
181
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
182
182
|
};
|
|
183
183
|
};
|
|
184
184
|
};
|
|
@@ -228,7 +228,7 @@ export declare class EstimatesRecipe extends SmrtRecipe {
|
|
|
228
228
|
icon: string;
|
|
229
229
|
description: string;
|
|
230
230
|
};
|
|
231
|
-
static models: (typeof
|
|
231
|
+
static models: (typeof ContractLineItem | typeof Estimate)[];
|
|
232
232
|
static nav: {
|
|
233
233
|
label: string;
|
|
234
234
|
model: typeof Estimate;
|
|
@@ -274,7 +274,7 @@ export declare class EstimatesRecipe extends SmrtRecipe {
|
|
|
274
274
|
readonly visibility: "hidden";
|
|
275
275
|
};
|
|
276
276
|
readonly taxRate: {
|
|
277
|
-
readonly help: "
|
|
277
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
278
278
|
};
|
|
279
279
|
};
|
|
280
280
|
};
|
|
@@ -293,7 +293,7 @@ export declare class WholesaleRecipe extends SmrtRecipe {
|
|
|
293
293
|
icon: string;
|
|
294
294
|
description: string;
|
|
295
295
|
};
|
|
296
|
-
static models: (typeof
|
|
296
|
+
static models: (typeof ContractLineItem | typeof WholesaleOrder)[];
|
|
297
297
|
static nav: {
|
|
298
298
|
label: string;
|
|
299
299
|
model: typeof WholesaleOrder;
|
|
@@ -342,7 +342,7 @@ export declare class WholesaleRecipe extends SmrtRecipe {
|
|
|
342
342
|
readonly visibility: "hidden";
|
|
343
343
|
};
|
|
344
344
|
readonly taxRate: {
|
|
345
|
-
readonly help: "
|
|
345
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
346
346
|
};
|
|
347
347
|
};
|
|
348
348
|
};
|
|
@@ -366,7 +366,7 @@ export declare class InvoicingRecipe extends SmrtRecipe {
|
|
|
366
366
|
icon: string;
|
|
367
367
|
description: string;
|
|
368
368
|
};
|
|
369
|
-
static models: (typeof
|
|
369
|
+
static models: (typeof Invoice | typeof InvoiceLineItem | typeof Payment | typeof PaymentAllocation)[];
|
|
370
370
|
static nav: ({
|
|
371
371
|
label: string;
|
|
372
372
|
model: typeof Invoice;
|
|
@@ -456,7 +456,7 @@ export declare class InvoicingRecipe extends SmrtRecipe {
|
|
|
456
456
|
readonly visibility: "hidden";
|
|
457
457
|
};
|
|
458
458
|
readonly taxRate: {
|
|
459
|
-
readonly help: "
|
|
459
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
460
460
|
};
|
|
461
461
|
};
|
|
462
462
|
};
|
|
@@ -622,7 +622,7 @@ export declare class AgreementsRecipe extends SmrtRecipe {
|
|
|
622
622
|
readonly visibility: "hidden";
|
|
623
623
|
};
|
|
624
624
|
readonly taxRate: {
|
|
625
|
-
readonly help: "
|
|
625
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
626
626
|
};
|
|
627
627
|
};
|
|
628
628
|
};
|
|
@@ -653,7 +653,7 @@ export declare class LeasesRecipe extends SmrtRecipe {
|
|
|
653
653
|
icon: string;
|
|
654
654
|
description: string;
|
|
655
655
|
};
|
|
656
|
-
static models: (typeof
|
|
656
|
+
static models: (typeof ContractLineItem | typeof Lease)[];
|
|
657
657
|
static nav: {
|
|
658
658
|
label: string;
|
|
659
659
|
model: typeof Lease;
|
|
@@ -705,7 +705,7 @@ export declare class LeasesRecipe extends SmrtRecipe {
|
|
|
705
705
|
readonly visibility: "hidden";
|
|
706
706
|
};
|
|
707
707
|
readonly taxRate: {
|
|
708
|
-
readonly help: "
|
|
708
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
709
709
|
};
|
|
710
710
|
};
|
|
711
711
|
};
|
|
@@ -729,7 +729,7 @@ export declare class LicensesRecipe extends SmrtRecipe {
|
|
|
729
729
|
icon: string;
|
|
730
730
|
description: string;
|
|
731
731
|
};
|
|
732
|
-
static models: (typeof
|
|
732
|
+
static models: (typeof ContractLineItem | typeof LicenseSale)[];
|
|
733
733
|
static nav: {
|
|
734
734
|
label: string;
|
|
735
735
|
model: typeof LicenseSale;
|
|
@@ -781,7 +781,7 @@ export declare class LicensesRecipe extends SmrtRecipe {
|
|
|
781
781
|
readonly visibility: "hidden";
|
|
782
782
|
};
|
|
783
783
|
readonly taxRate: {
|
|
784
|
-
readonly help: "
|
|
784
|
+
readonly help: "Stored fraction: 0.05 means 5%.";
|
|
785
785
|
};
|
|
786
786
|
};
|
|
787
787
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Pure journal-entry construction from frozen commercial money snapshots. */
|
|
2
|
+
export interface RevenueJournalDocumentInput {
|
|
3
|
+
id: string;
|
|
4
|
+
number?: string | null;
|
|
5
|
+
currency: string;
|
|
6
|
+
lineAmountType: 'exclusive' | 'inclusive' | 'none';
|
|
7
|
+
subtotalMinor: number;
|
|
8
|
+
taxMinor: number;
|
|
9
|
+
totalMinor: number;
|
|
10
|
+
providerTaxMinor: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RevenueJournalLineInput {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Stored gross-for-inclusive or net-for-exclusive line amount. */
|
|
15
|
+
amountMinor: number;
|
|
16
|
+
/** Stored total line tax, excluding document-level provider tax. */
|
|
17
|
+
taxMinor: number;
|
|
18
|
+
revenueAccountId?: string | null;
|
|
19
|
+
}
|
|
20
|
+
/** Stored DocumentTax snapshot fields used by revenue posting. */
|
|
21
|
+
export interface RevenueJournalTaxInput {
|
|
22
|
+
currency: string;
|
|
23
|
+
taxCodeId: string | null;
|
|
24
|
+
taxCodeCode: string | null;
|
|
25
|
+
taxComponentId: string | null;
|
|
26
|
+
liabilityAccountId: string | null;
|
|
27
|
+
taxMinor: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RevenueJournalOptions {
|
|
30
|
+
arAccountId: string;
|
|
31
|
+
revenueAccountId: string;
|
|
32
|
+
taxAccountId?: string | null;
|
|
33
|
+
/** Explicit ledger book currency; conversion is outside this helper. */
|
|
34
|
+
bookCurrency: string;
|
|
35
|
+
/** Use -1 for an exact reversal or credit direction. */
|
|
36
|
+
direction?: 1 | -1;
|
|
37
|
+
}
|
|
38
|
+
export interface RevenueJournalEntry {
|
|
39
|
+
accountId: string;
|
|
40
|
+
debit?: number;
|
|
41
|
+
credit?: number;
|
|
42
|
+
currency: string;
|
|
43
|
+
memo: string;
|
|
44
|
+
}
|
|
45
|
+
export interface RevenueJournalWarning {
|
|
46
|
+
code: 'LEGACY_TAX_ACCOUNT_FALLBACK';
|
|
47
|
+
taxCodeId: string;
|
|
48
|
+
taxCodeCode: string;
|
|
49
|
+
taxComponentId: string;
|
|
50
|
+
accountId: string;
|
|
51
|
+
}
|
|
52
|
+
export interface RevenueJournalResult {
|
|
53
|
+
entries: RevenueJournalEntry[];
|
|
54
|
+
warnings: RevenueJournalWarning[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build balanced entries from persisted values only. Positive source amounts
|
|
58
|
+
* debit receivables and credit revenue/tax; direction -1 swaps those sides.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildRevenueJournalEntries(document: RevenueJournalDocumentInput, lines: readonly RevenueJournalLineInput[], taxes: readonly RevenueJournalTaxInput[], options: RevenueJournalOptions): RevenueJournalResult;
|
|
61
|
+
//# sourceMappingURL=revenue-journal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revenue-journal.d.ts","sourceRoot":"","sources":["../src/revenue-journal.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAO9E,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,WAAW,GAAG,WAAW,GAAG,MAAM,CAAC;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,6BAA6B,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,QAAQ,EAAE,qBAAqB,EAAE,CAAC;CACnC;AA4DD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,2BAA2B,EACrC,KAAK,EAAE,SAAS,uBAAuB,EAAE,EACzC,KAAK,EAAE,SAAS,sBAAsB,EAAE,EACxC,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CA8KtB"}
|