Skip to content

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: pendingdepositedcertificate_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 / tdsPayableAccountId point 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:

  1. 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.
  2. 194J-style — only thresholdSingle > 0. Exempt when this single payment is below the threshold.
  3. 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 .fvu file directly.

8. Tests

src/lib/tds.test.ts covers:

  • Rate resolution (exact vs any fallback, 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.


  • 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 three finance.tds.* permissions and their role grants.
  • src/lib/api.ts:24289handleFinanceTds (three endpoints).