> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# QWED Tax guards for AI payroll and tax verification

> Reference for QWED Tax guards covering US and India workflows including classification, nexus, payroll, withholding, and GST verification.

QWED-Tax verifies logic deterministically. No probabilities, just rules.

## Statutory audit trace

Several guards attach a structured, machine-readable `audit_trace` to their verdict so an auditor can see exactly **which statute drove the decision** — instead of parsing free-text reasons. The field is **additive**: existing result keys are unchanged, so callers that ignore it keep working.

```python theme={null}
{
  "verified": False,
  "reason": "ITC is blocked for 'catering' under Section 17(5) / VAT Rules.",
  "audit_trace": {
    "rule_id": "ITC_BLOCKED_17_5",
    "statute": "CGST Act, Section 17(5)",
    "jurisdiction": "INDIA",
    "outcome": "BLOCKED",
    "inputs": {"expense_category": "CATERING"}
  }
}
```

Guards currently emitting `audit_trace` include **InputCreditGuard** (ITC), **TDSGuard** (Sec 194J/194C/194H/194I), and **GSTGuard** (RCM). Rule identifiers and statute strings are centralized in `qwed_tax/audit.py`.

## Structured diagnostics (`TaxDiagnosticResult`)

`TaxDiagnosticResult` is an opt-in, three-layer model that converts a guard's legacy dict return into a typed, tri-state verdict with a cryptographic proof reference. The legacy `{"verified": ..., "audit_trace": ...}` dict is unchanged — `to_diagnostic()` is additive, so existing callers keep working.

Use it when you need a single, uniform shape across guards (for API responses, gating logic, or audit pipelines) instead of branching on guard-specific keys.

### The three layers

| Layer         | Field                      | Purpose                                                                                                                                                         |
| ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Agent-safe | `agent_message: str`       | Short, model-facing summary. No statute IDs, no rule IDs, no detection logic — safe to feed back to an LLM for correction.                                      |
| 2. Developer  | `developer_fields: dict`   | Structured evidence: `constraint_id`, `statute`, `jurisdiction`, `audit_trace`, plus guard-specific fields like `deduction`, `net_payable`, `allowable_credit`. |
| 3. Proof      | `proof_ref: Optional[str]` | `sha256:…` hash of the retained proof artifact. Present **only** when `status == VERIFIED`. This is the authority bit.                                          |

### Status states

`TaxDiagnosticStatus` is a strict tri-state:

* **`VERIFIED`** — the tax decision was deterministically proven. `proof_ref` MUST be present. Downstream gates MAY admit for control flow.
* **`UNVERIFIABLE`** — the decision could not be proven (insufficient evidence, computation-only mode, unknown rule). `proof_ref` MUST be `None`. Gates MUST NOT admit.
* **`BLOCKED`** — verification could not even be attempted (missing fields, parse error, unsupported service). `proof_ref` MUST be `None`. Gates MUST NOT admit.

Richer distinctions (e.g. "below threshold" vs. "unknown service") live in `developer_fields.constraint_id`, not in the status.

<Warning>
  **Authority contract.** `proof_ref is not None` is the **only** signal that a verdict is admissible for control flow. A `VERIFIED` status without a `proof_ref` is structurally impossible — the dataclass raises in `__post_init__`. Do not infer authority from any other field.
</Warning>

### Calling `to_diagnostic()`

`TDSGuard`, `InputCreditGuard`, and `GSTGuard` expose a `to_diagnostic()` static method that converts their existing dict result into a `TaxDiagnosticResult`.

```python theme={null}
from qwed_tax.guards.tds_guard import TDSGuard
from qwed_tax.diagnostics import TaxDiagnosticStatus

guard = TDSGuard()
raw = guard.calculate_deduction(service="professional_fees", amount=50_000)

diag = TDSGuard.to_diagnostic(raw)

if diag.status is TaxDiagnosticStatus.VERIFIED:
    # diag.proof_ref is guaranteed non-None here
    submit_payment(net_payable=diag.developer_fields["net_payable"])
else:
    # diag.proof_ref is None — never admit for control flow
    escalate(diag.agent_message, constraint_id=diag.developer_fields["constraint_id"])
```

The same shape applies to `InputCreditGuard.to_diagnostic()` (ITC) and `GSTGuard.to_diagnostic()` (RCM).

### Proof references

`compute_proof_ref(evidence)` returns a deterministic `sha256:…` hash over a JSON-serialized evidence dict. `trace_proof_ref(trace)` is a convenience wrapper for the output of `build_trace()`. Both fail closed if the evidence is not JSON-serializable.

```python theme={null}
from qwed_tax.audit import build_trace, trace_proof_ref, TDS_194J

trace = build_trace(TDS_194J, outcome="DEDUCTION_REQUIRED", inputs={"amount": "50000"})
proof = trace_proof_ref(trace)
# "sha256:9f4c…"
```

The proof reference binds a verdict to the exact evidence that justified it. If any input, rule, or outcome changes, the hash changes — making verdict/evidence drift structurally detectable in downstream audit logs.

### Constructing results directly

For custom guards or wrapper code, use the factory methods rather than the raw constructor:

```python theme={null}
from qwed_tax.diagnostics import TaxDiagnosticResult

# VERIFIED — proof_ref is computed from evidence
from qwed_tax.audit import build_trace, TDS_194J
trace = build_trace(TDS_194J, outcome="DEDUCTION_REQUIRED", inputs={"amount": "50000"})
diag = TaxDiagnosticResult.verified(
    agent_message="Tax deduction verified.",
    developer_fields={"constraint_id": "TDS_194J", "deduction": "5000"},
    evidence=trace,
)

# UNVERIFIABLE — no proof was established
diag = TaxDiagnosticResult.unverifiable(
    agent_message="Amount is below the deduction threshold; no TDS required.",
    developer_fields={"constraint_id": "TDS_194J_BELOW_THRESHOLD"},
)

# BLOCKED — verification could not be attempted
diag = TaxDiagnosticResult.blocked(
    agent_message="Unknown service type. Cannot determine deduction.",
    developer_fields={"constraint_id": "TDS_UNKNOWN"},
)
```

### Advisory checks

`TaxAdvisoryCheck` attaches non-proof-bearing analysis as metadata. The `advisory_only=True` invariant is enforced in `__post_init__` — advisory checks populate `developer_fields["advisory_checks"]` and **never** influence `status` or `proof_ref`. Use them to surface useful context (e.g. "supplier GSTIN appears inactive") without making it part of the verdict.

### Serialization

`TaxDiagnosticResult` is frozen and provides `to_dict()` / `from_dict()` for API responses. `to_dict()` includes a flat `is_authoritative` boolean for clients that don't want to inspect `proof_ref` directly.

### Migration status

`to_diagnostic()` is currently available on **TDSGuard**, **InputCreditGuard**, and **GSTGuard** — the three guards that already emit `audit_trace`. The remaining guards still return their legacy dict shapes; subsequent releases will extend `to_diagnostic()` coverage.

## United States (IRS)

### ClassificationGuard (IRS common law)

**Goal:** Prevent "Employee Misclassification" lawsuits. **Logic:** Uses the IRS Common Law test to determine if a worker is a W-2 Employee or 1099 Contractor.

* **Behavioral Control:** Does the employer provide tools/instructions?
* **Financial Control:** Does the employer reimburse expenses?
* **Relationship:** Is it indefinite?

**Rule:** If you control *how* they work and *pay* their expenses, they are an Employee (W-2), even if the AI says "1099".

```python theme={null}
from qwed_tax.guards.classification_guard import ClassificationGuard

guard = ClassificationGuard()

result = guard.verify_classification_claim(
    llm_claim="1099",
    facts={
        "provides_tools": True,
        "reimburses_expenses": True,
        "indefinite_relationship": True
    }
)
# {"verified": False, "error": "Misclassification Risk: Facts indicate W2, but AI claimed 1099..."}
```

<Note>
  **Fails closed on ambiguous facts.** `verify_worker_status` returns `WorkerType.CONTRACTOR` only when **no** employee indicators are present. When some — but not all — of `behavioral_control`, `financial_control`, and `relationship_permanence` are true, it returns `None`, and `verify_classification_claim` then returns `{"verified": False, "error": "Ambiguous classification: facts contain mixed employee/contractor indicators. Cannot deterministically classify — manual review required."}`. The previous default-to-contractor path on mixed signals has been removed.
</Note>

### ClassificationGuard (ABC Test / Z3)

**Goal:** Formal verification of worker classification under state-specific ABC Test laws (CA AB5, NJ, MA). Uses the **Z3 theorem prover** to prove classification correctness.

**Rule:** A worker is a contractor only if **all three** criteria are met:

* **A:** Free from control and direction
* **B:** Work is outside the usual course of business
* **C:** Customarily engaged in an independent trade

```python theme={null}
from qwed_tax.jurisdictions.us.classification_guard import ClassificationGuard
from qwed_tax.models import WorkerClassificationParams, State

guard = ClassificationGuard()

params = WorkerClassificationParams(
    worker_id="W001",
    freedom_from_control=True,
    work_outside_usual_business=False,  # Fails criterion B
    customarily_engaged_independently=True,
    state=State.CA
)

result = guard.verify_classification(params, claimed_status_contractor=True)
# {"verified": False, "classification": "Employee (W-2)",
#  "message": "MISCLASSIFICATION: Laws in CA require Employee (W-2). Reasons: Failed B (Core Business Work)"}
```

| Parameter                   | Type                         | Required | Description                                  |
| --------------------------- | ---------------------------- | -------- | -------------------------------------------- |
| `params`                    | `WorkerClassificationParams` | Yes      | Worker facts for ABC Test                    |
| `claimed_status_contractor` | `bool`                       | Yes      | `True` if the AI claims 1099, `False` if W-2 |

### NexusGuard (economic nexus)

**Goal:** Prevent Sales Tax Evasion. **Logic:** Checks local state thresholds for 2025.

* **NY/TX/CA:** > \$500,000 Sales
* **FL/IL/PA:** > \$100,000 Sales

**Rule:** If YTD Sales > Threshold and AI says "No Tax", **BLOCK**.

| State | Amount threshold | Transaction threshold |
| ----- | ---------------- | --------------------- |
| CA    | \$500,000        | —                     |
| NY    | \$500,000        | 100                   |
| TX    | \$500,000        | —                     |
| FL    | \$100,000        | —                     |
| IL    | \$100,000        | 200                   |
| PA    | \$100,000        | —                     |
| OH    | \$100,000        | 200                   |
| GA    | \$100,000        | 200                   |

<Note>
  **Fails closed on unmodeled states.** A call with a `state` code that is not in the threshold table returns `{"verified": False, "error": "State <CODE> not in configured nexus threshold table. Cannot verify nexus liability — block pending rule configuration."}`. The guard never falls back to "not high-risk → no tax" for jurisdictions it has not been configured for.
</Note>

### PayrollGuard (FICA limits)

**Goal:** Verify paycheck math. **Logic:**

* **Gross-to-Net:** `Gross - Taxes - Deductions == Net` (Exact Decimal match).
* **Social Security Cap:** Enforces 2025 Wage Base Limit (\$176,100). Tax stops after this amount.

```python theme={null}
from qwed_tax.jurisdictions.us.payroll_guard import PayrollGuard
from qwed_tax.models import PayrollEntry, TaxEntry, DeductionEntry, DeductionType
from decimal import Decimal

guard = PayrollGuard()

entry = PayrollEntry(
    employee_id="E001",
    gross_pay=Decimal("5000.00"),
    taxes=[TaxEntry(name="Federal Income Tax", amount=Decimal("800.00"))],
    deductions=[DeductionEntry(name="401k", amount=Decimal("250.00"), type=DeductionType.PRE_TAX)],
    net_pay_claimed=Decimal("3950.00"),
    currency="USD"
)

result = guard.verify_gross_to_net(entry)
# VerificationResult(verified=True, recalculated_net_pay=Decimal('3950.00'), discrepancy=Decimal('0.00'), ...)
```

### WithholdingGuard (W-4 / Z3)

**Goal:** Verify W-4 exempt status claims using the Z3 theorem prover. **Logic:** An employee can only claim "Exempt" if they had **zero tax liability** last year AND expect **no liability** this year (IRS Pub 505).

```python theme={null}
from qwed_tax.jurisdictions.us.withholding_guard import WithholdingGuard, W4Form

guard = WithholdingGuard()

form = W4Form(
    employee_id="E001",
    claim_exempt=True,
    tax_liability_last_year=5000.0,  # Had liability last year
    expect_refund_this_year=True
)

result = guard.verify_exempt_status(form)
# {"verified": False, "message": "IRS VIOLATION: Cannot claim 'Exempt' if you had tax liability last year..."}
```

| Parameter | Type     | Required | Description                                                                     |
| --------- | -------- | -------- | ------------------------------------------------------------------------------- |
| `form`    | `W4Form` | Yes      | W-4 form data including exempt claim, prior year liability, and expected refund |

### ReciprocityGuard (State tax)

**Goal:** Determine which state receives income tax withholding when an employee lives in one state and works in another. The guard is a deterministic lookup against a fixed table of reciprocity agreements — it **fails closed** whenever the agreement, or either state, is not recognized.

**Covered reciprocity pairs:** NJ-PA, PA-NJ, MD-PA, PA-MD, VA-MD, MD-VA.

<Note>
  The pair table is limited to the states modeled in the `State` enum (NY, NJ, CA, TX, PA, FL, MD, VA). Pennsylvania has additional reciprocity agreements (with IN, MI, OH, VA, WV, WI) that are not modeled here because those states are not in the enum. VA-PA is a known gap — callers will receive `verified=False` for that pair until the enum and table are extended.
</Note>

| Method                                                             | Description                                                                                                                                         |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verify_reciprocity(residence_state, work_state, same_state=None)` | String-input API. Pass two-letter state codes (or `State` enum values); optionally pass `same_state` to assert the claim and have it cross-checked. |
| `determine_withholding_state(arrangement)`                         | `WorkArrangement`-input API. Extracts residence and work states from the addresses on the arrangement and delegates to the same lookup.             |

Both methods return the same result shape:

| Condition                                           | `verified` | Returned fields                                                                      |
| --------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------ |
| Residence == work state                             | `True`     | `withholding_state`, `reason`                                                        |
| Known reciprocity pair (e.g., NJ ↔ PA)              | `True`     | `withholding_state` (residence), `reason`                                            |
| Different states, no reciprocity agreement          | `False`    | `message` explaining the work-state default and that the claim could not be verified |
| Unknown residence or work state                     | `False`    | `message` naming the unrecognized state                                              |
| `same_state` claim conflicts with the actual states | `False`    | `message` flagging the conflict                                                      |

<Note>
  **Evaluation order:** The `same_state` conflict check runs **before** the same-state and reciprocity lookups. If a caller passes `same_state=True` with different states (or `same_state=False` with identical states), the guard returns `verified=False` immediately — the same-state and reciprocity branches are never reached. Pass `same_state=None` (the default) to skip the conflict check and let the guard evaluate states on their own.
</Note>

```python theme={null}
from qwed_tax.jurisdictions.us.reciprocity_guard import ReciprocityGuard
from qwed_tax.models import WorkArrangement, Address, State

guard = ReciprocityGuard()

# String API
result = guard.verify_reciprocity(residence_state="NJ", work_state="PA")
# {"verified": True, "withholding_state": State.NJ,
#  "reason": "Reciprocity Agreement exists between NJ and PA. Withhold for Residence (NJ)."}

# WorkArrangement API
arrangement = WorkArrangement(
    employee_id="E001",
    residence_address=Address(street="123 Main St", city="Newark", state=State.NJ, zip_code="07102"),
    work_address=Address(street="456 Market St", city="Philadelphia", state=State.PA, zip_code="19103"),
    is_remote=False
)
result = guard.determine_withholding_state(arrangement)
# Same shape as above.

# Fails closed when no agreement exists
guard.verify_reciprocity("NJ", "NY")
# {"verified": False, "message": "No reciprocity agreement between NJ and NY. ..."}
```

<Note>
  ReciprocityGuard no longer uses a Z3 solver. A prior implementation built a Z3 expression that was tautologically satisfiable regardless of the input states, causing the guard to return `verified=True` for arrangements that had no reciprocity agreement. The current implementation is a deterministic lookup against the table above — same input, same output, no solver state.
</Note>

### AddressGuard

**Goal:** Verify that zip codes match their claimed state. Uses a simplified heuristic lookup of zip code prefixes.

```python theme={null}
from qwed_tax.address_guard import AddressGuard
from qwed_tax.models import Address, State

guard = AddressGuard()

result = guard.verify_address(Address(
    street="123 Main St", city="Newark", state=State.NJ, zip_code="90210"
))
# {"verified": False, "message": "MISMATCH: Zip 90210 does not belong to NJ."}
```

<Note>
  **Fails closed on unmodeled states.** States that are not in the simplified zip-prefix table (currently CA, FL, NJ, NY, PA, TX) return `{"verified": False, "message": "State <CODE> not in validation database. Address cannot be auto-verified — manual review required."}`. The guard never assumes an unknown state is valid.
</Note>

### Form1099Guard

**Goal:** Verify IRS 1099 filing requirements for contractor payments. **Logic:**

* Checks payment amount against filing thresholds by payment type.
* Determines which form is required (1099-NEC or 1099-MISC).

| Payment Type              | Form      | Threshold |
| ------------------------- | --------- | --------- |
| Non-employee compensation | 1099-NEC  | \$600     |
| Rent                      | 1099-MISC | \$600     |
| Royalties                 | 1099-MISC | \$10      |
| Attorney fees             | 1099-MISC | \$600     |

```python theme={null}
from qwed_tax.jurisdictions.us.form1099_guard import Form1099Guard
from qwed_tax.models import ContractorPayment, PaymentType
from decimal import Decimal

guard = Form1099Guard()

payment = ContractorPayment(
    contractor_id="C001",
    payment_type=PaymentType.NON_EMPLOYEE_COMPENSATION,
    amount=Decimal("700.00"),
    calendar_year=2024
)

result = guard.verify_filing_requirement(payment)
# {"filing_required": True, "form": "1099-NEC", "reason": "..."}
```

<Note>
  **Fails closed on unmodeled payment types.** Payment types without a defined filing rule return `{"filing_required": "UNVERIFIABLE", "form": None, "reason": "No filing rule configured for payment type '<TYPE>'. Cannot verify filing requirement — manual determination required."}`. Treat any value other than `True` or `False` as a hold-and-escalate signal — the guard will not silently report `filing_required=False` for a payment category it has not been configured to evaluate.
</Note>

## India (CBDT)

### CryptoTaxGuard (Sec 115BBH)

**Rule:** Losses from Virtual Digital Assets (VDA) cannot be set off against any other income (including other VDA gains).

Two verification methods:

* `verify_set_off()` — Blocks any AI attempt to reduce tax liability using crypto losses.
* `verify_flat_tax_rate()` — Verifies the strict 30% flat tax on positive VDA income.

```python theme={null}
from qwed_tax.jurisdictions.india.guards.crypto_guard import CryptoTaxGuard
from decimal import Decimal

guard = CryptoTaxGuard()

# Verify set-off compliance
result = guard.verify_set_off(
    losses={"VDA": Decimal("-5000")},
    gains={"BUSINESS": Decimal("10000")}
)
# TaxResult(verified=False, message="Section 115BBH Alert: Loss from VDA cannot be set off...")

# Verify flat tax rate
result = guard.verify_flat_tax_rate(
    vda_income=Decimal("100000"),
    claimed_tax=Decimal("30000")
)
# TaxResult(verified=True, message="VDA Tax correct (30% of 100000)")
```

<Note>
  **Negative VDA income is a loss, not "no income".** `verify_flat_tax_rate` distinguishes three cases:

  * `vda_income == 0` — verifies `claimed_tax == 0`. Any non-zero claim returns `verified=False`.
  * `vda_income < 0` — returns `verified=False` with the message `"VDA income is negative (<X>) — this is a loss, not income. Use verify_set_off for loss treatment."` Losses must be routed to `verify_set_off` (and under Section 115BBH, they lapse rather than offsetting other heads).
  * `vda_income > 0` — verifies the 30% flat tax.

  The previous behavior — silently returning `verified=True` for any `vda_income <= 0` and ignoring `claimed_tax` — has been removed.
</Note>

<Note>
  **Comparison is exact at the paise (1/100) level.** Both `expected_tax` (`vda_income * 0.30`) and `claimed_tax` are quantized to two decimal places using `ROUND_HALF_UP` before an exact `==` comparison. A 1-paise deviation now returns `verified=False`. There is no `Decimal("0.1")` rounding tolerance — callers must round their claim to two decimal places with `ROUND_HALF_UP` to match.
</Note>

### GSTGuard (RCM)

**Rule:** Certain notified services require the **Reverse Charge Mechanism** (the recipient pays the tax instead of the provider). RCM applicability is expressed as a declarative rule table — one entry per notified service, each carrying its predicate and statutory reference.

| Service                  | Provider           | Recipient                    | Liability       | Statutory reference                |
| ------------------------ | ------------------ | ---------------------------- | --------------- | ---------------------------------- |
| GTA                      | Any                | Body Corporate / Partnership | RCM (Recipient) | Notification 13/2017-CT(R), Sl. 1  |
| Legal                    | Any                | Body Corporate / Partnership | RCM (Recipient) | Notification 13/2017-CT(R), Sl. 2  |
| Security                 | Non-Body Corporate | Body Corporate               | RCM (Recipient) | Notification 29/2018-CT(R)         |
| Director                 | Any                | Body Corporate               | RCM (Recipient) | Notification 13/2017-CT(R), Sl. 6  |
| Sponsorship              | Any                | Body Corporate / Partnership | RCM (Recipient) | Notification 13/2017-CT(R), Sl. 4  |
| Renting of motor vehicle | Non-Body Corporate | Body Corporate               | RCM (Recipient) | Notification 13/2017-CT(R), Sl. 15 |
| Import of service        | Any                | Any (recipient in India)     | RCM (Recipient) | Notification 10/2017-IT(R), Sl. 1  |

Any service/recipient combination outside these rules resolves to **forward charge** (the provider pays).

`verify_rcm_applicability` runs in two modes:

* **Verification mode** (recommended) — pass `claimed_is_rcm` and the guard compares the computed RCM liability against the claim. `verified=True` only on exact match.
* **Calculation mode** (backward compatible) — omit `claimed_is_rcm` and the guard returns the computed result with `computed_only=True` to signal that no claim was checked.

```python theme={null}
from qwed_tax.jurisdictions.india.guards.gst_guard import GSTGuard, ServiceType, EntityType

guard = GSTGuard()

# Verification mode — compare the agent's claim against the rule table
result = guard.verify_rcm_applicability(
    service=ServiceType.LEGAL,
    provider=EntityType.INDIVIDUAL,
    recipient=EntityType.BODY_CORPORATE,
    claimed_is_rcm=True,
)
# {"verified": True, "liability": "RECIPIENT (RCM)", "is_rcm": True, "claimed_is_rcm": True,
#  "reason": "Legal service to a Business Entity (Body Corporate/Partnership) attracts RCM.", ...}

# Mismatch — agent claimed forward charge on a notified service
result = guard.verify_rcm_applicability(
    service=ServiceType.LEGAL,
    provider=EntityType.INDIVIDUAL,
    recipient=EntityType.BODY_CORPORATE,
    claimed_is_rcm=False,
)
# {"verified": False, "error": "RCM mismatch: computed is_rcm=True, claimed is_rcm=False. ..."}

# Calculation mode — no claim supplied
result = guard.verify_rcm_applicability(
    service=ServiceType.LEGAL,
    provider=EntityType.INDIVIDUAL,
    recipient=EntityType.BODY_CORPORATE,
)
# {"computed_only": True, "liability": "RECIPIENT (RCM)", "is_rcm": True, ...}
```

| Parameter        | Type                 | Required | Description                                                                                                                                                       |
| ---------------- | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service`        | `ServiceType \| str` | Yes      | Notified service. Enum or raw string (e.g. `"LEGAL"`)                                                                                                             |
| `provider`       | `EntityType \| str`  | Yes      | Supplier entity. Enum or raw string                                                                                                                               |
| `recipient`      | `EntityType \| str`  | Yes      | Recipient entity. Enum or raw string                                                                                                                              |
| `claimed_is_rcm` | `bool`               | No       | When provided, the guard verifies the claim against the computed RCM liability. When omitted, the response carries `computed_only=True` and is not a verification |

<Note>
  `verify_rcm_applicability` accepts either enum members or their raw string values (e.g. `"LEGAL"`, `"BODY_CORPORATE"`), so JSON-sourced payloads work without pre-conversion.
</Note>

<Note>
  **Fails closed on unknown service/entity values.** Service names outside `ServiceType` and entity values outside `EntityType` now return `{"verified": False, "error": "Unknown service type '<X>'. Cannot determine RCM applicability.", "is_rcm": None}` (and equivalents for provider/recipient). The previous behavior of silently coercing unknown services to `OTHER` and unknown entities to `INDIVIDUAL` — which could suppress a statutory RCM liability — has been removed. Normalize inputs to a known enum value before calling, or treat the error as a hold-and-escalate signal.
</Note>

### GSTGuard (CGST/SGST/IGST split)

**Goal:** Verify that a claimed tax breakup matches the **place of supply**. This verifies the split — the GST rate is an input, not something the guard derives.

* **Intra-state** (supplier state == place of supply): `CGST = SGST = value × rate / 200`, and `IGST` must be `0`.
* **Inter-state** (supplier state != place of supply): `IGST = value × rate / 100`, and `CGST`/`SGST` must be `0`.

A small rounding tolerance (2 paise) applies only to tax-carrying legs (CGST/SGST for intra-state supplies, IGST for inter-state supplies). Legs that must be exactly zero (the wrong tax type for the supply) get **no** tolerance, so even a tiny wrong-type amount is rejected. Negative claimed amounts fail closed.

```python theme={null}
from qwed_tax.jurisdictions.india.guards.gst_guard import GSTGuard

guard = GSTGuard()

# Intra-state supply: 18% of 1000 -> CGST 90 + SGST 90, IGST 0
result = guard.verify_gst_split(
    supplier_state="KA",
    place_of_supply="KA",
    taxable_value=1000,
    gst_rate=18,
    claimed_cgst=90,
    claimed_sgst=90,
    claimed_igst=0,
)
# {"verified": True, "supply_type": "INTRA_STATE",
#  "expected": {"cgst": "90", "sgst": "90", "igst": "0"}, ...}
```

| Parameter         | Type                             | Required | Description                                               |
| ----------------- | -------------------------------- | -------- | --------------------------------------------------------- |
| `supplier_state`  | `str`                            | Yes      | Supplier's state code                                     |
| `place_of_supply` | `str`                            | Yes      | Place-of-supply state code (case-insensitive)             |
| `taxable_value`   | `Decimal \| str \| int \| float` | Yes      | Taxable value. Must be finite and non-negative            |
| `gst_rate`        | `Decimal \| str \| int \| float` | Yes      | GST rate as a percentage. Must be finite and non-negative |
| `claimed_cgst`    | `Decimal \| str \| int \| float` | Yes      | Claimed CGST. Must be non-negative                        |
| `claimed_sgst`    | `Decimal \| str \| int \| float` | Yes      | Claimed SGST. Must be non-negative                        |
| `claimed_igst`    | `Decimal \| str \| int \| float` | Yes      | Claimed IGST. Must be non-negative                        |

<Note>
  The guard **fails closed**: missing states, non-finite values, or negative amounts return `{"verified": False, "error": "..."}` rather than raising.
</Note>

### InvestmentGuard (Trading / Z3)

**Goal:** Classify stock market income into the correct tax head using the Z3 theorem prover.

* **Intraday** = Speculative Business Income (Slab Rate, Sec 43(5))
* **Delivery** = Capital Gains (STCG/LTCG based on holding period)
* **F\&O** = Non-Speculative Business Income

```python theme={null}
from qwed_tax.jurisdictions.india.guards.investment_guard import InvestmentGuard, TransactionType

guard = InvestmentGuard()

result = guard.verify_classification(
    tx_type=TransactionType.INTRADAY,
    holding_period_days=0
)
# {"classification": "Speculative Business Income", "tax_treatment": "Added to Total Income (Slab Rate)", "verified": True}
```

### InterHeadAdjustmentGuard (Set-off matrix)

**Goal:** Enforce the full inter-head set-off rules for Indian income tax. Uses a prohibition matrix to block illegal loss adjustments.

| Loss head                | Can set off against                                     |
| ------------------------ | ------------------------------------------------------- |
| Speculative Business     | Only Speculative Business profit                        |
| Long-term Capital Gains  | Only Long-term Capital Gains                            |
| Short-term Capital Gains | Short-term or Long-term Capital Gains                   |
| VDA (Crypto)             | Nothing (lapses entirely)                               |
| Salary                   | Nothing (cannot generate a loss for inter-head set-off) |

Heads with no inter-head restrictions per the Income Tax Act — `HOUSE_PROPERTY`, `BUSINESS_NON_SPECULATIVE`, and `OTHER_SOURCES` — are on an explicit allowlist and always return `verified=True`.

```python theme={null}
from qwed_tax.jurisdictions.india.guards.setoff_guard import InterHeadAdjustmentGuard, TaxHead

guard = InterHeadAdjustmentGuard()

result = guard.verify_setoff(
    loss_head=TaxHead.VDA,
    profit_head=TaxHead.SALARY
)
# {"verified": False, "message": "Illegal Set-Off: Loss from VDA cannot be set off against anything (it lapses)."}
```

<Note>
  **Fails closed on unknown heads and blocks SALARY losses.** Loss heads that are neither in the prohibition matrix nor on the explicit allowlist return `{"verified": False, "message": "Loss head <X> is not in the configured prohibition matrix or allowlist. Cannot verify set-off legality — manual review required."}`. `TaxHead.SALARY` is now in the prohibition matrix with `["ALL"]` — agents that try to set off a salary loss against any profit head are blocked. The previous "default allow" path for any head not in the matrix has been removed.
</Note>

### DepositRateGuard

**Goal:** Verify bank deposit interest rates, specifically senior citizen premiums (60+).

```python theme={null}
from qwed_tax.jurisdictions.india.guards.deposit_guard import DepositRateGuard
from decimal import Decimal

guard = DepositRateGuard()

result = guard.verify_fd_rate(
    age=65,
    base_rate=Decimal("7.00"),
    claimed_rate=Decimal("7.00"),  # Missing senior premium
    senior_premium=Decimal("0.50")
)
# RateCheckResult(verified=False, expected_rate=Decimal('7.50'), claimed_rate=Decimal('7.00'),
#  message="Rate Error: Age 65 should get 7.50%, but LLM claimed 7.00%.")
```

| Parameter        | Type      | Required | Default | Description             |
| ---------------- | --------- | -------- | ------- | ----------------------- |
| `age`            | `int`     | Yes      | —       | Customer age            |
| `base_rate`      | `Decimal` | Yes      | —       | Base FD interest rate   |
| `claimed_rate`   | `Decimal` | Yes      | —       | Rate claimed by the AI  |
| `senior_premium` | `Decimal` | No       | `0.50`  | Additional rate for 60+ |

### SpeculationGuard

**Rule:** Intraday (Speculative) losses can **only** be set off against Intraday (Speculative) profits. They cannot reduce F\&O or Delivery income. Losses must be carried forward for up to 4 years.

`verify_setoff` classifies the loss and profit sources against a fixed vocabulary:

* **Speculative:** `intraday`
* **Non-speculative:** `f&o`, `f_o`, `futures`, `options`, `delivery`, `business`, `capital_gains`

```python theme={null}
from qwed_tax.guards.speculation_guard import SpeculationGuard

guard = SpeculationGuard()

# Intraday loss vs F&O profit — illegal set-off
result = guard.verify_setoff(
    loss_source="intraday",
    loss_amount="50000",
    profit_source="f&o",
)
# {"verified": False,
#  "error": "Illegal Set-Off: Intraday (Speculative) loss of 50000 cannot reduce f&o.",
#  "fix": "Loss of 50000 must be CARRIED FORWARD (4 years). It cannot be consumed now."}
```

<Note>
  **Fails closed on unknown source strings.** The guard no longer relies on substring matching (the previous `"intraday" in source` check treated everything else as non-speculative). Source names outside the known vocabulary return `{"verified": False, "error": "Unrecognized loss source '<X>'. Known sources: ..."}` with a `fix` hint. Normalize agent output to one of the recognized names before calling.
</Note>

### CapitalGainsGuard

**Goal:** Classify assets as STCG or LTCG based on holding period and verify the statutory tax rate.

| Asset type  | LTCG threshold              | LTCG rate (FY 2024-25) | STCG rate |
| ----------- | --------------------------- | ---------------------- | --------- |
| Equity      | > 365 days                  | 12.5%                  | 20%       |
| Real estate | > 730 days                  | —                      | —         |
| Debt        | > 1095 days                 | Slab rate              | Slab rate |
| Debt fund   | Always STCG (post-Apr 2023) | —                      | Slab rate |

<Note>
  **`debt_fund` always classifies as STCG.** `determine_term` returns `"STCG"` for `asset_type="debt_fund"` regardless of holding period, reflecting the Budget 2023 amendment. However, `verify_tax_rate` does not have a configured rate for `debt_fund_STCG` — it will return `{"verified": False, "error": "No statutory rate configured for debt_fund_STCG. Cannot verify claimed rate."}`. Callers must handle this fail-closed response and verify the slab rate against the taxpayer's bracket through a separate path.
</Note>

<Note>
  **Fails closed on unmodeled asset/term pairs.** `verify_tax_rate` returns `{"verified": False, "error": "No statutory rate configured for <asset>_<term>. Cannot verify claimed rate."}` when the `(asset_type, term)` combination is not in the rate table. The previous "no hard constraint, assume verified" path has been removed — the guard no longer signs off on rates it cannot independently check.
</Note>

<Note>
  **Slab-rated assets cannot be verified without the taxpayer's bracket.** When the rate table resolves to `SLAB` (e.g. `debt_LTCG`, `debt_STCG`), `verify_tax_rate` returns `{"verified": False, "error": "Rate for <key> is subject to slab rates — cannot deterministically verify claimed rate of <X>. Taxpayer's slab band is required for verification."}`. Slab rates depend on the filer's total income bracket and are out of scope for the rate-table check.
</Note>

<Note>
  **`determine_term` raises `ValueError` on unparseable dates and unknown asset types.** Pass `purchase_date` and `sale_date` as `YYYY-MM-DD` strings and an `asset_type` of `equity`, `real_estate`, `debt`, or `debt_fund`. Anything else raises `ValueError` — the previous behavior of returning an `"ERROR_DATE_FORMAT"` sentinel (which then flowed to `verified=True` upstream) or fabricating a 1095-day threshold for unknown assets has been removed. Callers should catch `ValueError` and block. `TaxPreFlight._check_capital_gains` now does this and produces a structured block with the error message.
</Note>

### Accounts payable guards (indirect tax)

**Goal:** Automate GST/VAT and TDS compliance. **Logic:**

* **InputCreditGuard:** Checks Section 17(5) "Blocked List".
  * *Food/Beverages:* Blocked.
  * *Motor Vehicles:* Blocked (unless transport biz).
  * *Gift to Employee:* Blocked only above INR 50,000. Gifts below the threshold are ITC-eligible.
* **InputCreditGuard.verify\_gstin\_format:** Validates a GSTIN's structure **and its 15th-digit checksum** (base-36 GSTN algorithm). A string that matches the format but carries an incorrect check digit is rejected with a generic `"Invalid GSTIN checksum."` error (the correct digit is never echoed back).
* **TDSGuard:** Calculates withholding based on service type.
  * *Professional Fees:* 10% (Sec 194J).
  * *Contractors:* 1% or 2% (Sec 194C).
  * *Commission:* 5% (Sec 194H).
  * *Rent (Land):* 10% (Sec 194I).

```python theme={null}
from qwed_tax.guards.indirect_tax_guard import InputCreditGuard

guard = InputCreditGuard()

guard.verify_gstin_format("27AAPFU0939F1ZV")   # {"verified": True}
guard.verify_gstin_format("22AAAAA0000A1Z5")   # {"verified": False, "error": "Invalid GSTIN checksum."}
guard.verify_gstin_format("INVALID")           # {"verified": False, "error": "Invalid GSTIN format."}
```

| Service type            | Threshold (INR) | TDS rate | Section |
| ----------------------- | --------------- | -------- | ------- |
| Professional Fees       | 30,000          | 10%      | 194J    |
| Contractor (Individual) | 30,000          | 1%       | 194C    |
| Contractor (Firm)       | 30,000          | 2%       | 194C    |
| Commission              | 15,000          | 5%       | 194H    |
| Rent (Land)             | 2,40,000        | 10%      | 194I    |

<Note>
  Category matching for `InputCreditGuard` uses exact match (not substring). Ensure you pass the canonical category name (e.g., `FOOD_AND_BEVERAGE`, `MOTOR_VEHICLE`, `GIFT_TO_EMPLOYEE`).
</Note>

<Warning>
  **TDSGuard fails closed on unknown service types.** `calculate_deduction` now returns `{"verified": False, "error": "No TDS rule configured for service type '<TYPE>'. Cannot verify — block pending rule configuration."}` when `service_type` does not match a configured rule. Previously, unknown service types returned `verified=True` with `deduction="0"`, which `TaxPreFlight._check_invoice_tds` would treat as a clean payment and let through with zero withholding. Any integration that branched on `verified` must now handle `verified=False` for the unknown-rule path.
</Warning>

<Note>
  **InputCreditGuard flags default-allow categories.** ITC is "allowed unless specifically blocked" under GST law, so unknown categories still return `verified=True` — but the response now also carries `"unverified_category": True` and `audit_trace.inputs.category_match: "default_allow"`. Consumers that need to distinguish "explicitly eligible" from "allowed by default" should branch on `unverified_category` before posting the credit.
</Note>

### CorporateGuard (Loans and valuation)

**Goal:** Corporate Governance (Sec 185/Valuation). **Logic:**

* **RelatedPartyGuard:** Prohibits loans to Directors/Relatives/Holding-Co-Directors unless specific exemptions apply. Also enforces interest rate benchmarking under Section 186.
* **ValuationGuard:** Deterministically calculates Convertible Note conversion prices (`min(Cap, Discount)`).

**Prohibited borrower roles:** `DIRECTOR`, `DIRECTOR_RELATIVE`, `PARTNER`, `PARTNER_OF_DIRECTOR`, `HOLDING_COMPANY_DIRECTOR`.

#### ValuationGuard.verify\_conversion

```python theme={null}
from qwed_tax.guards.valuation_guard import ValuationGuard

guard = ValuationGuard()

result = guard.verify_conversion(
    investment="100000",
    cap="5.00",
    discount="0.20",
    next_round_price="10.00"
)
# {"verified": True, "deterministic_price": "5.00", "shares_issued": "20000", "method": "CAP"}
```

| Parameter          | Type  | Required | Description                                                                             |
| ------------------ | ----- | -------- | --------------------------------------------------------------------------------------- |
| `investment`       | `str` | Yes      | Investment amount. Must parse as a Decimal and be strictly positive                     |
| `cap`              | `str` | Yes      | Valuation cap price per share. Must parse as a Decimal and be strictly positive         |
| `discount`         | `str` | Yes      | Discount rate expressed as a fraction (e.g. `"0.20"` for 20%). Must be `>= 0` and `< 1` |
| `next_round_price` | `str` | Yes      | Price per share in the priced round. Must parse as a Decimal and be strictly positive   |

<Note>
  **Fails closed on edge-case inputs.** The guard returns `{"verified": False, "error": "..."}` when:

  * Any input fails to parse as a `Decimal` (`"Invalid numerical input for valuation."`).
  * `discount` is outside `[0, 1)` (`"Discount must be between 0 and 1."`). A discount of exactly `1` is rejected because it would force the discounted price to zero; values above `1` would otherwise produce negative shares.
  * `cap`, `next_round_price`, or `investment` is zero or negative (`"Cap, next round price, and investment must be positive."`).
  * The final share price resolves to zero through any other path (`"Final price resolved to zero — cannot compute shares."`).

  Callers must handle the `verified=False` branch explicitly — there is no exception to catch.
</Note>

## International

### DTAAGuard (foreign tax credit)

**Goal:** Verify Foreign Tax Credit (FTC) eligibility under Double Taxation Avoidance Agreements. **Logic:**

* **Basic Credit:** Allowable credit = min(Foreign Tax Paid, Home Tax Payable on foreign income).
* **Treaty Rate Limit:** When a DTAA treaty rate is provided, the credit is further capped at the treaty-limited amount.
* **Excess Lapsed:** Any foreign tax paid above the allowable credit is reported as lapsed.

Numeric inputs are parsed through a hardened Decimal helper. Monetary outputs (`allowable_credit`, `excess_tax_lapsed`) are returned as stable plain-string Decimals rather than floats to preserve exact precision across serialization boundaries.

```python theme={null}
from qwed_tax.guards.dtaa_guard import DTAAGuard

guard = DTAAGuard()

# Without treaty rate — simple min(foreign_tax, home_tax)
result = guard.verify_foreign_tax_credit(
    foreign_income=1000,
    foreign_tax_paid=200,
    home_tax_rate=15.0
)
# {"verified": True, "allowable_credit": "150", "excess_tax_lapsed": "50", ...}

# With treaty rate — credit further capped by treaty limit
result = guard.verify_foreign_tax_credit(
    foreign_income=1000,
    foreign_tax_paid=200,
    home_tax_rate=30.0,
    foreign_tax_limit_rate=10.0  # Treaty caps at 10%
)
# {"verified": True, "allowable_credit": "100", "excess_tax_lapsed": "100", ...}
```

| Parameter                | Type                             | Required | Default | Description                                                                                                    |
| ------------------------ | -------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `foreign_income`         | `Decimal \| str \| int \| float` | Yes      | —       | Income earned in foreign jurisdiction. Must be finite and non-negative                                         |
| `foreign_tax_paid`       | `Decimal \| str \| int \| float` | Yes      | —       | Tax paid in foreign jurisdiction. Must be finite and non-negative                                              |
| `home_tax_rate`          | `Decimal \| str \| int \| float` | Yes      | —       | Home country tax rate (percentage). Must be finite and non-negative                                            |
| `foreign_tax_limit_rate` | `Decimal \| str \| int \| float` | No       | `None`  | DTAA treaty rate limit (percentage). Must be non-negative when provided. When `None`, no treaty cap is applied |

**Response fields:**

| Field               | Type   | Description                                                                                                          |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `verified`          | `bool` | `True` when inputs are valid and credit is computed. `False` on non-numeric, non-finite, boolean, or negative inputs |
| `message`           | `str`  | Human-readable explanation, including the capped amount and the components that produced the cap (Home / Treaty)     |
| `allowable_credit`  | `str`  | Decimal-as-string representation of the allowable FTC. Defaults to `"0"` on validation failure                       |
| `excess_tax_lapsed` | `str`  | Decimal-as-string representation of foreign tax above the allowable credit                                           |

<Note>
  The guard **fails closed**: any non-numeric, non-finite (NaN / Infinity), boolean, or negative input returns `verified: False` with a descriptive `message` and zeroed credit fields rather than raising an exception.
</Note>

### TransferPricingGuard (arm's length price)

**Goal:** Verify that related-party transactions are priced within an acceptable range of the Arm's Length Price (ALP). Ref: OECD Guidelines, US Sec 482, India Sec 92C.

Monetary inputs accept `Decimal`, `str`, `int`, or `float`. Outputs (`safe_harbour_range`, `potential_adjustment`, `adjustment_required`) are returned as stable plain-string Decimals.

```python theme={null}
from qwed_tax.guards.transfer_pricing_guard import TransferPricingGuard

guard = TransferPricingGuard()

result = guard.verify_arms_length_price(
    transaction_price=80.0,
    benchmark_price=100.0,
    method="CUP",
    tolerance_percent=3.0
)
# {"verified": False, "risk": "TRANSFER_PRICING_ADJUSTMENT",
#  "message": "Price 80 deviates from ALP 100 beyond 3.0% tolerance.",
#  "safe_harbour_range": ["97.00", "103.00"], "potential_adjustment": "20.00"}
```

| Parameter           | Type                             | Required | Default | Description                                              |
| ------------------- | -------------------------------- | -------- | ------- | -------------------------------------------------------- |
| `transaction_price` | `Decimal \| str \| int \| float` | Yes      | —       | Actual price charged to/by related party. Must be finite |
| `benchmark_price`   | `Decimal \| str \| int \| float` | Yes      | —       | Arm's Length Price (ALP) from analysis. Must be finite   |
| `method`            | `str`                            | No       | `"CUP"` | Transfer pricing method (e.g., CUP, TNMM)                |
| `tolerance_percent` | `Decimal \| str \| int \| float` | No       | `"3.0"` | Safe harbour tolerance percentage. Must be finite        |

<Note>
  On any non-numeric, non-finite, or boolean input the guard **fails closed** and returns `{"verified": False, "risk": "INVALID_NUMERIC_INPUT", "message": "...", "safe_harbour_range": [], "potential_adjustment": "0"}`.
</Note>

### PoEMGuard (place of effective management)

**Goal:** Determine tax residency of foreign companies under CBDT Circular 6 of 2017 and OECD models.

**Logic:** A foreign company is treated as Indian **Resident** if it fails the Active Business Outside India (ABOI) test **AND** its key management is in India.

**ABOI test criteria (all must be true to pass):**

* Assets outside India >= 50%
* Employees outside India >= 50%
* Payroll outside India >= 50%

```python theme={null}
from qwed_tax.guards.poem_guard import PoEMGuard

guard = PoEMGuard()

result = guard.determine_residency(
    company_name="GlobalCorp Ltd",
    is_foreign_incorp=True,
    turnover_total=10000000,
    turnover_outside_india=3000000,
    assets_total=5000000,
    assets_outside_india=1000000,   # 20% — fails ABOI
    employees_total=100,
    employees_outside_india=20,     # 20% — fails ABOI
    payroll_total=1000000,
    payroll_outside_india=200000,   # 20% — fails ABOI
    key_management_location="India"
)
# {"verified": True, "residency": "RESIDENT", "is_aboi": False,
#  "metrics": {"assets_outside_ratio": "0.2", "employees_outside_ratio": "0.2", "payroll_outside_ratio": "0.2"},
#  "reason": "Fails ABOI test AND Key Management is in India (PoEM established)."}
```

| Parameter                 | Type                             | Required | Description                                                                   |
| ------------------------- | -------------------------------- | -------- | ----------------------------------------------------------------------------- |
| `company_name`            | `str`                            | Yes      | Name of the company                                                           |
| `is_foreign_incorp`       | `bool`                           | Yes      | Whether incorporated outside India                                            |
| `turnover_total`          | `Decimal \| str \| int \| float` | Yes      | Total turnover. Validated for input shape; not used in the ABOI ratio         |
| `turnover_outside_india`  | `Decimal \| str \| int \| float` | Yes      | Turnover outside India. Validated for input shape; not used in the ABOI ratio |
| `assets_total`            | `Decimal \| str \| int \| float` | Yes      | Total assets. Must be non-negative                                            |
| `assets_outside_india`    | `Decimal \| str \| int \| float` | Yes      | Assets outside India. Must be non-negative and ≤ `assets_total`               |
| `employees_total`         | `int`                            | Yes      | Total employee count. Must be non-negative                                    |
| `employees_outside_india` | `int`                            | Yes      | Employees outside India. Must be non-negative and ≤ `employees_total`         |
| `payroll_total`           | `Decimal \| str \| int \| float` | Yes      | Total payroll expense. Must be non-negative                                   |
| `payroll_outside_india`   | `Decimal \| str \| int \| float` | Yes      | Payroll expense outside India. Must be non-negative and ≤ `payroll_total`     |
| `key_management_location` | `str`                            | Yes      | Where key management decisions are made                                       |

**Response fields:**

The `metrics` object returns each ratio as a stable plain-string Decimal rounded to four decimal places (`ROUND_HALF_UP`). The underlying unrounded ratios are used for the ABOI 50% threshold comparison.

<Note>
  The guard **fails closed** with `{"verified": False, "residency": "UNVERIFIABLE", "reason": "..."}` when any numeric input is non-numeric, non-finite, boolean, or negative, or when an "outside India" component exceeds its total (assets, employees, or payroll).
</Note>

### RemittanceGuard (FEMA/LRS)

**Goal:** Prevent forex violations. **Logic:**

* **LRS Limit:** Enforces \$250,000 annual limit per PAN.
* **Prohibited List:** Blocks Gambling, Lottery, Racing, and **Margin Trading**.
* **TCS Logic:** Applies 20% tax for generic remittance, 5% for Education/Medical. Education with loan funding is 0.5%. Threshold exemption of INR 7,00,000.

`verify_lrs_limit` accepts `Decimal`, `str`, `int`, or `float` for `amount_usd` and `financial_year_usage`. Non-numeric, non-finite, or boolean inputs fail closed with `{"verified": False, "error": "BLOCKED: ..."}`. Negative values for either parameter are also rejected (`"BLOCKED: Remittance amount must be non-negative."` / `"BLOCKED: Financial year usage must be non-negative."`) so a negative usage can't push a real transaction back under the \$250,000 LRS cap. `calculate_tcs` returns a `Decimal` and raises `ValueError` on invalid numeric input.

| Purpose                 | TCS rate | Notes                  |
| ----------------------- | -------- | ---------------------- |
| Education (loan-funded) | 0.5%     | Above INR 7L threshold |
| Education (self-funded) | 5%       | Above INR 7L threshold |
| Medical                 | 5%       | Above INR 7L threshold |
| All other               | 20%      | Above INR 7L threshold |
