@trajectonomics/tax 0.2.0-alpha.0 → 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/{clamp-Bse4u0H-.mjs → clamp-CdcDymBJ.mjs} +52 -13
- package/dist/clamp-CdcDymBJ.mjs.map +1 -0
- package/dist/{clamp-BKG7kjVf.cjs → clamp-DWYNU1V8.cjs} +57 -12
- package/dist/clamp-DWYNU1V8.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/th/index.cjs +1069 -1
- package/dist/th/index.cjs.map +1 -1
- package/dist/th/index.d.cts +415 -1
- package/dist/th/index.d.mts +415 -1
- package/dist/th/index.mjs +1062 -2
- package/dist/th/index.mjs.map +1 -1
- package/dist/us/index.cjs +1 -1
- package/dist/us/index.mjs +1 -1
- package/package.json +2 -1
- package/dist/clamp-BKG7kjVf.cjs.map +0 -1
- package/dist/clamp-Bse4u0H-.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -12,6 +12,67 @@ Pure, deterministic statutory tax calculation engine with subpaths for Thailand
|
|
|
12
12
|
- State inputs also include `stateDependents`, `stateWithholding`, and `stateCredits` (`nonRefundable` / `refundable`).
|
|
13
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
14
|
|
|
15
|
+
## Thailand Levy for sim-core
|
|
16
|
+
|
|
17
|
+
`@trajectonomics/tax/th` exports `emitThLevy(profile)`, which freezes one TH 2026 taxpayer profile into a [sim-core](https://github.com/trajectonomics/sim-core) `Levy`. The emitted graph uses only `const`, `tally`, `add`, `sub`, `min`, `max`, `scale`, and `pick`, so sim-core evaluates it in exact integer satang without importing this package. The dependency points this way only: the package takes a dev-dependency on `@trajectonomics/sim` for its golden corpus. That dependency pins `0.2.0-alpha.4`, the first published version that exports `evaluateMeasures` (the stable `0.1.0` predates it); nothing in the built package imports it.
|
|
18
|
+
|
|
19
|
+
The profile fixes the filing status, the fixed allowances (personal, spouse, children, parents, disabled care, social security, life/health insurance, parent health insurance, spouse life insurance, mortgage interest, and donation requests), the standard-versus-actual expense elections, the Section 40(4) final-withholding elections with the Section 47 bis corporate rate, and the Levy identity (`id` defaults to `th-2026-pit`, period to 1 January, settlement to 31 March). Income, expense, and contribution amounts are not in the profile: they arrive as Flow tallies under the labels below.
|
|
20
|
+
|
|
21
|
+
`Levy.due` is the audited `tax.net_payable` Measure: regular tax (the larger of the progressive brackets and the Section 48(2) minimum gross tax) plus final withholding tax, less the final withholding and Section 47 bis credits. It equals `calculateThTax(...).netTaxPayable` when the input carries no Section 48(5) separate sheet, no Section 48(4) real estate transfer, and no other withholding. Payroll withholding is out of scope: the caller puts `prepaymentBps` on its Flows and the Settlement corrects it.
|
|
22
|
+
|
|
23
|
+
Audited Measure ids are exported as `ThLevyMeasureId` (`assessable.total`, `expenses.total`, `allowances.total`, `taxable.income`, `tax.progressive`, `tax.minimum`, `tax.regular`, `tax.final_withholding`, `credit.dividend`, `tax.total`, `tax.net_payable`).
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { emitThLevy, ThLevyIncomeLabel } from "@trajectonomics/tax/th"
|
|
27
|
+
|
|
28
|
+
const levy = emitThLevy({ filingStatus: "single", allowances: { children: { totalCount: 1 } } })
|
|
29
|
+
// Label income Flows with ThLevyIncomeLabel values and contribution Flows with
|
|
30
|
+
// ThLevyContributionLabel values; sim-core tallies them and settles `levy.due`.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Income tally labels (`gross` basis)
|
|
34
|
+
|
|
35
|
+
| Label | Income class |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `income.section40_1` | Section 40(1) employment |
|
|
38
|
+
| `income.section40_2` | Section 40(2) service and hire of work |
|
|
39
|
+
| `income.section40_3` | Section 40(3) royalties, goodwill, copyright |
|
|
40
|
+
| `income.section40_4` | Section 40(4) income with no source election |
|
|
41
|
+
| `income.section40_4.interest` | Section 40(4) interest |
|
|
42
|
+
| `income.section40_4.dividends` | Section 40(4) dividends |
|
|
43
|
+
| `income.section40_4.digitalAssets` | Section 40(4) digital asset income |
|
|
44
|
+
| `income.section40_5.building` | Section 40(5) building rental |
|
|
45
|
+
| `income.section40_5.agriculturalLand` | Section 40(5) agricultural land rental |
|
|
46
|
+
| `income.section40_5.nonAgriculturalLand` | Section 40(5) non-agricultural land rental |
|
|
47
|
+
| `income.section40_5.vehicles` | Section 40(5) vehicle rental |
|
|
48
|
+
| `income.section40_5.other` | Section 40(5) other rental |
|
|
49
|
+
| `income.section40_6.medical` | Section 40(6) medical profession |
|
|
50
|
+
| `income.section40_6.law` | Section 40(6) law profession |
|
|
51
|
+
| `income.section40_6.engineering` | Section 40(6) engineering profession |
|
|
52
|
+
| `income.section40_6.architecture` | Section 40(6) architecture profession |
|
|
53
|
+
| `income.section40_6.accounting` | Section 40(6) accounting profession |
|
|
54
|
+
| `income.section40_6.fineArts` | Section 40(6) fine arts profession |
|
|
55
|
+
| `income.section40_6.other` | Section 40(6) other liberal profession |
|
|
56
|
+
| `income.section40_7` | Section 40(7) contracting with materials |
|
|
57
|
+
| `income.section40_8.general` | Section 40(8) general business and commerce |
|
|
58
|
+
| `income.section40_8.entertainer` | Section 40(8) entertainer (tiered expense) |
|
|
59
|
+
|
|
60
|
+
Section 40(4) is split into four labels because each source carries its own final-withholding election; every other class is one label per Section 40 input field, so no income is pooled across different statutory expense rates.
|
|
61
|
+
|
|
62
|
+
### Contribution tally labels (`funded` basis)
|
|
63
|
+
|
|
64
|
+
| Label | Allowance |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `allowance.retirement.rmf` | RMF contribution |
|
|
67
|
+
| `allowance.retirement.ssf` | SSF contribution |
|
|
68
|
+
| `allowance.retirement.pvd` | PVD contribution |
|
|
69
|
+
| `allowance.retirement.gpf` | GPF contribution |
|
|
70
|
+
| `allowance.retirement.nsf` | National Savings Fund contribution |
|
|
71
|
+
| `allowance.retirement.pension_life` | Pension life insurance premium |
|
|
72
|
+
| `allowance.thai_esg` | ThaiESG contribution |
|
|
73
|
+
|
|
74
|
+
The golden corpus in `src/th/tests/levy-golden.test.ts` generates over 3,000 deterministic cases — every bracket boundary, every expense and allowance ceiling on both sides, the minimum gross tax thresholds, and half-satang ties in the brackets, the minimum gross tax, and the Section 47 bis credit — and asserts that eleven audited Measures match `calculateThTax` to the satang for each one.
|
|
75
|
+
|
|
15
76
|
## Commands
|
|
16
77
|
|
|
17
78
|
- `bun run check`: Complete verification suite (`lint`, `typecheck`, `test` with 100% coverage, `build`, `lint:package`).
|
|
@@ -150,6 +150,54 @@ function fromMinorUnits(minorUnits, currency, field = "minorUnits", options) {
|
|
|
150
150
|
const factor = 10 ** precision;
|
|
151
151
|
return (minorUnits < 0 ? -1 : 1) * Number((Math.abs(minorUnits) / factor).toFixed(precision)) + 0;
|
|
152
152
|
}
|
|
153
|
+
// src/core/money/ratio.ts
|
|
154
|
+
/** Diagnostic field name for a statutory rate ratio, shared by every rate consumer. */
|
|
155
|
+
const RATE_FIELD = "rate";
|
|
156
|
+
/** Largest integer that a JavaScript number represents exactly. */
|
|
157
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
158
|
+
/**
|
|
159
|
+
* Converts a statutory rate ratio into the exact BigInt fraction it was written as.
|
|
160
|
+
*
|
|
161
|
+
* Purpose: Single exact parser for both the BigInt arithmetic and the emitted `scale` fraction.
|
|
162
|
+
* Math: Reads the decimal string (expanding exponent notation), so `0.35` becomes `35/100`;
|
|
163
|
+
* the fraction is not reduced, so `den` always matches the written precision.
|
|
164
|
+
* Units: Dimensionless ratio in, integer numerator/denominator out.
|
|
165
|
+
* Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].
|
|
166
|
+
*
|
|
167
|
+
* @param rate Statutory rate ratio (e.g. `0.05` for 5%).
|
|
168
|
+
* @param field Diagnostic field name reported on failure.
|
|
169
|
+
* @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).
|
|
170
|
+
*/
|
|
171
|
+
function rateBigFraction(rate, field = RATE_FIELD) {
|
|
172
|
+
validateRate(rate, field);
|
|
173
|
+
const parts = (rate.toString().includes("e") ? rate.toFixed(10).replace(/\.?0+$/, "") : rate.toString()).split(".");
|
|
174
|
+
const decimals = parts[1] ? parts[1].length : 0;
|
|
175
|
+
return {
|
|
176
|
+
num: BigInt(parts.join("")),
|
|
177
|
+
den: 10n ** BigInt(decimals)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Converts a statutory rate ratio into a safe-integer fraction for a `scale` Measure.
|
|
182
|
+
*
|
|
183
|
+
* Purpose: Gives the emitter the exact numerator and denominator sim-core's `scale` accepts.
|
|
184
|
+
* Math: Same decimal expansion as {@link rateBigFraction}, then a safe-integer bound check.
|
|
185
|
+
* Units: Dimensionless ratio in, integer numerator/denominator out.
|
|
186
|
+
* Throws: TaxValidationError if the rate is invalid or its decimal expansion leaves the safe
|
|
187
|
+
* integer range (more than roughly 15 decimal digits).
|
|
188
|
+
*
|
|
189
|
+
* @param rate Statutory rate ratio (e.g. `0.05` for 5%).
|
|
190
|
+
* @param field Diagnostic field name reported on failure.
|
|
191
|
+
* @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).
|
|
192
|
+
*/
|
|
193
|
+
function rateFraction(rate, field = RATE_FIELD) {
|
|
194
|
+
const { num, den } = rateBigFraction(rate, field);
|
|
195
|
+
if (num > MAX_SAFE || den > MAX_SAFE) throw new TaxValidationError(field, rate, "Rate precision exceeds the safe integer range");
|
|
196
|
+
return {
|
|
197
|
+
num: Number(num),
|
|
198
|
+
den: Number(den)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
153
201
|
// src/core/money/rounding.ts
|
|
154
202
|
/**
|
|
155
203
|
* Rounds a number to the nearest integer, breaking ties away from zero (standard half-up).
|
|
@@ -163,14 +211,6 @@ function roundHalfUp(value) {
|
|
|
163
211
|
if (value < 0) return -Math.round(Math.abs(value)) + 0;
|
|
164
212
|
return Math.round(value) + 0;
|
|
165
213
|
}
|
|
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
214
|
/**
|
|
175
215
|
* Applies a statutory percentage rate to an integer minor unit amount using exact rational arithmetic.
|
|
176
216
|
*
|
|
@@ -188,8 +228,7 @@ function parseRateRational(rate) {
|
|
|
188
228
|
*/
|
|
189
229
|
function applyPercentage(minorUnits, rate, rounding = "half-up", options) {
|
|
190
230
|
validateIntegerMinorUnits(minorUnits, "minorUnits", options);
|
|
191
|
-
|
|
192
|
-
const { num, den } = parseRateRational(rate);
|
|
231
|
+
const { num, den } = rateBigFraction(rate);
|
|
193
232
|
const prod = BigInt(minorUnits) * num;
|
|
194
233
|
let result;
|
|
195
234
|
if (rounding === "floor") result = prod >= 0n ? prod / den : (prod - (den - 1n)) / den;
|
|
@@ -218,7 +257,7 @@ function applyGrossUp(minorUnits, rate, field = "minorUnits") {
|
|
|
218
257
|
validateIntegerMinorUnits(minorUnits, field);
|
|
219
258
|
validateRate(rate, "rate");
|
|
220
259
|
if (rate >= 1) throw new TaxValidationError("rate", rate, "Gross-up rate must be strictly less than 1");
|
|
221
|
-
const { num, den } =
|
|
260
|
+
const { num, den } = rateBigFraction(rate);
|
|
222
261
|
const divisor = den - num;
|
|
223
262
|
const result = Number((BigInt(minorUnits) * num * 2n + divisor) / (divisor * 2n)) + 0;
|
|
224
263
|
if (!Number.isSafeInteger(result)) throw new TaxValidationError(field, minorUnits, "Gross-up result exceeds the safe integer range");
|
|
@@ -421,6 +460,6 @@ function clampCap(requested, capOrOptions, field = "amount") {
|
|
|
421
460
|
wasClamped
|
|
422
461
|
};
|
|
423
462
|
}
|
|
424
|
-
export { usd as a, roundHalfUp as c,
|
|
463
|
+
export { usd as a, roundHalfUp as c, toMinorUnits as d, multiplySafeInteger as f, TaxValidationError as h, thb as i, rateFraction as l, validateSafeIntegerCount as m, evaluateBrackets as n, applyGrossUp as o, validateMonetaryAmount as p, money as r, applyPercentage as s, clampCap as t, fromMinorUnits as u };
|
|
425
464
|
|
|
426
|
-
//# sourceMappingURL=clamp-
|
|
465
|
+
//# sourceMappingURL=clamp-CdcDymBJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clamp-CdcDymBJ.mjs","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/ratio.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 { validateRate } from \"./validate\"\n\n/** Diagnostic field name for a statutory rate ratio, shared by every rate consumer. */\nexport const RATE_FIELD = \"rate\"\n\n/** Largest integer that a JavaScript number represents exactly. */\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER)\n\n/**\n * Exact non-negative integer fraction `num / den` with `den` at least 1.\n *\n * Purpose: Carries a statutory rate as integers into exact rational arithmetic.\n * Math: `num / den` equals the rate; the fraction is not reduced.\n * Units: Dimensionless rate ratio (0.35 is 35/100); no currency.\n * Throws: Nothing; construction happens in {@link rateFraction}.\n * @statute Jurisdiction-neutral arithmetic type; no statutory citation.\n */\nexport interface RateFraction {\n\t/** Numerator, equal to the rate's decimal digits without the point. */\n\treadonly num: number\n\t/** Denominator, `10` to the power of the rate's decimal places. */\n\treadonly den: number\n}\n\n/**\n * Exact decimal expansion of a rate as a BigInt fraction.\n *\n * Purpose: Keeps percentage arithmetic exact for any rate, however many decimal places it has.\n * Math: `num / den` equals the rate; the fraction is not reduced.\n * Units: Dimensionless rate ratio; no currency.\n * Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport interface RateBigFraction {\n\t/** Numerator, equal to the rate's decimal digits without the point. */\n\treadonly num: bigint\n\t/** Denominator, `10n` to the power of the rate's decimal places. */\n\treadonly den: bigint\n}\n\n/**\n * Converts a statutory rate ratio into the exact BigInt fraction it was written as.\n *\n * Purpose: Single exact parser for both the BigInt arithmetic and the emitted `scale` fraction.\n * Math: Reads the decimal string (expanding exponent notation), so `0.35` becomes `35/100`;\n * the fraction is not reduced, so `den` always matches the written precision.\n * Units: Dimensionless ratio in, integer numerator/denominator out.\n * Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].\n *\n * @param rate Statutory rate ratio (e.g. `0.05` for 5%).\n * @param field Diagnostic field name reported on failure.\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport function rateBigFraction(rate: number, field: string = RATE_FIELD): RateBigFraction {\n\tvalidateRate(rate, field)\n\tconst text = rate.toString().includes(\"e\")\n\t\t? rate.toFixed(10).replace(/\\.?0+$/, \"\")\n\t\t: rate.toString()\n\tconst parts = text.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 * Converts a statutory rate ratio into a safe-integer fraction for a `scale` Measure.\n *\n * Purpose: Gives the emitter the exact numerator and denominator sim-core's `scale` accepts.\n * Math: Same decimal expansion as {@link rateBigFraction}, then a safe-integer bound check.\n * Units: Dimensionless ratio in, integer numerator/denominator out.\n * Throws: TaxValidationError if the rate is invalid or its decimal expansion leaves the safe\n * integer range (more than roughly 15 decimal digits).\n *\n * @param rate Statutory rate ratio (e.g. `0.05` for 5%).\n * @param field Diagnostic field name reported on failure.\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport function rateFraction(rate: number, field: string = RATE_FIELD): RateFraction {\n\tconst { num, den } = rateBigFraction(rate, field)\n\tif (num > MAX_SAFE || den > MAX_SAFE) {\n\t\tthrow new TaxValidationError(field, rate, \"Rate precision exceeds the safe integer range\")\n\t}\n\treturn { num: Number(num), den: Number(den) }\n}\n","import { TaxValidationError } from \"./errors\"\nimport { rateBigFraction } from \"./ratio\"\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\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\tconst { num, den } = rateBigFraction(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 } = rateBigFraction(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":";;;;;AAIA,IAAa,qBAAb,MAAa,2BAA2B,MAAM;CAC7C;CACA;CACA;CAEA,YAAY,OAAe,OAAgB,QAAgB;EAC1D,MAAM,kCAAkC,MAAM,KAAK,OAAO,cAAc,OAAO,KAAK,EAAE,EAAE;EACxF,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,QAAQ;EACb,KAAK,SAAS;EACd,OAAO,eAAe,MAAM,mBAAmB,SAAS;CACzD;AACD;;;;;;;;ACDA,MAAa,qBAAqE;CACjF,KAAK;CACL,KAAK;AACN;;;;;;;;;;ACDA,SAAgB,uBACf,OACA,OACA,SACO;CACP,IAAI,OAAO,MAAM,KAAK,GACrB,MAAM,IAAI,mBAAmB,OAAO,OAAO,qBAAqB;CAEjE,IAAI,CAAC,OAAO,SAAS,KAAK,GACzB,MAAM,IAAI,mBAAmB,OAAO,OAAO,+BAA+B;CAE3E,IAAI,CAAC,SAAS,iBAAiB,QAAQ,GACtC,MAAM,IAAI,mBAAmB,OAAO,OAAO,0BAA0B;AAEvE;;;;;;;;AASA,SAAgB,aAAa,MAAc,OAAqB;CAC/D,IAAI,OAAO,MAAM,IAAI,KAAK,CAAC,OAAO,SAAS,IAAI,GAC9C,MAAM,IAAI,mBAAmB,OAAO,MAAM,8BAA8B;CAEzE,IAAI,OAAO,KAAK,OAAO,GACtB,MAAM,IAAI,mBACT,OACA,MACA,0DACD;AAEF;;;;;;;;;AAUA,SAAgB,0BACf,YACA,OACA,SACO;CACP,uBAAuB,YAAY,OAAO,OAAO;CACjD,IAAI,CAAC,OAAO,cAAc,UAAU,GACnC,MAAM,IAAI,mBAAmB,OAAO,YAAY,2CAA2C;AAE7F;;;;;;;;;;;;;;AAeA,SAAgB,yBAAyB,OAAe,OAAe,MAAM,GAAS;CACrF,IAAI,CAAC,OAAO,cAAc,KAAK,GAC9B,MAAM,IAAI,mBAAmB,OAAO,OAAO,8BAA8B;CAE1E,IAAI,QAAQ,KACX,MAAM,IAAI,mBAAmB,OAAO,OAAO,0BAA0B,KAAK;AAE5E;;;;;;;;;;;;;;AAeA,SAAgB,oBAAoB,GAAW,GAAW,OAAuB;CAChF,MAAM,UAAU,IAAI;CACpB,IAAI,CAAC,OAAO,cAAc,OAAO,GAChC,MAAM,IAAI,mBAAmB,OAAO,GAAG,8CAA8C;CAEtF,OAAO,UAAU;AAClB;;AC1GA,SAAS,mBAAmB,KAAa,WAA2B;CACnE,MAAM,SAAS,MAAM;CACrB,IAAI,OAAO,GAAG,CAAC,CAAC,SAAS,GAAG,GAC3B,OAAO,MAAM;CAEd,MAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG;CACnC,MAAM,UAAU,MAAM,MAAM;CAC5B,MAAM,WAAW,MAAM,MAAM,GAAA,CAAI,OAAO,WAAW,GAAG;CACtD,MAAM,QAAQ,UAAU,QAAQ,MAAM,GAAG,SAAS;CAClD,MAAM,WAAW,QAAQ,MAAM,SAAS;CACxC,OAAO,OAAO,SAAS,SAAS,SAAS,IAAI,IAAI,aAAa,GAAG;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,aACf,QACA,UACA,QAAQ,UACR,SACS;CACT,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,MAAM,YAAY,mBAAmB;CACrC,MAAM,OAAO,SAAS,IAAI,KAAK;CAE/B,MAAM,SAAS,mBADH,KAAK,IAAI,MACe,GAAG,SAAS;CAChD,MAAM,SAAS,OAAO,KAAK,MAAM,MAAM,IAAI;CAC3C,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,mBACT,OACA,QACA,kEACD;CAED,OAAO;AACR;;;;;;;;;;;;AAaA,SAAgB,eACf,YACA,UACA,QAAQ,cACR,SACS;CACT,0BAA0B,YAAY,OAAO,OAAO;CACpD,MAAM,YAAY,mBAAmB;CACrC,MAAM,SAAS,MAAM;CAGrB,QAFa,aAAa,IAAI,KAAK,KAErB,QADG,KAAK,IAAI,UACG,IAAI,OAAA,CAAQ,QAAQ,SAAS,CAAC,IAAI;AAChE;;;AC5EA,MAAa,aAAa;;AAG1B,MAAM,WAAW,OAAO,OAAO,gBAAgB;;;;;;;;;;;;;;AA+C/C,SAAgB,gBAAgB,MAAc,QAAgB,YAA6B;CAC1F,aAAa,MAAM,KAAK;CAIxB,MAAM,SAHO,KAAK,SAAS,CAAC,CAAC,SAAS,GAAG,IACtC,KAAK,QAAQ,EAAE,CAAC,CAAC,QAAQ,UAAU,EAAE,IACrC,KAAK,SAAS,EAAA,CACE,MAAM,GAAG;CAC5B,MAAM,WAAW,MAAM,KAAK,MAAM,EAAE,CAAC,SAAS;CAC9C,OAAO;EACN,KAAK,OAAO,MAAM,KAAK,EAAE,CAAC;EAC1B,KAAK,OAAO,OAAO,QAAQ;CAC5B;AACD;;;;;;;;;;;;;;AAeA,SAAgB,aAAa,MAAc,QAAgB,YAA0B;CACpF,MAAM,EAAE,KAAK,QAAQ,gBAAgB,MAAM,KAAK;CAChD,IAAI,MAAM,YAAY,MAAM,UAC3B,MAAM,IAAI,mBAAmB,OAAO,MAAM,+CAA+C;CAE1F,OAAO;EAAE,KAAK,OAAO,GAAG;EAAG,KAAK,OAAO,GAAG;CAAE;AAC7C;;;;;;;;;;ACrEA,SAAgB,YAAY,OAAuB;CAClD,IAAI,QAAQ,GAEX,OAAO,CADM,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAC1B;CAEd,OAAO,KAAK,MAAM,KAAK,IAAI;AAC5B;;;;;;;;;;;;;;;;AAiBA,SAAgB,gBACf,YACA,MACA,WAAyB,WACzB,SACS;CACT,0BAA0B,YAAY,cAAc,OAAO;CAC3D,MAAM,EAAE,KAAK,QAAQ,gBAAgB,IAAI;CAEzC,MAAM,OADI,OAAO,UACJ,IAAI;CAEjB,IAAI;CACJ,IAAI,aAAa,SAChB,SAAS,QAAQ,KAAK,OAAO,OAAO,QAAQ,MAAM,OAAO;MACnD,IAAI,aAAa,QACvB,SAAS,QAAQ,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO;MAGzD,SAAS,QAAQ,MAAM,OAAO,KAAK,QAAQ,MAAM,OAAO,OAAO,KAAK,QAAQ,MAAM;CAGnF,OAAO,OAAO,MAAM,IAAI;AACzB;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,aAAa,YAAoB,MAAc,QAAQ,cAAsB;CAC5F,0BAA0B,YAAY,KAAK;CAC3C,aAAa,MAAM,MAAM;CACzB,IAAI,QAAQ,GACX,MAAM,IAAI,mBAAmB,QAAQ,MAAM,4CAA4C;CAExF,MAAM,EAAE,KAAK,QAAQ,gBAAgB,IAAI;CACzC,MAAM,UAAU,MAAM;CACtB,MAAM,SAAS,QAAQ,OAAO,UAAU,IAAI,MAAM,KAAK,YAAY,UAAU,GAAG,IAAI;CACpF,IAAI,CAAC,OAAO,cAAc,MAAM,GAC/B,MAAM,IAAI,mBACT,OACA,YACA,gDACD;CAED,OAAO;AACR;;;;;;;;;;;;AC7EA,SAAgB,MACf,QACA,WACA,QAAQ,UACR,SACW;CACX,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,OAAO;AACR;;;;;;;;;;AAWA,SAAgB,IACf,QACA,QAAQ,UACR,SACe;CACf,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,OAAO;AACR;;;;;;;;;;AAWA,SAAgB,IACf,QACA,QAAQ,UACR,SACe;CACf,uBAAuB,QAAQ,OAAO,OAAO;CAC7C,OAAO;AACR;;;;;;;;;ACxDA,SAAgB,eAAe,UAAiC,UAA0B;CACzF,IAAI,QAAQ;CACZ,KAAK,MAAM,WAAW,UAAU;EAC/B,MAAM,QAAQ,QAAQ;EACtB,IAAI,WAAW,UAAU,UAAU,QAAQ,CAAC,OAAO,SAAS,KAAK,KAAK,YAAY,QACjF,OAAO,QAAQ;EAEhB,IAAI,UAAU,QAAQ,CAAC,OAAO,SAAS,KAAK,GAC3C;EAED,QAAQ;CACT;CACA,OAAO,SAAS,EAAE,EAAE,QAAQ;AAC7B;;;;;;;;;;;ACVA,SAAgB,oBACf,YACA,SACA,eACA,OACe;CACf,MAAM,aAAa,QAAQ;CAC3B,IAAI,eAAe;CAEnB,IAAI,gBAAgB,YAAY;EAC/B,IAAI,eAAe,QAAQ,CAAC,OAAO,SAAS,UAAU,GACrD,eAAe,gBAAgB;OAE/B,eAAe,KAAK,IAAI,eAAe,UAAU,IAAI;CAEvD;CAEA,MAAM,WAAW,gBAAgB,cAAc,QAAQ,IAAI;CAE3D,OAAO;EACN,cAAc;EACd;EACA;EACA,MAAM,QAAQ;EACd,eAAe;EACf,KAAK;CACN;AACD;;AClCA,SAAS,sBACR,SACA,OACA,QACA,WACS;CACT,aAAa,QAAQ,MAAM,WAAW,MAAM,OAAO;CACnD,IAAI,QAAQ,SAAS,MAAM;EAC1B,IAAI,CAAC,QACJ,MAAM,IAAI,mBACT,WAAW,MAAM,SACjB,QAAQ,MACR,6DACD;EAED,OAAO;CACR;CACA,uBAAuB,QAAQ,MAAM,WAAW,MAAM,OAAO;CAC7D,IAAI,CAAC,OAAO,cAAc,QAAQ,IAAI,KAAK,QAAQ,QAAQ,WAC1D,MAAM,IAAI,mBACT,WAAW,MAAM,SACjB,QAAQ,MACR,qDAAqD,WACtD;CAED,OAAO,QAAQ;AAChB;AAEA,SAAS,iBAAiB,UAAuC;CAChE,IAAI,SAAS,WAAW,GACvB,MAAM,IAAI,mBAAmB,YAAY,UAAU,8BAA8B;CAElF,IAAI,YAAY;CAChB,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACzC,MAAM,UAAU,SAAS;EACzB,IAAI,CAAC,SAAS;EACd,YAAY,sBAAsB,SAAS,GAAG,MAAM,SAAS,SAAS,GAAG,SAAS;CACnF;CACA,MAAM,OAAO,SAAS,SAAS,SAAS;CACxC,IAAI,QAAQ,KAAK,SAAS,MACzB,MAAM,IAAI,mBAAmB,YAAY,UAAU,yCAAyC;AAE9F;;;;;;;;;;;;;;AAeA,SAAgB,iBACf,eACA,UACA,aACA,QAAQ,iBACkB;CAC1B,uBAAuB,eAAe,KAAK;CAC3C,IAAI,gBAAgB,KAAA,GACnB,uBAAuB,aAAa,aAAa;CAElD,iBAAiB,QAAQ;CAEzB,IAAI,WAAW;CACf,IAAI,aAAa;CACjB,MAAM,SAAyB,CAAC;CAEhC,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACzC,MAAM,UAAU,SAAS;EACzB,IAAI,CAAC,SAAS;EAEd,MAAM,QAAQ,oBAAoB,YAAY,SAAS,eAAe,CAAC;EACvE,OAAO,KAAK,KAAK;EACjB,YAAY,MAAM;EAElB,IAAI,QAAQ,SAAS,QAAQ,CAAC,OAAO,SAAS,QAAQ,IAAI,GACzD;EAED,aAAa,QAAQ;CACtB;CAEA,MAAM,mBAAmB,eAAe;CACxC,MAAM,gBAAgB,mBAAmB,IAAI,WAAW,mBAAmB;CAC3E,MAAM,eAAe,eAAe,UAAU,gBAAgB,CAAC;CAE/D,OAAO;EACN;EACA;EACA;EACA;CACD;AACD;;;;;;;;;;ACpFA,SAAgB,WAAW,SAA8B,eAAe,OAAe;CACtF,MAAM,YAAY,QAAQ,SAAS;CACnC,MAAM,EAAE,MAAM,MAAM,SAAS,aAAa;CAE1C,IAAI,cAAc,OAAO;CAEzB,IAAI,SAAS,KAAA,KAAa,SAAS,KAAA,GAAW;EAC7C,uBAAuB,MAAM,GAAG,UAAU,KAAK;EAC/C,aAAa,MAAM,GAAG,UAAU,KAAK;EACrC,IAAI,UAGH,cAAc,eADG,gBADC,aAAa,MAAM,UAAU,GAAG,UAAU,KAC3B,GAAW,IACf,GAAU,QAAQ;OACzC,IAAI,OAAO,cAAc,IAAI,GACnC,cAAc,gBAAgB,MAAM,IAAI;OAExC,cAAc,YAAY,OAAO,IAAI;CAEvC;CAEA,IAAI,YAAY,KAAA,GAAW;EAC1B,uBAAuB,SAAS,GAAG,UAAU,QAAQ;EACrD,cAAc,KAAK,IAAI,aAAa,OAAO;CAC5C;CAEA,uBAAuB,aAAa,GAAG,UAAU,IAAI;CACrD,OAAO;AACR;;;;;;;;;;;;AC/BA,SAAgB,SACf,WACA,cACA,QAAQ,UACQ;CAChB,uBAAuB,WAAW,KAAK;CAEvC,MAAM,MAAM,OAAO,iBAAiB,WAAW,eAAe,WAAW,cAAc,KAAK;CAE5F,uBAAuB,KAAK,GAAG,MAAM,IAAI;CAEzC,MAAM,aAAa,YAAY;CAG/B,OAAO;EACN;EACA,SAJe,aAAa,MAAM;EAKlC;EACA;CACD;AACD"}
|
|
@@ -150,6 +150,54 @@ function fromMinorUnits(minorUnits, currency, field = "minorUnits", options) {
|
|
|
150
150
|
const factor = 10 ** precision;
|
|
151
151
|
return (minorUnits < 0 ? -1 : 1) * Number((Math.abs(minorUnits) / factor).toFixed(precision)) + 0;
|
|
152
152
|
}
|
|
153
|
+
// src/core/money/ratio.ts
|
|
154
|
+
/** Diagnostic field name for a statutory rate ratio, shared by every rate consumer. */
|
|
155
|
+
const RATE_FIELD = "rate";
|
|
156
|
+
/** Largest integer that a JavaScript number represents exactly. */
|
|
157
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
158
|
+
/**
|
|
159
|
+
* Converts a statutory rate ratio into the exact BigInt fraction it was written as.
|
|
160
|
+
*
|
|
161
|
+
* Purpose: Single exact parser for both the BigInt arithmetic and the emitted `scale` fraction.
|
|
162
|
+
* Math: Reads the decimal string (expanding exponent notation), so `0.35` becomes `35/100`;
|
|
163
|
+
* the fraction is not reduced, so `den` always matches the written precision.
|
|
164
|
+
* Units: Dimensionless ratio in, integer numerator/denominator out.
|
|
165
|
+
* Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].
|
|
166
|
+
*
|
|
167
|
+
* @param rate Statutory rate ratio (e.g. `0.05` for 5%).
|
|
168
|
+
* @param field Diagnostic field name reported on failure.
|
|
169
|
+
* @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).
|
|
170
|
+
*/
|
|
171
|
+
function rateBigFraction(rate, field = RATE_FIELD) {
|
|
172
|
+
validateRate(rate, field);
|
|
173
|
+
const parts = (rate.toString().includes("e") ? rate.toFixed(10).replace(/\.?0+$/, "") : rate.toString()).split(".");
|
|
174
|
+
const decimals = parts[1] ? parts[1].length : 0;
|
|
175
|
+
return {
|
|
176
|
+
num: BigInt(parts.join("")),
|
|
177
|
+
den: 10n ** BigInt(decimals)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Converts a statutory rate ratio into a safe-integer fraction for a `scale` Measure.
|
|
182
|
+
*
|
|
183
|
+
* Purpose: Gives the emitter the exact numerator and denominator sim-core's `scale` accepts.
|
|
184
|
+
* Math: Same decimal expansion as {@link rateBigFraction}, then a safe-integer bound check.
|
|
185
|
+
* Units: Dimensionless ratio in, integer numerator/denominator out.
|
|
186
|
+
* Throws: TaxValidationError if the rate is invalid or its decimal expansion leaves the safe
|
|
187
|
+
* integer range (more than roughly 15 decimal digits).
|
|
188
|
+
*
|
|
189
|
+
* @param rate Statutory rate ratio (e.g. `0.05` for 5%).
|
|
190
|
+
* @param field Diagnostic field name reported on failure.
|
|
191
|
+
* @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).
|
|
192
|
+
*/
|
|
193
|
+
function rateFraction(rate, field = RATE_FIELD) {
|
|
194
|
+
const { num, den } = rateBigFraction(rate, field);
|
|
195
|
+
if (num > MAX_SAFE || den > MAX_SAFE) throw new TaxValidationError(field, rate, "Rate precision exceeds the safe integer range");
|
|
196
|
+
return {
|
|
197
|
+
num: Number(num),
|
|
198
|
+
den: Number(den)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
153
201
|
// src/core/money/rounding.ts
|
|
154
202
|
/**
|
|
155
203
|
* Rounds a number to the nearest integer, breaking ties away from zero (standard half-up).
|
|
@@ -163,14 +211,6 @@ function roundHalfUp(value) {
|
|
|
163
211
|
if (value < 0) return -Math.round(Math.abs(value)) + 0;
|
|
164
212
|
return Math.round(value) + 0;
|
|
165
213
|
}
|
|
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
214
|
/**
|
|
175
215
|
* Applies a statutory percentage rate to an integer minor unit amount using exact rational arithmetic.
|
|
176
216
|
*
|
|
@@ -188,8 +228,7 @@ function parseRateRational(rate) {
|
|
|
188
228
|
*/
|
|
189
229
|
function applyPercentage(minorUnits, rate, rounding = "half-up", options) {
|
|
190
230
|
validateIntegerMinorUnits(minorUnits, "minorUnits", options);
|
|
191
|
-
|
|
192
|
-
const { num, den } = parseRateRational(rate);
|
|
231
|
+
const { num, den } = rateBigFraction(rate);
|
|
193
232
|
const prod = BigInt(minorUnits) * num;
|
|
194
233
|
let result;
|
|
195
234
|
if (rounding === "floor") result = prod >= 0n ? prod / den : (prod - (den - 1n)) / den;
|
|
@@ -218,7 +257,7 @@ function applyGrossUp(minorUnits, rate, field = "minorUnits") {
|
|
|
218
257
|
validateIntegerMinorUnits(minorUnits, field);
|
|
219
258
|
validateRate(rate, "rate");
|
|
220
259
|
if (rate >= 1) throw new TaxValidationError("rate", rate, "Gross-up rate must be strictly less than 1");
|
|
221
|
-
const { num, den } =
|
|
260
|
+
const { num, den } = rateBigFraction(rate);
|
|
222
261
|
const divisor = den - num;
|
|
223
262
|
const result = Number((BigInt(minorUnits) * num * 2n + divisor) / (divisor * 2n)) + 0;
|
|
224
263
|
if (!Number.isSafeInteger(result)) throw new TaxValidationError(field, minorUnits, "Gross-up result exceeds the safe integer range");
|
|
@@ -469,6 +508,12 @@ Object.defineProperty(exports, "multiplySafeInteger", {
|
|
|
469
508
|
return multiplySafeInteger;
|
|
470
509
|
}
|
|
471
510
|
});
|
|
511
|
+
Object.defineProperty(exports, "rateFraction", {
|
|
512
|
+
enumerable: true,
|
|
513
|
+
get: function() {
|
|
514
|
+
return rateFraction;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
472
517
|
Object.defineProperty(exports, "roundHalfUp", {
|
|
473
518
|
enumerable: true,
|
|
474
519
|
get: function() {
|
|
@@ -506,4 +551,4 @@ Object.defineProperty(exports, "validateSafeIntegerCount", {
|
|
|
506
551
|
}
|
|
507
552
|
});
|
|
508
553
|
|
|
509
|
-
//# sourceMappingURL=clamp-
|
|
554
|
+
//# sourceMappingURL=clamp-DWYNU1V8.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clamp-DWYNU1V8.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/ratio.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 { validateRate } from \"./validate\"\n\n/** Diagnostic field name for a statutory rate ratio, shared by every rate consumer. */\nexport const RATE_FIELD = \"rate\"\n\n/** Largest integer that a JavaScript number represents exactly. */\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER)\n\n/**\n * Exact non-negative integer fraction `num / den` with `den` at least 1.\n *\n * Purpose: Carries a statutory rate as integers into exact rational arithmetic.\n * Math: `num / den` equals the rate; the fraction is not reduced.\n * Units: Dimensionless rate ratio (0.35 is 35/100); no currency.\n * Throws: Nothing; construction happens in {@link rateFraction}.\n * @statute Jurisdiction-neutral arithmetic type; no statutory citation.\n */\nexport interface RateFraction {\n\t/** Numerator, equal to the rate's decimal digits without the point. */\n\treadonly num: number\n\t/** Denominator, `10` to the power of the rate's decimal places. */\n\treadonly den: number\n}\n\n/**\n * Exact decimal expansion of a rate as a BigInt fraction.\n *\n * Purpose: Keeps percentage arithmetic exact for any rate, however many decimal places it has.\n * Math: `num / den` equals the rate; the fraction is not reduced.\n * Units: Dimensionless rate ratio; no currency.\n * Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport interface RateBigFraction {\n\t/** Numerator, equal to the rate's decimal digits without the point. */\n\treadonly num: bigint\n\t/** Denominator, `10n` to the power of the rate's decimal places. */\n\treadonly den: bigint\n}\n\n/**\n * Converts a statutory rate ratio into the exact BigInt fraction it was written as.\n *\n * Purpose: Single exact parser for both the BigInt arithmetic and the emitted `scale` fraction.\n * Math: Reads the decimal string (expanding exponent notation), so `0.35` becomes `35/100`;\n * the fraction is not reduced, so `den` always matches the written precision.\n * Units: Dimensionless ratio in, integer numerator/denominator out.\n * Throws: TaxValidationError if the rate is NaN, non-finite, or outside [0, 1].\n *\n * @param rate Statutory rate ratio (e.g. `0.05` for 5%).\n * @param field Diagnostic field name reported on failure.\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport function rateBigFraction(rate: number, field: string = RATE_FIELD): RateBigFraction {\n\tvalidateRate(rate, field)\n\tconst text = rate.toString().includes(\"e\")\n\t\t? rate.toFixed(10).replace(/\\.?0+$/, \"\")\n\t\t: rate.toString()\n\tconst parts = text.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 * Converts a statutory rate ratio into a safe-integer fraction for a `scale` Measure.\n *\n * Purpose: Gives the emitter the exact numerator and denominator sim-core's `scale` accepts.\n * Math: Same decimal expansion as {@link rateBigFraction}, then a safe-integer bound check.\n * Units: Dimensionless ratio in, integer numerator/denominator out.\n * Throws: TaxValidationError if the rate is invalid or its decimal expansion leaves the safe\n * integer range (more than roughly 15 decimal digits).\n *\n * @param rate Statutory rate ratio (e.g. `0.05` for 5%).\n * @param field Diagnostic field name reported on failure.\n * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).\n */\nexport function rateFraction(rate: number, field: string = RATE_FIELD): RateFraction {\n\tconst { num, den } = rateBigFraction(rate, field)\n\tif (num > MAX_SAFE || den > MAX_SAFE) {\n\t\tthrow new TaxValidationError(field, rate, \"Rate precision exceeds the safe integer range\")\n\t}\n\treturn { num: Number(num), den: Number(den) }\n}\n","import { TaxValidationError } from \"./errors\"\nimport { rateBigFraction } from \"./ratio\"\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\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\tconst { num, den } = rateBigFraction(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 } = rateBigFraction(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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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_clamp = require("./clamp-
|
|
2
|
+
const require_clamp = require("./clamp-DWYNU1V8.cjs");
|
|
3
3
|
// src/core/system/runner.ts
|
|
4
4
|
/**
|
|
5
5
|
* Standard runner that executes tax calculation on a given TaxSystem instance.
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as usd, c as roundHalfUp,
|
|
1
|
+
import { a as usd, c as roundHalfUp, d as toMinorUnits, h as TaxValidationError, i as thb, n as evaluateBrackets, p as validateMonetaryAmount, r as money, s as applyPercentage, t as clampCap, u as fromMinorUnits } from "./clamp-CdcDymBJ.mjs";
|
|
2
2
|
// src/core/system/runner.ts
|
|
3
3
|
/**
|
|
4
4
|
* Standard runner that executes tax calculation on a given TaxSystem instance.
|