"""Tests des agregats de revenus par immeuble. Ces endpoints joignent revenus et locataires a partir du lot. Comme un lot accumule les locataires successifs, une jointure naive multiplie chaque ligne de revenu par le nombre d'occupants passes et gonfle silencieusement les totaux : aucune erreur, juste des montants faux qui derivent avec l'anciennete du parc. """ import copy from datetime import date, timedelta import pytest from plesna_gerance.database.models import Locataire, Revenu from plesna_gerance.database.service import DatabaseService def _mois_glissant(recul: int) -> str: """Date du 15 du mois, `recul` mois en arriere. Les resumes ne portent que sur les derniers mois : des dates fixes sortiraient de la fenetre avec le temps et videraient les tests de leur substance sans jamais les faire echouer. """ jour = date.today().replace(day=15) for _ in range(recul): jour = (jour.replace(day=1) - timedelta(days=1)).replace(day=15) return jour.isoformat() @pytest.fixture def lot_a_deux_locataires(db_session, sample_data): """Un lot occupe successivement par deux locataires, un revenu chacun. Reproduit la situation courante d'une relocation : meme immeuble, meme lot, deux noms differents sur deux comptes rendus. """ service = DatabaseService(db_session) premier = copy.deepcopy(sample_data) premier["metadata"]["document"]["date"] = _mois_glissant(2) service.save_document(data=premier) suivant = copy.deepcopy(sample_data) suivant["metadata"]["document"]["reference"] = "REF002" suivant["metadata"]["document"]["date"] = _mois_glissant(1) suivant["situation_locataires"][0]["locataire"]["nom"] = "MARTIN" suivant["situation_locataires"][0]["lignes"][0]["loyers"] = 600.0 suivant["situation_locataires"][0]["lignes"][0]["total"] = 600.0 suivant["situation_locataires"][0]["lignes"][0]["regles"] = 400.0 suivant["situation_locataires"][0]["lignes"][0]["impayes"] = 200.0 service.save_document(data=suivant) # Le decor doit bien etre celui qu'on veut tester, sinon le test ne prouve rien. assert db_session.query(Locataire).count() == 2 assert db_session.query(Revenu).count() == 2 return db_session #: Le decor : 500 puis 600 factures, 500 puis 400 regles, 200 encore dus. FACTURE = 1100.0 ENCAISSE = 900.0 RESTANT_DU = 200.0 def test_immeubles_ne_compte_pas_les_revenus_en_double( api_client, lot_a_deux_locataires ): """/api/revenus/immeubles somme chaque revenu une seule fois.""" response = api_client.get("/api/revenus/immeubles") assert response.status_code == 200 (immeuble,) = response.json() assert immeuble["total_revenus"] == FACTURE assert immeuble["total_regles"] == ENCAISSE assert immeuble["total_impayes"] == RESTANT_DU def test_summary_ne_compte_pas_les_revenus_en_double( api_client, lot_a_deux_locataires ): """Le bloc by_immeuble de /api/revenus/summary somme comme les KPIs.""" response = api_client.get("/api/revenus/summary") assert response.status_code == 200 corps = response.json() (immeuble,) = corps["by_immeuble"] assert immeuble["total_revenus"] == FACTURE assert immeuble["total_regles"] == ENCAISSE assert immeuble["total_impayes"] == RESTANT_DU # Les deux blocs doivent raconter la meme chose. assert corps["kpis"]["total_revenus"] == immeuble["total_revenus"] assert corps["kpis"]["total_impayes"] == immeuble["total_impayes"] def test_immeubles_compte_lots_et_locataires(api_client, lot_a_deux_locataires): """Les effectifs restent justes : un lot, ses deux occupants successifs.""" (immeuble,) = api_client.get("/api/revenus/immeubles").json() assert immeuble["nb_lots"] == 1 assert immeuble["nb_locataires"] == 2 def test_taux_recouvrement_reste_coherent(api_client, lot_a_deux_locataires): """Le taux se deduit des totaux : il doit suivre la meme correction.""" (immeuble,) = api_client.get("/api/revenus/immeubles").json() assert immeuble["taux_recouvrement"] == round(ENCAISSE / FACTURE * 100, 1)