@happyvertical/smrt-commerce 0.55.13 → 0.55.14
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 +5 -0
- package/README.md +7 -0
- package/agents/cost-billing.md +41 -0
- package/dist/cost-billing.d.ts +58 -0
- package/dist/cost-billing.d.ts.map +1 -0
- package/dist/cost-billing.js +126 -0
- package/dist/cost-billing.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +13 -6
- package/package.json +13 -9
package/AGENTS.md
CHANGED
|
@@ -193,3 +193,8 @@ extension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md)
|
|
|
193
193
|
`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including
|
|
194
194
|
retained reduction rows; the server validates review fingerprints before recording.
|
|
195
195
|
See [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.
|
|
196
|
+
|
|
197
|
+
## Exact cost billing
|
|
198
|
+
|
|
199
|
+
Pure signed cost billing and explicit tax rounding are available through the
|
|
200
|
+
independent `cost-billing` subpath; see [contract](agents/cost-billing.md).
|
package/README.md
CHANGED
|
@@ -576,3 +576,10 @@ for server handlers from `@happyvertical/smrt-commerce/invoices`.
|
|
|
576
576
|
caller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),
|
|
577
577
|
[calculation rules](agents/invoice-calculations.md), and the required
|
|
578
578
|
[existing-database storage migration](agents/invoice-quantity.md).
|
|
579
|
+
|
|
580
|
+
## Exact cost billing
|
|
581
|
+
|
|
582
|
+
The independent `@happyvertical/smrt-commerce/cost-billing` entry point provides
|
|
583
|
+
exact signed reimbursement/fee calculations, ordered generic markup provenance,
|
|
584
|
+
and explicit line or invoice tax rounding. See [the cost billing contract](agents/cost-billing.md).
|
|
585
|
+
It does not alter Invoice persistence, editor defaults or settlement behavior.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Exact cost billing
|
|
2
|
+
|
|
3
|
+
`@happyvertical/smrt-commerce/cost-billing` is an independent, transport-neutral
|
|
4
|
+
entry point without model, server or Svelte dependencies. It does not persist,
|
|
5
|
+
post, authorize, select a currency/jurisdiction, or implement credit-note lifecycle.
|
|
6
|
+
Existing Invoice calculations and editor defaults are unchanged. In particular,
|
|
7
|
+
`calculateInvoiceMinorLine` retains negative ties toward positive infinity; the
|
|
8
|
+
new opt-in helpers use symmetric half-away-from-zero ties. Positive-only invoice
|
|
9
|
+
editor validation remains distinct from these generic signed calculations.
|
|
10
|
+
|
|
11
|
+
Call `resolveCostMarkup(overrides)` with caller-approved precedence, most specific
|
|
12
|
+
first. Each item has `source`, optional `policy` and `version`, and optional
|
|
13
|
+
`percent`. The first defined percent wins, including `"0"`. At least one is
|
|
14
|
+
required; even shadowed inputs are validated. The result contains the percent,
|
|
15
|
+
selected index, and detached ordered provenance. Persist accepted snapshots in
|
|
16
|
+
the owning application; these helpers never reprice them automatically.
|
|
17
|
+
|
|
18
|
+
Call `calculateCostBilling({ basisMinor, mode, markup })` with that result. Money
|
|
19
|
+
is signed safe-integer minor units in a single caller-selected currency. The
|
|
20
|
+
basis is explicitly chosen by the caller (for example excluding recoverable tax).
|
|
21
|
+
`reimbursement-plus-fee` returns principal once plus rounded fee; `fee-only`
|
|
22
|
+
returns only the fee; `nonbillable` and `fixed-contract` return zero charge.
|
|
23
|
+
The result includes `principalMinor`, `feeMinor`, `totalMinor`, and provenance.
|
|
24
|
+
Percent strings use plain nonnegative decimal notation, range 0–100000 percent,
|
|
25
|
+
at most six fractional places, without whitespace, exponent or leading zeroes.
|
|
26
|
+
The finite range and scale bound input parsing and arithmetic resource use; they
|
|
27
|
+
are API limits, not tax or industry policy. Arithmetic uses BigInt intermediates and half-away-from-zero rounding. Invalid
|
|
28
|
+
inputs, unknown modes, inconsistent provenance and unsafe outputs throw.
|
|
29
|
+
|
|
30
|
+
`calculateCostBillingTax({ lines, rules, roundingScope })` accepts line
|
|
31
|
+
`id`, signed `amountMinor`, and `category`; rules explicitly map every used
|
|
32
|
+
category to `{ id, percent }` components. An empty component array explicitly
|
|
33
|
+
means exempt. Components are additive, never compounded. Shared component IDs
|
|
34
|
+
must use equal rates across categories; distinct rate regimes need distinct IDs.
|
|
35
|
+
The caller owns taxable basis and jurisdiction mapping. Duplicate category,
|
|
36
|
+
component-within-category or line IDs fail closed. `line` scope rounds each
|
|
37
|
+
line/component pair; `invoice` scope aggregates exact amounts per component and
|
|
38
|
+
rounds once. Two 10-unit lines at 5 percent yield tax 2 versus 1 respectively.
|
|
39
|
+
Returns `taxMinor` and component totals, deliberately without allocating rounded
|
|
40
|
+
invoice tax back to lines. Signed reversals use symmetric rounding, but mixed
|
|
41
|
+
positive/negative lines may differ between rounding scopes.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Exact, transport-neutral cost pricing. All money is signed integer minor units. */
|
|
2
|
+
export type CostBillingMode = 'reimbursement-plus-fee' | 'fee-only' | 'nonbillable' | 'fixed-contract';
|
|
3
|
+
/** Caller-defined precedence, most specific first. No industry hierarchy is assumed. */
|
|
4
|
+
export interface CostMarkupOverride {
|
|
5
|
+
source: string;
|
|
6
|
+
policy?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
/** Exact percent in [0, 100000], with at most six decimal places. */
|
|
9
|
+
percent?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Detached provenance suitable for the caller's accepted pricing snapshot. */
|
|
12
|
+
export interface ResolvedCostMarkup {
|
|
13
|
+
percent: string;
|
|
14
|
+
selectedIndex: number;
|
|
15
|
+
overrides: CostMarkupOverride[];
|
|
16
|
+
}
|
|
17
|
+
/** Select the first supplied percent, validating even shadowed overrides. */
|
|
18
|
+
export declare function resolveCostMarkup(overrides: readonly CostMarkupOverride[]): ResolvedCostMarkup;
|
|
19
|
+
/** Calculate an explicitly supplied basis; this never reprices persisted snapshots. */
|
|
20
|
+
export declare function calculateCostBilling(input: {
|
|
21
|
+
basisMinor: number;
|
|
22
|
+
mode: CostBillingMode;
|
|
23
|
+
markup: ResolvedCostMarkup;
|
|
24
|
+
}): {
|
|
25
|
+
principalMinor: number;
|
|
26
|
+
feeMinor: number;
|
|
27
|
+
totalMinor: number;
|
|
28
|
+
markup: ResolvedCostMarkup;
|
|
29
|
+
};
|
|
30
|
+
/** Explicit noncompounding tax components; an empty array means tax exempt. */
|
|
31
|
+
export interface CostBillingTaxRule {
|
|
32
|
+
category: string;
|
|
33
|
+
components: readonly {
|
|
34
|
+
id: string;
|
|
35
|
+
percent: string;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tax signed amounts with half-away-from-zero rounding. Invoice scope rounds each
|
|
40
|
+
* component once after aggregation; line scope rounds each line/component pair.
|
|
41
|
+
* A shared component ID must have the same exact rate across categories.
|
|
42
|
+
*/
|
|
43
|
+
export declare function calculateCostBillingTax(input: {
|
|
44
|
+
lines: readonly {
|
|
45
|
+
id: string;
|
|
46
|
+
amountMinor: number;
|
|
47
|
+
category: string;
|
|
48
|
+
}[];
|
|
49
|
+
rules: readonly CostBillingTaxRule[];
|
|
50
|
+
roundingScope: 'line' | 'invoice';
|
|
51
|
+
}): {
|
|
52
|
+
taxMinor: number;
|
|
53
|
+
components: {
|
|
54
|
+
id: string;
|
|
55
|
+
taxMinor: number;
|
|
56
|
+
}[];
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=cost-billing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-billing.d.ts","sourceRoot":"","sources":["../src/cost-billing.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,MAAM,eAAe,GACvB,wBAAwB,GACxB,UAAU,GACV,aAAa,GACb,gBAAgB,CAAC;AAErB,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC;AA4CD,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,kBAAkB,EAAE,GACvC,kBAAkB,CAmBpB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,kBAAkB,CAAC;CAC5B,GAAG;IACF,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;CAC5B,CA+BA;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,KAAK,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAiDvE"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/cost-billing.ts
|
|
2
|
+
var SCALE = 1000000n;
|
|
3
|
+
var DENOMINATOR = 100n * SCALE;
|
|
4
|
+
var LIMIT = BigInt(Number.MAX_SAFE_INTEGER);
|
|
5
|
+
function percentUnits(value) {
|
|
6
|
+
if (typeof value !== "string" || !/^(0|[1-9]\d{0,5})(\.\d{1,6})?$/.test(value)) throw new TypeError("Percent must be an exact decimal string with at most six decimal places");
|
|
7
|
+
const [whole, fraction = ""] = value.split(".");
|
|
8
|
+
const units = BigInt(whole) * SCALE + BigInt(fraction.padEnd(6, "0"));
|
|
9
|
+
if (units > 100000n * SCALE) throw new RangeError("Percent exceeds 100000");
|
|
10
|
+
return units;
|
|
11
|
+
}
|
|
12
|
+
function money(value) {
|
|
13
|
+
if (!Number.isSafeInteger(value)) throw new RangeError("Money must be safe integer minor units");
|
|
14
|
+
return BigInt(value);
|
|
15
|
+
}
|
|
16
|
+
function safe(value) {
|
|
17
|
+
if (value > LIMIT || value < -LIMIT) throw new RangeError("Calculated money exceeds safe integer minor units");
|
|
18
|
+
return Number(value);
|
|
19
|
+
}
|
|
20
|
+
function rounded(numerator) {
|
|
21
|
+
const result = ((numerator < 0n ? -numerator : numerator) + DENOMINATOR / 2n) / DENOMINATOR;
|
|
22
|
+
return numerator < 0n ? -result : result;
|
|
23
|
+
}
|
|
24
|
+
function identifier(value) {
|
|
25
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new TypeError("Identifiers must be nonempty strings");
|
|
26
|
+
}
|
|
27
|
+
function resolveCostMarkup(overrides) {
|
|
28
|
+
let selectedIndex = -1;
|
|
29
|
+
const copy = overrides.map((override, index) => {
|
|
30
|
+
identifier(override.source);
|
|
31
|
+
if (override.policy !== void 0) identifier(override.policy);
|
|
32
|
+
if (override.version !== void 0) identifier(override.version);
|
|
33
|
+
if (override.percent !== void 0) {
|
|
34
|
+
percentUnits(override.percent);
|
|
35
|
+
if (selectedIndex === -1) selectedIndex = index;
|
|
36
|
+
}
|
|
37
|
+
return { ...override };
|
|
38
|
+
});
|
|
39
|
+
if (selectedIndex === -1) throw new TypeError("An explicit markup percent is required");
|
|
40
|
+
return {
|
|
41
|
+
percent: copy[selectedIndex].percent,
|
|
42
|
+
selectedIndex,
|
|
43
|
+
overrides: copy
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function calculateCostBilling(input) {
|
|
47
|
+
const basis = money(input.basisMinor);
|
|
48
|
+
const markup = resolveCostMarkup(input.markup.overrides);
|
|
49
|
+
if (markup.percent !== input.markup.percent || markup.selectedIndex !== input.markup.selectedIndex) throw new TypeError("Resolved markup does not match its provenance");
|
|
50
|
+
let principal = 0n;
|
|
51
|
+
let fee = 0n;
|
|
52
|
+
switch (input.mode) {
|
|
53
|
+
case "reimbursement-plus-fee":
|
|
54
|
+
principal = basis;
|
|
55
|
+
fee = rounded(basis * percentUnits(markup.percent));
|
|
56
|
+
break;
|
|
57
|
+
case "fee-only":
|
|
58
|
+
fee = rounded(basis * percentUnits(markup.percent));
|
|
59
|
+
break;
|
|
60
|
+
case "nonbillable":
|
|
61
|
+
case "fixed-contract": break;
|
|
62
|
+
default: throw new TypeError("Unknown cost billing mode");
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
principalMinor: safe(principal),
|
|
66
|
+
feeMinor: safe(fee),
|
|
67
|
+
totalMinor: safe(principal + fee),
|
|
68
|
+
markup
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function calculateCostBillingTax(input) {
|
|
72
|
+
if (input.roundingScope !== "line" && input.roundingScope !== "invoice") throw new TypeError("Unknown tax rounding scope");
|
|
73
|
+
const categories = /* @__PURE__ */ new Map();
|
|
74
|
+
const rates = /* @__PURE__ */ new Map();
|
|
75
|
+
const totals = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const rule of input.rules) {
|
|
77
|
+
identifier(rule.category);
|
|
78
|
+
if (categories.has(rule.category)) throw new TypeError("Duplicate tax category");
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
const components2 = rule.components.map((component) => {
|
|
81
|
+
identifier(component.id);
|
|
82
|
+
const rate = percentUnits(component.percent);
|
|
83
|
+
if (seen.has(component.id)) throw new TypeError("Duplicate tax component");
|
|
84
|
+
if (rates.has(component.id) && rates.get(component.id) !== rate) throw new TypeError("Conflicting tax component rate");
|
|
85
|
+
seen.add(component.id);
|
|
86
|
+
rates.set(component.id, rate);
|
|
87
|
+
return {
|
|
88
|
+
id: component.id,
|
|
89
|
+
rate
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
categories.set(rule.category, components2);
|
|
93
|
+
}
|
|
94
|
+
const ids = /* @__PURE__ */ new Set();
|
|
95
|
+
for (const line of input.lines) {
|
|
96
|
+
identifier(line.id);
|
|
97
|
+
identifier(line.category);
|
|
98
|
+
if (ids.has(line.id)) throw new TypeError("Duplicate tax line");
|
|
99
|
+
ids.add(line.id);
|
|
100
|
+
const amount = money(line.amountMinor);
|
|
101
|
+
const components2 = categories.get(line.category);
|
|
102
|
+
if (!components2) throw new TypeError("Missing explicit tax category mapping");
|
|
103
|
+
for (const component of components2) {
|
|
104
|
+
const numerator = amount * component.rate;
|
|
105
|
+
const tax = input.roundingScope === "line" ? rounded(numerator) : numerator;
|
|
106
|
+
totals.set(component.id, (totals.get(component.id) ?? 0n) + tax);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let total = 0n;
|
|
110
|
+
const components = Array.from(totals, ([id, value]) => {
|
|
111
|
+
const tax = input.roundingScope === "invoice" ? rounded(value) : value;
|
|
112
|
+
total += tax;
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
taxMinor: safe(tax)
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
taxMinor: safe(total),
|
|
120
|
+
components
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { calculateCostBilling, calculateCostBillingTax, resolveCostMarkup };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=cost-billing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-billing.js","names":["components"],"sources":["../src/cost-billing.ts"],"sourcesContent":["/** Exact, transport-neutral cost pricing. All money is signed integer minor units. */\nexport type CostBillingMode =\n | 'reimbursement-plus-fee'\n | 'fee-only'\n | 'nonbillable'\n | 'fixed-contract';\n\n/** Caller-defined precedence, most specific first. No industry hierarchy is assumed. */\nexport interface CostMarkupOverride {\n source: string;\n policy?: string;\n version?: string;\n /** Exact percent in [0, 100000], with at most six decimal places. */\n percent?: string;\n}\n\n/** Detached provenance suitable for the caller's accepted pricing snapshot. */\nexport interface ResolvedCostMarkup {\n percent: string;\n selectedIndex: number;\n overrides: CostMarkupOverride[];\n}\n\nconst SCALE = 1000000n;\nconst DENOMINATOR = 100n * SCALE;\nconst LIMIT = BigInt(Number.MAX_SAFE_INTEGER);\n\nfunction percentUnits(value: string): bigint {\n if (\n typeof value !== 'string' ||\n !/^(0|[1-9]\\d{0,5})(\\.\\d{1,6})?$/.test(value)\n ) {\n throw new TypeError(\n 'Percent must be an exact decimal string with at most six decimal places',\n );\n }\n const [whole, fraction = ''] = value.split('.');\n const units = BigInt(whole) * SCALE + BigInt(fraction.padEnd(6, '0'));\n if (units > 100000n * SCALE) throw new RangeError('Percent exceeds 100000');\n return units;\n}\n\nfunction money(value: number): bigint {\n if (!Number.isSafeInteger(value))\n throw new RangeError('Money must be safe integer minor units');\n return BigInt(value);\n}\n\nfunction safe(value: bigint): number {\n if (value > LIMIT || value < -LIMIT)\n throw new RangeError('Calculated money exceeds safe integer minor units');\n return Number(value);\n}\n\nfunction rounded(numerator: bigint): bigint {\n const magnitude = numerator < 0n ? -numerator : numerator;\n const result = (magnitude + DENOMINATOR / 2n) / DENOMINATOR;\n return numerator < 0n ? -result : result;\n}\n\nfunction identifier(value: string): void {\n if (typeof value !== 'string' || value.trim().length === 0)\n throw new TypeError('Identifiers must be nonempty strings');\n}\n\n/** Select the first supplied percent, validating even shadowed overrides. */\nexport function resolveCostMarkup(\n overrides: readonly CostMarkupOverride[],\n): ResolvedCostMarkup {\n let selectedIndex = -1;\n const copy = overrides.map((override, index) => {\n identifier(override.source);\n if (override.policy !== undefined) identifier(override.policy);\n if (override.version !== undefined) identifier(override.version);\n if (override.percent !== undefined) {\n percentUnits(override.percent);\n if (selectedIndex === -1) selectedIndex = index;\n }\n return { ...override };\n });\n if (selectedIndex === -1)\n throw new TypeError('An explicit markup percent is required');\n return {\n percent: copy[selectedIndex].percent as string,\n selectedIndex,\n overrides: copy,\n };\n}\n\n/** Calculate an explicitly supplied basis; this never reprices persisted snapshots. */\nexport function calculateCostBilling(input: {\n basisMinor: number;\n mode: CostBillingMode;\n markup: ResolvedCostMarkup;\n}): {\n principalMinor: number;\n feeMinor: number;\n totalMinor: number;\n markup: ResolvedCostMarkup;\n} {\n const basis = money(input.basisMinor);\n const markup = resolveCostMarkup(input.markup.overrides);\n if (\n markup.percent !== input.markup.percent ||\n markup.selectedIndex !== input.markup.selectedIndex\n ) {\n throw new TypeError('Resolved markup does not match its provenance');\n }\n let principal = 0n;\n let fee = 0n;\n switch (input.mode) {\n case 'reimbursement-plus-fee':\n principal = basis;\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'fee-only':\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'nonbillable':\n case 'fixed-contract':\n break;\n default:\n throw new TypeError('Unknown cost billing mode');\n }\n return {\n principalMinor: safe(principal),\n feeMinor: safe(fee),\n totalMinor: safe(principal + fee),\n markup,\n };\n}\n\n/** Explicit noncompounding tax components; an empty array means tax exempt. */\nexport interface CostBillingTaxRule {\n category: string;\n components: readonly { id: string; percent: string }[];\n}\n\n/**\n * Tax signed amounts with half-away-from-zero rounding. Invoice scope rounds each\n * component once after aggregation; line scope rounds each line/component pair.\n * A shared component ID must have the same exact rate across categories.\n */\nexport function calculateCostBillingTax(input: {\n lines: readonly { id: string; amountMinor: number; category: string }[];\n rules: readonly CostBillingTaxRule[];\n roundingScope: 'line' | 'invoice';\n}): { taxMinor: number; components: { id: string; taxMinor: number }[] } {\n if (input.roundingScope !== 'line' && input.roundingScope !== 'invoice') {\n throw new TypeError('Unknown tax rounding scope');\n }\n const categories = new Map<string, { id: string; rate: bigint }[]>();\n const rates = new Map<string, bigint>();\n const totals = new Map<string, bigint>();\n for (const rule of input.rules) {\n identifier(rule.category);\n if (categories.has(rule.category))\n throw new TypeError('Duplicate tax category');\n const seen = new Set<string>();\n const components = rule.components.map((component) => {\n identifier(component.id);\n const rate = percentUnits(component.percent);\n if (seen.has(component.id))\n throw new TypeError('Duplicate tax component');\n if (rates.has(component.id) && rates.get(component.id) !== rate)\n throw new TypeError('Conflicting tax component rate');\n seen.add(component.id);\n rates.set(component.id, rate);\n return { id: component.id, rate };\n });\n categories.set(rule.category, components);\n }\n const ids = new Set<string>();\n for (const line of input.lines) {\n identifier(line.id);\n identifier(line.category);\n if (ids.has(line.id)) throw new TypeError('Duplicate tax line');\n ids.add(line.id);\n const amount = money(line.amountMinor);\n const components = categories.get(line.category);\n if (!components)\n throw new TypeError('Missing explicit tax category mapping');\n for (const component of components) {\n const numerator = amount * component.rate;\n const tax =\n input.roundingScope === 'line' ? rounded(numerator) : numerator;\n totals.set(component.id, (totals.get(component.id) ?? 0n) + tax);\n }\n }\n let total = 0n;\n const components = Array.from(totals, ([id, value]) => {\n const tax = input.roundingScope === 'invoice' ? rounded(value) : value;\n total += tax;\n return { id, taxMinor: safe(tax) };\n });\n return { taxMinor: safe(total), components };\n}\n"],"mappings":"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"}
|