diff --git a/backend/agents/deterministic/financial_reality.py b/backend/agents/deterministic/financial_reality.py index 8fafd69..9eee192 100644 --- a/backend/agents/deterministic/financial_reality.py +++ b/backend/agents/deterministic/financial_reality.py @@ -2,13 +2,29 @@ Financial Reality Agent — Pure math, no AI. Standard amortization formula. Deterministic output for the same inputs every time. No imports from Dev 2's files. + +Feature additions: + Feature 3: Bank FOIR (Eligibility) Underwriting Check + - Computes total FOIR = (new EMI + existing EMIs) / monthly income + - Flags breach if FOIR > 50% (RBI benchmark) + + Feature 5: Property Age & Structural Depreciation Factor + - RCC building expected lifespan: 60 years + - Banks typically reduce LTV below standard 80% for buildings >20 yrs old + - Buildings >40 yrs: max LTV 50–60%; structurally risky """ +from datetime import date +from typing import Optional, Tuple from schemas.schemas import ( UserInput, FinancialRealityOutput, AffordabilityStatus, IndiaCostBreakdown ) from engines.india_defaults import calculate_true_total_cost +# --------------------------------------------------------------------------- +# EMI helpers +# --------------------------------------------------------------------------- + def _calculate_emi(principal: float, annual_rate: float, tenure_years: int) -> float: """Standard EMI amortization formula: EMI = P * r * (1+r)^n / ((1+r)^n - 1)""" if principal <= 0: @@ -37,12 +53,105 @@ def _reverse_loan_from_emi(target_emi: float, annual_rate: float, tenure_years: return principal +# --------------------------------------------------------------------------- +# Feature 3: FOIR Underwriting Check +# --------------------------------------------------------------------------- + +FOIR_LIMIT = 0.50 # RBI benchmark; some banks use 0.55 for HNI + +def _compute_foir( + emi: float, + existing_obligations: float, + monthly_income: float, +) -> Tuple[float, bool, Optional[str]]: + """ + Returns (foir_ratio, foir_breach, warning_message). + + foir_ratio = (new_EMI + all_existing_EMIs) / monthly_income + """ + if monthly_income <= 0: + return 1.0, True, "Income not provided — FOIR cannot be calculated." + + total_obligations = emi + existing_obligations + foir = total_obligations / monthly_income + + if foir > FOIR_LIMIT: + warning = ( + f"FOIR is {foir*100:.1f}% — exceeds the RBI 50% underwriting limit. " + f"Total monthly debt (₹{total_obligations:,.0f}) on income of " + f"₹{monthly_income:,.0f} will likely trigger a bank rejection. " + f"Consider reducing loan amount or increasing income documentation." + ) + return round(foir, 4), True, warning + + return round(foir, 4), False, None + + +# --------------------------------------------------------------------------- +# Feature 5: Property Age & Structural Depreciation +# --------------------------------------------------------------------------- + +RCC_LIFESPAN_YEARS = 60 # Standard RCC building life expectancy +LTV_STANDARD = 0.80 # Bank standard LTV for properties < 20 yrs +LTV_MODERATE = 0.70 # 20–30 yrs old +LTV_AGED = 0.60 # 30–40 yrs old +LTV_OLD = 0.50 # > 40 yrs old + + +def _compute_depreciation( + construction_year: Optional[int], +) -> Tuple[Optional[int], Optional[float], bool, Optional[str]]: + """ + Returns (structural_life_remaining, ltv_recommended_pct, ltv_age_risk, warning). + """ + if construction_year is None: + return None, None, False, None + + current_year = date.today().year + building_age = current_year - construction_year + life_remaining = max(0, RCC_LIFESPAN_YEARS - building_age) + + if building_age < 20: + ltv_pct = LTV_STANDARD + risk = False + warning = None + elif building_age < 30: + ltv_pct = LTV_MODERATE + risk = True + warning = ( + f"Building is {building_age} years old. Banks may restrict LTV to 70% " + f"(vs. standard 80%), increasing the required down payment significantly." + ) + elif building_age < 40: + ltv_pct = LTV_AGED + risk = True + warning = ( + f"Building is {building_age} years old — entering the aging bracket. " + f"Expect bank LTV of ~60%. Structural survey strongly recommended before purchase." + ) + else: + ltv_pct = LTV_OLD + risk = True + warning = ( + f"CRITICAL: Building is {building_age} years old with only " + f"~{life_remaining} years of structural life remaining. " + f"Banks typically offer max LTV of 50%. High risk of future maintenance " + f"levy, redevelopment disputes, and difficulty reselling." + ) + + return life_remaining, ltv_pct, risk, warning + + +# --------------------------------------------------------------------------- +# Main calculation +# --------------------------------------------------------------------------- + def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: """ Calculates the complete financial reality of a home purchase. Pure math. Zero LLM involvement. Deterministic. """ - # --- India cost breakdown --- + # --- India cost breakdown (now with GST slab, district stamp duty, hidden fees) --- loan_amount = user_input.property_price - user_input.down_payment if loan_amount < 0: loan_amount = 0.0 @@ -53,6 +162,9 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: property_type=user_input.property_type.value, loan_amount=loan_amount, area_sqft=user_input.area_sqft if user_input.area_sqft else 1000, + area_sqm=user_input.area_sqm, + district=user_input.district, + is_female_buyer=user_input.is_female_buyer, ) # --- EMI calculation --- @@ -70,7 +182,6 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: monthly_surplus_after_emi = user_input.monthly_income - user_input.monthly_expenses - emi # --- 12-month cash flow projection --- - # Shows cumulative savings each month starting from current savings cash_flow_12_months = [] running_savings = user_input.total_savings - user_input.down_payment savings_depletion_month = None @@ -82,12 +193,10 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: savings_depletion_month = month # --- Safe and maximum property prices --- - # Safe: EMI = exactly 35% of income target_emi_safe = user_input.monthly_income * 0.35 safe_loan = _reverse_loan_from_emi(target_emi_safe, user_input.annual_interest_rate, user_input.tenure_years) safe_property_price = safe_loan + user_input.down_payment - # Maximum: EMI = exactly 50% of income target_emi_max = user_input.monthly_income * 0.50 max_loan = _reverse_loan_from_emi(target_emi_max, user_input.annual_interest_rate, user_input.tenure_years) maximum_property_price = max_loan + user_input.down_payment @@ -100,6 +209,18 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: else: affordability_status = AffordabilityStatus.OVEREXTENDED + # --- Feature 3: FOIR Underwriting Check --- + foir_ratio, foir_breach, foir_warning = _compute_foir( + emi=emi, + existing_obligations=user_input.existing_emi_obligations, + monthly_income=user_input.monthly_income, + ) + + # --- Feature 5: Property Age & Structural Depreciation --- + structural_life_remaining, ltv_recommended_pct, ltv_age_risk, age_depreciation_warning = ( + _compute_depreciation(user_input.construction_year) + ) + return FinancialRealityOutput( emi=round(emi, 2), emi_to_income_ratio=round(emi_to_income_ratio, 4), @@ -112,4 +233,14 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput: india_cost_breakdown=india_cost_breakdown, loan_amount=round(loan_amount, 2), total_interest_payable=round(total_interest_payable, 2), + # Feature 3: FOIR + foir_ratio=foir_ratio, + foir_limit=FOIR_LIMIT, + foir_breach=foir_breach, + foir_warning=foir_warning, + # Feature 5: Depreciation + structural_life_remaining=structural_life_remaining, + ltv_recommended_pct=ltv_recommended_pct, + ltv_age_risk=ltv_age_risk, + age_depreciation_warning=age_depreciation_warning, ) diff --git a/backend/engines/compute.py b/backend/engines/compute.py new file mode 100644 index 0000000..453f940 --- /dev/null +++ b/backend/engines/compute.py @@ -0,0 +1,67 @@ +""" +Headless Compute Engine — All deterministic math in one function call. + +compute_all() runs every deterministic agent in sequence and returns the +complete bundled result. This is the function behind the /api/v1/calculate +endpoint — designed for sub-50ms execution with zero LLM involvement. + +Use cases: + - Frontend real-time sliders that update as the user drags + - Comparison tools that need fast recalculation + - Batch processing without burning API quota + - Unit testing the math pipeline in isolation +""" +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from schemas.schemas import UserInput, ComputeAllOutput +from engines.india_defaults import calculate_true_total_cost +from agents.deterministic.financial_reality import calculate_affordability +from agents.deterministic.scenario_simulation import run_all_scenarios +from agents.deterministic.risk_scorer import calculate_risk_score + + +def compute_all(user_input: UserInput) -> ComputeAllOutput: + """ + Run every deterministic calculation and return the bundled result. + + Execution order (respects dependencies): + 1. india_defaults — hidden cost breakdown (independent) + 2. affordability — EMI, ratios, cash flow (independent) + 3. scenarios — stress tests (depends on #2) + 4. risk_score — composite score (depends on #2 and #3) + + Total execution time target: < 50ms on a single CPU core. + All functions are pure Python arithmetic with no I/O. + """ + loan_amount = user_input.property_price - user_input.down_payment + if loan_amount < 0: + loan_amount = 0.0 + + india_costs = calculate_true_total_cost( + base_price=user_input.property_price, + state=user_input.state, + property_type=user_input.property_type.value, + loan_amount=loan_amount, + area_sqft=user_input.area_sqft if user_input.area_sqft else 1000, + area_sqm=user_input.area_sqm, + district=user_input.district, + is_female_buyer=user_input.is_female_buyer, + ) + + financial_reality = calculate_affordability(user_input) + all_scenarios = run_all_scenarios(user_input, financial_reality) + risk_score = calculate_risk_score( + financial_reality=financial_reality, + all_scenarios=all_scenarios, + age=user_input.age, + tenure_years=user_input.tenure_years, + ) + + return ComputeAllOutput( + india_cost_breakdown=india_costs, + financial_reality=financial_reality, + all_scenarios=all_scenarios, + risk_score=risk_score, + ) diff --git a/backend/engines/india_defaults.py b/backend/engines/india_defaults.py index 724a875..1ae8e8d 100644 --- a/backend/engines/india_defaults.py +++ b/backend/engines/india_defaults.py @@ -1,35 +1,210 @@ """ India real estate hidden cost calculations. -All stamp duty rates, registration fees, GST, maintenance deposit, -loan processing, legal charges, and Section 80C/24B tax benefits. + +Feature upgrades (Institutional-Grade Financial Math): + 1. Dynamic GST Slab Auto-Classifier + - 0% for ready-to-move + - 1% for affordable housing (≤₹45L AND ≤90 sqm carpet area) + - 5% for all other under-construction properties + + 2. Exact District-Level Stamp Duty Engine + - District-level overrides for MH, DL, KA, TN, GJ, HR + - 1% female buyer concession where applicable + + 3. Bank FOIR (Eligibility) Underwriting Check [computed in financial_reality.py] + + 4. The "Hidden Fee" Aggregator + - Bank processing fee (SBI 0.35% benchmark) + - Legal / technical verification fee + - Annual BMC / municipal property tax estimate + + 5. Property Age & Structural Depreciation Factor [computed in financial_reality.py] + No AI. Pure data and math. """ +import math +from datetime import date +from typing import Optional, Dict, Tuple, Set from schemas.schemas import IndiaCostBreakdown -# State-specific stamp duty rates (as of 2024-2025) -STAMP_DUTY_RATES = { - "maharashtra": 0.05, - "karnataka": 0.056, - "delhi": 0.06, - "tamil_nadu": 0.07, - "gujarat": 0.045, - "rajasthan": 0.06, - "west_bengal": 0.06, - "telangana": 0.05, - "andhra_pradesh": 0.05, - "punjab": 0.07, - "haryana": 0.07, - "uttar_pradesh": 0.07, - "madhya_pradesh": 0.075, - "kerala": 0.08, - "goa": 0.035, + +# --------------------------------------------------------------------------- +# Feature 1: GST Slab Classification +# --------------------------------------------------------------------------- + +AFFORDABLE_HOUSING_PRICE_LIMIT = 4_500_000 # ₹45 lakh +AFFORDABLE_HOUSING_SQM_LIMIT = 90 # 90 sq metres carpet area + +GST_SLAB_EXEMPT = "exempt" +GST_SLAB_AFFORDABLE = "affordable_1pct" +GST_SLAB_STANDARD = "standard_5pct" + + +def _classify_gst_slab( + property_type: str, + base_price: float, + area_sqm: Optional[float], +) -> Tuple[str, float, float]: + """ + Determines the correct GST slab. + + Returns: + (slab_code, gst_rate, gst_amount) + """ + if property_type != "under_construction": + return GST_SLAB_EXEMPT, 0.0, 0.0 + + # Affordable housing: price ≤ ₹45L AND carpet area ≤ 90 sqm + if ( + base_price <= AFFORDABLE_HOUSING_PRICE_LIMIT + and area_sqm is not None + and area_sqm <= AFFORDABLE_HOUSING_SQM_LIMIT + ): + return GST_SLAB_AFFORDABLE, 0.01, base_price * 0.01 + + return GST_SLAB_STANDARD, 0.05, base_price * 0.05 + + +# --------------------------------------------------------------------------- +# Feature 2: District-Level Stamp Duty Engine +# --------------------------------------------------------------------------- + +# State-level base rates (2024-25) +STATE_STAMP_DUTY_RATES: Dict[str, float] = { + "maharashtra": 0.05, + "karnataka": 0.056, + "delhi": 0.06, + "tamil_nadu": 0.07, + "gujarat": 0.045, + "rajasthan": 0.06, + "west_bengal": 0.06, + "telangana": 0.05, + "andhra_pradesh": 0.05, + "punjab": 0.07, + "haryana": 0.07, + "uttar_pradesh": 0.07, + "madhya_pradesh": 0.075, + "kerala": 0.08, + "goa": 0.035, +} + +# District-level overrides (state → district → rate) +DISTRICT_STAMP_DUTY_OVERRIDES: dict[str, Dict[str, float]] = { + "maharashtra": { + "mumbai_city": 0.05, # flat 5% (registration capped ₹30k) + "mumbai_suburban":0.05, + "pune": 0.07, # Pune adds 1% metro cess + LBT + "thane": 0.07, + "nagpur": 0.06, + "nashik": 0.06, + "aurangabad": 0.06, + }, + "delhi": { + "new_delhi": 0.06, # male buyer + "south_delhi": 0.06, + "north_delhi": 0.06, + "east_delhi": 0.06, + "west_delhi": 0.06, + "central_delhi": 0.06, + }, + "karnataka": { + "bengaluru_urban":0.056, + "mysuru": 0.056, + "mangaluru": 0.056, + "hubballi": 0.056, + }, + "tamil_nadu": { + "chennai": 0.07, + "coimbatore": 0.07, + "madurai": 0.07, + "tiruchirappalli":0.07, + }, + "haryana": { + "gurugram": 0.07, + "faridabad": 0.07, + "sonipat": 0.07, + "panipat": 0.07, + }, + "gujarat": { + "ahmedabad": 0.045, + "surat": 0.045, + "vadodara": 0.045, + "rajkot": 0.045, + }, +} + +# States that offer female buyer concession (1% off stamp duty) +FEMALE_CONCESSION_STATES: Set[str] = { + "maharashtra", "delhi", "haryana", "uttar_pradesh", + "rajasthan", "punjab", "himachal_pradesh", } -# Default stamp duty rate if state is not in the lookup DEFAULT_STAMP_DUTY_RATE = 0.06 +def _calculate_stamp_duty( + base_price: float, + state: str, + district: Optional[str], + is_female_buyer: bool, +) -> tuple[float, float, bool]: + """ + Returns (stamp_duty_amount, effective_rate, concession_applied). + """ + # Look up district first, fall back to state, then global default + state_districts = DISTRICT_STAMP_DUTY_OVERRIDES.get(state, {}) + if district and district in state_districts: + base_rate = state_districts[district] + else: + base_rate = STATE_STAMP_DUTY_RATES.get(state, DEFAULT_STAMP_DUTY_RATE) + + # Apply female buyer concession + concession_applied = False + if is_female_buyer and state in FEMALE_CONCESSION_STATES: + effective_rate = max(0.0, base_rate - 0.01) + concession_applied = True + else: + effective_rate = base_rate + + stamp_duty = base_price * effective_rate + return stamp_duty, effective_rate, concession_applied + + +# --------------------------------------------------------------------------- +# Feature 4: Hidden Fee Aggregator +# --------------------------------------------------------------------------- + +# Bank processing fee (SBI benchmark: 0.35%, min ₹2k max ₹10k) +BANK_PROCESSING_FEE_RATE = 0.0035 +BANK_PROCESSING_FEE_MIN = 2_000 +BANK_PROCESSING_FEE_MAX = 10_000 + +# Legal / technical verification fee charged by bank (approximate) +LEGAL_VERIFICATION_FEE = 8_500.0 + +# Annual municipal property tax: approximately 0.1% of property value +# (BMC in Mumbai charges ₹1–3/sqft/year; 0.1% is a conservative city estimate) +ANNUAL_PROPERTY_TAX_RATE = 0.001 + + +def _compute_hidden_fees( + loan_amount: float, + base_price: float, +) -> Tuple[float, float, float]: + """ + Returns (bank_processing_fee, legal_verification_fee, annual_property_tax). + """ + bank_fee = loan_amount * BANK_PROCESSING_FEE_RATE + bank_fee = max(BANK_PROCESSING_FEE_MIN, min(BANK_PROCESSING_FEE_MAX, bank_fee)) + annual_tax = base_price * ANNUAL_PROPERTY_TAX_RATE + return round(bank_fee, 2), round(LEGAL_VERIFICATION_FEE, 2), round(annual_tax, 2) + + +# --------------------------------------------------------------------------- +# Main function +# --------------------------------------------------------------------------- + def calculate_true_total_cost( base_price: float, state: str, @@ -37,53 +212,59 @@ def calculate_true_total_cost( loan_amount: float, area_sqft: float = 1000, maintenance_per_sqft: float = 3.0, + # Feature 1 + area_sqm: Optional[float] = None, + # Feature 2 + district: Optional[str] = None, + is_female_buyer: bool = False, ) -> IndiaCostBreakdown: """ Calculates the true total cost of acquiring an Indian property, including all hidden fees that buyers typically don't account for. - Args: - base_price: Listed property price in rupees. - state: Indian state name (used for stamp duty lookup). - property_type: "under_construction" or "ready_to_move". - loan_amount: Principal loan amount in rupees. - area_sqft: Property area in sq ft for maintenance calculation. - maintenance_per_sqft: Monthly maintenance rate per sq ft. - - Returns: - IndiaCostBreakdown with every cost component and the true total. + Feature upgrades included: + - GST auto-classification (0% / 1% affordable / 5% standard) + - District-level stamp duty with female buyer concession + - Itemised bank processing fee, legal verification fee, annual property tax """ - normalized_state = state.lower().strip().replace(" ", "_") + normalized_state = state.lower().strip().replace(" ", "_") + normalized_district = district.lower().strip().replace(" ", "_") if district else None - # --- Stamp duty --- - stamp_duty_rate = STAMP_DUTY_RATES.get(normalized_state, DEFAULT_STAMP_DUTY_RATE) - stamp_duty = base_price * stamp_duty_rate + # --- Feature 2: Stamp duty (district-aware, female concession) --- + stamp_duty, stamp_duty_rate, female_concession = _calculate_stamp_duty( + base_price=base_price, + state=normalized_state, + district=normalized_district, + is_female_buyer=is_female_buyer, + ) # --- Registration fee --- # Generally 1% across India registration_fee = base_price * 0.01 # Maharashtra specific cap: max ₹30,000 - if normalized_state == "maharashtra" and registration_fee > 30000: - registration_fee = 30000.0 - - # --- GST --- - # 5% on under-construction properties, 0% on ready-to-move - gst_applicable = property_type == "under_construction" - if gst_applicable: - gst = base_price * 0.05 - else: - gst = 0.0 + if normalized_state == "maharashtra" and registration_fee > 30_000: + registration_fee = 30_000.0 + + # --- Feature 1: GST Auto-Classifier --- + gst_slab, _gst_rate, gst = _classify_gst_slab(property_type, base_price, area_sqm) + gst_applicable = gst_slab != GST_SLAB_EXEMPT # --- Maintenance deposit --- # Typically 24 months of maintenance upfront maintenance_deposit = maintenance_per_sqft * area_sqft * 24.0 - # --- Loan processing fee --- - # 0.75% of loan amount + # --- Loan processing fee (legacy, kept for backward compat) --- + # Now superseded by itemised bank_processing_fee below loan_processing_fee = loan_amount * 0.0075 - # --- Legal charges --- - legal_charges = 15000.0 + # --- Feature 4: Hidden fee aggregator --- + bank_processing_fee, legal_verification_fee, annual_property_tax = _compute_hidden_fees( + loan_amount=loan_amount, + base_price=base_price, + ) + + # --- Legacy legal charges (solicitor / title search) --- + legal_charges = 15_000.0 # --- True total cost --- true_total_cost = ( @@ -92,27 +273,33 @@ def calculate_true_total_cost( + registration_fee + gst + maintenance_deposit - + loan_processing_fee + + bank_processing_fee # replaces old loan_processing_fee in true cost + + legal_verification_fee + legal_charges + # annual_property_tax is recurring so NOT added to one-time true_total_cost + # but surfaced separately in the breakdown for transparency ) # --- Tax benefits --- - # Section 80C: deduction on principal repayment, max ₹1,50,000/year - tax_benefit_80c = 150000.0 - # Section 24B: deduction on interest payment, max ₹2,00,000/year - tax_benefit_24b = 200000.0 + tax_benefit_80c = 150_000.0 # Section 80C: max ₹1.5L/yr on principal + tax_benefit_24b = 200_000.0 # Section 24B: max ₹2L/yr on interest return IndiaCostBreakdown( base_price=base_price, - stamp_duty=stamp_duty, - stamp_duty_rate=stamp_duty_rate, - registration_fee=registration_fee, - gst=gst, + stamp_duty=round(stamp_duty, 2), + stamp_duty_rate=round(stamp_duty_rate, 4), + female_buyer_concession_applied=female_concession, + registration_fee=round(registration_fee, 2), + gst=round(gst, 2), gst_applicable=gst_applicable, - maintenance_deposit=maintenance_deposit, - loan_processing_fee=loan_processing_fee, - legal_charges=legal_charges, - true_total_cost=true_total_cost, + gst_slab=gst_slab, + maintenance_deposit=round(maintenance_deposit, 2), + loan_processing_fee=round(loan_processing_fee, 2), + bank_processing_fee=bank_processing_fee, + legal_verification_fee=legal_verification_fee, + annual_property_tax=annual_property_tax, + legal_charges=round(legal_charges, 2), + true_total_cost=round(true_total_cost, 2), tax_benefit_80c=tax_benefit_80c, tax_benefit_24b=tax_benefit_24b, ) diff --git a/backend/schemas/schemas.py b/backend/schemas/schemas.py index 7724f9e..a75c843 100644 --- a/backend/schemas/schemas.py +++ b/backend/schemas/schemas.py @@ -129,6 +129,19 @@ class UserInput(BaseModel): area_sqft: Optional[float] = None # Session this input belongs to session_id: str + # --- Feature 1: GST Auto-Classifier --- + # Property carpet area in sq metres (needed for affordable housing GST classification) + area_sqm: Optional[float] = None + # --- Feature 2: District-Level Stamp Duty --- + # Specific district within the state (e.g. 'mumbai_city', 'pune') for exact rates + district: Optional[str] = None + # True if primary buyer is female (gets 1% concession in most states) + is_female_buyer: bool = False + # --- Feature 5: Property Age / Depreciation --- + # Year the building was constructed (for resale properties) + construction_year: Optional[int] = None + # Existing loan obligations per month in rupees (for FOIR calculation) + existing_emi_obligations: float = 0.0 class BehavioralAnswer(BaseModel): @@ -161,11 +174,21 @@ class IndiaCostBreakdown(BaseModel): base_price: float stamp_duty: float stamp_duty_rate: float + # True if female buyer concession was applied + female_buyer_concession_applied: bool = False registration_fee: float gst: float gst_applicable: bool + # GST classification: 'exempt', 'affordable_1pct', 'standard_5pct' + gst_slab: str = "exempt" maintenance_deposit: float loan_processing_fee: float + # Itemised bank processing fee (e.g. SBI 0.35%) + bank_processing_fee: float = 0.0 + # One-time legal / technical verification fee charged by bank + legal_verification_fee: float = 0.0 + # Estimated annual BMC/municipal property tax + annual_property_tax: float = 0.0 legal_charges: float true_total_cost: float # Annual tax benefit under Section 80C on principal repayment @@ -194,6 +217,24 @@ class FinancialRealityOutput(BaseModel): india_cost_breakdown: IndiaCostBreakdown loan_amount: float total_interest_payable: float + # --- Feature 3: FOIR Underwriting Check --- + # Total Fixed Obligation to Income Ratio (EMI + existing obligations / income) + foir_ratio: float = 0.0 + # RBI standard threshold (typically 0.50–0.55) + foir_limit: float = 0.50 + # True if FOIR exceeds limit — bank will likely reject the loan + foir_breach: bool = False + # Human-readable FOIR warning message + foir_warning: Optional[str] = None + # --- Feature 5: Property Age / Depreciation --- + # Remaining structural life in years (RCC buildings: 60yr lifespan) + structural_life_remaining: Optional[int] = None + # Recommended max LTV banks will offer based on building age + ltv_recommended_pct: Optional[float] = None + # True if bank may reduce LTV due to building age + ltv_age_risk: bool = False + # Human-readable age risk warning + age_depreciation_warning: Optional[str] = None class ScenarioOutput(BaseModel): @@ -576,4 +617,16 @@ class SessionStartResponse(BaseModel): class ConversationResponse(BaseModel): session_id: str conversation_output: ConversationOutput - updated_analysis: Optional[AnalysisResponse] = None \ No newline at end of file + updated_analysis: Optional[AnalysisResponse] = None + + +# ----------------------------------------------------------------------------- +# Headless compute engine output (PR: headless-calculate-endpoint) +# ----------------------------------------------------------------------------- + +class ComputeAllOutput(BaseModel): + """Bundled output of all deterministic calculations — no LLM involved.""" + india_cost_breakdown: IndiaCostBreakdown + financial_reality: FinancialRealityOutput + all_scenarios: AllScenariosOutput + risk_score: RiskScoreOutput \ No newline at end of file diff --git a/frontend/niv-ai .html b/frontend/niv-ai .html index 548803c..c51db39 100644 --- a/frontend/niv-ai .html +++ b/frontend/niv-ai .html @@ -1356,12 +1356,40 @@