@trajectonomics/tax 0.1.1-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/dist/th/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as roundHalfUp, d as multiplySafeInteger, f as validateMonetaryAmount, i as thb, l as fromMinorUnits, m as TaxValidationError, n as evaluateBrackets, o as applyGrossUp, p as validateSafeIntegerCount, s as applyPercentage, t as clampCap, u as toMinorUnits } from "../clamp-Bse4u0H-.mjs";
1
+ import { c as roundHalfUp, d as toMinorUnits, f as multiplySafeInteger, h as TaxValidationError, i as thb, l as rateFraction, m as validateSafeIntegerCount, n as evaluateBrackets, o as applyGrossUp, p as validateMonetaryAmount, s as applyPercentage, t as clampCap, u as fromMinorUnits } from "../clamp-CdcDymBJ.mjs";
2
2
  // src/th/parameters-brackets.ts
3
3
  /**
4
4
  * Section 48(1) progressive net-income brackets for Tax Year 2026 (THB major units, rates as ratios).
@@ -1454,6 +1454,1066 @@ function runThTaxPipeline(input = {}) {
1454
1454
  function calculateThTax(input = {}) {
1455
1455
  return runThTaxPipeline(input);
1456
1456
  }
1457
+ // src/th/levy/graph-types.ts
1458
+ /**
1459
+ * Tally bases sim-core's `tally` Measure reads.
1460
+ *
1461
+ * Purpose: Distinguishes income Flows from contribution Flows at the tally.
1462
+ * Math: No arithmetic; a discriminator only.
1463
+ * Units: Dimensionless.
1464
+ * Throws: Nothing.
1465
+ * @statute Thailand Revenue Code Sections 40(1)–(8) (gross) and 47 (funded).
1466
+ */
1467
+ const ThLevyTallyBasis = {
1468
+ /** Sums income Flows. */
1469
+ Gross: "gross",
1470
+ /** Sums contribution Flows. */
1471
+ Funded: "funded"
1472
+ };
1473
+ /**
1474
+ * Rounding modes sim-core's `scale` Measure accepts.
1475
+ *
1476
+ * Purpose: Names the three rounding policies embedded in emitted Measures.
1477
+ * Math: `Floor` rounds down, `Ceil` up, and `HalfUp` sends ties away from zero.
1478
+ * Units: Dimensionless.
1479
+ * Throws: Nothing.
1480
+ * @statute Thailand Revenue Code Sections 47 bis, 48(1), 48(2).
1481
+ */
1482
+ const ThLevyRounding = {
1483
+ Floor: "floor",
1484
+ HalfUp: "halfUp",
1485
+ Ceil: "ceil"
1486
+ };
1487
+ // src/th/levy/graph-rates.ts
1488
+ /**
1489
+ * Emits a satang sum of Measure ids; an empty list becomes a zero const.
1490
+ *
1491
+ * Purpose: Keeps statutory totals readable without a fold at every call site.
1492
+ * Math: Left-associative `add`; empty input yields `const 0`. When `name` is given, a final
1493
+ * zero addition exposes the running total under an audited id (sim-core has no rename op).
1494
+ * Units: Integer satang in and out.
1495
+ * Throws: TaxValidationError if the graph rejects an id or constant.
1496
+ * @statute Jurisdiction-neutral graph helper; no statutory citation.
1497
+ */
1498
+ function sumMeasures(graph, ids, name) {
1499
+ const [first, ...rest] = ids;
1500
+ let total = first ?? graph.constant(0);
1501
+ for (const id of rest) total = graph.add(total, id);
1502
+ return name === void 0 ? total : graph.add(total, graph.constant(0), name);
1503
+ }
1504
+ /**
1505
+ * Emits `of` rounded down to a whole baht while keeping satang units.
1506
+ *
1507
+ * Purpose: Reproduces the calculator's whole-baht `Math.floor` steps exactly.
1508
+ * Math: `100 * floor(x / 100)` via two integer scales.
1509
+ * Units: Integer satang in and out.
1510
+ * Throws: Nothing; `of` must name an emitted Measure.
1511
+ * @statute Thailand Revenue Code Section 42 bis (whole-baht expense rounding).
1512
+ */
1513
+ function flooredToBaht(graph, of) {
1514
+ return graph.scale(graph.scale(of, 1, 100, ThLevyRounding.Floor), 100, 1, ThLevyRounding.Floor);
1515
+ }
1516
+ /**
1517
+ * Emits `floor(income * rate)`, rounded down to a whole baht.
1518
+ *
1519
+ * Purpose: Mirrors the calculator's `Math.floor(major * rate)` expense deduction.
1520
+ * Math: `100 * floor(x * num / (den * 100))`.
1521
+ * Units: Integer satang in and out; `rate` is dimensionless.
1522
+ * Throws: TaxValidationError if `rate` is outside [0, 1].
1523
+ * @statute Thailand Revenue Code Sections 42 bis, 42 ter, 43–46.
1524
+ */
1525
+ function rateFlooredToBaht(graph, of, rate) {
1526
+ const { num, den } = rateFraction(rate);
1527
+ return flooredToBaht(graph, graph.scale(of, num, den, ThLevyRounding.Floor));
1528
+ }
1529
+ /**
1530
+ * Emits `min(of, ceiling)`, the shape `clampCap` uses for an absolute statutory ceiling.
1531
+ *
1532
+ * Purpose: Reproduces the absolute-ceiling branch of `clampCap`.
1533
+ * Math: `min(x, ceiling)`.
1534
+ * Units: Integer satang in and out.
1535
+ * Throws: TaxValidationError if the ceiling is not safe integer satang.
1536
+ * @statute Thailand Revenue Code Section 47 (allowance ceilings).
1537
+ */
1538
+ function capAt(graph, of, ceilingSatang) {
1539
+ return graph.min(of, graph.constant(ceilingSatang));
1540
+ }
1541
+ /**
1542
+ * Emits `min(of * rate, ceiling)` with half-up satang rounding, the shape `resolveCap` computes.
1543
+ *
1544
+ * Purpose: Percentage-of-income retirement caps round to the nearest satang, not to a baht.
1545
+ * Math: `min(round_half_up(x * num / den), ceiling)`.
1546
+ * Units: Integer satang in and out; `rate` is dimensionless.
1547
+ * Throws: TaxValidationError if `rate` is outside [0, 1] or the ceiling is invalid.
1548
+ * @statute Thailand Revenue Code Section 47; Ministerial Regulations No. 126, 390.
1549
+ */
1550
+ function rateCapAt(graph, of, rate, ceilingSatang) {
1551
+ const { num, den } = rateFraction(rate);
1552
+ return capAt(graph, graph.scale(of, num, den, ThLevyRounding.HalfUp), ceilingSatang);
1553
+ }
1554
+ // src/th/levy/measure-ids.ts
1555
+ /**
1556
+ * Audited Measure ids a TH 2026 Levy exposes by name.
1557
+ *
1558
+ * Purpose: Lets callers and tests compare each intermediate statutory figure without knowing
1559
+ * the emit order.
1560
+ * Math: No arithmetic; Measure ids only.
1561
+ * Units: The referenced Measures are integer satang.
1562
+ * Throws: Nothing.
1563
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 47 bis, 48(1)–(2).
1564
+ */
1565
+ let ThLevyMeasureId = /* @__PURE__ */ function(ThLevyMeasureId) {
1566
+ ThLevyMeasureId["AssessableTotal"] = "assessable.total";
1567
+ ThLevyMeasureId["GrossNonEmployment"] = "assessable.non_employment";
1568
+ ThLevyMeasureId["ExpensesTotal"] = "expenses.total";
1569
+ ThLevyMeasureId["AllowancesFixed"] = "allowances.fixed";
1570
+ ThLevyMeasureId["RetirementCluster"] = "allowances.retirement_cluster";
1571
+ ThLevyMeasureId["ThaiEsg"] = "allowances.thai_esg";
1572
+ ThLevyMeasureId["Donations"] = "allowances.donations";
1573
+ ThLevyMeasureId["AllowancesTotal"] = "allowances.total";
1574
+ ThLevyMeasureId["NetTaxableIncome"] = "taxable.income";
1575
+ ThLevyMeasureId["ProgressiveTax"] = "tax.progressive";
1576
+ ThLevyMeasureId["MinimumGrossTax"] = "tax.minimum";
1577
+ ThLevyMeasureId["RegularTax"] = "tax.regular";
1578
+ ThLevyMeasureId["FinalWithholdingTax"] = "tax.final_withholding";
1579
+ ThLevyMeasureId["DividendCredit"] = "credit.dividend";
1580
+ ThLevyMeasureId["TotalTax"] = "tax.total";
1581
+ ThLevyMeasureId["NetTaxPayable"] = "tax.net_payable";
1582
+ return ThLevyMeasureId;
1583
+ }({});
1584
+ // src/th/levy/allowances-donations.ts
1585
+ /**
1586
+ * Emits the Section 47(7) donation allowances against the net income before donations.
1587
+ *
1588
+ * Purpose: Reproduces the 2x-then-1x donation caps as Measures.
1589
+ * Math: 2x donations are doubled, then `min(doubled, floor(net * 10%))`; 1x donations are
1590
+ * `min(amount, floor(remaining net * 10%))`. Taxable income is `max(0, remaining - 1x allowed)`.
1591
+ * Units: Profile amounts are THB major units; Measures are integer satang.
1592
+ * Throws: TaxValidationError if a donation amount is invalid or the doubled amount overflows.
1593
+ *
1594
+ * @param graph Measure graph being built.
1595
+ * @param netBefore `max(0, assessable - expenses - non-donation allowances)` Measure.
1596
+ * @param requested2x Requested 2x-qualified donation in THB.
1597
+ * @param requested1x Requested general donation in THB.
1598
+ * @statute Thailand Revenue Code Section 47(7); Royal Decrees No. 428, 558.
1599
+ */
1600
+ function addDonationAllowances(graph, netBefore, requested2x, requested1x) {
1601
+ const p = TH_2026_PARAMETERS.allowances.donations;
1602
+ const raw2x = requested2x ?? 0;
1603
+ const raw1x = requested1x ?? 0;
1604
+ validateMonetaryAmount(raw2x, "allowance.donation_2x");
1605
+ validateMonetaryAmount(raw1x, "allowance.donation_1x");
1606
+ const doubled = multiplySafeInteger(toSatang(raw2x, "allowance.donation_2x"), p.donation2xMultiplier, "allowance.donation_2x");
1607
+ const net = graph.max(netBefore, graph.constant(0));
1608
+ const cap2x = rateFlooredToBaht(graph, net, p.donation2xMaxRate);
1609
+ const allowed2x = graph.min(graph.constant(doubled), cap2x);
1610
+ const remaining = graph.max(graph.sub(net, allowed2x), graph.constant(0));
1611
+ const cap1x = rateFlooredToBaht(graph, remaining, p.donation1xMaxRate);
1612
+ const allowed1x = graph.min(graph.constant(toSatang(raw1x, "allowance.donation_1x")), cap1x);
1613
+ return {
1614
+ total: graph.add(allowed2x, allowed1x, "allowances.donations"),
1615
+ netTaxable: graph.max(graph.sub(remaining, allowed1x), graph.constant(0), "taxable.income")
1616
+ };
1617
+ }
1618
+ // src/th/levy/allowances-fixed.ts
1619
+ /**
1620
+ * Computes the fixed allowance total frozen into the Levy.
1621
+ *
1622
+ * Purpose: Reuses the calculator's own family and insurance stages, so the const Measure cannot
1623
+ * drift from the rules it mirrors.
1624
+ * Math: Sum of the allowed personal, spouse, children, parents, disabled, social security,
1625
+ * life/health insurance, parent health, spouse life, and mortgage interest amounts.
1626
+ * Units: THB major units in, integer satang out.
1627
+ * Throws: TaxValidationError if any profile allowance amount is invalid.
1628
+ *
1629
+ * @param allowances Fixed profile allowances.
1630
+ * @param status Filing status governing spouse and parent eligibility.
1631
+ * @statute Thailand Revenue Code Section 47(1)(a)–(j).
1632
+ */
1633
+ function fixedAllowanceSatang(allowances, status) {
1634
+ const family = calculateFamilyAllowances(allowances, status);
1635
+ const insurance = calculateInsuranceAllowances(allowances, status);
1636
+ return [
1637
+ family.personal.allowed,
1638
+ family.spouse.allowed,
1639
+ family.children.allowed,
1640
+ family.parents.allowed,
1641
+ family.disabled.allowed,
1642
+ insurance.socialSecurity.allowed,
1643
+ insurance.lifeInsurance.allowed,
1644
+ insurance.parentHealthInsurance.allowed,
1645
+ insurance.spouseLifeInsurance.allowed,
1646
+ insurance.mortgageInterest.allowed
1647
+ ].reduce((sum, value) => sum + toSatang(value), 0);
1648
+ }
1649
+ /**
1650
+ * Emits the Section 47 fixed allowance total as one const Measure.
1651
+ *
1652
+ * Purpose: Freezes the profile-only allowances into a single named Measure.
1653
+ * Math: `const(fixedAllowanceSatang(...))`.
1654
+ * Units: Integer satang.
1655
+ * Throws: TaxValidationError if a profile allowance amount is invalid.
1656
+ *
1657
+ * @param graph Measure graph being built.
1658
+ * @param allowances Fixed profile allowances.
1659
+ * @param status Filing status governing eligibility.
1660
+ * @statute Thailand Revenue Code Section 47(1)(a)–(j).
1661
+ */
1662
+ function addFixedAllowances(graph, allowances, status) {
1663
+ return graph.constant(fixedAllowanceSatang(allowances, status), "allowances.fixed");
1664
+ }
1665
+ // src/th/levy/labels.ts
1666
+ /**
1667
+ * Tally labels a TH 2026 Levy reads from income Flows (`gross` basis).
1668
+ *
1669
+ * Purpose: One label per assessable income class under Section 40(1)–(8), with Section 40(4)
1670
+ * split per source because each source carries its own final-withholding election at emit time.
1671
+ * Math: No arithmetic; the label selects which tally a Measure reads.
1672
+ * Units: Dimensionless label string; the tallied amount is integer satang.
1673
+ * Throws: Nothing.
1674
+ * @statute Thailand Revenue Code Sections 40(1)–(8), 40(4), 50(2)(g).
1675
+ */
1676
+ const ThLevyIncomeLabel = {
1677
+ /** Section 40(1) employment income. */
1678
+ Section40_1: "income.section40_1",
1679
+ /** Section 40(2) service and hire-of-work income. */
1680
+ Section40_2: "income.section40_2",
1681
+ /** Section 40(3) royalties, goodwill, and copyright income. */
1682
+ Section40_3: "income.section40_3",
1683
+ /** Section 40(4) income with no source-specific election. */
1684
+ Section40_4Other: "income.section40_4",
1685
+ /** Section 40(4) interest, elected final or included at emit time. */
1686
+ Section40_4Interest: "income.section40_4.interest",
1687
+ /** Section 40(4) dividends, elected final or included at emit time. */
1688
+ Section40_4Dividends: "income.section40_4.dividends",
1689
+ /** Section 40(4) digital asset income, elected final or included at emit time. */
1690
+ Section40_4DigitalAssets: "income.section40_4.digitalAssets",
1691
+ /** Section 40(5) building rental income. */
1692
+ Section40_5Building: "income.section40_5.building",
1693
+ /** Section 40(5) agricultural land rental income. */
1694
+ Section40_5AgriculturalLand: "income.section40_5.agriculturalLand",
1695
+ /** Section 40(5) non-agricultural land rental income. */
1696
+ Section40_5NonAgriculturalLand: "income.section40_5.nonAgriculturalLand",
1697
+ /** Section 40(5) vehicle rental income. */
1698
+ Section40_5Vehicles: "income.section40_5.vehicles",
1699
+ /** Section 40(5) other rental income. */
1700
+ Section40_5Other: "income.section40_5.other",
1701
+ /** Section 40(6) medical profession income. */
1702
+ Section40_6Medical: "income.section40_6.medical",
1703
+ /** Section 40(6) law profession income. */
1704
+ Section40_6Law: "income.section40_6.law",
1705
+ /** Section 40(6) engineering profession income. */
1706
+ Section40_6Engineering: "income.section40_6.engineering",
1707
+ /** Section 40(6) architecture profession income. */
1708
+ Section40_6Architecture: "income.section40_6.architecture",
1709
+ /** Section 40(6) accounting profession income. */
1710
+ Section40_6Accounting: "income.section40_6.accounting",
1711
+ /** Section 40(6) fine arts profession income. */
1712
+ Section40_6FineArts: "income.section40_6.fineArts",
1713
+ /** Section 40(6) other liberal profession income. */
1714
+ Section40_6Other: "income.section40_6.other",
1715
+ /** Section 40(7) contracting income. */
1716
+ Section40_7: "income.section40_7",
1717
+ /** Section 40(8) general business and commerce income. */
1718
+ Section40_8General: "income.section40_8.general",
1719
+ /** Section 40(8) entertainer income (tiered expense rate). */
1720
+ Section40_8Entertainer: "income.section40_8.entertainer"
1721
+ };
1722
+ /**
1723
+ * Tally labels a TH 2026 Levy reads from contribution Flows (`funded` basis).
1724
+ *
1725
+ * Purpose: One label per contribution-driven retirement allowance.
1726
+ * Math: No arithmetic; the label selects which tally a Measure reads.
1727
+ * Units: Dimensionless label string; the tallied amount is integer satang.
1728
+ * Throws: Nothing.
1729
+ * @statute Thailand Revenue Code Section 47; Ministerial Regulations No. 126, 390.
1730
+ */
1731
+ const ThLevyContributionLabel = {
1732
+ /** RMF contribution. */
1733
+ Rmf: "allowance.retirement.rmf",
1734
+ /** SSF contribution. */
1735
+ Ssf: "allowance.retirement.ssf",
1736
+ /** PVD contribution. */
1737
+ Pvd: "allowance.retirement.pvd",
1738
+ /** GPF contribution. */
1739
+ Gpf: "allowance.retirement.gpf",
1740
+ /** National Savings Fund contribution. */
1741
+ Nsf: "allowance.retirement.nsf",
1742
+ /** Pension life insurance premium. */
1743
+ PensionLifeInsurance: "allowance.retirement.pension_life",
1744
+ /** ThaiESG fund contribution. */
1745
+ ThaiEsg: "allowance.thai_esg"
1746
+ };
1747
+ /**
1748
+ * Every income tally label the emitter reads, in Section 40 order.
1749
+ *
1750
+ * Purpose: Ordered contract a caller can iterate to label or audit income Flows.
1751
+ * Math: No arithmetic.
1752
+ * Units: Dimensionless label strings.
1753
+ * Throws: Nothing.
1754
+ * @statute Thailand Revenue Code Sections 40(1)–(8), 40(4).
1755
+ */
1756
+ const TH_LEVY_INCOME_LABELS = [
1757
+ ThLevyIncomeLabel.Section40_1,
1758
+ ThLevyIncomeLabel.Section40_2,
1759
+ ThLevyIncomeLabel.Section40_3,
1760
+ ThLevyIncomeLabel.Section40_4Other,
1761
+ ThLevyIncomeLabel.Section40_4Interest,
1762
+ ThLevyIncomeLabel.Section40_4Dividends,
1763
+ ThLevyIncomeLabel.Section40_4DigitalAssets,
1764
+ ThLevyIncomeLabel.Section40_5Building,
1765
+ ThLevyIncomeLabel.Section40_5AgriculturalLand,
1766
+ ThLevyIncomeLabel.Section40_5NonAgriculturalLand,
1767
+ ThLevyIncomeLabel.Section40_5Vehicles,
1768
+ ThLevyIncomeLabel.Section40_5Other,
1769
+ ThLevyIncomeLabel.Section40_6Medical,
1770
+ ThLevyIncomeLabel.Section40_6Law,
1771
+ ThLevyIncomeLabel.Section40_6Engineering,
1772
+ ThLevyIncomeLabel.Section40_6Architecture,
1773
+ ThLevyIncomeLabel.Section40_6Accounting,
1774
+ ThLevyIncomeLabel.Section40_6FineArts,
1775
+ ThLevyIncomeLabel.Section40_6Other,
1776
+ ThLevyIncomeLabel.Section40_7,
1777
+ ThLevyIncomeLabel.Section40_8General,
1778
+ ThLevyIncomeLabel.Section40_8Entertainer
1779
+ ];
1780
+ /**
1781
+ * Every contribution tally label the emitter reads, in retirement-cluster order.
1782
+ *
1783
+ * Purpose: Ordered contract a caller can iterate to label or audit contribution Flows.
1784
+ * Math: No arithmetic.
1785
+ * Units: Dimensionless label strings.
1786
+ * Throws: Nothing.
1787
+ * @statute Thailand Revenue Code Section 47; Ministerial Regulations No. 126, 390.
1788
+ */
1789
+ const TH_LEVY_CONTRIBUTION_LABELS = [
1790
+ ThLevyContributionLabel.Rmf,
1791
+ ThLevyContributionLabel.Ssf,
1792
+ ThLevyContributionLabel.Pvd,
1793
+ ThLevyContributionLabel.Gpf,
1794
+ ThLevyContributionLabel.Nsf,
1795
+ ThLevyContributionLabel.PensionLifeInsurance,
1796
+ ThLevyContributionLabel.ThaiEsg
1797
+ ];
1798
+ // src/th/levy/allowances-retirement.ts
1799
+ /**
1800
+ * Emits the Retirement Cluster and ThaiESG allowances from `funded` tallies.
1801
+ *
1802
+ * Purpose: Mirrors the percentage and ceiling caps of both retirement allowances.
1803
+ * Math: RMF/SSF/PVD/GPF and pension life insurance are capped at `min(income * rate, ceiling)`
1804
+ * (PVD/GPF on Section 40(1) wage, the rest on assessable income); NSF at its flat ceiling; the
1805
+ * six are then capped at the 500,000 THB cluster ceiling. ThaiESG is capped at
1806
+ * `min(assessable * 30%, 300,000)`.
1807
+ * Units: Integer satang Measures.
1808
+ * Throws: TaxValidationError if a statutory ceiling is not valid satang.
1809
+ *
1810
+ * @param graph Measure graph being built.
1811
+ * @param tallies Income and contribution tally ids.
1812
+ * @param assessable Assessable income Measure used as the percentage base.
1813
+ * @statute Thailand Revenue Code Section 47; Ministerial Regulations No. 126, 390.
1814
+ */
1815
+ function addRetirementAllowances(graph, tallies, assessable) {
1816
+ const p = TH_2026_PARAMETERS.allowances.retirementCluster;
1817
+ const income = tallies.income;
1818
+ const funded = tallies.contributions;
1819
+ const wage = income[ThLevyIncomeLabel.Section40_1];
1820
+ const individual = sumMeasures(graph, [
1821
+ graph.min(funded[ThLevyContributionLabel.Rmf], rateCapAt(graph, assessable, p.rmf.maxRate, toSatang(p.rmf.ceiling, "allowance.retirement.rmf"))),
1822
+ graph.min(funded[ThLevyContributionLabel.Ssf], rateCapAt(graph, assessable, p.ssf.maxRate, toSatang(p.ssf.ceiling, "allowance.retirement.ssf"))),
1823
+ graph.min(funded[ThLevyContributionLabel.Pvd], rateCapAt(graph, wage, p.pvd.maxRate, toSatang(p.pvd.ceiling, "allowance.retirement.pvd"))),
1824
+ graph.min(funded[ThLevyContributionLabel.Gpf], rateCapAt(graph, wage, p.gpf.maxRate, toSatang(p.gpf.ceiling, "allowance.retirement.gpf"))),
1825
+ graph.min(funded[ThLevyContributionLabel.Nsf], graph.constant(toSatang(p.nsf.ceiling, "allowance.retirement.nsf"))),
1826
+ graph.min(funded[ThLevyContributionLabel.PensionLifeInsurance], rateCapAt(graph, assessable, p.pensionLifeInsurance.maxRate, toSatang(p.pensionLifeInsurance.ceiling, "allowance.retirement.pension_life")))
1827
+ ]);
1828
+ const cluster = graph.min(individual, graph.constant(toSatang(p.combinedCeiling, "allowance.retirement_cluster")), "allowances.retirement_cluster");
1829
+ const esg = TH_2026_PARAMETERS.allowances.thaiEsg;
1830
+ return {
1831
+ cluster,
1832
+ thaiEsg: graph.min(funded[ThLevyContributionLabel.ThaiEsg], rateCapAt(graph, assessable, esg.maxRate, toSatang(esg.ceiling, "allowance.thai_esg")), "allowances.thai_esg")
1833
+ };
1834
+ }
1835
+ // src/th/levy/allowances.ts
1836
+ /**
1837
+ * Emits every Section 47 allowance and derives net taxable income.
1838
+ *
1839
+ * Purpose: Orders the fixed, retirement, ThaiESG, and donation rules into one pipeline.
1840
+ * Math: `netBefore = max(0, assessable - expenses - fixed - cluster - ThaiESG)`, then donations
1841
+ * are capped against that net; `netTaxable = max(0, netBefore - allowed donations)`.
1842
+ * Units: Integer satang Measures.
1843
+ * Throws: TaxValidationError for an invalid profile allowance or donation amount.
1844
+ *
1845
+ * @param graph Measure graph being built.
1846
+ * @param tallies Income and contribution tally ids.
1847
+ * @param profile Emit-time filing status and allowances.
1848
+ * @param assessable Assessable income Measure used as the percentage base.
1849
+ * @param expenses Total expense deduction Measure.
1850
+ * @statute Thailand Revenue Code Section 47.
1851
+ */
1852
+ function addAllowanceMeasures(graph, tallies, profile, assessable, expenses) {
1853
+ const fixed = addFixedAllowances(graph, profile.allowances, profile.filingStatus ?? "single");
1854
+ const retirement = addRetirementAllowances(graph, tallies, assessable);
1855
+ const nonDonation = sumMeasures(graph, [
1856
+ fixed,
1857
+ retirement.cluster,
1858
+ retirement.thaiEsg
1859
+ ]);
1860
+ const donations = addDonationAllowances(graph, graph.max(graph.sub(graph.sub(assessable, expenses), nonDonation), graph.constant(0)), profile.allowances?.donation2x, profile.allowances?.donation1x);
1861
+ return {
1862
+ total: graph.add(nonDonation, donations.total, "allowances.total"),
1863
+ netTaxable: donations.netTaxable
1864
+ };
1865
+ }
1866
+ // src/th/levy/assessable.ts
1867
+ /**
1868
+ * Emits the Section 40 assessable income totals.
1869
+ *
1870
+ * Purpose: Sums every Section 40 class after the Section 40(4) election split.
1871
+ * Math: total = 40(1) + 40(2) + 40(3) + 40(4) assessable + 40(5) + 40(6) + 40(7) + 40(8);
1872
+ * gross non-employment = the same sum without Section 40(1).
1873
+ * Units: Integer satang Measures.
1874
+ * Throws: Nothing; every input is a caller-supplied tally Measure.
1875
+ *
1876
+ * @param graph Measure graph being built.
1877
+ * @param tallies Income tally ids.
1878
+ * @param section40_4 Resolved Section 40(4) assessable Measure.
1879
+ * @statute Thailand Revenue Code Sections 40(1)–(8), 48(2).
1880
+ */
1881
+ function addAssessableMeasures(graph, tallies, section40_4) {
1882
+ const income = tallies.income;
1883
+ const rental = sumMeasures(graph, [
1884
+ income[ThLevyIncomeLabel.Section40_5Building],
1885
+ income[ThLevyIncomeLabel.Section40_5AgriculturalLand],
1886
+ income[ThLevyIncomeLabel.Section40_5NonAgriculturalLand],
1887
+ income[ThLevyIncomeLabel.Section40_5Vehicles],
1888
+ income[ThLevyIncomeLabel.Section40_5Other]
1889
+ ]);
1890
+ const professions = sumMeasures(graph, [
1891
+ income[ThLevyIncomeLabel.Section40_6Medical],
1892
+ income[ThLevyIncomeLabel.Section40_6Law],
1893
+ income[ThLevyIncomeLabel.Section40_6Engineering],
1894
+ income[ThLevyIncomeLabel.Section40_6Architecture],
1895
+ income[ThLevyIncomeLabel.Section40_6Accounting],
1896
+ income[ThLevyIncomeLabel.Section40_6FineArts],
1897
+ income[ThLevyIncomeLabel.Section40_6Other]
1898
+ ]);
1899
+ const business = sumMeasures(graph, [income[ThLevyIncomeLabel.Section40_8General], income[ThLevyIncomeLabel.Section40_8Entertainer]]);
1900
+ const employment = income[ThLevyIncomeLabel.Section40_1];
1901
+ const nonEmployment = [
1902
+ income[ThLevyIncomeLabel.Section40_2],
1903
+ income[ThLevyIncomeLabel.Section40_3],
1904
+ section40_4.assessable,
1905
+ rental,
1906
+ professions,
1907
+ income[ThLevyIncomeLabel.Section40_7],
1908
+ business
1909
+ ];
1910
+ return {
1911
+ total: sumMeasures(graph, [employment, ...nonEmployment], "assessable.total"),
1912
+ grossNonEmployment: sumMeasures(graph, nonEmployment, "assessable.non_employment")
1913
+ };
1914
+ }
1915
+ // src/th/levy/brackets.ts
1916
+ /**
1917
+ * Emits progressive Section 48(1) tax as one slice per bracket tier.
1918
+ *
1919
+ * Purpose: Reproduces `evaluateBrackets` slice by slice as Measures.
1920
+ * Math: For tier `[lo, hi)` at rate `r`, tax += `round_half_up(min(max(x - lo, 0), hi - lo) * r)`;
1921
+ * the unbounded top tier omits the upper cap. Slices match `evaluateBrackets` exactly.
1922
+ * Units: Taxable income and tax are integer satang Measures; rates are dimensionless.
1923
+ * Throws: TaxValidationError if a bracket bound or rate is invalid.
1924
+ *
1925
+ * @param graph Measure graph being built.
1926
+ * @param taxable Net taxable income Measure.
1927
+ * @param brackets Ordered statutory bracket table in THB major units.
1928
+ * @statute Thailand Revenue Code Section 48(1); Royal Decree No. 650.
1929
+ */
1930
+ function addProgressiveTax(graph, taxable, brackets = TH_2026_PARAMETERS.brackets) {
1931
+ const taxes = [];
1932
+ let lower = 0;
1933
+ for (const bracket of brackets) {
1934
+ const upper = bracket.upTo === null ? null : toSatang(bracket.upTo);
1935
+ const excess = graph.max(graph.sub(taxable, graph.constant(lower)), graph.constant(0));
1936
+ const slice = upper === null ? excess : graph.min(excess, graph.constant(upper - lower));
1937
+ const { num, den } = rateFraction(bracket.rate);
1938
+ taxes.push(graph.scale(slice, num, den, ThLevyRounding.HalfUp));
1939
+ lower = upper ?? lower;
1940
+ }
1941
+ return sumMeasures(graph, taxes, "tax.progressive");
1942
+ }
1943
+ /**
1944
+ * Emits the Section 48(2) minimum gross tax and its applicability test.
1945
+ *
1946
+ * Purpose: Encodes both statutory thresholds as `pick` Measures.
1947
+ * Math: `calculated = gross >= 120,000 ? round_half_up(gross * 0.5%) : 0`;
1948
+ * `applicable = calculated > 5,000 ? calculated : 0`.
1949
+ * Units: Integer satang Measures; the threshold comparison uses satang.
1950
+ * Throws: TaxValidationError if the rate or a threshold is invalid.
1951
+ *
1952
+ * @param graph Measure graph being built.
1953
+ * @param grossNonEmployment Section 40(2)–(8) gross income Measure.
1954
+ * @statute Thailand Revenue Code Section 48(2).
1955
+ */
1956
+ function addMinimumGrossTax(graph, grossNonEmployment) {
1957
+ const p = TH_2026_PARAMETERS.minimumGrossTax;
1958
+ const { num, den } = rateFraction(p.rate);
1959
+ const computed = graph.scale(grossNonEmployment, num, den, ThLevyRounding.HalfUp);
1960
+ const calculated = graph.pick(grossNonEmployment, graph.constant(toSatang(p.incomeThreshold) - 1), computed, graph.constant(0), "tax.minimum");
1961
+ return {
1962
+ calculated,
1963
+ applicable: graph.pick(calculated, graph.constant(toSatang(p.taxThreshold)), calculated, graph.constant(0))
1964
+ };
1965
+ }
1966
+ /**
1967
+ * Emits the regular-schedule tax: the larger of progressive tax and applicable minimum gross tax.
1968
+ *
1969
+ * Purpose: Encodes Section 48(2)'s "payable only if it exceeds" rule as `max`.
1970
+ * Math: `max(progressive, applicableMinimum)`.
1971
+ * Units: Integer satang Measures.
1972
+ * Throws: Nothing.
1973
+ *
1974
+ * @param graph Measure graph being built.
1975
+ * @param progressive Progressive bracket tax Measure.
1976
+ * @param applicableMinimum Minimum gross tax once the 5,000 THB floor applies.
1977
+ * @statute Thailand Revenue Code Sections 48(1)–(2).
1978
+ */
1979
+ function addRegularTax(graph, progressive, applicableMinimum) {
1980
+ return graph.max(progressive, applicableMinimum, "tax.regular");
1981
+ }
1982
+ // src/th/levy/expense-item.ts
1983
+ /**
1984
+ * Emits the actual-expense branch: `min(actual, income)`, matching `clampCap(actual, income)`.
1985
+ *
1986
+ * Purpose: Shared actual-expense Measure for every electable Section 40 item.
1987
+ * Math: `min(tally, actualExpense)` in satang.
1988
+ * Units: Actual expense is THB major units in, satang Measures out.
1989
+ * Throws: TaxValidationError if the actual expense is negative, NaN, or non-finite.
1990
+ *
1991
+ * @param graph Measure graph being built.
1992
+ * @param tally Tally Measure of the item's income.
1993
+ * @param choice Election carrying the actual expense.
1994
+ * @param field Diagnostic field name.
1995
+ * @statute Thailand Revenue Code Sections 42 ter, 43–46; Royal Decree No. 11.
1996
+ */
1997
+ function addActualExpense(graph, tally, choice, field) {
1998
+ const actual = choice?.actualExpense ?? 0;
1999
+ validateMonetaryAmount(actual, field);
2000
+ return graph.min(tally, graph.constant(toSatang(actual, field)));
2001
+ }
2002
+ /**
2003
+ * Emits one Section 40 expense deduction, either actual-capped-at-income or the standard percentage.
2004
+ *
2005
+ * Purpose: Selects the election branch and applies the item's statutory formula.
2006
+ * Math: Actual election gives `min(actual, income)`; otherwise `min(floor(income * rate), cap)`
2007
+ * expressed in whole baht, reproducing the calculator's `Math.floor(major * rate)` step.
2008
+ * Units: Measures are integer satang; `rate` is dimensionless.
2009
+ * Throws: TaxValidationError if the actual expense is invalid.
2010
+ *
2011
+ * @param graph Measure graph being built.
2012
+ * @param line Expense line specification.
2013
+ * @statute Thailand Revenue Code Sections 42 bis, 42 ter, 43–46; Royal Decree No. 11.
2014
+ */
2015
+ function addItemExpense(graph, line) {
2016
+ if (line.choice?.election === "actual") return addActualExpense(graph, line.tally, line.choice, line.field);
2017
+ const standard = rateFlooredToBaht(graph, line.tally, line.rate);
2018
+ return line.capSatang === void 0 ? standard : capAt(graph, standard, line.capSatang);
2019
+ }
2020
+ /**
2021
+ * Emits and sums a list of expense lines, one per Section 40 item or sub-category.
2022
+ *
2023
+ * Purpose: Keeps multi-class categories such as rentals and professions readable.
2024
+ * Math: `sum(addItemExpense(line))`; an empty list is zero.
2025
+ * Units: Integer satang Measures.
2026
+ * Throws: TaxValidationError if an elected actual expense is invalid.
2027
+ *
2028
+ * @param graph Measure graph being built.
2029
+ * @param lines Expense lines for one Section 40 category.
2030
+ * @statute Thailand Revenue Code Sections 42 bis, 42 ter, 43–46; Royal Decree No. 11.
2031
+ */
2032
+ function addExpenseLines(graph, lines) {
2033
+ return sumMeasures(graph, lines.map((line) => addItemExpense(graph, line)));
2034
+ }
2035
+ // src/th/levy/expenses-business.ts
2036
+ function addEntertainerExpense(graph, tally, choice) {
2037
+ const entertainer = choice?.entertainer;
2038
+ const field = "expense.40_8.entertainer";
2039
+ if (entertainer?.election === "actual") return addActualExpense(graph, tally, entertainer, field);
2040
+ const p = TH_2026_PARAMETERS.expenses.section40_8;
2041
+ const threshold = toSatang(p.entertainerFirstTierThreshold, field);
2042
+ const firstTier = graph.min(tally, graph.constant(threshold));
2043
+ const secondTier = graph.max(graph.sub(tally, graph.constant(threshold)), graph.constant(0));
2044
+ return capAt(graph, graph.add(rateFlooredToBaht(graph, firstTier, p.entertainerFirstTierRate), rateFlooredToBaht(graph, secondTier, p.entertainerSecondTierRate)), toSatang(p.entertainerCeiling, field));
2045
+ }
2046
+ /**
2047
+ * Emits the Section 40(8) business expense: general income at 60%, entertainer income on the
2048
+ * 60%/40% two-tier schedule capped at 600,000 THB; actual expenses cap at their income.
2049
+ *
2050
+ * Purpose: Mirrors both Section 40(8) expense schedules and their actual-expense elections.
2051
+ * Math: General `floor(income * 0.6)`. Entertainer
2052
+ * `min(floor(min(income, 300000) * 0.6) + floor(max(income - 300000, 0) * 0.4), 600000)`.
2053
+ * Units: Integer satang Measures.
2054
+ * Throws: TaxValidationError if an elected actual expense is invalid.
2055
+ * @statute Thailand Royal Decree No. 11 Section 8.
2056
+ */
2057
+ function addSection40_8Expense(graph, tallies, choice) {
2058
+ const rates = TH_2026_PARAMETERS.expenses.section40_8;
2059
+ const income = tallies.income;
2060
+ return sumMeasures(graph, [addItemExpense(graph, {
2061
+ tally: income[ThLevyIncomeLabel.Section40_8General],
2062
+ choice: choice?.general,
2063
+ rate: rates.generalRate,
2064
+ field: "expense.40_8.general"
2065
+ }), addEntertainerExpense(graph, income[ThLevyIncomeLabel.Section40_8Entertainer], choice)]);
2066
+ }
2067
+ // src/th/levy/expenses-contracting.ts
2068
+ /**
2069
+ * Emits the Section 40(7) contracting expense: standard 60%, or actual capped at the income.
2070
+ *
2071
+ * Purpose: Mirrors the contracting expense deduction and its actual-expense election.
2072
+ * Math: `floor(s7 * 0.6)` or `min(actual, s7)`.
2073
+ * Units: Integer satang Measures.
2074
+ * Throws: TaxValidationError if the elected actual expense is invalid.
2075
+ * @statute Thailand Royal Decree No. 11 Section 7.
2076
+ */
2077
+ function addSection40_7Expense(graph, tallies, choice) {
2078
+ return addItemExpense(graph, {
2079
+ tally: tallies.income[ThLevyIncomeLabel.Section40_7],
2080
+ choice,
2081
+ rate: TH_2026_PARAMETERS.expenses.section40_7.rate,
2082
+ field: "expense.40_7"
2083
+ });
2084
+ }
2085
+ // src/th/levy/expenses-income.ts
2086
+ /**
2087
+ * Emits the shared Section 40(1) and 40(2) expense: 50% of their combined income, capped at
2088
+ * 100,000 THB.
2089
+ *
2090
+ * Purpose: Mirrors the shared employment/service expense deduction as Measures.
2091
+ * Math: `min(floor((s1 + s2) * 0.5), 100000)` in whole baht, summed before the percentage.
2092
+ * Units: Integer satang Measures.
2093
+ * Throws: Nothing; both tallies are caller-supplied integer satang.
2094
+ * @statute Thailand Revenue Code Section 42 bis.
2095
+ */
2096
+ function addSection40_1_2Expense(graph, tallies) {
2097
+ const income = tallies.income;
2098
+ const combined = graph.add(income[ThLevyIncomeLabel.Section40_1], income[ThLevyIncomeLabel.Section40_2]);
2099
+ const rate = TH_2026_PARAMETERS.expenses.section40_1_2;
2100
+ return addItemExpense(graph, {
2101
+ tally: combined,
2102
+ rate: rate.rate,
2103
+ field: "expense.40_1_2",
2104
+ capSatang: toSatang(rate.ceiling, "expense.40_1_2")
2105
+ });
2106
+ }
2107
+ /**
2108
+ * Emits the Section 40(3) royalty expense: standard 50% capped at 100,000 THB, or actual capped
2109
+ * at the royalty income.
2110
+ *
2111
+ * Purpose: Mirrors the royalty expense deduction, including its actual-expense election.
2112
+ * Math: `min(floor(s3 * 0.5), 100000)` or `min(actual, s3)`.
2113
+ * Units: Integer satang Measures.
2114
+ * Throws: TaxValidationError if the elected actual expense is invalid.
2115
+ * @statute Thailand Revenue Code Section 42 ter; Royal Decree No. 11 Section 4.
2116
+ */
2117
+ function addSection40_3Expense(graph, tallies, choice) {
2118
+ const rate = TH_2026_PARAMETERS.expenses.section40_3;
2119
+ return addItemExpense(graph, {
2120
+ tally: tallies.income[ThLevyIncomeLabel.Section40_3],
2121
+ choice,
2122
+ rate: rate.rate,
2123
+ field: "expense.40_3",
2124
+ capSatang: toSatang(rate.ceiling, "expense.40_3")
2125
+ });
2126
+ }
2127
+ /**
2128
+ * Emits the Section 40(4) expense, statutorily zero.
2129
+ *
2130
+ * Purpose: Keeps the expense total's shape identical to the calculator's.
2131
+ * Math: `0`.
2132
+ * Units: Integer satang.
2133
+ * Throws: Nothing.
2134
+ * @statute Thailand Revenue Code Section 40(4).
2135
+ */
2136
+ function addSection40_4Expense(graph) {
2137
+ return graph.constant(0);
2138
+ }
2139
+ // src/th/levy/expenses-professions.ts
2140
+ /**
2141
+ * Emits the Section 40(6) liberal profession expense: 60% for medical, 30% for every other
2142
+ * profession; actual expenses cap at their class income.
2143
+ *
2144
+ * Purpose: Mirrors each profession class's standard percentage or actual-expense election.
2145
+ * Math: Per class, `floor(income * rate)` with medical 60% and the rest 30%.
2146
+ * Units: Integer satang Measures.
2147
+ * Throws: TaxValidationError if an elected actual expense is invalid.
2148
+ * @statute Thailand Royal Decree No. 11 Section 6.
2149
+ */
2150
+ function addSection40_6Expense(graph, tallies, choice) {
2151
+ const income = tallies.income;
2152
+ const rates = TH_2026_PARAMETERS.expenses.section40_6;
2153
+ return addExpenseLines(graph, [
2154
+ {
2155
+ tally: income[ThLevyIncomeLabel.Section40_6Medical],
2156
+ choice: choice?.medical,
2157
+ rate: rates.medical,
2158
+ field: "expense.40_6.medical"
2159
+ },
2160
+ {
2161
+ tally: income[ThLevyIncomeLabel.Section40_6Law],
2162
+ choice: choice?.law,
2163
+ rate: rates.other,
2164
+ field: "expense.40_6.law"
2165
+ },
2166
+ {
2167
+ tally: income[ThLevyIncomeLabel.Section40_6Engineering],
2168
+ choice: choice?.engineering,
2169
+ rate: rates.other,
2170
+ field: "expense.40_6.eng"
2171
+ },
2172
+ {
2173
+ tally: income[ThLevyIncomeLabel.Section40_6Architecture],
2174
+ choice: choice?.architecture,
2175
+ rate: rates.other,
2176
+ field: "expense.40_6.arch"
2177
+ },
2178
+ {
2179
+ tally: income[ThLevyIncomeLabel.Section40_6Accounting],
2180
+ choice: choice?.accounting,
2181
+ rate: rates.other,
2182
+ field: "expense.40_6.acc"
2183
+ },
2184
+ {
2185
+ tally: income[ThLevyIncomeLabel.Section40_6FineArts],
2186
+ choice: choice?.fineArts,
2187
+ rate: rates.other,
2188
+ field: "expense.40_6.art"
2189
+ },
2190
+ {
2191
+ tally: income[ThLevyIncomeLabel.Section40_6Other],
2192
+ choice: choice?.other,
2193
+ rate: rates.other,
2194
+ field: "expense.40_6.other"
2195
+ }
2196
+ ]);
2197
+ }
2198
+ // src/th/levy/expenses-rental.ts
2199
+ /**
2200
+ * Emits the Section 40(5) rental expense across the five property classes.
2201
+ *
2202
+ * Purpose: Mirrors each rental class's standard percentage or actual-expense election.
2203
+ * Math: Per class, `floor(income * rate)` with building 30%, agricultural land 20%,
2204
+ * non-agricultural land 15%, vehicles 30%, other 10%; actual expenses cap at their class income.
2205
+ * Units: Integer satang Measures.
2206
+ * Throws: TaxValidationError if an elected actual expense is invalid.
2207
+ * @statute Thailand Royal Decree No. 11 Section 5.
2208
+ */
2209
+ function addSection40_5Expense(graph, tallies, choice) {
2210
+ const income = tallies.income;
2211
+ const rates = TH_2026_PARAMETERS.expenses.section40_5;
2212
+ return addExpenseLines(graph, [
2213
+ {
2214
+ tally: income[ThLevyIncomeLabel.Section40_5Building],
2215
+ choice: choice?.building,
2216
+ rate: rates.building,
2217
+ field: "expense.40_5.building"
2218
+ },
2219
+ {
2220
+ tally: income[ThLevyIncomeLabel.Section40_5AgriculturalLand],
2221
+ choice: choice?.agriculturalLand,
2222
+ rate: rates.agriculturalLand,
2223
+ field: "expense.40_5.agri"
2224
+ },
2225
+ {
2226
+ tally: income[ThLevyIncomeLabel.Section40_5NonAgriculturalLand],
2227
+ choice: choice?.nonAgriculturalLand,
2228
+ rate: rates.nonAgriculturalLand,
2229
+ field: "expense.40_5.nonAgri"
2230
+ },
2231
+ {
2232
+ tally: income[ThLevyIncomeLabel.Section40_5Vehicles],
2233
+ choice: choice?.vehicles,
2234
+ rate: rates.vehicles,
2235
+ field: "expense.40_5.vehicles"
2236
+ },
2237
+ {
2238
+ tally: income[ThLevyIncomeLabel.Section40_5Other],
2239
+ choice: choice?.other,
2240
+ rate: rates.other,
2241
+ field: "expense.40_5.other"
2242
+ }
2243
+ ]);
2244
+ }
2245
+ // src/th/levy/expenses.ts
2246
+ /**
2247
+ * Emits every Section 40(1)–40(8) expense deduction and totals them.
2248
+ *
2249
+ * Purpose: Reproduces the calculator's `calculateExpenseDeductions` as Measures.
2250
+ * Math: Sum of the seven statutory expense buckets, each already rounded the way its stage does.
2251
+ * Units: Integer satang Measures.
2252
+ * Throws: TaxValidationError if an elected actual expense is invalid.
2253
+ *
2254
+ * @param graph Measure graph being built.
2255
+ * @param tallies Income tally ids.
2256
+ * @param elections Standard-versus-actual choices fixed at emit time.
2257
+ * @statute Thailand Revenue Code Sections 42 bis, 42 ter, 43–46; Royal Decree No. 11.
2258
+ */
2259
+ function addExpenseMeasures(graph, tallies, elections = {}) {
2260
+ return sumMeasures(graph, [
2261
+ addSection40_1_2Expense(graph, tallies),
2262
+ addSection40_3Expense(graph, tallies, elections.section40_3),
2263
+ addSection40_4Expense(graph),
2264
+ addSection40_5Expense(graph, tallies, elections.section40_5),
2265
+ addSection40_6Expense(graph, tallies, elections.section40_6),
2266
+ addSection40_7Expense(graph, tallies, elections.section40_7),
2267
+ addSection40_8Expense(graph, tallies, elections.section40_8)
2268
+ ], "expenses.total");
2269
+ }
2270
+ // src/th/levy/graph.ts
2271
+ /** Auto-generated Measure id prefix; explicit audited ids never start with it. */
2272
+ const AUTO_ID = "auto";
2273
+ /**
2274
+ * Builder for a sim-core `Levy` rule graph.
2275
+ *
2276
+ * Purpose: The only place that constructs Measures, so the emitted operation set is closed by
2277
+ * the {@link ThLevyMeasure} union and every id is unique.
2278
+ * Math: Each method appends one Measure; arithmetic is deferred to sim-core's exact evaluator.
2279
+ * Units: `const` values are integer satang; `num`/`den` of `scale` are dimensionless integers.
2280
+ * Throws: TaxValidationError for a duplicate id, non-safe const, or invalid scale fraction.
2281
+ *
2282
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 47 bis, 48(1)–(2).
2283
+ */
2284
+ var ThLevyGraph = class {
2285
+ list = [];
2286
+ ids = /* @__PURE__ */ new Set();
2287
+ serial = 0;
2288
+ /** Appends a fixed satang amount; `id` names an audited Measure. */
2289
+ constant(value, id) {
2290
+ if (!Number.isSafeInteger(value)) throw new TaxValidationError("levy.measure.const.value", value, "Const must be safe integer satang");
2291
+ return this.append({
2292
+ id: this.claim(id),
2293
+ op: "const",
2294
+ value
2295
+ });
2296
+ }
2297
+ /** Appends a tally over one label and basis. */
2298
+ tally(label, basis, id) {
2299
+ return this.append({
2300
+ id: this.claim(id),
2301
+ op: "tally",
2302
+ label,
2303
+ basis
2304
+ });
2305
+ }
2306
+ /** Appends `a + b`. */
2307
+ add(a, b, id) {
2308
+ return this.append({
2309
+ id: this.claim(id),
2310
+ op: "add",
2311
+ a,
2312
+ b
2313
+ });
2314
+ }
2315
+ /** Appends `a - b`. */
2316
+ sub(a, b, id) {
2317
+ return this.append({
2318
+ id: this.claim(id),
2319
+ op: "sub",
2320
+ a,
2321
+ b
2322
+ });
2323
+ }
2324
+ /** Appends `min(a, b)`. */
2325
+ min(a, b, id) {
2326
+ return this.append({
2327
+ id: this.claim(id),
2328
+ op: "min",
2329
+ a,
2330
+ b
2331
+ });
2332
+ }
2333
+ /** Appends `max(a, b)`. */
2334
+ max(a, b, id) {
2335
+ return this.append({
2336
+ id: this.claim(id),
2337
+ op: "max",
2338
+ a,
2339
+ b
2340
+ });
2341
+ }
2342
+ /** Appends `of * num / den`, rounded as `rounding` says. */
2343
+ scale(of, num, den, rounding, id) {
2344
+ if (!Number.isSafeInteger(num)) throw new TaxValidationError("levy.measure.scale.num", num, "Scale numerator must be an integer");
2345
+ if (!Number.isSafeInteger(den) || den < 1) throw new TaxValidationError("levy.measure.scale.den", den, "Scale denominator must be >= 1");
2346
+ return this.append({
2347
+ id: this.claim(id),
2348
+ op: "scale",
2349
+ of,
2350
+ num,
2351
+ den,
2352
+ rounding
2353
+ });
2354
+ }
2355
+ /** Appends `then` when `a > b`, otherwise `otherwise`. */
2356
+ pick(a, b, then, otherwise, id) {
2357
+ return this.append({
2358
+ id: this.claim(id),
2359
+ op: "pick",
2360
+ a,
2361
+ b,
2362
+ then,
2363
+ else: otherwise
2364
+ });
2365
+ }
2366
+ /** Finishes the graph as a Levy, checking that every operand and the due id exist. */
2367
+ build(id, period, settlement, due) {
2368
+ if (!this.ids.has(due)) throw new TaxValidationError("levy.measure.due", due, "Due must name an emitted Measure");
2369
+ for (const measure of this.list) for (const operand of operandsOf(measure)) if (!this.ids.has(operand)) throw new TaxValidationError("levy.measure.id", operand, "Operand must name an emitted Measure");
2370
+ return {
2371
+ id,
2372
+ period,
2373
+ settlement,
2374
+ measures: [...this.list],
2375
+ due
2376
+ };
2377
+ }
2378
+ append(measure) {
2379
+ this.list.push(measure);
2380
+ return measure.id;
2381
+ }
2382
+ claim(id) {
2383
+ const resolved = id ?? `${AUTO_ID}.${this.serial++}`;
2384
+ if (this.ids.has(resolved)) throw new TaxValidationError("levy.measure.id", resolved, "Duplicate Measure id");
2385
+ this.ids.add(resolved);
2386
+ return resolved;
2387
+ }
2388
+ };
2389
+ function operandsOf(measure) {
2390
+ switch (measure.op) {
2391
+ case "const":
2392
+ case "tally": return [];
2393
+ case "scale": return [measure.of];
2394
+ case "pick": return [
2395
+ measure.a,
2396
+ measure.b,
2397
+ measure.then,
2398
+ measure.else
2399
+ ];
2400
+ default: return [measure.a, measure.b];
2401
+ }
2402
+ }
2403
+ // src/th/levy/regimes.ts
2404
+ function withheld(graph, tally, rate) {
2405
+ const { num, den } = rateFraction(rate);
2406
+ return graph.scale(tally, num, den, ThLevyRounding.HalfUp);
2407
+ }
2408
+ /**
2409
+ * Emits the Section 40(4) split: sources elected final leave regular brackets and carry their
2410
+ * withholding tax; included sources join Section 40(4) income, and included dividends also earn
2411
+ * the Section 47 bis gross-up credit.
2412
+ *
2413
+ * Purpose: Applies each source's emit-time election and builds the Section 47 bis credit.
2414
+ * Math: Final tax = `round_half_up(amount * rate)`; credit = `round_half_up(dividend * r / (1-r))`;
2415
+ * assessable = sum of included sources + credit.
2416
+ * Units: Integer satang Measures; rates are dimensionless.
2417
+ * Throws: TaxValidationError if the corporate tax rate is outside [0, 1).
2418
+ *
2419
+ * @param graph Measure graph being built.
2420
+ * @param tallies Income tally ids.
2421
+ * @param elections Final-withholding and Section 47 bis elections fixed at emit time.
2422
+ * @statute Thailand Revenue Code Sections 40(4), 47 bis, 50(2)(g), 57 ter.
2423
+ */
2424
+ function addSection40_4(graph, tallies, elections = {}) {
2425
+ const rates = TH_2026_PARAMETERS.finalWithholding;
2426
+ const income = tallies.income;
2427
+ const corpTaxRate = elections.dividendCorpTaxRate;
2428
+ if (corpTaxRate !== void 0) validateCorpTaxRate(corpTaxRate);
2429
+ const isFinal = (election) => election === ThFinalWithholdingElection.Final;
2430
+ const included = [income[ThLevyIncomeLabel.Section40_4Other]];
2431
+ const finalTaxes = [];
2432
+ const interest = income[ThLevyIncomeLabel.Section40_4Interest];
2433
+ if (isFinal(elections.interest)) finalTaxes.push(withheld(graph, interest, rates.interestRate));
2434
+ else included.push(interest);
2435
+ const dividend = income[ThLevyIncomeLabel.Section40_4Dividends];
2436
+ const isDividendIncluded = !isFinal(elections.dividends);
2437
+ if (isDividendIncluded) included.push(dividend);
2438
+ else finalTaxes.push(withheld(graph, dividend, rates.dividendRate));
2439
+ const digital = income[ThLevyIncomeLabel.Section40_4DigitalAssets];
2440
+ if (isFinal(elections.digitalAssets)) finalTaxes.push(withheld(graph, digital, rates.digitalAssetRate));
2441
+ else included.push(digital);
2442
+ let credit;
2443
+ if (isDividendIncluded && corpTaxRate !== void 0) {
2444
+ const { num, den } = rateFraction(corpTaxRate);
2445
+ credit = graph.scale(dividend, num, den - num, ThLevyRounding.HalfUp, "credit.dividend");
2446
+ included.push(credit);
2447
+ } else credit = graph.constant(0, "credit.dividend");
2448
+ return {
2449
+ assessable: sumMeasures(graph, included),
2450
+ finalWithholdingTax: sumMeasures(graph, finalTaxes, "tax.final_withholding"),
2451
+ dividendCredit: credit
2452
+ };
2453
+ }
2454
+ // src/th/levy/tallies.ts
2455
+ function tallyRecord(graph, labels, basis) {
2456
+ const record = {};
2457
+ for (const label of labels) record[label] = graph.tally(label, basis);
2458
+ return record;
2459
+ }
2460
+ /**
2461
+ * Emits one tally Measure per exported label, so a caller can label any subset of Flows.
2462
+ *
2463
+ * Purpose: Fixes the caller-facing contract before any statutory arithmetic depends on it.
2464
+ * Math: No arithmetic; each Measure only reads its label from the matching sim-core tally map.
2465
+ * Units: Tallied amounts are integer satang supplied by the caller.
2466
+ * Throws: Nothing; the graph validates ids only.
2467
+ *
2468
+ * @param graph Measure graph being built.
2469
+ * @statute Thailand Revenue Code Sections 40(1)–(8), 47.
2470
+ */
2471
+ function addThLevyTallies(graph) {
2472
+ return {
2473
+ income: tallyRecord(graph, TH_LEVY_INCOME_LABELS, ThLevyTallyBasis.Gross),
2474
+ contributions: tallyRecord(graph, TH_LEVY_CONTRIBUTION_LABELS, ThLevyTallyBasis.Funded)
2475
+ };
2476
+ }
2477
+ // src/th/levy/emit.ts
2478
+ /** Default Levy id: Thailand Personal Income Tax for the 2026 tax year. */
2479
+ const DEFAULT_ID = "th-2026-pit";
2480
+ /** Default period start: the calendar tax year. */
2481
+ const DEFAULT_PERIOD = {
2482
+ month: 1,
2483
+ day: 1
2484
+ };
2485
+ /** Default Settlement: the annual return date following the tax year (Revenue Code Section 56). */
2486
+ const DEFAULT_SETTLEMENT = {
2487
+ month: 3,
2488
+ day: 31
2489
+ };
2490
+ /**
2491
+ * Emits a Thailand 2026 Personal Income Tax rule set as sim-core Levy data.
2492
+ *
2493
+ * Purpose: Lets sim-core charge the annual PIT assessment from Flow tallies without importing
2494
+ * this package. The profile, elections, and 2026 statutory parameters are frozen at emit time;
2495
+ * income and contribution amounts arrive as Flow tallies under the exported labels.
2496
+ * Math: `due = max(progressive, minimum gross tax) + final withholding tax
2497
+ * - final withholding tax - Section 47 bis credit`, which is the calculator's `netTaxPayable`
2498
+ * when no separate sheet, real estate, or other withholding is supplied.
2499
+ * Units: Every monetary Measure is integer satang.
2500
+ * Throws: TaxValidationError for an invalid profile allowance, donation, actual expense, or rate.
2501
+ *
2502
+ * @param profile Taxpayer profile and elections fixed at emit time.
2503
+ * @statute Thailand Revenue Code Sections 40, 42 bis, 47, 47 bis, 48(1)–(2), 50(2)(g), 56.
2504
+ */
2505
+ function emitThLevy(profile = {}) {
2506
+ const graph = new ThLevyGraph();
2507
+ const tallies = addThLevyTallies(graph);
2508
+ const section40_4 = addSection40_4(graph, tallies, profile.section40_4);
2509
+ const assessable = addAssessableMeasures(graph, tallies, section40_4);
2510
+ const expenses = addExpenseMeasures(graph, tallies, profile.expenses);
2511
+ const regular = addRegularTax(graph, addProgressiveTax(graph, addAllowanceMeasures(graph, tallies, profile, assessable.total, expenses).netTaxable), addMinimumGrossTax(graph, assessable.grossNonEmployment).applicable);
2512
+ const total = graph.add(regular, section40_4.finalWithholdingTax, "tax.total");
2513
+ const credits = graph.add(section40_4.finalWithholdingTax, section40_4.dividendCredit);
2514
+ const netPayable = graph.sub(total, credits, "tax.net_payable");
2515
+ return graph.build(profile.levy?.id ?? DEFAULT_ID, profile.levy?.period ?? DEFAULT_PERIOD, profile.levy?.settlement ?? DEFAULT_SETTLEMENT, netPayable);
2516
+ }
1457
2517
  // src/th/system.ts
1458
2518
  /**
1459
2519
  * Thailand Tax System implementation for 2026.
@@ -1468,6 +2528,6 @@ const th = {
1468
2528
  return calculateThTax(input);
1469
2529
  }
1470
2530
  };
1471
- export { TH_2026_PARAMETERS, ThFinalWithholdingElection, ThRealEstateAcquisitionType, ThRealEstateExpenseBasis, ThSeparateSheetPaymentType, calculateThTax, th };
2531
+ export { TH_2026_PARAMETERS, TH_LEVY_CONTRIBUTION_LABELS, TH_LEVY_INCOME_LABELS, ThFinalWithholdingElection, ThLevyContributionLabel, ThLevyIncomeLabel, ThLevyMeasureId, ThLevyRounding, ThLevyTallyBasis, ThRealEstateAcquisitionType, ThRealEstateExpenseBasis, ThSeparateSheetPaymentType, calculateThTax, emitThLevy, th };
1472
2532
 
1473
2533
  //# sourceMappingURL=index.mjs.map