Skip to content

Commit 16dd101

Browse files
HR-codingclaude
andcommitted
fix(tax): correct slabs to current law + single source of truth
- tax_rules.json: current new-regime slabs (0-4L..24L+), 87A rebate at 12L/60k with marginal relief, and special rates STCG 20% / LTCG 12.5% / 1.25L exemption - app/core/tax_rules.py: single loader read by both calculators and the agent's retrieve_tax_rules_tool; removed duplicated hardcoded slab functions - new-regime 87A marginal relief implemented (tax capped at income over the limit) - README: Links + Team sections, honest "prepares a file-ready return" scope note - frontend: drop year label, reframe overclaiming copy - tests updated to current rates (174 passing) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e1b5cbf commit 16dd101

9 files changed

Lines changed: 186 additions & 147 deletions

File tree

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ untrusted and can never tamper with tax data or leak personal information.
1111

1212
> Architecture details: see **[SYSTEM_ARCHITECTURE.md](SYSTEM_ARCHITECTURE.md)**.
1313
14+
## Links
15+
16+
- **Live app:** https://taxassist-267786664719.asia-south1.run.app/
17+
- **Demo video (≤3 min):** <!-- TODO: paste the public YouTube/Vimeo URL here -->
18+
- **Partner track:** **MongoDB** — via the official MongoDB MCP server (see [MongoDB MCP integration](SYSTEM_ARCHITECTURE.md#mongodb-mcp-integration)).
19+
- **License:** [Apache-2.0](LICENSE) — also set in the GitHub repo's *About* sidebar.
20+
21+
## Team
22+
23+
<!-- TODO: list every team member (name — role / handle). All members must also be added on the submission form. -->
24+
- _Your Name_ — role
25+
- _Teammate_ — role
26+
1427
---
1528

1629
## What it does
@@ -19,12 +32,18 @@ untrusted and can never tamper with tax data or leak personal information.
1932
with Gemini 2.5-flash vision (handles dense tables and scans).
2033
- **Human-in-the-loop over email**: emails the user to confirm extracted values /
2134
approve computation; reads the replies and resumes — as durable, resumable runs.
22-
- **Deterministic tax engine**: ITR-1 & ITR-2 computed from gov slab rates (no LLM in
23-
the math), old vs new regime comparison.
35+
- **Deterministic tax engine**: ITR-1 & ITR-2 computed from official slab rates (no LLM in
36+
the math), old vs new regime comparison, with section 87A rebate and new-regime marginal
37+
relief. All rates live in a **single source of truth** (`app/core/tax_rules.json`) that the
38+
calculators and the agent's `retrieve_tax_rules_tool` both read — so they can never diverge.
2439
- **Google Workspace**: Gmail, Calendar (deadline reminders), Sheets (findings + result),
2540
Drive — per user.
2641
- **Multi-tenant**: one account → many profiles (e.g. self + spouse), each isolated.
2742

43+
> **Scope:** TaxAssist prepares a **file-ready** return — it extracts, verifies, computes and
44+
> writes the results to your Google Sheet. It does **not** submit to the income-tax portal;
45+
> the final e-filing step stays with you. Surcharge on incomes above ₹50L is not yet modelled.
46+
2847
## Tech stack
2948

3049
| Layer | Tech |

app/core/itr1_calculator.py

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""
2-
Deterministic ITR-1 tax calculator for FY 2025-26 (AY 2026-27).
3-
Source: https://www.incometaxindia.gov.in/
2+
Deterministic ITR-1 tax calculator.
3+
Slab rates, rebate and cess are loaded from the single source of truth
4+
(tax_rules.json via app.core.tax_rules), grounded in
5+
https://www.incometaxindia.gov.in/.
46
57
Reads from the new array-based schema (schemas.jsonc).
68
Supports both old and new tax regimes.
79
"""
810
from app.core.field_calculator import _sum_list, _nf
11+
from app.core import tax_rules
912

1013

1114
def calculate_itr1_tax(itr_doc: dict) -> dict:
@@ -29,7 +32,7 @@ def calculate_itr1_tax(itr_doc: dict) -> dict:
2932
if net_salary == 0.0:
3033
gross = _nf(si, "gross_salary")
3134
exempt = _nf(si, "exempt_allowances")
32-
std_ded = 75000.0 if tax_regime == "NEW" else 50000.0
35+
std_ded = tax_rules.standard_deduction(tax_regime)
3336
professional_tax = _nf(si, "professional_tax")
3437
net_salary = max(gross - exempt - std_ded - professional_tax, 0.0)
3538

@@ -62,21 +65,11 @@ def calculate_itr1_tax(itr_doc: dict) -> dict:
6265

6366
taxable_income = max(gross_total_income - total_deductions, 0.0)
6467

65-
# ── Tax slab computation ──────────────────────────────────────────────
66-
if tax_regime == "OLD":
67-
tax = _compute_old_regime_tax(taxable_income)
68-
rebate_limit = 500000.0
69-
rebate_max = 12500.0
70-
else:
71-
tax = _compute_new_regime_tax(taxable_income)
72-
rebate_limit = 700000.0
73-
rebate_max = 25000.0
74-
75-
# Section 87A rebate
76-
if taxable_income <= rebate_limit:
77-
tax = max(tax - min(tax, rebate_max), 0.0)
78-
79-
tax = round(tax * 1.04, 2) # 4% cess
68+
# ── Tax slab computation (rates from the single source of truth) ──────
69+
tax = tax_rules.slab_tax(taxable_income, tax_regime)
70+
# Section 87A rebate (+ new-regime marginal relief just above the limit)
71+
tax = tax_rules.apply_rebate_and_relief(taxable_income, tax, tax_regime)
72+
tax = round(tax * (1.0 + tax_rules.cess_rate(tax_regime)), 2) # health & education cess
8073

8174
# ── Taxes paid ────────────────────────────────────────────────────────
8275
tp = itr_doc.get("taxes_paid", {})
@@ -141,35 +134,3 @@ def calculate_itr1_with_comparison(itr_doc: dict, chosen: str = "NEW") -> dict:
141134
"new_regime_payable": new["net_tax_payable"],
142135
"old_regime_payable": old["net_tax_payable"],
143136
}
144-
145-
146-
def _compute_old_regime_tax(income: float) -> float:
147-
tax = 0.0
148-
if income > 1000000:
149-
tax += (income - 1000000) * 0.30
150-
income = 1000000.0
151-
if income > 500000:
152-
tax += (income - 500000) * 0.20
153-
income = 500000.0
154-
if income > 250000:
155-
tax += (income - 250000) * 0.05
156-
return tax
157-
158-
159-
def _compute_new_regime_tax(income: float) -> float:
160-
tax = 0.0
161-
if income > 1500000:
162-
tax += (income - 1500000) * 0.30
163-
income = 1500000.0
164-
if income > 1200000:
165-
tax += (income - 1200000) * 0.20
166-
income = 1200000.0
167-
if income > 900000:
168-
tax += (income - 900000) * 0.15
169-
income = 900000.0
170-
if income > 600000:
171-
tax += (income - 600000) * 0.10
172-
income = 600000.0
173-
if income > 300000:
174-
tax += (income - 300000) * 0.05
175-
return tax

app/core/itr2_calculator.py

Lines changed: 19 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""
2-
Deterministic ITR-2 tax calculator for FY 2025-26 (AY 2026-27).
3-
Source: https://www.incometaxindia.gov.in/
2+
Deterministic ITR-2 tax calculator.
3+
Slab rates, rebate, cess and the capital-gains / VDA special rates are loaded
4+
from the single source of truth (tax_rules.json via app.core.tax_rules),
5+
grounded in https://www.incometaxindia.gov.in/.
46
57
Reads from the new schema structure (schemas.jsonc).
68
"""
79
from app.core.field_calculator import _sum_list, _sum_nf_list, _nf
10+
from app.core import tax_rules
811

912

1013
def calculate_itr2_tax(itr_doc: dict) -> dict:
@@ -49,8 +52,8 @@ def calculate_itr2_tax(itr_doc: dict) -> dict:
4952
for i in cg.get("long_term_gains", [])
5053
if isinstance(i, dict)
5154
)
52-
# LTCG exemption: 1L under Sec 112A for listed shares
53-
ltg_taxable = max(ltg - 100000.0, 0.0)
55+
# LTCG exemption under Sec 112A for listed shares (rate from single source)
56+
ltg_taxable = max(ltg - tax_rules.capital_gains_rates()["ltcg_exemption"], 0.0)
5457
else:
5558
stg = ltg_taxable = 0.0
5659

@@ -91,30 +94,21 @@ def calculate_itr2_tax(itr_doc: dict) -> dict:
9194
regular_income = gross_salary + hp_income + other_income
9295
taxable_income = max(regular_income - total_deductions, 0.0)
9396

94-
# ── Tax slabs on regular income only ────────────────────────────────
97+
# ── Tax on regular slab income (rates from the single source of truth)
9598
tax_regime = itr_doc.get("tax_regime", "NEW").upper()
96-
if tax_regime == "OLD":
97-
tax = _compute_old_regime_tax(taxable_income)
98-
rebate_limit = 500000.0
99-
rebate_max = 12500.0
100-
else:
101-
tax = _compute_new_regime_tax(taxable_income)
102-
rebate_limit = 700000.0
103-
rebate_max = 25000.0
104-
105-
# STCG at 15% (Sec 111A), LTCG at 10% (Sec 112A), VDA at 30% — special flat rates
106-
stg_tax = stg * 0.15
107-
ltg_tax = ltg_taxable * 0.10
108-
vda_tax = vda_income * 0.30
109-
tax = tax + stg_tax + ltg_tax + vda_tax
99+
regular_tax = tax_rules.slab_tax(taxable_income, tax_regime)
100+
# Section 87A rebate / marginal relief applies only to regular slab tax,
101+
# never to the special-rate capital-gains / VDA income.
102+
regular_tax = tax_rules.apply_rebate_and_relief(taxable_income, regular_tax, tax_regime)
110103

111-
# Section 87A rebate applies only to regular slab tax (not CG/VDA)
112-
if taxable_income <= rebate_limit:
113-
regular_tax = tax - stg_tax - ltg_tax - vda_tax
114-
rebate = min(regular_tax, rebate_max)
115-
tax = max(tax - rebate, 0.0)
104+
# Special flat rates: STCG (Sec 111A), LTCG (Sec 112A), VDA (Sec 115BBH)
105+
rates = tax_rules.capital_gains_rates()
106+
stg_tax = stg * rates["stcg_rate"]
107+
ltg_tax = ltg_taxable * rates["ltcg_rate"]
108+
vda_tax = vda_income * rates["vda_rate"]
109+
tax = regular_tax + stg_tax + ltg_tax + vda_tax
116110

117-
tax = round(tax * 1.04, 2) # 4% cess
111+
tax = round(tax * (1.0 + tax_rules.cess_rate(tax_regime)), 2) # health & education cess
118112

119113
# ── Taxes paid ────────────────────────────────────────────────────────
120114
tp = itr_doc.get("taxes_paid", {})
@@ -145,35 +139,3 @@ def calculate_itr2_tax(itr_doc: dict) -> dict:
145139
"refund_due": round(refund_due, 2),
146140
"tax_regime": tax_regime
147141
}
148-
149-
150-
def _compute_old_regime_tax(income: float) -> float:
151-
tax = 0.0
152-
if income > 1000000:
153-
tax += (income - 1000000) * 0.30
154-
income = 1000000.0
155-
if income > 500000:
156-
tax += (income - 500000) * 0.20
157-
income = 500000.0
158-
if income > 250000:
159-
tax += (income - 250000) * 0.05
160-
return tax
161-
162-
163-
def _compute_new_regime_tax(income: float) -> float:
164-
tax = 0.0
165-
if income > 1500000:
166-
tax += (income - 1500000) * 0.30
167-
income = 1500000.0
168-
if income > 1200000:
169-
tax += (income - 1200000) * 0.20
170-
income = 1200000.0
171-
if income > 900000:
172-
tax += (income - 900000) * 0.15
173-
income = 900000.0
174-
if income > 600000:
175-
tax += (income - 600000) * 0.10
176-
income = 600000.0
177-
if income > 300000:
178-
tax += (income - 300000) * 0.05
179-
return tax

app/core/tax_rules.json

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
2-
"_comment": "Indian Income Tax Rules FY 2025-26 (AY 2026-27). Source: https://www.incometaxindia.gov.in/",
2+
"_comment": "Indian Income Tax slab rates and limits — single source of truth, grounded in https://www.incometaxindia.gov.in/. The applicable assessment year is not hardcoded in product copy.",
33

44
"new_regime": {
5-
"_note": "Default regime from FY 2023-24. No chapter VIA deductions except 80CCD(2) and 80CCH.",
5+
"_note": "Default regime. No chapter VIA deductions except 80CCD(2) and 80CCH. Income up to the 87A rebate limit is fully rebated; marginal relief applies just above it.",
66
"standard_deduction": 75000,
7-
"rebate_87a_limit": 700000,
8-
"rebate_87a_amount": 25000,
7+
"rebate_87a_limit": 1200000,
8+
"rebate_87a_amount": 60000,
9+
"rebate_87a_marginal_relief": true,
910
"surcharge_rates": [
1011
{"above": 5000000, "rate": 0.10},
1112
{"above": 10000000, "rate": 0.15},
@@ -14,12 +15,13 @@
1415
],
1516
"cess_rate": 0.04,
1617
"tax_slabs": [
17-
{"from": 0, "to": 300000, "rate": 0.00, "label": "0–3L: 0%"},
18-
{"from": 300000, "to": 600000, "rate": 0.05, "label": "3L–6L: 5%"},
19-
{"from": 600000, "to": 900000, "rate": 0.10, "label": "6L–9L: 10%"},
20-
{"from": 900000, "to": 1200000, "rate": 0.15, "label": "9L–12L: 15%"},
21-
{"from": 1200000, "to": 1500000, "rate": 0.20, "label": "12L–15L: 20%"},
22-
{"from": 1500000, "to": null, "rate": 0.30, "label": "15L+: 30%"}
18+
{"from": 0, "to": 400000, "rate": 0.00, "label": "0–4L: 0%"},
19+
{"from": 400000, "to": 800000, "rate": 0.05, "label": "4L–8L: 5%"},
20+
{"from": 800000, "to": 1200000, "rate": 0.10, "label": "8L–12L: 10%"},
21+
{"from": 1200000, "to": 1600000, "rate": 0.15, "label": "12L–16L: 15%"},
22+
{"from": 1600000, "to": 2000000, "rate": 0.20, "label": "16L–20L: 20%"},
23+
{"from": 2000000, "to": 2400000, "rate": 0.25, "label": "20L–24L: 25%"},
24+
{"from": 2400000, "to": null, "rate": 0.30, "label": "24L+: 30%"}
2325
]
2426
},
2527

@@ -86,8 +88,9 @@
8688
"schedule_foreign_assets": "PENDING",
8789
"schedule_via_deductions": "PENDING"
8890
},
89-
"stcg_rate": 0.15,
90-
"ltcg_rate": 0.10,
91-
"ltcg_exemption": 100000
91+
"stcg_rate": 0.20,
92+
"ltcg_rate": 0.125,
93+
"ltcg_exemption": 125000,
94+
"vda_rate": 0.30
9295
}
9396
}

app/core/tax_rules.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Single source of truth for Indian income-tax slab rates, rebates and special
3+
rates.
4+
5+
Every number used by the deterministic calculators (slabs, standard deduction,
6+
87A rebate, cess, and the capital-gains / VDA special rates) is loaded from
7+
``tax_rules.json`` — grounded in https://www.incometaxindia.gov.in/. No other
8+
module hardcodes slab numbers, so the rates are updated in exactly one place and
9+
the agent's ``retrieve_tax_rules_tool`` and the calculators can never diverge.
10+
11+
All functions here are pure and deterministic — no LLM, no network.
12+
"""
13+
import os
14+
import json
15+
import functools
16+
17+
_PATH = os.path.join(os.path.dirname(__file__), "tax_rules.json")
18+
19+
20+
@functools.lru_cache(maxsize=1)
21+
def load_rules() -> dict:
22+
with open(_PATH, "r", encoding="utf-8") as f:
23+
return json.load(f)
24+
25+
26+
def regime_rules(regime: str) -> dict:
27+
rules = load_rules()
28+
key = (regime or "new").lower() + "_regime"
29+
return rules.get(key, rules["new_regime"])
30+
31+
32+
def standard_deduction(regime: str) -> float:
33+
return float(regime_rules(regime).get("standard_deduction", 0.0))
34+
35+
36+
def cess_rate(regime: str) -> float:
37+
return float(regime_rules(regime).get("cess_rate", 0.04))
38+
39+
40+
def rebate_87a(regime: str):
41+
"""Return (income_limit, max_rebate) for the section 87A rebate."""
42+
r = regime_rules(regime)
43+
return (float(r.get("rebate_87a_limit", 0.0)),
44+
float(r.get("rebate_87a_amount", 0.0)))
45+
46+
47+
def has_marginal_relief(regime: str) -> bool:
48+
return bool(regime_rules(regime).get("rebate_87a_marginal_relief", False))
49+
50+
51+
def slab_tax(taxable: float, regime: str) -> float:
52+
"""Progressive slab tax computed from the JSON slab table for the regime."""
53+
tax = 0.0
54+
for slab in regime_rules(regime).get("tax_slabs", []):
55+
lo = float(slab.get("from", 0) or 0)
56+
hi = slab.get("to")
57+
rate = float(slab.get("rate", 0) or 0)
58+
if taxable > lo:
59+
upper = taxable if hi is None else min(taxable, float(hi))
60+
tax += (upper - lo) * rate
61+
return tax
62+
63+
64+
def apply_rebate_and_relief(taxable: float, slab_tax_amount: float, regime: str) -> float:
65+
"""
66+
Apply the section 87A rebate and (where applicable) marginal relief to the
67+
*slab* tax for a regime.
68+
69+
- At or below the rebate income limit: the rebate (capped at the configured
70+
max) reduces the tax, fully wiping it out when the limit equals the tax.
71+
- Just above the limit (new-regime marginal relief): the tax can never
72+
exceed the amount by which income crosses the limit.
73+
"""
74+
limit, max_rebate = rebate_87a(regime)
75+
if taxable <= limit:
76+
return max(slab_tax_amount - min(slab_tax_amount, max_rebate), 0.0)
77+
if has_marginal_relief(regime):
78+
return min(slab_tax_amount, taxable - limit)
79+
return slab_tax_amount
80+
81+
82+
def capital_gains_rates() -> dict:
83+
"""Special flat rates for ITR-2 (capital gains and virtual digital assets)."""
84+
itr2 = load_rules().get("ITR2", {})
85+
return {
86+
"stcg_rate": float(itr2.get("stcg_rate", 0.20)),
87+
"ltcg_rate": float(itr2.get("ltcg_rate", 0.125)),
88+
"ltcg_exemption": float(itr2.get("ltcg_exemption", 125000.0)),
89+
"vda_rate": float(itr2.get("vda_rate", 0.30)),
90+
}

app/tests/integration/test_endpoints.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,12 @@ def test_new_user_no_prereqs_halts(self):
154154
self.assertTrue(len(result["unmet_dependencies"]) > 0)
155155

156156
def test_prereqs_done_no_doc_calculates_if_checklist_clear(self):
157-
# All checklist VERIFIED, milestones done → tax should calculate
157+
# All checklist VERIFIED, milestones done → tax should calculate.
158+
# Income is above the 12L new-regime 87A rebate limit, so tax is payable.
158159
itr_doc = {
159160
"itr_type": "ITR1", "tax_regime": "NEW",
160-
"salary_income": {"gross_salary": {"value": 1000000},
161-
"net_salary_income": {"value": 925000}},
161+
"salary_income": {"gross_salary": {"value": 1500000},
162+
"net_salary_income": {"value": 1425000}},
162163
"house_property": {"net_house_property_income": {"value": 0}},
163164
"other_sources": {"net_other_sources_income": {"value": 0}},
164165
"deductions": {"total_chapter_via_deductions": {"value": 0}},

0 commit comments

Comments
 (0)