TDS (India)
Tax Deducted at Source on supplier payments — sections 194C / 194J / 194Q. Calculator, threshold logic, 26Q CSV export, and the known-outstanding UI work.
1. Sections covered
Sourced from the seed rates in
supabase/migrations/20260418150000_tds_scaffolding.sql:113-134 and
mirrored in src/lib/tds.ts:38-79.
| Section | Description | Deductee types | Rate | Single threshold | Aggregate threshold |
|---|---|---|---|---|---|
194C |
Contractor / works contract | individual, huf |
1.0% | ₹30,000 | ₹1,00,000 |
194C |
Contractor / works contract | company |
2.0% | ₹30,000 | ₹1,00,000 |
194J |
Professional / technical fees | any |
10.0% | ₹30,000 | — (none) |
194Q |
Purchase of goods (FY aggregate > ₹50L) | any |
0.1% | — | ₹50,00,000 |
Payroll TDS is out of scope
Sections 192 / 24Q (salary TDS) belong to the payroll module. They
use a slab-based calculation, not a flat rate. Do not extend
src/lib/tds.ts for payroll.
2. Data model
TDSRate — rate master
Keyed by (section, deducteeType, effectiveFrom) so rates can be revised
without losing history. Fields:
section—'194C' | '194J' | '194Q'(constraint-free text).deducteeType—'individual' | 'huf' | 'company' | 'any'.rate— percentage, e.g.1.000.thresholdSingle,thresholdAggregate.effectiveFrom,effectiveTo,active.
TDSDeduction — ledger
One row per actual deduction applied. Links to the source transaction
(paymentId or supplierTransactionId), the challan (Form 281), and
the certificate (Form 16A).
Status: pending → deposited → certificate_issued.
Supplier — new columns
tdsSection— default TDS section for this supplier (e.g.'194C').deducteeType— default deductee type (default'company').panNumber— for the 26Q return.
Pre-populates the "Deduct TDS" toggle on supplier-payment forms.
Chart of accounts
2401 TDS Payable— Current Liability, under Duties & Taxes.FinanceConfig.tdsPayableAccountCode/tdsPayableAccountIdpoint at it. Default is'2401'.
3. Calculator
src/lib/tds.ts. All pure functions.
resolveTdsRate(section, deducteeType, rates?)
src/lib/tds.ts:112-121. Finds the rate row. Exact deducteeType match
wins; falls back to the 'any' row for that section; returns null if
no row matches.
calculateTds(input)
src/lib/tds.ts:136-214. Input:
{
section: '194C' | '194J' | '194Q',
deducteeType: 'individual' | 'huf' | 'company' | 'any',
amount: number,
aggregateYearToDate?: number,
rates?: TDSRate[], // override for testing
}
Output includes tdsAmount, rateApplied, netAmount, grossAmount,
belowThreshold, rate, reason.
Threshold logic (matches the Act)
Three shapes, selected by which thresholds are non-zero:
- 194Q-style — only
thresholdAggregate > 0. TDS applies only on the portion exceeding the FY aggregate cap (src/lib/tds.ts:154-179). The first ₹50 lakh is exempt; every rupee after is taxed at the rate. - 194J-style — only
thresholdSingle > 0. Exempt when this single payment is below the threshold. - 194C-style — both thresholds set. Exempt only when both the single payment AND the YTD aggregate are below their thresholds. Either exceeding triggers the deduction on the full amount.
Returns belowThreshold: true, tdsAmount: 0 when exempt, with a
human-readable reason for the UI to surface.
Rounding
Amounts are rounded via roundMoney(...) (paise). The calculator
never emits fractional paise.
4. 26Q CSV export
generate26QCsv(deductions, fromDate?, toDate?) at
src/lib/tds.ts:282-319.
Columns (14)
src/lib/tds.ts:249-264. A subset of the CBDT 26Q schema — the fields
most audits actually check:
Serial No, Deductee PAN, Deductee Name, Section, Deductee Type,
Payment Date, Gross Amount, Rate (%), TDS Amount, Net Paid,
Challan No, Challan Date, Certificate No, Status
Not the full return
This is a quarterly-review spreadsheet, not the upload file. The full 26Q return is filed via the TDS utility using this same data (re-keyed into the utility).
Dates are filtered by the first 10 chars of createdAt so the
comparison is timezone-agnostic.
5. Endpoints
All live in handleFinanceTds at src/lib/api.ts:24289.
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET |
/finance/tds/rates |
finance.tds.view |
Active rate master, sorted by section + effectiveFrom. |
GET |
/finance/tds/deductions |
finance.tds.view |
Deduction ledger with supplier name. Filterable by section, status, fromDate, toDate. |
GET |
/finance/tds/26q |
finance.tds.export |
Returns { csv, count, fromDate, toDate, filename }. |
6. Permissions
From supabase/migrations/20260418150100_tds_permissions.sql:
| Permission | Effect |
|---|---|
finance.tds.view |
Read rate master + deduction ledger. |
finance.tds.deduct |
Apply TDS on a supplier payment. |
finance.tds.export |
Export the quarterly 26Q CSV. |
Role grants
| Role | view | deduct | export |
|---|---|---|---|
CEO, GM, IT_ADMIN, ADMIN_HR |
✓ | ✓ | ✓ |
FINANCE_MANAGER, ACCOUNTANT |
✓ | ✓ | ✓ |
CASHIER |
✓ | ✓ | — |
AUDITOR |
✓ | — | ✓ |
7. UI — current state and follow-ups
A port-forward TDS UI toggle on the supplier-payment voucher form is the primary near-term piece of work.
Known follow-ups (explicitly marked TODO)
- UI toggle on supplier payments — surface the "Deduct TDS" switch
on the supplier-payment voucher form so cashiers with
finance.tds.deductcan apply TDS while recording the payment. Pre-populate section + deducteeType + PAN fromSupplier.tdsSection / deducteeType / panNumber. - Auto-compute
aggregateYearToDate— the calculator takes a YTD argument but nothing populates it today. Needs a helper that sumsTDSDeduction.grossAmountYTD per(supplierId, section)and threads it into thecalculateTdscall. - Challan PATCH endpoint — no
PATCH /finance/tds/deductions/:idexists. Once a challan is deposited, operators should be able to flip status todepositedand fillchallanNo/challanDatefrom the UI. - Certificate upload (Form 16A) — future work.
certificateNois already on the row but there is no file-attach flow.
8. Tests
src/lib/tds.test.ts covers:
- Rate resolution (exact vs
anyfallback, no-match path). - 194C below / above thresholds on both sides (single, aggregate).
- 194J (single-threshold only).
- 194Q (aggregate-only, taxed only on excess).
- 26Q CSV header row, ordering, date filter, CSV escaping.
Run: npx vitest run src/lib/tds.test.ts.
9. Related files
src/lib/tds.ts— calculator + 26Q generator.src/lib/tds.test.ts— fixtures mirror the seed rates exactly.supabase/migrations/20260418150000_tds_scaffolding.sql— tables, seeds, supplier columns, chart-of-accounts entry.supabase/migrations/20260418150100_tds_permissions.sql— the threefinance.tds.*permissions and their role grants.src/lib/api.ts:24289—handleFinanceTds(three endpoints).