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 (search for
handleFinanceTds).
| 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. |
PATCH |
/finance/tds/deductions/:id |
finance.tds.deduct |
Update challanNo, challanDate, status after the monthly Form 281 is paid. Body: { challanNo?, challanDate?, status? }. Idempotent — replaying the same patch short-circuits with { idempotent: true } and emits no database write. |
POST |
/finance/tds/deductions/:id/certificate |
finance.tds.deduct |
Multipart upload for the Form 16A certificate (PDF or image — PNG/JPG/WEBP/GIF). Stores the file in the lead-attachments bucket under tds-certificates/<id>/…, records the URL + storage key on the row, and flips status to certificate_issued. Optional certificateNo form field writes the TRACES certificate number in the same call. |
GET |
/finance/tds/deductions/:id/certificate |
finance.tds.view |
Returns { certificateUrl, certificateStorageKey, certificateUploadedAt, certificateNo, status }. 404s when no certificate has been uploaded yet. |
GET |
/finance/tds/26q |
finance.tds.export |
Returns { csv, count, fromDate, toDate, filename }. |
Behavioural tests for the new endpoints live in
src/lib/api.finance.tds.test.ts.
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
The supplier-payment voucher (Finance → Vouchers → Payment) exposes
a Deduct TDS switch when the operator holds finance.tds.deduct
and the party type is supplier. Flipping it on pre-populates TDS
section + deductee type from the supplier's master defaults
(Supplier.tdsSection / deducteeType / panNumber) and shows a live
gross/TDS/net preview powered by calculateTds(...) from
src/lib/tds.ts. On submit the voucher posts through
POST /suppliers/:id/transactions with an applyTds payload; the
backend takes the 3-line path (Dr Supplier, Cr Bank net, Cr 2401 TDS
Payable) and writes the matching TDSDeduction row.
The year-to-date aggregate is summed from
supplierVoucherTransactions already loaded for the allocation
register — no extra round-trip. When the preview says "below
threshold", the voucher still posts but falls back to the plain 2-line
supplier-payment journal (see createSupplierPaymentWithTdsPosting in
src/lib/financeOperations.ts).
Challan management — operators flip a deduction to deposited with
challanNo + challanDate via the PATCH endpoint above. Form 16A
certificate upload/download rides the POST/GET pair on
/finance/tds/deductions/:id/certificate.
Remaining follow-ups
- Surface a TDS admin screen under Finance → Reports so operators can filter pending/deposited/certificate_issued deductions and hit the PATCH endpoint from a single table instead of the API. The endpoints exist; the page doesn't.
- 26Q return upload — today the quarterly CSV is a review sheet
the operator re-keys into the Protean RPU. A future pass could emit
the
.fvufile directly.
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).