"""Tests de la fiche d'un lot. Cette page restitue un lot tel que les comptes rendus le portent. Les tests protègent donc ce qui la rendrait fausse ou trompeuse : cumuler un report de solde, ventiler des charges d'immeuble qu'aucun document n'attribue, ou regrouper des lignes que le compte rendu a émises séparément. """ import pytest from plesna_gerance.database.models import ( Document, Immeuble, Locataire, Lot, LotCaracteristiques, ) from plesna_gerance.database.service import DatabaseService @pytest.fixture def donnees(db_session, sample_data): """Deux comptes rendus sur un lot : un loyer réglé, puis un report impayé. Le second document facture un loyer resté impayé et reporte le solde du premier — la configuration exacte où un cumul naïf compterait deux fois la même dette. """ service = DatabaseService(db_session) service.save_document(data=sample_data) suivant = { **sample_data, "metadata": { **sample_data["metadata"], "document": { "reference": "REF002", "date": "2024-02-15", "type": "COMPTE RENDU DE GESTION", }, }, "situation_locataires": [ { "lot": {"numero": "01", "type": "Appartement"}, "locataire": {"nom": "DUPONT"}, "lignes": [ { "type": "solde_anterieur", "total": 300.0, "regles": 0.0, "impayes": 300.0, }, { "type": "loyer", "periode": {"debut": "2024-02-01", "fin": "2024-02-29"}, "loyers": 500.0, "total": 500.0, "regles": 200.0, "impayes": 300.0, }, ], } ], "recapitulatif_operations": [ { "categorie": "DEPENSES_NON_RECUPERABLES", "sous_categorie": "Travaux divers", "fournisseur": "PLOMBERIE", "description": "S01 ACOMPTE 40% remplacement chaudière", # Le parser déduit ce numéro du préfixe de la description ; la # fixture le fournit tel qu'il arrive en base. "lot_numero": "01", "montants": {"debit": 400.0, "deductible": 400.0}, }, { "categorie": "DEPENSES_NON_RECUPERABLES", "sous_categorie": "Travaux divers", "fournisseur": "PLOMBERIE", "description": "S01 SOLDE remplacement chaudière", "lot_numero": "01", "montants": {"debit": 600.0, "deductible": 600.0}, }, { "categorie": "HONORAIRES_DE_GESTION", "sous_categorie": "Frais d'expert", "fournisseur": "EXPERTISE", "description": "S01 - Remise état des lieux sortie", "lot_numero": "01", # Un avoir : la ligne rend au propriétaire au lieu de lui coûter. "montants": {"debit": 0.0, "credit": 35.4}, }, ], } service.save_document(data=suivant) immeuble = db_session.query(Immeuble).filter(Immeuble.code == "IMM1").one() lot = db_session.query(Lot).filter(Lot.immeuble_id == immeuble.id).one() return immeuble, lot def test_lot_inconnu_donne_404(api_client, donnees): assert api_client.get("/api/lots/999999/analyse").status_code == 404 def test_identite_montre_les_trous_de_la_fiche(api_client, donnees): """Une caractéristique non saisie reste nulle : la page doit le montrer.""" _, lot = donnees identite = api_client.get(f"/api/lots/{lot.id}/analyse").json()["identite"] assert identite["numero"] == "01" assert identite["immeuble_code"] == "IMM1" assert identite["type_effectif"] == "Appartement" assert identite["surface"] is None assert identite["dpe_classe"] is None assert identite["locataires"] == ["DUPONT"] def test_le_report_de_solde_ne_gonfle_pas_le_facture(api_client, donnees): """Le facturé ne retient que les loyers, jamais la dette reportée. Deux loyers de 500 € : cumuler en plus le report de 300 € afficherait 1300 € facturés pour un lot qui n'a jamais rien facturé de tel. """ _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chiffres"] assert chiffres["facture"] == 1000.0 assert chiffres["encaisse"] == 700.0 #: Photo du dernier compte rendu (300 de report + 300 de loyer), pas un cumul. assert chiffres["restant_du"] == 600.0 assert chiffres["taux_recouvrement"] == 70.0 def test_les_charges_d_immeuble_restent_hors_du_lot(api_client, donnees): """Le nettoyage de l'immeuble ne doit pas atterrir dans un lot. Aucune donnée ne dit quelle part revient à quel lot : la ventiler inventerait des montants. Elle est exposée à part, comme contexte. """ _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chiffres"] # 400 + 600 de travaux imputés au lot, sans le nettoyage de l'immeuble. assert chiffres["depenses_debit"] == 1000.0 assert chiffres["depenses_credit"] == 35.4 assert chiffres["nb_operations"] == 3 # Le nettoyage du premier compte rendu, resté sans lot. assert chiffres["depenses_immeuble_non_reparties"] == 50.0 # Encaissé (700) moins décaissé (1000), avoir rendu (35,40) : charges # communes exclues. assert chiffres["solde"] == -264.6 def test_la_chronologie_garde_les_lignes_telles_qu_extraites(api_client, donnees): """Acompte et solde restent deux lignes : le compte rendu les porte ainsi.""" _, lot = donnees chronologie = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chronologie"] travaux = [ligne for ligne in chronologie if ligne["fournisseur"] == "PLOMBERIE"] assert len(travaux) == 2 assert {ligne["montant"] for ligne in travaux} == {400.0, 600.0} def test_la_chronologie_est_ordonnee_et_signale_les_reports(api_client, donnees): """L'ordre des comptes rendus est l'ordre de lecture de la page.""" _, lot = donnees chronologie = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chronologie"] dates = [ligne["date"] for ligne in chronologie] assert dates == sorted(dates) assert dates[0] == "2024-01-15" reports = [ligne for ligne in chronologie if ligne["est_report"]] assert len(reports) == 1 assert reports[0]["montant"] == 300.0 def test_les_intervenants_agregent_le_fournisseur_du_compte_rendu(api_client, donnees): """Une entreprise, ses interventions et son montant — rien de déduit.""" _, lot = donnees intervenants = api_client.get(f"/api/lots/{lot.id}/analyse").json()["intervenants"] par_nom = {ligne["fournisseur"]: ligne for ligne in intervenants} assert set(par_nom) == {"PLOMBERIE", "EXPERTISE"} assert par_nom["PLOMBERIE"]["nb_interventions"] == 2 assert par_nom["PLOMBERIE"]["montant"] == 1000.0 assert par_nom["PLOMBERIE"]["derniere_date"] == "2024-02-15" def test_un_avoir_rend_le_montant_de_l_intervenant_negatif(api_client, donnees): """Le crédit est déduit : une remise ne doit pas s'afficher comme un coût.""" _, lot = donnees intervenants = api_client.get(f"/api/lots/{lot.id}/analyse").json()["intervenants"] expertise = next( ligne for ligne in intervenants if ligne["fournisseur"] == "EXPERTISE" ) assert expertise["montant"] == -35.4 # La plus engagée en tête : un avoir se classe donc en dernier. assert intervenants[-1]["fournisseur"] == "EXPERTISE" def test_le_total_d_un_intervenant_est_celui_de_ses_lignes(api_client, donnees): """Invariant du dépliage : le détail doit retrouver le total affiché. Les deux chiffres viennent de calculs séparés (agrégat SQL d'un côté, lignes de la chronologie de l'autre) ; les laisser diverger ferait mentir la ligne qu'on vient d'ouvrir. """ _, lot = donnees analyse = api_client.get(f"/api/lots/{lot.id}/analyse").json() for intervenant in analyse["intervenants"]: lignes = [ ligne for ligne in analyse["chronologie"] if ligne["fournisseur"] == intervenant["fournisseur"] ] assert len(lignes) == intervenant["nb_interventions"] assert ( round(sum(ligne["montant"] for ligne in lignes), 2) == intervenant["montant"] ) def test_le_loyer_se_lit_mois_par_mois(api_client, donnees): """Deux comptes rendus, deux mois : la fiche les remet sur un axe de temps.""" _, lot = donnees loyer = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"] assert [point["mois"] for point in loyer["serie"]] == ["2024-01", "2024-02"] assert [point["loyer"] for point in loyer["serie"]] == [500.0, 500.0] assert loyer["en_vigueur"]["loyer"] == 500.0 assert loyer["en_vigueur"]["depuis"] == "2024-01" # Un seul niveau depuis le premier compte rendu : aucune révision à montrer. assert loyer["en_vigueur"]["precedent"] is None def test_sans_surface_saisie_le_loyer_au_m2_reste_vide(api_client, donnees): """Le ratio manquant se voit ; un zéro laisserait croire à un loyer nul.""" _, lot = donnees loyer = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"] assert loyer["surface"] is None assert all(point["loyer_m2"] is None for point in loyer["serie"]) assert loyer["en_vigueur"]["loyer_m2"] is None def test_la_surface_saisie_allume_le_loyer_au_m2(api_client, db_session, donnees): """La fiche saisie est la seule source de surface : aucun PDF n'en porte.""" _, lot = donnees db_session.add(LotCaracteristiques(lot_id=lot.id, surface=50.0)) db_session.commit() loyer = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"] assert loyer["surface"] == 50.0 assert loyer["en_vigueur"]["loyer_m2"] == 10.0 def test_sans_periode_demandee_rien_n_est_borne(api_client, donnees): """Le défaut ne filtre pas : la page s'ouvre sur tout l'historique. Un filtre par défaut cacherait des opérations dès l'arrivée sur la fiche, sans que rien ne le signale. """ _, lot = donnees periode = api_client.get(f"/api/lots/{lot.id}/analyse").json()["periode"] assert periode["mois"] is None assert periode["debut"] is None assert periode["fin"] is None assert periode["lignes_masquees"] == 0 # L'ancre est renvoyée quand même : la page sait sur quoi une fenêtre se # calerait avant même d'en demander une. assert periode["ancre"] == "2024-02-15" def test_la_fenetre_se_cale_sur_le_dernier_compte_rendu_du_lot(api_client, donnees): """Comptée à rebours des données, jamais d'aujourd'hui. Ces comptes rendus datent de 2024 : une fenêtre calée sur la date du jour viderait la fiche et ferait passer un lot documenté pour un lot sans activité. """ _, lot = donnees periode = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["periode"] assert periode["fin"] == "2024-02-15" assert periode["debut"] == "2024-01-16" def test_une_fenetre_d_un_mois_ne_retient_qu_un_compte_rendu(api_client, donnees): """Un mois de fenêtre, un compte rendu : le précédent tombe dehors. Bornes incluses des deux côtés, le compte rendu du 15 janvier entrerait dans une fenêtre d'un mois finissant le 15 février — elle en couvrirait deux. """ _, lot = donnees analyse = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json() dates = {ligne["date"] for ligne in analyse["chronologie"]} assert dates == {"2024-02-15"} # Le premier compte rendu portait un loyer et une opération de nettoyage # d'immeuble ; seules ses lignes de lot comptent ici. assert analyse["periode"]["lignes_masquees"] == 1 def test_les_chiffres_suivent_la_fenetre(api_client, donnees): """Facturé, dépenses et charges communes se recalculent sur la période.""" _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["chiffres"] # Le seul loyer du second compte rendu, sans celui de janvier. assert chiffres["facture"] == 500.0 assert chiffres["encaisse"] == 200.0 assert chiffres["nb_operations"] == 3 # Le nettoyage de l'immeuble datait du premier compte rendu. assert chiffres["depenses_immeuble_non_reparties"] == 0.0 def test_le_restant_du_echappe_a_la_fenetre(api_client, donnees): """Un stock ne se borne pas : la dette reste celle du dernier compte rendu. La ramener à la fenêtre l'annulerait dès qu'aucun compte rendu n'y tombe — un lot afficherait 0 € dû tout en devant plusieurs milliers. """ _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["chiffres"] assert chiffres["restant_du"] == 600.0 def test_les_intervenants_suivent_la_fenetre(api_client, donnees): """Le tableau des entreprises décrit la même période que la chronologie.""" _, lot = donnees analyse = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json() # Invariant du dépliage, sous fenêtre comme sans : le détail doit retrouver # le total. Les deux viennent de calculs séparés — un agrégat SQL borné # d'un côté, les lignes filtrées de l'autre — et une fenêtre appliquée d'un # seul côté les ferait diverger sans que rien ne le signale. assert analyse["intervenants"] for intervenant in analyse["intervenants"]: lignes = [ ligne for ligne in analyse["chronologie"] if ligne["fournisseur"] == intervenant["fournisseur"] ] assert len(lignes) == intervenant["nb_interventions"] assert ( round(sum(ligne["montant"] for ligne in lignes), 2) == intervenant["montant"] ) def test_la_courbe_du_loyer_suit_la_fenetre(api_client, donnees): """Le graphe se limite aux mois de la période, et dit ce qu'il ne trace pas. Le mois de la borne haute compte pour un : une fenêtre d'un mois finissant en février trace février seul, sans quoi le graphe démentirait son libellé. """ _, lot = donnees loyer = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["loyer"] assert [point["mois"] for point in loyer["serie"]] == ["2024-02"] assert loyer["depuis_mois"] == "2024-02" assert loyer["mois_masques"] == 1 def test_le_loyer_en_vigueur_ignore_la_fenetre(api_client, donnees): """Les paliers restent lus sur toute la série, même courbe tronquée. Recalculés sur les seuls mois tracés, « depuis » daterait de la borne du filtre : ce loyer semblerait révisé en février alors qu'il n'a jamais bougé depuis janvier. """ _, lot = donnees loyer = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["loyer"] assert loyer["en_vigueur"]["depuis"] == "2024-01" assert loyer["en_vigueur"]["loyer"] == 500.0 # La comparaison au parc garde son mois de référence : le dernier mois loué. assert loyer["parc"]["mois"] == "2024-02" def test_sans_fenetre_la_courbe_reste_entiere(api_client, donnees): """Le défaut ne coupe rien, et ne prétend pas avoir coupé.""" _, lot = donnees loyer = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"] assert [point["mois"] for point in loyer["serie"]] == ["2024-01", "2024-02"] assert loyer["depuis_mois"] is None assert loyer["mois_masques"] == 0 def test_la_fenetre_ne_garde_que_les_occupants_de_la_periode( api_client, db_session, donnees, sample_data ): """Un locataire appartient à la période si un compte rendu l'y porte. Aucune date d'entrée ni de sortie n'est extraite : le document où le nom figure est le seul rattachement au temps dont on dispose. """ _, lot = donnees DatabaseService(db_session).save_document( data={ **sample_data, "metadata": { **sample_data["metadata"], "document": { "reference": "REF003", "date": "2024-03-15", "type": "COMPTE RENDU DE GESTION", }, }, "situation_locataires": [ { "lot": {"numero": "01", "type": "Appartement"}, "locataire": {"nom": "MARTIN"}, "lignes": [ { "type": "loyer", "periode": {"debut": "2024-03-01", "fin": "2024-03-31"}, "loyers": 500.0, "total": 500.0, "regles": 500.0, "impayes": 0.0, } ], } ], } ) identite = api_client.get(f"/api/lots/{lot.id}/analyse?mois=1").json()["identite"] assert identite["locataires"] == ["MARTIN"] # DUPONT n'est pas effacé pour autant : la fiche dit qu'il en manque un. assert identite["locataires_masques"] == 1 entier = api_client.get(f"/api/lots/{lot.id}/analyse").json()["identite"] assert entier["locataires"] == ["DUPONT", "MARTIN"] assert entier["locataires_masques"] == 0 def test_sans_fenetre_un_locataire_sans_ligne_reste_liste( api_client, db_session, donnees ): """La vue par défaut ne filtre rien, pas même par les lignes rattachées. La base porte des locataires dont aucune ligne ne dépend ; les faire disparaître de la fiche entière serait un filtre que personne n'a demandé. """ _, lot = donnees db_session.add(Locataire(lot_id=lot.id, nom="ORPHELIN")) db_session.commit() identite = api_client.get(f"/api/lots/{lot.id}/analyse").json()["identite"] assert identite["locataires"] == ["DUPONT", "ORPHELIN"] def test_un_lot_sans_ligne_garde_une_periode_ouverte(api_client, db_session, donnees): """Rien à quoi caler la fenêtre : la fiche s'ouvre au lieu d'échouer.""" immeuble, _ = donnees vide = Lot(immeuble_id=immeuble.id, numero="99") db_session.add(vide) db_session.commit() analyse = api_client.get(f"/api/lots/{vide.id}/analyse?mois=3").json() assert analyse["periode"]["ancre"] is None assert analyse["periode"]["debut"] is None assert analyse["chronologie"] == [] @pytest.fixture def parc(db_session, sample_data): """Un compte rendu portant trois lots, dont un sans surface saisie. Se situer suppose des voisins : le nuage n'a de sens qu'à plusieurs. Les surfaces sont volontairement contrastées (20 m² à 15 €/m², 50 m² à 10 €/m²) pour reproduire la pente du parc réel, où le petit se loue plus cher au m². """ def locataire(numero, nom, loyer): return { "lot": {"numero": numero, "type": "Appartement"}, "locataire": {"nom": nom}, "lignes": [ { "type": "loyer", "periode": {"debut": "2024-01-01", "fin": "2024-01-31"}, "loyers": loyer, "total": loyer, "regles": loyer, "impayes": 0.0, } ], } DatabaseService(db_session).save_document( data={ **sample_data, "situation_locataires": [ locataire("01", "DUPONT", 500.0), locataire("02", "MARTIN", 300.0), locataire("03", "DURAND", 700.0), ], } ) immeuble = db_session.query(Immeuble).filter(Immeuble.code == "IMM1").one() lots = { lot.numero: lot for lot in db_session.query(Lot).filter(Lot.immeuble_id == immeuble.id) } db_session.add(LotCaracteristiques(lot_id=lots["01"].id, surface=50.0)) db_session.add(LotCaracteristiques(lot_id=lots["02"].id, surface=20.0)) # Le lot 03 reste sans fiche : c'est le cas majoritaire en base. db_session.commit() return lots def test_le_nuage_situe_le_lot_parmi_ses_voisins(api_client, parc): """Trié par surface, le lot courant présent et signalé. Il figure dans le nuage — s'y voir situé est tout l'objet — alors qu'il est exclu des médianes, qu'il tirerait vers lui. """ nuage = api_client.get(f"/api/lots/{parc['01'].id}/analyse").json()["loyer"][ "parc" ]["nuage"] assert [(point["surface"], point["loyer_m2"]) for point in nuage] == [ (20.0, 15.0), (50.0, 10.0), ] assert [point["est_ce_lot"] for point in nuage] == [False, True] assert nuage[0]["numero"] == "02" def test_un_lot_sans_surface_n_entre_pas_dans_le_nuage(api_client, parc): """Sans surface, aucune abscisse : le lot ne peut pas être placé. Il n'est pas pour autant oublié — `sans_surface` le compte, et la page le dit sous les médianes. """ comparaison = api_client.get(f"/api/lots/{parc['01'].id}/analyse").json()["loyer"][ "parc" ] assert len(comparaison["nuage"]) == 2 assert parc["03"].id not in [point["lot_id"] for point in comparaison["nuage"]] assert comparaison["sans_surface"] == 1 def test_le_nuage_garde_le_lot_courant_meme_seul(api_client, db_session, donnees): """Seul lot mesuré du parc : le nuage le porte quand même. Le vider dans ce cas ferait disparaître le point qu'on cherche justement à situer, et la page ne dirait plus rien du lot ouvert. """ _, lot = donnees db_session.add(LotCaracteristiques(lot_id=lot.id, surface=50.0)) db_session.commit() nuage = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"]["parc"][ "nuage" ] assert len(nuage) == 1 assert nuage[0]["est_ce_lot"] is True def test_la_comparaison_compte_les_lots_qu_elle_ne_peut_pas_voir( api_client, db_session, donnees ): """Un lot sans surface ne peut pas entrer dans une médiane au m². Taire ces lots ferait passer une médiane sur une poignée de lots pour une médiane sur tout le parc — c'est le chiffre, et non son effectif, qui tromperait. """ _, lot = donnees db_session.add(LotCaracteristiques(lot_id=lot.id, surface=50.0)) db_session.commit() parc = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"]["parc"] assert parc["mois"] == "2024-02" assert parc["loyer_m2"] == 10.0 # Seul lot de la base : rien à quoi le comparer, et la médiane ne se # rabat pas sur lui-même. assert parc["mediane_immeuble"] is None assert parc["nb_immeuble"] == 0 assert parc["sans_surface"] == 0 @pytest.fixture def bail_qui_change(db_session, sample_data): """Un lot dont le bail s'arrête en cours de mois, puis reprend au suivant. C'est la chronologie du lot 15 du parc : deux mois pleins, une sortie le 10 mars, quatre mois vides, puis une entrée le 8 juillet facturée au seul prorata. Le dernier compte rendu ne porte donc aucun loyer plein, alors que le lot est bel et bien reloué. """ def compte_rendu(reference, date_cr, lignes): return { **sample_data, "metadata": { **sample_data["metadata"], "document": { "reference": reference, "date": date_cr, "type": "COMPTE RENDU DE GESTION", }, }, "situation_locataires": [ { "lot": {"numero": "01", "type": "Appartement"}, "locataire": {"nom": lignes["nom"]}, "lignes": [ { "type": "loyer", "periode": { "debut": lignes["debut"], "fin": lignes["fin"], }, "loyers": lignes["loyers"], "total": lignes["loyers"], "regles": lignes["loyers"], "impayes": 0.0, } ], } ], "recapitulatif_operations": [], } service = DatabaseService(db_session) for reference, date_cr, lignes in [ ( "CR01", "2026-01-26", { "nom": "SORTANT", "debut": "2026-01-01", "fin": "2026-01-31", "loyers": 1447.05, }, ), ( "CR02", "2026-02-26", { "nom": "SORTANT", "debut": "2026-02-01", "fin": "2026-02-28", "loyers": 1447.05, }, ), ( "CR03", "2026-03-23", { "nom": "SORTANT", "debut": "2026-03-01", "fin": "2026-03-10", "loyers": 482.35, }, ), ( "CR04", "2026-07-28", { "nom": "ENTRANT", "debut": "2026-07-08", "fin": "2026-07-31", "loyers": 1111.67, }, ), ]: service.save_document(data=compte_rendu(reference, date_cr, lignes)) immeuble = db_session.query(Immeuble).filter(Immeuble.code == "IMM1").one() lot = db_session.query(Lot).filter(Lot.immeuble_id == immeuble.id).one() return lot def test_un_lot_reloue_au_prorata_n_est_pas_un_lot_arrete(api_client, bail_qui_change): """Le palier s'arrête en février, le lot est reloué en juillet. Sans cette lecture, la fiche annonce « arrêté après févr. » sur un lot qui vient de retrouver un locataire — et la courbe juste dessous, où la barre de juillet est bien là, la contredit. """ vigueur = api_client.get(f"/api/lots/{bail_qui_change.id}/analyse").json()["loyer"][ "en_vigueur" ] assert vigueur["reloue_depuis"] == "2026-07" assert vigueur["sortie_en"] is None # Le montant reste celui du bail précédent : le nouveau n'a été facturé # qu'au prorata, et en tirer un loyer mensuel l'inventerait. assert vigueur["mois"] == "2026-02" assert vigueur["loyer"] == 1447.05 assert vigueur["toujours_loue"] is False def test_une_sortie_en_cours_de_mois_deborde_le_dernier_palier( api_client, db_session, bail_qui_change ): """Sans relocation, c'est la sortie qui date la fin de la location. Le dernier loyer plein est celui de février, mais le lot est resté loué jusqu'au 10 mars : dire « arrêté après févr. » avancerait la sortie d'un mois. """ dernier = ( db_session.query(Document).order_by(Document.date.desc()).first() ) db_session.delete(dernier) db_session.commit() vigueur = api_client.get(f"/api/lots/{bail_qui_change.id}/analyse").json()["loyer"][ "en_vigueur" ] assert vigueur["sortie_en"] == "2026-03" assert vigueur["reloue_depuis"] is None def test_un_loyer_qui_s_arrete_net_reste_un_loyer_arrete(api_client, donnees): """Aucun prorata après le dernier palier : rien ne nuance l'arrêt.""" _, lot = donnees vigueur = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"][ "en_vigueur" ] assert vigueur["reloue_depuis"] is None assert vigueur["sortie_en"] is None