"""Structural + unit: API handlers call shipped eligibility/strategy/tax functions.""" from __future__ import annotations import inspect import pytest from fastapi import HTTPException from endpoints import company as company_ep from core.eligibility.spine import ( build_eligibility_report, eligibility_override_is_complete, ) from core.strategy.recommend import PATH_TAX, recommend_strategy_paths from core.strategy.tax_paths import ( PARENT_STRATEGY_PATH_ID, TAX_PATH_IP_BOX, TAX_PATH_PSI, TAX_PATH_ULGA_BR, ) def test_company_router_has_eligibility_and_strategy_routes(): paths = {getattr(r, "path", "") for r in company_ep.router.routes} # FastAPI may store path without prefix on route joined = " ".join(sorted(paths)) assert "eligibility" in joined assert "strategy" in joined or "recommend" in joined assert "tax" in joined def test_tax_router_has_paths_route(): """Plan §8 GET /api/tax/paths + company /tax/paths both exposed.""" company_paths = " ".join( sorted(getattr(r, "path", "") for r in company_ep.router.routes) ) assert "tax" in company_paths and "paths" in company_paths tax_paths = {getattr(r, "path", "") for r in company_ep.tax_router.routes} joined = " ".join(sorted(tax_paths)) assert "paths" in joined assert any( getattr(r, "path", "").endswith("/paths") or getattr(r, "path", "") == "/paths" for r in company_ep.tax_router.routes ) def test_get_eligibility_handler_uses_build_report(): src = inspect.getsource(company_ep.get_company_eligibility) assert "build_eligibility_report" in src def test_strategy_handler_uses_recommend(): src = inspect.getsource(company_ep.post_strategy_recommend) assert "recommend_strategy_paths" in src # POST must load profile spine when eligibility omitted (parity with GET) assert "company_data_from_profile" in src assert "get_db" in src or "db" in src # Incomplete body.eligibility must rebuild (not fail-open) assert "eligibility_override_is_complete" in src src2 = inspect.getsource(company_ep.get_strategy_recommend) assert "recommend_strategy_paths" in src2 def test_handlers_are_callable_with_pure_backend(): # Direct pure path the handlers wrap rep = build_eligibility_report( nip="8721990161", company_data={"msp_status": "mikro", "de_minimis_manual_eur": 100}, ) strat = recommend_strategy_paths(eligibility=rep, goal="B+R", company_data={}) assert rep["nip"] assert len(strat["paths"]) >= 4 tax = next(p for p in strat["paths"] if p["id"] == PATH_TAX) assert tax.get("tax_paths") and len(tax["tax_paths"]) == 3 assert tax.get("never_say_submit_to_parp") is True def test_get_tax_paths_api_surface(): """GET tax-paths returns PSI / ulga_br / ip_box with PSI PARP guardrail.""" out = company_ep.get_tax_paths(path_id=None, token_data={"sub": "user_tax_api"}) assert out["status"] == "ok" assert out["parent_strategy_path_id"] == PARENT_STRATEGY_PATH_ID assert out["checklist_ids"] == [TAX_PATH_PSI, TAX_PATH_ULGA_BR, TAX_PATH_IP_BOX] assert len(out["paths"]) == 3 ids = [p["id"] for p in out["paths"]] assert ids == [TAX_PATH_PSI, TAX_PATH_ULGA_BR, TAX_PATH_IP_BOX] psi = next(p for p in out["paths"] if p["id"] == TAX_PATH_PSI) assert psi["never_say_submit_to_parp"] is True assert isinstance(psi["checklist"], list) and len(psi["checklist"]) >= 3 blob = " ".join(psi["checklist"] + psi.get("advisor_notes", [])).lower() assert "parp" in blob def test_get_tax_paths_single_id_and_404(): one = company_ep.get_tax_paths(path_id="psi", token_data={"sub": "user_tax_api"}) assert one["status"] == "ok" assert one["path"]["id"] == TAX_PATH_PSI assert one["checklist_ids"] == [TAX_PATH_PSI] with pytest.raises(HTTPException) as ei: company_ep.get_tax_paths(path_id="not_a_path", token_data={"sub": "user_tax_api"}) assert ei.value.status_code == 404 def test_get_tax_paths_plan_alias_delegates(): """Plan-canonical /api/tax/paths handler shares response shape.""" a = company_ep.get_tax_paths(path_id=None, token_data={"sub": "u"}) b = company_ep.get_tax_paths_plan_alias(path_id=None, token_data={"sub": "u"}) assert a["checklist_ids"] == b["checklist_ids"] assert len(a["paths"]) == len(b["paths"]) == 3 def test_get_tax_paths_compact_alias(): """GET /api/company/tax-paths compact surface (enabled flag + catalog).""" out = company_ep.get_tax_paths_compact(token_data={"sub": "user_tax_compact"}) assert out["status"] == "ok" assert out["enabled"] is True assert len(out["tax_paths"]) == 3 ids = [p["id"] for p in out["tax_paths"]] assert ids == [TAX_PATH_PSI, TAX_PATH_ULGA_BR, TAX_PATH_IP_BOX] psi = next(p for p in out["tax_paths"] if p["id"] == TAX_PATH_PSI) assert psi["never_say_submit_to_parp"] is True def test_post_strategy_incomplete_eligibility_rebuilds_and_includes_tax(): """Thin body.eligibility rebuilds spine; strategy still embeds tax checklists.""" class _EmptyDB: def query(self, *a, **k): class _Q: def filter(self, *a, **k): return self def first(self): return None return _Q() body = company_ep.StrategyRecommendBody( nip="8721990161", goal="B+R", eligibility={"msp": {"status": "mikro"}}, # incomplete — no de_minimis company_data={"msp_status": "mikro", "de_minimis_manual_eur": 100}, ) assert eligibility_override_is_complete(body.eligibility) is False out = company_ep.post_strategy_recommend( body=body, token_data={"sub": "user_tax_strat"}, db=_EmptyDB() ) elig = out["eligibility"] assert elig.get("de_minimis", {}).get("used_eur") == 100.0 assert elig.get("msp", {}).get("status") tax = next(p for p in out["strategy"]["paths"] if p["id"] == PATH_TAX) assert tax.get("tax_paths") and len(tax["tax_paths"]) == 3 assert tax.get("never_say_submit_to_parp") is True def test_post_strategy_incomplete_eligibility_rebuilds_in_handler_source(): """Guard residual: thin eligibility must not skip spine rebuild.""" assert eligibility_override_is_complete({"msp": {"status": "mikro"}}) is False src = inspect.getsource(company_ep.post_strategy_recommend) assert "if not eligibility_override_is_complete" in src assert "build_eligibility_report" in src