@trajectonomics/tax 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 excited.live
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # @trajectonomics/tax
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pure, deterministic statutory tax calculation engine with subpaths for Thailand (`@trajectonomics/tax/th`) and the United States (`@trajectonomics/tax/us`), covering 2026 federal tax plus all 50 state income tax systems and the District of Columbia.
4
+
5
+ ## United States State Tax
6
+
7
+ `calculateUsTax` accepts an optional `state` code (`AL` through `WY` plus `DC`). When present, the result includes `breakdown.state` with state taxable income, bracket slices, personal/dependent credits, state withholding, and net state tax, and the state liability is folded into the top-level `totalTax` and `netTaxPayable`.
8
+
9
+ - Nine no-income-tax states (`AK`, `FL`, `NV`, `NH`, `SD`, `TN`, `TX`, `WA`, `WY`) return zero state tax and zero state rates.
10
+ - Sixteen flat-rate states (including `LA` and `OH`, which are single-rate in 2026) and twenty-five graduated-rate states plus `DC` use their 2026 standard deductions, personal/dependent exemptions, and statutory bracket tables.
11
+ - `MA` includes the 4% millionaire surtax and `CA` includes the 1% mental health services surtax.
12
+ - State inputs also include `stateDependents`, `stateWithholding`, and `stateCredits` (`nonRefundable` / `refundable`).
13
+ - State calculations start from federal AGI and apply the state standard deduction, personal/dependent exemptions or credits, and the state bracket table. Top-level `marginalRate` is the federal ordinary rate plus the state marginal rate. Head-of-household uses the state's joint schedule in `NJ`, `OR`, and `OK` and the single schedule elsewhere unless a dedicated table is encoded (`CA`, `MN`, `NY`, `ME`); head-of-household and separate-filer standard deductions outside the federal-conforming states are derived (1.5x single, joint/2). Top-level `withholding` is federal-only: `breakdown.state.withholding` carries state withholding, and `netTaxPayable` subtracts both. `stateDependents`, `stateWithholding`, and `stateCredits` require a `state` code and throw otherwise. Separate filers use half the joint thresholds in `MN` and `WI`; `MO` joint returns use doubled single thresholds (each spouse taxed on half). Known gaps: Utah's credit phase-out, Connecticut/Illinois/Maryland/Oregon/Rhode Island/Wisconsin income-tested phase-outs, the Maryland capital-gains and Minnesota net-investment-income surtaxes, and Maine's 2026 high-income surcharge are not modeled; New York uses the 2025 return rate schedule; Nebraska head-of-household uses the single schedule; head-of-household brackets outside the states listed in the parameter tables fall back to the single schedule. Local/city income taxes, state itemized deductions, and income-tested phase-outs not encoded in the parameter tables are out of scope.
14
+
15
+ ## Commands
16
+
17
+ - `bun run check`: Complete verification suite (`lint`, `typecheck`, `test` with 100% coverage, `build`, `lint:package`).
18
+ - `bun run build`: Build ESM and CJS bundles via `tsdown`.
19
+ - `bun test`: Run test suite with 100% coverage enforcement.
20
+ - `bun run typecheck`: Run strict TypeScript compiler checks.
21
+ - `bun run lint`: Lint with Biome (zero warnings allowed).
22
+ - `bun run format`: Auto-format and organize imports with Biome.
23
+ - `bun run lint:package`: Validate package manifests and type exports with `publint` and `@arethetypeswrong/cli`.
24
+
25
+ ## Releasing
26
+
27
+ Versions are computed, never edited by hand (`package.json` says `0.0.0-development` on purpose). `scripts/version.ts` reads the last `vX.Y.Z` git tag and the conventional commit messages since then:
28
+
29
+ | Commits since last release | Bump |
30
+ | --- | --- |
31
+ | any `feat!:` / `fix!:` / `BREAKING CHANGE:` | major (minor while on 0.x) |
32
+ | any `feat:` | minor |
33
+ | anything else | patch |
34
+
35
+ - **Alpha, automatic:** Every merge to `main` that touches code publishes `<next>-alpha.<n>` under the npm dist-tag `alpha` (`bun add @trajectonomics/tax@alpha`). `n` counts up per upcoming version.
36
+ - **Stable, on demand:** Actions → Release → Run workflow (on `main`). It publishes `<next>` under `latest`, tags `vX.Y.Z`, and creates a GitHub release with generated notes. The bump input overrides the computed one; pick `major` to cut 1.0.0.
37
+
38
+ PR titles must be conventional commits (enforced by the `PR title` check) because squash-merge commit subjects become the basis for version calculation.
39
+
40
+ Preview the next version locally:
41
+
42
+ ```bash
43
+ bun scripts/version.ts --channel alpha
44
+ bun scripts/version.ts --channel stable
45
+ ```
46
+
47
+ ## License
48
+
49
+ MIT
@@ -0,0 +1,509 @@
1
+ // src/core/money/errors.ts
2
+ /**
3
+ * Error thrown when input values violate mathematical boundaries or statutory constraints.
4
+ * Exposes field, raw value, and validation reason for transparency and structured error reporting.
5
+ */
6
+ var TaxValidationError = class TaxValidationError extends Error {
7
+ field;
8
+ value;
9
+ reason;
10
+ constructor(field, value, reason) {
11
+ super(`Tax validation error on field '${field}': ${reason} (received: ${String(value)})`);
12
+ this.name = "TaxValidationError";
13
+ this.field = field;
14
+ this.value = value;
15
+ this.reason = reason;
16
+ Object.setPrototypeOf(this, TaxValidationError.prototype);
17
+ }
18
+ };
19
+ // src/core/money/currency.ts
20
+ /**
21
+ * Minor-unit precision (fractional decimal digits) per supported currency.
22
+ *
23
+ * Units: THB 2 (1 baht = 100 satang), USD 2 (1 dollar = 100 cents).
24
+ * Internal to the money module; not part of the public package API.
25
+ */
26
+ const CURRENCY_PRECISION = {
27
+ THB: 2,
28
+ USD: 2
29
+ };
30
+ // src/core/money/validate.ts
31
+ /**
32
+ * Validates a monetary input against numerical boundaries (NaN, Infinity, negative).
33
+ *
34
+ * @param value Number to validate.
35
+ * @param field Diagnostic field name for error reporting.
36
+ * @param options Validation options.
37
+ * @throws {TaxValidationError} if value is NaN, non-finite, or negative without allowNegative.
38
+ */
39
+ function validateMonetaryAmount(value, field, options) {
40
+ if (Number.isNaN(value)) throw new TaxValidationError(field, value, "Value cannot be NaN");
41
+ if (!Number.isFinite(value)) throw new TaxValidationError(field, value, "Value must be a finite number");
42
+ if (!options?.allowNegative && value < 0) throw new TaxValidationError(field, value, "Value cannot be negative");
43
+ }
44
+ /**
45
+ * Validates a statutory percentage rate (e.g. 0.05 for 5%).
46
+ *
47
+ * @param rate Rate ratio between 0 and 1.
48
+ * @param field Diagnostic field name.
49
+ * @throws {TaxValidationError} if rate is NaN, non-finite, or outside [0, 1].
50
+ */
51
+ function validateRate(rate, field) {
52
+ if (Number.isNaN(rate) || !Number.isFinite(rate)) throw new TaxValidationError(field, rate, "Rate must be a finite number");
53
+ if (rate < 0 || rate > 1) throw new TaxValidationError(field, rate, "Statutory rate ratio must be between 0 and 1 (inclusive)");
54
+ }
55
+ /**
56
+ * Validates that an amount is an exact integer minor unit.
57
+ *
58
+ * @param minorUnits Integer minor units.
59
+ * @param field Diagnostic field name.
60
+ * @param options Validation options.
61
+ * @throws {TaxValidationError} if not a safe integer.
62
+ */
63
+ function validateIntegerMinorUnits(minorUnits, field, options) {
64
+ validateMonetaryAmount(minorUnits, field, options);
65
+ if (!Number.isSafeInteger(minorUnits)) throw new TaxValidationError(field, minorUnits, "Minor units amount must be a safe integer");
66
+ }
67
+ /**
68
+ * Validates a statutory count (children, years of service, holding years).
69
+ *
70
+ * Purpose: Guards integer counts that later multiply money, rejecting fractions and unsafe values.
71
+ * Math: Requires `Number.isSafeInteger(value)` and `value >= min`.
72
+ * Units: Dimensionless count (not currency).
73
+ * Throws: TaxValidationError if value is not a safe integer or is below `min`.
74
+ *
75
+ * @param value Count to validate.
76
+ * @param field Diagnostic field name.
77
+ * @param min Inclusive lower bound (defaults to 0).
78
+ * @statute Jurisdiction-neutral input bound (AGENTS rule 8); no statutory citation.
79
+ */
80
+ function validateSafeIntegerCount(value, field, min = 0) {
81
+ if (!Number.isSafeInteger(value)) throw new TaxValidationError(field, value, "Count must be a safe integer");
82
+ if (value < min) throw new TaxValidationError(field, value, `Count must be at least ${min}`);
83
+ }
84
+ /**
85
+ * Multiplies two integers and fails loudly if the product leaves the safe integer range.
86
+ *
87
+ * Purpose: Prevents silent precision loss when scaling minor units by a count (years, days).
88
+ * Math: `a * b`, requiring `Number.isSafeInteger(a * b)`.
89
+ * Units: Typically integer minor units times a dimensionless count; result in minor units.
90
+ * Throws: TaxValidationError (reported against `field`) if the product is not a safe integer.
91
+ *
92
+ * @param a Integer factor (e.g. minor units).
93
+ * @param b Integer factor (e.g. a validated count).
94
+ * @param field Diagnostic field name of the count or amount being scaled.
95
+ * @statute Jurisdiction-neutral input bound (AGENTS rule 8); no statutory citation.
96
+ */
97
+ function multiplySafeInteger(a, b, field) {
98
+ const product = a * b;
99
+ if (!Number.isSafeInteger(product)) throw new TaxValidationError(field, b, "Scaled amount exceeds the safe integer range");
100
+ return product + 0;
101
+ }
102
+ // src/core/money/minor-units.ts
103
+ function scaleDecimalString(abs, precision) {
104
+ const factor = 10 ** precision;
105
+ if (String(abs).includes("e")) return abs * factor;
106
+ const parts = String(abs).split(".");
107
+ const intPart = parts[0] ?? "0";
108
+ const decPart = (parts[1] ?? "").padEnd(precision, "0");
109
+ const whole = intPart + decPart.slice(0, precision);
110
+ const fraction = decPart.slice(precision);
111
+ return Number(whole + (fraction.length > 0 ? `.${fraction}` : ""));
112
+ }
113
+ /**
114
+ * Converts a major-unit monetary amount to integer minor units (satang/cent).
115
+ *
116
+ * Math: Decimal string expansion and half-away-from-zero rounding to eliminate IEEE 754 float drift.
117
+ * Units: Input is major currency unit (e.g. THB, USD); output is integer minor units (e.g. Satang, Cents).
118
+ *
119
+ * @param amount Amount in major units.
120
+ * @param currency Supported statutory currency (THB or USD).
121
+ * @param field Diagnostic field name.
122
+ * @param options Validation options (e.g. allowNegative for refund balances).
123
+ * @returns Integer minor units.
124
+ * @throws {TaxValidationError} if amount is NaN, infinite, or results in a non-safe integer.
125
+ * @example toMinorUnits(1.14, "USD") // 114 (114 cents)
126
+ */
127
+ function toMinorUnits(amount, currency, field = "amount", options) {
128
+ validateMonetaryAmount(amount, field, options);
129
+ const precision = CURRENCY_PRECISION[currency];
130
+ const sign = amount < 0 ? -1 : 1;
131
+ const scaled = scaleDecimalString(Math.abs(amount), precision);
132
+ const result = sign * Math.round(scaled) + 0;
133
+ if (!Number.isSafeInteger(result)) throw new TaxValidationError(field, amount, "Conversion to minor units failed or exceeded safe integer bounds");
134
+ return result;
135
+ }
136
+ /**
137
+ * Converts an integer minor unit amount back to major currency units.
138
+ *
139
+ * @param minorUnits Integer minor units.
140
+ * @param currency Supported statutory currency (THB or USD).
141
+ * @param field Diagnostic field name.
142
+ * @param options Validation options (e.g. allowNegative for refund balances).
143
+ * @returns Amount in major units.
144
+ * @throws {TaxValidationError} if minorUnits is not a safe integer.
145
+ * @example fromMinorUnits(1055, "THB") // 10.55
146
+ */
147
+ function fromMinorUnits(minorUnits, currency, field = "minorUnits", options) {
148
+ validateIntegerMinorUnits(minorUnits, field, options);
149
+ const precision = CURRENCY_PRECISION[currency];
150
+ const factor = 10 ** precision;
151
+ return (minorUnits < 0 ? -1 : 1) * Number((Math.abs(minorUnits) / factor).toFixed(precision)) + 0;
152
+ }
153
+ // src/core/money/rounding.ts
154
+ /**
155
+ * Rounds a number to the nearest integer, breaking ties away from zero (standard half-up).
156
+ *
157
+ * Math: Rounds fractional halves away from zero (e.g. 1.5 -> 2, -1.5 -> -2).
158
+ *
159
+ * @param value Floating-point value.
160
+ * @returns Rounded integer.
161
+ */
162
+ function roundHalfUp(value) {
163
+ if (value < 0) return -Math.round(Math.abs(value)) + 0;
164
+ return Math.round(value) + 0;
165
+ }
166
+ function parseRateRational(rate) {
167
+ const parts = (rate.toString().includes("e") ? rate.toFixed(10).replace(/\.?0+$/, "") : rate.toString()).split(".");
168
+ const decimals = parts[1] ? parts[1].length : 0;
169
+ return {
170
+ num: BigInt(parts.join("")),
171
+ den: 10n ** BigInt(decimals)
172
+ };
173
+ }
174
+ /**
175
+ * Applies a statutory percentage rate to an integer minor unit amount using exact rational arithmetic.
176
+ *
177
+ * Math: Exact rational scaling via BigInt `(minorUnits * num + modeAdjust) / den`
178
+ * to eliminate IEEE 754 binary floating-point drift on exact statutory tax rates.
179
+ * Units: Input and output are integer minor units (satang/cents).
180
+ *
181
+ * @param minorUnits Integer minor units amount.
182
+ * @param rate Statutory rate ratio (e.g. 0.05 for 5%, 0.35 for 35%).
183
+ * @param rounding Rounding mode (`half-up`, `floor`, or `ceil`).
184
+ * @param options Validation options (e.g. allowNegative).
185
+ * @returns Computed tax in integer minor units.
186
+ * @throws {TaxValidationError} if minorUnits is not a safe integer or rate is outside [0, 1].
187
+ * @example applyPercentage(90, 0.35) // 32 (exact statutory satang calculation)
188
+ */
189
+ function applyPercentage(minorUnits, rate, rounding = "half-up", options) {
190
+ validateIntegerMinorUnits(minorUnits, "minorUnits", options);
191
+ validateRate(rate, "rate");
192
+ const { num, den } = parseRateRational(rate);
193
+ const prod = BigInt(minorUnits) * num;
194
+ let result;
195
+ if (rounding === "floor") result = prod >= 0n ? prod / den : (prod - (den - 1n)) / den;
196
+ else if (rounding === "ceil") result = prod >= 0n ? (prod + (den - 1n)) / den : prod / den;
197
+ else result = prod >= 0n ? (prod * 2n + den) / (den * 2n) : (prod * 2n - den) / (den * 2n);
198
+ return Number(result) + 0;
199
+ }
200
+ /**
201
+ * Applies the statutory gross-up ratio `rate / (1 - rate)` to an integer minor unit amount.
202
+ *
203
+ * Purpose: Recovers the underlying tax from a net amount (e.g. a dividend tax credit).
204
+ * Math: Exact BigInt rational `(minorUnits * num) / (den - num)` where `rate = num / den`,
205
+ * rounded half-up (away from zero) so ties such as 0.5 satang never suffer binary float drift.
206
+ * Units: Input and output are integer minor units (satang/cents).
207
+ *
208
+ * @param minorUnits Non-negative integer minor units amount (the net amount).
209
+ * @param rate Underlying rate ratio in [0, 1) (e.g. 0.2 for a 20% corporate tax rate).
210
+ * @param field Diagnostic field name reported on failure (defaults to `"minorUnits"`).
211
+ * @returns Gross-up amount (`minorUnits * rate / (1 - rate)`) in integer minor units.
212
+ * @throws {TaxValidationError} if minorUnits is not a non-negative safe integer, rate is outside
213
+ * [0, 1), or the result is not a safe integer.
214
+ * @statute Jurisdiction-neutral; Thailand Revenue Code Section 47 bis is the first consumer.
215
+ * @example applyGrossUp(100_000, 0.2) // 25_000 (1,000.00 -> 250.00 credit at 20%)
216
+ */
217
+ function applyGrossUp(minorUnits, rate, field = "minorUnits") {
218
+ validateIntegerMinorUnits(minorUnits, field);
219
+ validateRate(rate, "rate");
220
+ if (rate >= 1) throw new TaxValidationError("rate", rate, "Gross-up rate must be strictly less than 1");
221
+ const { num, den } = parseRateRational(rate);
222
+ const divisor = den - num;
223
+ const result = Number((BigInt(minorUnits) * num * 2n + divisor) / (divisor * 2n)) + 0;
224
+ if (!Number.isSafeInteger(result)) throw new TaxValidationError(field, minorUnits, "Gross-up result exceeds the safe integer range");
225
+ return result;
226
+ }
227
+ // src/core/money/types.ts
228
+ /**
229
+ * Constructs a branded Money value with immediate input boundary validation.
230
+ *
231
+ * @param amount Numerical amount in major units.
232
+ * @param _currency Currency code tag.
233
+ * @param field Diagnostic field name.
234
+ * @param options Validation options (e.g. allowNegative for refund balances).
235
+ * @returns Branded Money value.
236
+ * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.
237
+ */
238
+ function money(amount, _currency, field = "amount", options) {
239
+ validateMonetaryAmount(amount, field, options);
240
+ return amount;
241
+ }
242
+ /**
243
+ * Constructs a branded Thai Baht (THB) money value with input boundary validation.
244
+ *
245
+ * @param amount Amount in Baht.
246
+ * @param field Diagnostic field name.
247
+ * @param options Validation options (e.g. allowNegative for refund balances).
248
+ * @returns Branded Money<"THB"> value.
249
+ * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.
250
+ */
251
+ function thb(amount, field = "amount", options) {
252
+ validateMonetaryAmount(amount, field, options);
253
+ return amount;
254
+ }
255
+ /**
256
+ * Constructs a branded US Dollar (USD) money value with input boundary validation.
257
+ *
258
+ * @param amount Amount in Dollars.
259
+ * @param field Diagnostic field name.
260
+ * @param options Validation options (e.g. allowNegative for refund balances).
261
+ * @returns Branded Money<"USD"> value.
262
+ * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.
263
+ */
264
+ function usd(amount, field = "amount", options) {
265
+ validateMonetaryAmount(amount, field, options);
266
+ return amount;
267
+ }
268
+ // src/core/brackets/marginal.ts
269
+ /**
270
+ * Determines the marginal tax rate that applies to the next unit of ordinary income.
271
+ *
272
+ * @param brackets Ordered progressive tax brackets.
273
+ * @param nextUnit Amount of income on the boundary transition (taxableAmount + 1).
274
+ * @returns Marginal statutory rate ratio.
275
+ */
276
+ function marginalRateAt(brackets, nextUnit) {
277
+ let lower = 0;
278
+ for (const bracket of brackets) {
279
+ const upper = bracket.upTo;
280
+ if (nextUnit > lower && (upper === null || !Number.isFinite(upper) || nextUnit <= upper)) return bracket.rate;
281
+ if (upper === null || !Number.isFinite(upper)) break;
282
+ lower = upper;
283
+ }
284
+ return brackets[0]?.rate ?? 0;
285
+ }
286
+ // src/core/brackets/slice.ts
287
+ /**
288
+ * Computes progressive tax and slice boundaries for a single bracket tier.
289
+ *
290
+ * @param lowerBound Lower threshold in minor units.
291
+ * @param bracket Current bracket specification.
292
+ * @param taxableAmount Total taxable income in minor units.
293
+ * @param index Bracket index.
294
+ * @returns Computed BracketSlice.
295
+ */
296
+ function computeBracketSlice(lowerBound, bracket, taxableAmount, index) {
297
+ const upperBound = bracket.upTo;
298
+ let sliceTaxable = 0;
299
+ if (taxableAmount > lowerBound) {
300
+ if (upperBound === null || !Number.isFinite(upperBound)) sliceTaxable = taxableAmount - lowerBound;
301
+ else sliceTaxable = Math.min(taxableAmount, upperBound) - lowerBound;
302
+ }
303
+ const sliceTax = applyPercentage(sliceTaxable, bracket.rate);
304
+ return {
305
+ bracketIndex: index,
306
+ lowerBound,
307
+ upperBound,
308
+ rate: bracket.rate,
309
+ taxableAmount: sliceTaxable,
310
+ tax: sliceTax
311
+ };
312
+ }
313
+ // src/core/brackets/evaluate.ts
314
+ function validateSingleBracket(bracket, index, isLast, prevBound) {
315
+ validateRate(bracket.rate, `bracket[${index}].rate`);
316
+ if (bracket.upTo === null) {
317
+ if (!isLast) throw new TaxValidationError(`bracket[${index}].upTo`, bracket.upTo, "Only the final bracket tier may have upTo: null (unbounded)");
318
+ return prevBound;
319
+ }
320
+ validateMonetaryAmount(bracket.upTo, `bracket[${index}].upTo`);
321
+ if (!Number.isSafeInteger(bracket.upTo) || bracket.upTo <= prevBound) throw new TaxValidationError(`bracket[${index}].upTo`, bracket.upTo, `Bracket bound must be a safe integer greater than ${prevBound}`);
322
+ return bracket.upTo;
323
+ }
324
+ function validateBrackets(brackets) {
325
+ if (brackets.length === 0) throw new TaxValidationError("brackets", brackets, "Bracket list cannot be empty");
326
+ let prevBound = 0;
327
+ for (let i = 0; i < brackets.length; i++) {
328
+ const bracket = brackets[i];
329
+ if (!bracket) continue;
330
+ prevBound = validateSingleBracket(bracket, i, i === brackets.length - 1, prevBound);
331
+ }
332
+ const last = brackets[brackets.length - 1];
333
+ if (last && last.upTo !== null) throw new TaxValidationError("brackets", brackets, "Final bracket tier must have upTo: null");
334
+ }
335
+ /**
336
+ * Evaluates progressive tax brackets slice-by-slice across multiple tiers.
337
+ *
338
+ * Math: Slices taxable income across progressive tiers and sums integer tax per slice.
339
+ * Units: All inputs and outputs must be integer minor units (satang/cents).
340
+ *
341
+ * @param taxableAmount Taxable income in integer minor units.
342
+ * @param brackets Ordered progressive statutory tax brackets.
343
+ * @param grossIncome Optional gross income in minor units for statutory effective rate calculation.
344
+ * @param field Diagnostic field name.
345
+ * @returns Evaluated bracket result including total tax, slices, effective rate, and marginal rate.
346
+ * @throws {TaxValidationError} if inputs are invalid.
347
+ */
348
+ function evaluateBrackets(taxableAmount, brackets, grossIncome, field = "taxableAmount") {
349
+ validateMonetaryAmount(taxableAmount, field);
350
+ if (grossIncome !== void 0) validateMonetaryAmount(grossIncome, "grossIncome");
351
+ validateBrackets(brackets);
352
+ let totalTax = 0;
353
+ let lowerBound = 0;
354
+ const slices = [];
355
+ for (let i = 0; i < brackets.length; i++) {
356
+ const bracket = brackets[i];
357
+ if (!bracket) continue;
358
+ const slice = computeBracketSlice(lowerBound, bracket, taxableAmount, i);
359
+ slices.push(slice);
360
+ totalTax += slice.tax;
361
+ if (bracket.upTo === null || !Number.isFinite(bracket.upTo)) break;
362
+ lowerBound = bracket.upTo;
363
+ }
364
+ const baseForEffective = grossIncome ?? taxableAmount;
365
+ const effectiveRate = baseForEffective > 0 ? totalTax / baseForEffective : 0;
366
+ const marginalRate = marginalRateAt(brackets, taxableAmount + 1);
367
+ return {
368
+ totalTax,
369
+ slices,
370
+ effectiveRate,
371
+ marginalRate
372
+ };
373
+ }
374
+ // src/core/caps/resolve.ts
375
+ /**
376
+ * Resolves a statutory cap limit from a formula configuration (percentage, ceiling, or min of both).
377
+ *
378
+ * @param options Statutory cap options specifying base, rate, and/or ceiling.
379
+ * @param defaultField Default diagnostic field name.
380
+ * @returns Resolved cap numerical ceiling.
381
+ * @throws {TaxValidationError} if inputs are invalid.
382
+ */
383
+ function resolveCap(options, defaultField = "cap") {
384
+ const fieldName = options.field ?? defaultField;
385
+ const { base, rate, ceiling, currency } = options;
386
+ let computedCap = Number.POSITIVE_INFINITY;
387
+ if (base !== void 0 && rate !== void 0) {
388
+ validateMonetaryAmount(base, `${fieldName}Base`);
389
+ validateRate(rate, `${fieldName}Rate`);
390
+ if (currency) computedCap = fromMinorUnits(applyPercentage(toMinorUnits(base, currency, `${fieldName}Base`), rate), currency);
391
+ else if (Number.isSafeInteger(base)) computedCap = applyPercentage(base, rate);
392
+ else computedCap = roundHalfUp(base * rate);
393
+ }
394
+ if (ceiling !== void 0) {
395
+ validateMonetaryAmount(ceiling, `${fieldName}Ceiling`);
396
+ computedCap = Math.min(computedCap, ceiling);
397
+ }
398
+ validateMonetaryAmount(computedCap, `${fieldName}Cap`);
399
+ return computedCap;
400
+ }
401
+ // src/core/caps/clamp.ts
402
+ /**
403
+ * Clamps a requested deduction, allowance, or expense against a statutory cap.
404
+ *
405
+ * @param requested Original requested amount.
406
+ * @param capOrOptions Numeric cap or StatutoryCapOptions specification.
407
+ * @param field Diagnostic field name.
408
+ * @returns Breakdown-ready ClampedAmount structure.
409
+ * @throws {TaxValidationError} if inputs are negative, NaN, or non-finite.
410
+ * @example clampCap(150_000, 100_000, "allowance") // { requested: 150000, allowed: 100000, cap: 100000, wasClamped: true }
411
+ */
412
+ function clampCap(requested, capOrOptions, field = "amount") {
413
+ validateMonetaryAmount(requested, field);
414
+ const cap = typeof capOrOptions === "number" ? capOrOptions : resolveCap(capOrOptions, field);
415
+ validateMonetaryAmount(cap, `${field}Cap`);
416
+ const wasClamped = requested > cap;
417
+ return {
418
+ requested,
419
+ allowed: wasClamped ? cap : requested,
420
+ cap,
421
+ wasClamped
422
+ };
423
+ }
424
+ Object.defineProperty(exports, "TaxValidationError", {
425
+ enumerable: true,
426
+ get: function() {
427
+ return TaxValidationError;
428
+ }
429
+ });
430
+ Object.defineProperty(exports, "applyGrossUp", {
431
+ enumerable: true,
432
+ get: function() {
433
+ return applyGrossUp;
434
+ }
435
+ });
436
+ Object.defineProperty(exports, "applyPercentage", {
437
+ enumerable: true,
438
+ get: function() {
439
+ return applyPercentage;
440
+ }
441
+ });
442
+ Object.defineProperty(exports, "clampCap", {
443
+ enumerable: true,
444
+ get: function() {
445
+ return clampCap;
446
+ }
447
+ });
448
+ Object.defineProperty(exports, "evaluateBrackets", {
449
+ enumerable: true,
450
+ get: function() {
451
+ return evaluateBrackets;
452
+ }
453
+ });
454
+ Object.defineProperty(exports, "fromMinorUnits", {
455
+ enumerable: true,
456
+ get: function() {
457
+ return fromMinorUnits;
458
+ }
459
+ });
460
+ Object.defineProperty(exports, "money", {
461
+ enumerable: true,
462
+ get: function() {
463
+ return money;
464
+ }
465
+ });
466
+ Object.defineProperty(exports, "multiplySafeInteger", {
467
+ enumerable: true,
468
+ get: function() {
469
+ return multiplySafeInteger;
470
+ }
471
+ });
472
+ Object.defineProperty(exports, "roundHalfUp", {
473
+ enumerable: true,
474
+ get: function() {
475
+ return roundHalfUp;
476
+ }
477
+ });
478
+ Object.defineProperty(exports, "thb", {
479
+ enumerable: true,
480
+ get: function() {
481
+ return thb;
482
+ }
483
+ });
484
+ Object.defineProperty(exports, "toMinorUnits", {
485
+ enumerable: true,
486
+ get: function() {
487
+ return toMinorUnits;
488
+ }
489
+ });
490
+ Object.defineProperty(exports, "usd", {
491
+ enumerable: true,
492
+ get: function() {
493
+ return usd;
494
+ }
495
+ });
496
+ Object.defineProperty(exports, "validateMonetaryAmount", {
497
+ enumerable: true,
498
+ get: function() {
499
+ return validateMonetaryAmount;
500
+ }
501
+ });
502
+ Object.defineProperty(exports, "validateSafeIntegerCount", {
503
+ enumerable: true,
504
+ get: function() {
505
+ return validateSafeIntegerCount;
506
+ }
507
+ });
508
+
509
+ //# sourceMappingURL=clamp-BKG7kjVf.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clamp-BKG7kjVf.cjs","names":[],"sources":["../src/core/money/errors.ts","../src/core/money/currency.ts","../src/core/money/validate.ts","../src/core/money/minor-units.ts","../src/core/money/rounding.ts","../src/core/money/types.ts","../src/core/brackets/marginal.ts","../src/core/brackets/slice.ts","../src/core/brackets/evaluate.ts","../src/core/caps/resolve.ts","../src/core/caps/clamp.ts"],"sourcesContent":["/**\n * Error thrown when input values violate mathematical boundaries or statutory constraints.\n * Exposes field, raw value, and validation reason for transparency and structured error reporting.\n */\nexport class TaxValidationError extends Error {\n\treadonly field: string\n\treadonly value: unknown\n\treadonly reason: string\n\n\tconstructor(field: string, value: unknown, reason: string) {\n\t\tsuper(`Tax validation error on field '${field}': ${reason} (received: ${String(value)})`)\n\t\tthis.name = \"TaxValidationError\"\n\t\tthis.field = field\n\t\tthis.value = value\n\t\tthis.reason = reason\n\t\tObject.setPrototypeOf(this, TaxValidationError.prototype)\n\t}\n}\n","/**\n * Currencies the tax engine accounts in: one statutory currency per supported jurisdiction.\n *\n * Purpose: Compile-time gate for every monetary API. Callers map their own currency codes\n * onto this union; there is no runtime validation or fallback.\n * @statute Thai Revenue Code (THB) and US Internal Revenue Code (USD) both require\n * amounts to be stated in the domestic currency.\n */\nexport type TaxSupportedCurrency = \"THB\" | \"USD\"\n\n/**\n * Minor-unit precision (fractional decimal digits) per supported currency.\n *\n * Units: THB 2 (1 baht = 100 satang), USD 2 (1 dollar = 100 cents).\n * Internal to the money module; not part of the public package API.\n */\nexport const CURRENCY_PRECISION: Readonly<Record<TaxSupportedCurrency, number>> = {\n\tTHB: 2,\n\tUSD: 2,\n}\n","import { TaxValidationError } from \"./errors\"\n\n/**\n * Options controlling monetary validation.\n */\nexport interface ValidateAmountOptions {\n\t/** If true, negative values will not throw an error (e.g. net payable after refundable credits). */\n\treadonly allowNegative?: boolean\n}\n\n/**\n * Validates a monetary input against numerical boundaries (NaN, Infinity, negative).\n *\n * @param value Number to validate.\n * @param field Diagnostic field name for error reporting.\n * @param options Validation options.\n * @throws {TaxValidationError} if value is NaN, non-finite, or negative without allowNegative.\n */\nexport function validateMonetaryAmount(\n\tvalue: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tif (Number.isNaN(value)) {\n\t\tthrow new TaxValidationError(field, value, \"Value cannot be NaN\")\n\t}\n\tif (!Number.isFinite(value)) {\n\t\tthrow new TaxValidationError(field, value, \"Value must be a finite number\")\n\t}\n\tif (!options?.allowNegative && value < 0) {\n\t\tthrow new TaxValidationError(field, value, \"Value cannot be negative\")\n\t}\n}\n\n/**\n * Validates a statutory percentage rate (e.g. 0.05 for 5%).\n *\n * @param rate Rate ratio between 0 and 1.\n * @param field Diagnostic field name.\n * @throws {TaxValidationError} if rate is NaN, non-finite, or outside [0, 1].\n */\nexport function validateRate(rate: number, field: string): void {\n\tif (Number.isNaN(rate) || !Number.isFinite(rate)) {\n\t\tthrow new TaxValidationError(field, rate, \"Rate must be a finite number\")\n\t}\n\tif (rate < 0 || rate > 1) {\n\t\tthrow new TaxValidationError(\n\t\t\tfield,\n\t\t\trate,\n\t\t\t\"Statutory rate ratio must be between 0 and 1 (inclusive)\",\n\t\t)\n\t}\n}\n\n/**\n * Validates that an amount is an exact integer minor unit.\n *\n * @param minorUnits Integer minor units.\n * @param field Diagnostic field name.\n * @param options Validation options.\n * @throws {TaxValidationError} if not a safe integer.\n */\nexport function validateIntegerMinorUnits(\n\tminorUnits: number,\n\tfield: string,\n\toptions?: ValidateAmountOptions,\n): void {\n\tvalidateMonetaryAmount(minorUnits, field, options)\n\tif (!Number.isSafeInteger(minorUnits)) {\n\t\tthrow new TaxValidationError(field, minorUnits, \"Minor units amount must be a safe integer\")\n\t}\n}\n\n/**\n * Validates a statutory count (children, years of service, holding years).\n *\n * Purpose: Guards integer counts that later multiply money, rejecting fractions and unsafe values.\n * Math: Requires `Number.isSafeInteger(value)` and `value >= min`.\n * Units: Dimensionless count (not currency).\n * Throws: TaxValidationError if value is not a safe integer or is below `min`.\n *\n * @param value Count to validate.\n * @param field Diagnostic field name.\n * @param min Inclusive lower bound (defaults to 0).\n * @statute Jurisdiction-neutral input bound (AGENTS rule 8); no statutory citation.\n */\nexport function validateSafeIntegerCount(value: number, field: string, min = 0): void {\n\tif (!Number.isSafeInteger(value)) {\n\t\tthrow new TaxValidationError(field, value, \"Count must be a safe integer\")\n\t}\n\tif (value < min) {\n\t\tthrow new TaxValidationError(field, value, `Count must be at least ${min}`)\n\t}\n}\n\n/**\n * Multiplies two integers and fails loudly if the product leaves the safe integer range.\n *\n * Purpose: Prevents silent precision loss when scaling minor units by a count (years, days).\n * Math: `a * b`, requiring `Number.isSafeInteger(a * b)`.\n * Units: Typically integer minor units times a dimensionless count; result in minor units.\n * Throws: TaxValidationError (reported against `field`) if the product is not a safe integer.\n *\n * @param a Integer factor (e.g. minor units).\n * @param b Integer factor (e.g. a validated count).\n * @param field Diagnostic field name of the count or amount being scaled.\n * @statute Jurisdiction-neutral input bound (AGENTS rule 8); no statutory citation.\n */\nexport function multiplySafeInteger(a: number, b: number, field: string): number {\n\tconst product = a * b\n\tif (!Number.isSafeInteger(product)) {\n\t\tthrow new TaxValidationError(field, b, \"Scaled amount exceeds the safe integer range\")\n\t}\n\treturn product + 0\n}\n","import { CURRENCY_PRECISION, type TaxSupportedCurrency } from \"./currency\"\nimport { TaxValidationError } from \"./errors\"\nimport {\n\ttype ValidateAmountOptions,\n\tvalidateIntegerMinorUnits,\n\tvalidateMonetaryAmount,\n} from \"./validate\"\n\nfunction scaleDecimalString(abs: number, precision: number): number {\n\tconst factor = 10 ** precision\n\tif (String(abs).includes(\"e\")) {\n\t\treturn abs * factor\n\t}\n\tconst parts = String(abs).split(\".\")\n\tconst intPart = parts[0] ?? \"0\"\n\tconst decPart = (parts[1] ?? \"\").padEnd(precision, \"0\")\n\tconst whole = intPart + decPart.slice(0, precision)\n\tconst fraction = decPart.slice(precision)\n\treturn Number(whole + (fraction.length > 0 ? `.${fraction}` : \"\"))\n}\n\n/**\n * Converts a major-unit monetary amount to integer minor units (satang/cent).\n *\n * Math: Decimal string expansion and half-away-from-zero rounding to eliminate IEEE 754 float drift.\n * Units: Input is major currency unit (e.g. THB, USD); output is integer minor units (e.g. Satang, Cents).\n *\n * @param amount Amount in major units.\n * @param currency Supported statutory currency (THB or USD).\n * @param field Diagnostic field name.\n * @param options Validation options (e.g. allowNegative for refund balances).\n * @returns Integer minor units.\n * @throws {TaxValidationError} if amount is NaN, infinite, or results in a non-safe integer.\n * @example toMinorUnits(1.14, \"USD\") // 114 (114 cents)\n */\nexport function toMinorUnits(\n\tamount: number,\n\tcurrency: TaxSupportedCurrency,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateMonetaryAmount(amount, field, options)\n\tconst precision = CURRENCY_PRECISION[currency]\n\tconst sign = amount < 0 ? -1 : 1\n\tconst abs = Math.abs(amount)\n\tconst scaled = scaleDecimalString(abs, precision)\n\tconst result = sign * Math.round(scaled) + 0\n\tif (!Number.isSafeInteger(result)) {\n\t\tthrow new TaxValidationError(\n\t\t\tfield,\n\t\t\tamount,\n\t\t\t\"Conversion to minor units failed or exceeded safe integer bounds\",\n\t\t)\n\t}\n\treturn result\n}\n\n/**\n * Converts an integer minor unit amount back to major currency units.\n *\n * @param minorUnits Integer minor units.\n * @param currency Supported statutory currency (THB or USD).\n * @param field Diagnostic field name.\n * @param options Validation options (e.g. allowNegative for refund balances).\n * @returns Amount in major units.\n * @throws {TaxValidationError} if minorUnits is not a safe integer.\n * @example fromMinorUnits(1055, \"THB\") // 10.55\n */\nexport function fromMinorUnits(\n\tminorUnits: number,\n\tcurrency: TaxSupportedCurrency,\n\tfield = \"minorUnits\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateIntegerMinorUnits(minorUnits, field, options)\n\tconst precision = CURRENCY_PRECISION[currency]\n\tconst factor = 10 ** precision\n\tconst sign = minorUnits < 0 ? -1 : 1\n\tconst absUnits = Math.abs(minorUnits)\n\treturn sign * Number((absUnits / factor).toFixed(precision)) + 0\n}\n","import { TaxValidationError } from \"./errors\"\nimport { type ValidateAmountOptions, validateIntegerMinorUnits, validateRate } from \"./validate\"\n\n/**\n * Supported rounding modes for statutory tax computations.\n */\nexport type RoundingMode = \"half-up\" | \"floor\" | \"ceil\"\n\n/**\n * Rounds a number to the nearest integer, breaking ties away from zero (standard half-up).\n *\n * Math: Rounds fractional halves away from zero (e.g. 1.5 -> 2, -1.5 -> -2).\n *\n * @param value Floating-point value.\n * @returns Rounded integer.\n */\nexport function roundHalfUp(value: number): number {\n\tif (value < 0) {\n\t\tconst res = -Math.round(Math.abs(value))\n\t\treturn res + 0\n\t}\n\treturn Math.round(value) + 0\n}\n\nfunction parseRateRational(rate: number): { num: bigint; den: bigint } {\n\tconst rateStr = rate.toString().includes(\"e\")\n\t\t? rate.toFixed(10).replace(/\\.?0+$/, \"\")\n\t\t: rate.toString()\n\tconst parts = rateStr.split(\".\")\n\tconst decimals = parts[1] ? parts[1].length : 0\n\treturn {\n\t\tnum: BigInt(parts.join(\"\")),\n\t\tden: 10n ** BigInt(decimals),\n\t}\n}\n\n/**\n * Applies a statutory percentage rate to an integer minor unit amount using exact rational arithmetic.\n *\n * Math: Exact rational scaling via BigInt `(minorUnits * num + modeAdjust) / den`\n * to eliminate IEEE 754 binary floating-point drift on exact statutory tax rates.\n * Units: Input and output are integer minor units (satang/cents).\n *\n * @param minorUnits Integer minor units amount.\n * @param rate Statutory rate ratio (e.g. 0.05 for 5%, 0.35 for 35%).\n * @param rounding Rounding mode (`half-up`, `floor`, or `ceil`).\n * @param options Validation options (e.g. allowNegative).\n * @returns Computed tax in integer minor units.\n * @throws {TaxValidationError} if minorUnits is not a safe integer or rate is outside [0, 1].\n * @example applyPercentage(90, 0.35) // 32 (exact statutory satang calculation)\n */\nexport function applyPercentage(\n\tminorUnits: number,\n\trate: number,\n\trounding: RoundingMode = \"half-up\",\n\toptions?: ValidateAmountOptions,\n): number {\n\tvalidateIntegerMinorUnits(minorUnits, \"minorUnits\", options)\n\tvalidateRate(rate, \"rate\")\n\n\tconst { num, den } = parseRateRational(rate)\n\tconst m = BigInt(minorUnits)\n\tconst prod = m * num\n\n\tlet result: bigint\n\tif (rounding === \"floor\") {\n\t\tresult = prod >= 0n ? prod / den : (prod - (den - 1n)) / den\n\t} else if (rounding === \"ceil\") {\n\t\tresult = prod >= 0n ? (prod + (den - 1n)) / den : prod / den\n\t} else {\n\t\t// half-up: away from zero\n\t\tresult = prod >= 0n ? (prod * 2n + den) / (den * 2n) : (prod * 2n - den) / (den * 2n)\n\t}\n\n\treturn Number(result) + 0\n}\n\n/**\n * Applies the statutory gross-up ratio `rate / (1 - rate)` to an integer minor unit amount.\n *\n * Purpose: Recovers the underlying tax from a net amount (e.g. a dividend tax credit).\n * Math: Exact BigInt rational `(minorUnits * num) / (den - num)` where `rate = num / den`,\n * rounded half-up (away from zero) so ties such as 0.5 satang never suffer binary float drift.\n * Units: Input and output are integer minor units (satang/cents).\n *\n * @param minorUnits Non-negative integer minor units amount (the net amount).\n * @param rate Underlying rate ratio in [0, 1) (e.g. 0.2 for a 20% corporate tax rate).\n * @param field Diagnostic field name reported on failure (defaults to `\"minorUnits\"`).\n * @returns Gross-up amount (`minorUnits * rate / (1 - rate)`) in integer minor units.\n * @throws {TaxValidationError} if minorUnits is not a non-negative safe integer, rate is outside\n * [0, 1), or the result is not a safe integer.\n * @statute Jurisdiction-neutral; Thailand Revenue Code Section 47 bis is the first consumer.\n * @example applyGrossUp(100_000, 0.2) // 25_000 (1,000.00 -> 250.00 credit at 20%)\n */\nexport function applyGrossUp(minorUnits: number, rate: number, field = \"minorUnits\"): number {\n\tvalidateIntegerMinorUnits(minorUnits, field)\n\tvalidateRate(rate, \"rate\")\n\tif (rate >= 1) {\n\t\tthrow new TaxValidationError(\"rate\", rate, \"Gross-up rate must be strictly less than 1\")\n\t}\n\tconst { num, den } = parseRateRational(rate)\n\tconst divisor = den - num\n\tconst result = Number((BigInt(minorUnits) * num * 2n + divisor) / (divisor * 2n)) + 0\n\tif (!Number.isSafeInteger(result)) {\n\t\tthrow new TaxValidationError(\n\t\t\tfield,\n\t\t\tminorUnits,\n\t\t\t\"Gross-up result exceeds the safe integer range\",\n\t\t)\n\t}\n\treturn result\n}\n","import type { TaxSupportedCurrency } from \"./currency\"\nimport { type ValidateAmountOptions, validateMonetaryAmount } from \"./validate\"\n\n/**\n * Branded nominal type representing a monetary value in a specific currency.\n * Enforces compile-time type safety preventing accidental cross-currency mixing.\n */\nexport type Money<C extends TaxSupportedCurrency = TaxSupportedCurrency> = number & {\n\treadonly __currencyBrand: C\n}\n\n/**\n * Constructs a branded Money value with immediate input boundary validation.\n *\n * @param amount Numerical amount in major units.\n * @param _currency Currency code tag.\n * @param field Diagnostic field name.\n * @param options Validation options (e.g. allowNegative for refund balances).\n * @returns Branded Money value.\n * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.\n */\nexport function money<C extends TaxSupportedCurrency>(\n\tamount: number,\n\t_currency?: C,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): Money<C> {\n\tvalidateMonetaryAmount(amount, field, options)\n\treturn amount as Money<C>\n}\n\n/**\n * Constructs a branded Thai Baht (THB) money value with input boundary validation.\n *\n * @param amount Amount in Baht.\n * @param field Diagnostic field name.\n * @param options Validation options (e.g. allowNegative for refund balances).\n * @returns Branded Money<\"THB\"> value.\n * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.\n */\nexport function thb(\n\tamount: number,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): Money<\"THB\"> {\n\tvalidateMonetaryAmount(amount, field, options)\n\treturn amount as Money<\"THB\">\n}\n\n/**\n * Constructs a branded US Dollar (USD) money value with input boundary validation.\n *\n * @param amount Amount in Dollars.\n * @param field Diagnostic field name.\n * @param options Validation options (e.g. allowNegative for refund balances).\n * @returns Branded Money<\"USD\"> value.\n * @throws {TaxValidationError} if amount is NaN, infinite, or negative without allowNegative.\n */\nexport function usd(\n\tamount: number,\n\tfield = \"amount\",\n\toptions?: ValidateAmountOptions,\n): Money<\"USD\"> {\n\tvalidateMonetaryAmount(amount, field, options)\n\treturn amount as Money<\"USD\">\n}\n","import type { TaxBracket } from \"./types\"\n\n/**\n * Determines the marginal tax rate that applies to the next unit of ordinary income.\n *\n * @param brackets Ordered progressive tax brackets.\n * @param nextUnit Amount of income on the boundary transition (taxableAmount + 1).\n * @returns Marginal statutory rate ratio.\n */\nexport function marginalRateAt(brackets: readonly TaxBracket[], nextUnit: number): number {\n\tlet lower = 0\n\tfor (const bracket of brackets) {\n\t\tconst upper = bracket.upTo\n\t\tif (nextUnit > lower && (upper === null || !Number.isFinite(upper) || nextUnit <= upper)) {\n\t\t\treturn bracket.rate\n\t\t}\n\t\tif (upper === null || !Number.isFinite(upper)) {\n\t\t\tbreak\n\t\t}\n\t\tlower = upper\n\t}\n\treturn brackets[0]?.rate ?? 0\n}\n","import { applyPercentage } from \"../money\"\nimport type { BracketSlice, TaxBracket } from \"./types\"\n\n/**\n * Computes progressive tax and slice boundaries for a single bracket tier.\n *\n * @param lowerBound Lower threshold in minor units.\n * @param bracket Current bracket specification.\n * @param taxableAmount Total taxable income in minor units.\n * @param index Bracket index.\n * @returns Computed BracketSlice.\n */\nexport function computeBracketSlice(\n\tlowerBound: number,\n\tbracket: TaxBracket,\n\ttaxableAmount: number,\n\tindex: number,\n): BracketSlice {\n\tconst upperBound = bracket.upTo\n\tlet sliceTaxable = 0\n\n\tif (taxableAmount > lowerBound) {\n\t\tif (upperBound === null || !Number.isFinite(upperBound)) {\n\t\t\tsliceTaxable = taxableAmount - lowerBound\n\t\t} else {\n\t\t\tsliceTaxable = Math.min(taxableAmount, upperBound) - lowerBound\n\t\t}\n\t}\n\n\tconst sliceTax = applyPercentage(sliceTaxable, bracket.rate)\n\n\treturn {\n\t\tbracketIndex: index,\n\t\tlowerBound,\n\t\tupperBound,\n\t\trate: bracket.rate,\n\t\ttaxableAmount: sliceTaxable,\n\t\ttax: sliceTax,\n\t}\n}\n","import { TaxValidationError, validateMonetaryAmount, validateRate } from \"../money\"\nimport { marginalRateAt } from \"./marginal\"\nimport { computeBracketSlice } from \"./slice\"\nimport type { BracketEvaluationResult, BracketSlice, TaxBracket } from \"./types\"\n\nfunction validateSingleBracket(\n\tbracket: TaxBracket,\n\tindex: number,\n\tisLast: boolean,\n\tprevBound: number,\n): number {\n\tvalidateRate(bracket.rate, `bracket[${index}].rate`)\n\tif (bracket.upTo === null) {\n\t\tif (!isLast) {\n\t\t\tthrow new TaxValidationError(\n\t\t\t\t`bracket[${index}].upTo`,\n\t\t\t\tbracket.upTo,\n\t\t\t\t\"Only the final bracket tier may have upTo: null (unbounded)\",\n\t\t\t)\n\t\t}\n\t\treturn prevBound\n\t}\n\tvalidateMonetaryAmount(bracket.upTo, `bracket[${index}].upTo`)\n\tif (!Number.isSafeInteger(bracket.upTo) || bracket.upTo <= prevBound) {\n\t\tthrow new TaxValidationError(\n\t\t\t`bracket[${index}].upTo`,\n\t\t\tbracket.upTo,\n\t\t\t`Bracket bound must be a safe integer greater than ${prevBound}`,\n\t\t)\n\t}\n\treturn bracket.upTo\n}\n\nfunction validateBrackets(brackets: readonly TaxBracket[]): void {\n\tif (brackets.length === 0) {\n\t\tthrow new TaxValidationError(\"brackets\", brackets, \"Bracket list cannot be empty\")\n\t}\n\tlet prevBound = 0\n\tfor (let i = 0; i < brackets.length; i++) {\n\t\tconst bracket = brackets[i]\n\t\tif (!bracket) continue\n\t\tprevBound = validateSingleBracket(bracket, i, i === brackets.length - 1, prevBound)\n\t}\n\tconst last = brackets[brackets.length - 1]\n\tif (last && last.upTo !== null) {\n\t\tthrow new TaxValidationError(\"brackets\", brackets, \"Final bracket tier must have upTo: null\")\n\t}\n}\n\n/**\n * Evaluates progressive tax brackets slice-by-slice across multiple tiers.\n *\n * Math: Slices taxable income across progressive tiers and sums integer tax per slice.\n * Units: All inputs and outputs must be integer minor units (satang/cents).\n *\n * @param taxableAmount Taxable income in integer minor units.\n * @param brackets Ordered progressive statutory tax brackets.\n * @param grossIncome Optional gross income in minor units for statutory effective rate calculation.\n * @param field Diagnostic field name.\n * @returns Evaluated bracket result including total tax, slices, effective rate, and marginal rate.\n * @throws {TaxValidationError} if inputs are invalid.\n */\nexport function evaluateBrackets(\n\ttaxableAmount: number,\n\tbrackets: readonly TaxBracket[],\n\tgrossIncome?: number,\n\tfield = \"taxableAmount\",\n): BracketEvaluationResult {\n\tvalidateMonetaryAmount(taxableAmount, field)\n\tif (grossIncome !== undefined) {\n\t\tvalidateMonetaryAmount(grossIncome, \"grossIncome\")\n\t}\n\tvalidateBrackets(brackets)\n\n\tlet totalTax = 0\n\tlet lowerBound = 0\n\tconst slices: BracketSlice[] = []\n\n\tfor (let i = 0; i < brackets.length; i++) {\n\t\tconst bracket = brackets[i]\n\t\tif (!bracket) continue\n\n\t\tconst slice = computeBracketSlice(lowerBound, bracket, taxableAmount, i)\n\t\tslices.push(slice)\n\t\ttotalTax += slice.tax\n\n\t\tif (bracket.upTo === null || !Number.isFinite(bracket.upTo)) {\n\t\t\tbreak\n\t\t}\n\t\tlowerBound = bracket.upTo\n\t}\n\n\tconst baseForEffective = grossIncome ?? taxableAmount\n\tconst effectiveRate = baseForEffective > 0 ? totalTax / baseForEffective : 0\n\tconst marginalRate = marginalRateAt(brackets, taxableAmount + 1)\n\n\treturn {\n\t\ttotalTax,\n\t\tslices,\n\t\teffectiveRate,\n\t\tmarginalRate,\n\t}\n}\n","import {\n\tapplyPercentage,\n\tfromMinorUnits,\n\troundHalfUp,\n\ttoMinorUnits,\n\tvalidateMonetaryAmount,\n\tvalidateRate,\n} from \"../money\"\nimport type { StatutoryCapOptions } from \"./types\"\n\n/**\n * Resolves a statutory cap limit from a formula configuration (percentage, ceiling, or min of both).\n *\n * @param options Statutory cap options specifying base, rate, and/or ceiling.\n * @param defaultField Default diagnostic field name.\n * @returns Resolved cap numerical ceiling.\n * @throws {TaxValidationError} if inputs are invalid.\n */\nexport function resolveCap(options: StatutoryCapOptions, defaultField = \"cap\"): number {\n\tconst fieldName = options.field ?? defaultField\n\tconst { base, rate, ceiling, currency } = options\n\n\tlet computedCap = Number.POSITIVE_INFINITY\n\n\tif (base !== undefined && rate !== undefined) {\n\t\tvalidateMonetaryAmount(base, `${fieldName}Base`)\n\t\tvalidateRate(rate, `${fieldName}Rate`)\n\t\tif (currency) {\n\t\t\tconst minorBase = toMinorUnits(base, currency, `${fieldName}Base`)\n\t\t\tconst minorCap = applyPercentage(minorBase, rate)\n\t\t\tcomputedCap = fromMinorUnits(minorCap, currency)\n\t\t} else if (Number.isSafeInteger(base)) {\n\t\t\tcomputedCap = applyPercentage(base, rate)\n\t\t} else {\n\t\t\tcomputedCap = roundHalfUp(base * rate)\n\t\t}\n\t}\n\n\tif (ceiling !== undefined) {\n\t\tvalidateMonetaryAmount(ceiling, `${fieldName}Ceiling`)\n\t\tcomputedCap = Math.min(computedCap, ceiling)\n\t}\n\n\tvalidateMonetaryAmount(computedCap, `${fieldName}Cap`)\n\treturn computedCap\n}\n","import { validateMonetaryAmount } from \"../money\"\nimport { resolveCap } from \"./resolve\"\nimport type { ClampedAmount, StatutoryCapOptions } from \"./types\"\n\n/**\n * Clamps a requested deduction, allowance, or expense against a statutory cap.\n *\n * @param requested Original requested amount.\n * @param capOrOptions Numeric cap or StatutoryCapOptions specification.\n * @param field Diagnostic field name.\n * @returns Breakdown-ready ClampedAmount structure.\n * @throws {TaxValidationError} if inputs are negative, NaN, or non-finite.\n * @example clampCap(150_000, 100_000, \"allowance\") // { requested: 150000, allowed: 100000, cap: 100000, wasClamped: true }\n */\nexport function clampCap(\n\trequested: number,\n\tcapOrOptions: number | StatutoryCapOptions,\n\tfield = \"amount\",\n): ClampedAmount {\n\tvalidateMonetaryAmount(requested, field)\n\n\tconst cap = typeof capOrOptions === \"number\" ? capOrOptions : resolveCap(capOrOptions, field)\n\n\tvalidateMonetaryAmount(cap, `${field}Cap`)\n\n\tconst wasClamped = requested > cap\n\tconst allowed = wasClamped ? cap : requested\n\n\treturn {\n\t\trequested,\n\t\tallowed,\n\t\tcap,\n\t\twasClamped,\n\t}\n}\n"],"mappings":"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"}