Skip to content
Merged
2 changes: 1 addition & 1 deletion examples/demo_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def run_audit_checks():
losses = {"VDA": Decimal("-20000")}
gains = {"BUSINESS": Decimal("50000")}

res_crypto = cg.verify_set_off(losses, gains)
res_crypto = cg.verify_set_off(losses)
print(f"Result: {res_crypto.message}")
print(f"Verified? {res_crypto.verified}")

Expand Down
Binary file modified qwed_tax/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file modified qwed_tax/__pycache__/models.cpython-311.pyc
Binary file not shown.
40 changes: 28 additions & 12 deletions qwed_tax/guards/capital_gains_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,39 @@ class CapitalGainsGuard:
def determine_term(self, purchase_date: str, sale_date: str, asset_type: str) -> str:
"""
Calculates Holding Period in days and returns 'LTCG' or 'STCG'.
Raises ValueError on unparseable dates or unknown asset types.
"""
try:
d1 = datetime.strptime(purchase_date, "%Y-%m-%d")
d2 = datetime.strptime(sale_date, "%Y-%m-%d")
days = (d2 - d1).days
except ValueError:
return "ERROR_DATE_FORMAT"

except (TypeError, ValueError) as exc:
raise ValueError(
f"Invalid date format. Expected YYYY-MM-DD, got '{purchase_date}' and '{sale_date}'."
) from exc

# Deterministic Thresholds (India FY 2024-25)
# Source: Income Tax Act
thresholds = {
"equity": 365, # > 1 year
"real_estate": 730, # > 2 years
"debt_fund": 0, # Wait, Debt Funds purchased after Apr 2023 are ALWAYS STCG (slab rate).
# But legacy debt funds might be 3 years (1095).
# We will use simplified logic for now: if holding > 3 years, likely LTCG treatment was intended?
# Actually, let's stick to standard thresholds for classification BEFORE tax rate application.
"debt": 1095
}

limit = thresholds.get(asset_type.lower(), 1095) # Default to 3 years

if not isinstance(asset_type, str) or not asset_type.strip():
raise ValueError(f"Unknown asset type '{asset_type}'. Known types: equity, real_estate, debt, debt_fund.")

asset_key = asset_type.strip().lower()

# Debt funds purchased after Apr 2023 are ALWAYS STCG (slab rate)
# regardless of holding period — special-case before threshold lookup
if asset_key == "debt_fund":
return "STCG"

if asset_key not in thresholds:
raise ValueError(f"Unknown asset type '{asset_type}'. Known types: equity, real_estate, debt, debt_fund.")

Comment thread
sentry[bot] marked this conversation as resolved.
limit = thresholds[asset_key]
return "LTCG" if days > limit else "STCG"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def verify_tax_rate(self, asset_type: str, term: str, claimed_rate: str) -> Dict[str, Any]:
Expand All @@ -55,9 +67,13 @@ def verify_tax_rate(self, asset_type: str, term: str, claimed_rate: str) -> Dict
return {"verified": False, "error": f"No statutory rate configured for {key}. Cannot verify claimed rate."}

if expected == "SLAB":
# If rate is variable (slab), we can't do simple string match.
# We accept it if LLM didn't claim a fixed low rate like '10%'
return {"verified": True, "note": "Subject to Slab Rates"}
return {
"verified": False,
"error": (
f"Rate for {key} is subject to slab rates — cannot deterministically "
f"verify claimed rate of {claimed_rate}. Taxpayer's slab band is required for verification."
),
}

if claimed_clean != expected:
return {
Expand Down
60 changes: 50 additions & 10 deletions qwed_tax/guards/classification_guard.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Dict, Any
from typing import Dict, Any, Optional

class WorkerType(Enum):
EMPLOYEE = "W2"
Expand All @@ -11,21 +11,44 @@ class ClassificationGuard:
Focuses on Behavioral and Financial Control.
"""

def verify_worker_status(self, behavioral_control: bool, financial_control: bool, relationship_permanence: bool) -> WorkerType:
def verify_worker_status(
self,
behavioral_control: bool,
financial_control: bool,
relationship_permanence: bool,
) -> Optional[WorkerType]:
"""
Deterministic IRS Common Law Test.
If an entity controls HOW work is done (behavioral) and pays expenses (financial),
they are an Employee, not a Contractor.

Returns:
WorkerType.EMPLOYEE — if employee indicators are present (deterministic)
WorkerType.CONTRACTOR — only if NO employee indicators are present
None — if mixed signals (some but not all employee indicators)
"""
# Strict rule: If you control behavior and finances, it's an employee.
# Count employee indicators
employee_indicators = 0
if behavioral_control and financial_control:
return WorkerType.EMPLOYEE

# If the relationship is permanent (indefinite), likely an employee unless completely independent

if relationship_permanence and behavioral_control:
return WorkerType.EMPLOYEE

# Default to Contractor only if significant control is absent
return WorkerType.EMPLOYEE

# Track individual indicators for mixed-signal detection
if behavioral_control:
employee_indicators += 1
if financial_control:
employee_indicators += 1
if relationship_permanence:
employee_indicators += 1

# Mixed signals: some employee indicators but not enough to conclusively
# classify as employee. Must not default to contractor.
if employee_indicators > 0:
return None # Ambiguous — caller must block or mark unverifiable

# No employee indicators at all — contractor is safe
return WorkerType.CONTRACTOR

def verify_classification_claim(self, llm_claim: str, facts: Dict[str, Any]) -> Dict[str, Any]:
Expand All @@ -37,14 +60,31 @@ def verify_classification_claim(self, llm_claim: str, facts: Dict[str, Any]) ->
facts.get("reimburses_expenses", False), # Financial Control
facts.get("indefinite_relationship", False) # Type of Relationship
)


# Mixed signals — cannot conclusively classify
if derived_status is None:
return {
"verified": False,
"error": (
"Ambiguous classification: facts contain mixed employee/contractor indicators. "
"Cannot deterministically classify — manual review required."
),
}

# Type guard — non-string claims must fail closed
if not isinstance(llm_claim, str) or not llm_claim.strip():
return {
"verified": False,
"error": "Invalid worker classification claim. Expected a non-empty string.",
}

# Normalize claim
claim_normalized = llm_claim.upper()
if "W-2" in claim_normalized or "EMPLOYEE" in claim_normalized:
claim_normalized = "W2"
elif "1099" in claim_normalized or "CONTRACTOR" in claim_normalized:
claim_normalized = "1099"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if derived_status.value != claim_normalized:
return {
"verified": False,
Expand Down
41 changes: 36 additions & 5 deletions qwed_tax/guards/speculation_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class SpeculationGuard:
Prevents 'Trapped Loss' errors where Speculative losses reduce Non-Speculative income.
"""

_KNOWN_SPECULATIVE = {"intraday"}
_KNOWN_NON_SPECULATIVE = {"f&o", "f_o", "futures", "options", "delivery", "business", "capital_gains", "capital gains"}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
_KNOWN_SOURCES = _KNOWN_SPECULATIVE | _KNOWN_NON_SPECULATIVE

def verify_setoff(self, loss_source: str, loss_amount: Any, profit_source: str) -> Dict[str, Any]:
"""
Deterministic Rule: Speculative losses (Intraday) cannot be set off against
Expand All @@ -23,10 +27,27 @@ def verify_setoff(self, loss_source: str, loss_amount: Any, profit_source: str)
parsed_loss_amount = parse_decimal_input(loss_amount, "loss_amount")
except ValueError as exc:
return {"verified": False, "error": str(exc), "fix": "Provide a finite numeric loss amount."}

is_speculative_loss = "intraday" in loss_source
is_speculative_profit = "intraday" in profit_source


# Classify sources against known vocabulary — reject unrecognized strings
loss_class = self._classify_source(loss_source)
profit_class = self._classify_source(profit_source)

if loss_class == "unknown":
return {
"verified": False,
"error": f"Unrecognized loss source '{loss_source}'. Known sources: {', '.join(sorted(self._KNOWN_SOURCES))}.",
"fix": "Use one of the recognized trading source names.",
}
if profit_class == "unknown":
return {
"verified": False,
"error": f"Unrecognized profit source '{profit_source}'. Known sources: {', '.join(sorted(self._KNOWN_SOURCES))}.",
"fix": "Use one of the recognized trading source names.",
}

is_speculative_loss = loss_class == "speculative"
is_speculative_profit = profit_class == "speculative"

# STRICT RULE: Intraday Loss can ONLY be set off against Intraday Profit.
if is_speculative_loss and not is_speculative_profit:
return {
Expand All @@ -40,5 +61,15 @@ def verify_setoff(self, loss_source: str, loss_amount: Any, profit_source: str)
"(4 years). It cannot be consumed now."
),
}

return {"verified": True, "note": "Set-off allowed."}

@classmethod
def _classify_source(cls, source: str) -> str:
"""Classify a trading source string. Returns 'speculative', 'non_speculative', or 'unknown'."""
normalized = source.strip().lower().replace("-", "_").replace(" ", "_")
if normalized in cls._KNOWN_SPECULATIVE:
return "speculative"
if normalized in cls._KNOWN_NON_SPECULATIVE:
return "non_speculative"
return "unknown"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
56 changes: 33 additions & 23 deletions qwed_tax/jurisdictions/india/guards/crypto_guard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from decimal import Decimal
from enum import Enum
from typing import Dict, List, Optional
from typing import Dict, Optional
from pydantic import BaseModel

class AssetClass(str, Enum):
Expand All @@ -19,27 +19,24 @@ class CryptoTaxGuard:
Key Rule: Loss from transfer of VDA cannot be set off against any other income.
"""

def verify_set_off(self, losses: Dict[str, Decimal], gains: Dict[str, Decimal]) -> TaxResult:
def verify_set_off(self, losses: Dict[str, Decimal], gains: Optional[Dict[str, Decimal]] = None) -> TaxResult:
"""
Verifies if the proposed set-off of losses is legal.
losses: Dict like {"VDA": -5000, "EQUITY": -200}
gains: Dict like {"BUSINESS": 10000}
gains: Optional Dict like {"BUSINESS": 10000} — reserved for future
inter-head adjustment verification.
"""


# Fail closed: gain-side verification is not implemented yet.
if gains:
return TaxResult(
verified=False,
message="Gain-side set-off verification is not implemented in CryptoTaxGuard. Provide losses-only payload or route to inter-head set-off guard.",
allowed_set_off=Decimal(0),
)
Comment thread
sentry[bot] marked this conversation as resolved.

# Rule 1: Check for VDA Losses being used
if "VDA" in losses and losses["VDA"] < 0:
# We must verify that VDA loss is NOT reducing taxable income from other heads

# Simple simulation:
# If Net Taxable Income < (Total Gains - Non-VDA Losses)
# It implies VDA loss was used.

total_gains = sum(gains.values(), Decimal(0))
non_vda_losses = sum((v for k, v in losses.items() if k != "VDA"), Decimal(0))

# This guard is meant to be called on a specific TRANSACTION attempt
# But here we verify the logic rule itself.

return TaxResult(
verified=False,
message="⚠️ Section 115BBH Alert: Loss from VDA (Crypto/NFT) cannot be set off against any other income. It must lapse.",
Expand All @@ -57,22 +54,35 @@ def verify_flat_tax_rate(self, vda_income: Decimal, claimed_tax: Decimal) -> Tax
Verifies strict 30% tax on positive VDA income (plus cess usually, simplified here).
"""
EXPECTED_RATE = Decimal("0.30")

if vda_income <= 0:
return TaxResult(verified=True, message="No VDA Income", allowed_set_off=Decimal(0))


if vda_income == 0:
if claimed_tax == 0:
return TaxResult(verified=True, message="No VDA Income — zero tax confirmed.", allowed_set_off=Decimal(0))
return TaxResult(
verified=False,
message=f"VDA income is zero but claimed tax is {claimed_tax}. Expected 0.",
allowed_set_off=Decimal(0),
)

if vda_income < 0:
return TaxResult(
verified=False,
message=f"VDA income is negative ({vda_income}) — this is a loss, not income. Use verify_set_off for loss treatment.",
allowed_set_off=Decimal(0),
)

expected_tax = vda_income * EXPECTED_RATE

# Allow small float tolerance if input wasn't decimal, but strict for now
if abs(claimed_tax - expected_tax) < Decimal("0.1"):
return TaxResult(
verified=True,
verified=True,
message=f"✅ VDA Tax correct (30% of {vda_income})",
allowed_set_off=Decimal(0)
)
else:
return TaxResult(
verified=False,
verified=False,
message=f"❌ Section 115BBH Violation: VDA Income taxed at 30% flat. Expected {expected_tax}, Claimed {claimed_tax}",
allowed_set_off=Decimal(0)
)
Loading
Loading