Files
pdf_oralia_vibe/tests/test_referentiel_api.py
Bertrand Benjamin fdffe27f06 feat: étend le référentiel au parc entier et à la suppression des lots
Trois manques que la saisie a fait apparaître :

Le tableau porte une colonne immeuble, la liste ne peut donc plus être
suspendue à un immeuble choisi d'avance : elle renvoie tout le parc, chaque
ligne emportant de quoi nommer son immeuble sans requête de plus.

Un lot qu'aucune ligne de compte rendu ne mentionne ne décrit rien : il
encombre la saisie et doit pouvoir disparaître. La garde est côté serveur —
un lot porteur de revenus ou de dépenses est refusé, ses montants
partiraient avec lui.

Le nom d'usage s'enregistre, et la réponse recalcule les compteurs de
l'immeuble plutôt que de les laisser à zéro : elle remplace l'immeuble dans
les listes du client, qui le croirait vide de lots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:41:24 +02:00

361 lines
12 KiB
Python

"""Tests du referentiel des logements.
Ces caracteristiques sont saisies a la main : elles n'ont aucune autre source
que l'utilisateur, donc rien ne les reconstituerait si un enregistrement les
perdait. Les tests portent sur ce qui menace cette saisie : l'ecraser depuis un
PDF, la vider a moitie, ou la laisser contredire l'extraction en silence.
"""
import pytest
from plesna_gerance.database.models import (
Depense,
Immeuble,
Lot,
LotCaracteristiques,
)
from plesna_gerance.database.service import DatabaseService
@pytest.fixture
def immeuble_et_lot(db_session, sample_data):
"""Un immeuble et son lot 01, tels que l'extraction les cree."""
DatabaseService(db_session).save_document(data=sample_data)
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
FICHE = {
"bat": "Rue",
"etage": "RC",
"type": "Loc. Commercial",
"surface": 148.0,
"surface_date_diag": "2019-06-01",
"chauffage": "Electrique",
"dpe_classe": "C",
"dpe_date_realisation": "2021-02-08",
"numero_fiscal": "690123456789",
"surface_impots": 146.0,
"note_impots": "Surface relevee sur l'avis 2024",
}
def test_liste_les_lots_sans_fiche(api_client, immeuble_et_lot):
"""Un lot jamais decrit doit apparaitre, sinon il n'est pas saisissable."""
immeuble, lot = immeuble_et_lot
response = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}")
assert response.status_code == 200
lignes = response.json()
assert len(lignes) == 1
assert lignes[0]["id"] == lot.id
assert lignes[0]["numero"] == "01"
assert lignes[0]["caracteristiques"] is None
assert lignes[0]["type_extrait"] == "Appartement"
assert lignes[0]["type_effectif"] == "Appartement"
assert lignes[0]["type_ecart"] is False
def test_liste_un_immeuble_inconnu_donne_404(api_client):
assert api_client.get("/api/lots/referentiel?immeuble_id=99999").status_code == 404
def test_liste_tout_le_parc_par_defaut(api_client, immeuble_et_lot, db_session):
"""Le tableau porte une colonne immeuble : il les montre donc tous.
Restreindre a un immeuble par defaut obligerait a le choisir avant de voir
quoi que ce soit, alors que le parc entier tient dans un ecran.
"""
autre = Immeuble(code="IMM2", denomination="Marietton")
db_session.add(autre)
db_session.flush()
db_session.add(Lot(immeuble_id=autre.id, numero="01"))
db_session.commit()
lignes = api_client.get("/api/lots/referentiel").json()
assert len(lignes) == 2
assert {ligne["immeuble_code"] for ligne in lignes} == {"IMM1", "IMM2"}
def test_expose_le_nom_d_usage_de_l_immeuble(api_client, immeuble_et_lot):
"""Chaque ligne porte de quoi nommer son immeuble sans requete de plus."""
immeuble, _ = immeuble_et_lot
api_client.put(f"/api/immeubles/{immeuble.id}", json={"denomination": "Servient"})
ligne = api_client.get("/api/lots/referentiel").json()[0]
assert ligne["immeuble_denomination"] == "Servient"
assert ligne["immeuble_code"] == "IMM1"
def test_enregistre_puis_relit_une_fiche(api_client, immeuble_et_lot):
immeuble, lot = immeuble_et_lot
response = api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
assert response.status_code == 200
fiche = response.json()["caracteristiques"]
assert fiche["surface"] == 148.0
assert fiche["etage"] == "RC"
assert fiche["numero_fiscal"] == "690123456789"
relu = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}").json()[0]
assert relu["caracteristiques"]["surface"] == 148.0
assert relu["caracteristiques"]["dpe_classe"] == "C"
def test_expose_les_valeurs_derivees(api_client, immeuble_et_lot):
"""L'echeance du DPE et l'ecart de surface arrivent calcules."""
_, lot = immeuble_et_lot
fiche = api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE).json()[
"caracteristiques"
]
assert fiche["dpe_echeance"] == "2031-02-08"
assert fiche["delta_surface"] == -2.0
def test_une_seconde_ecriture_met_a_jour_sans_dupliquer(api_client, immeuble_et_lot):
immeuble, lot = immeuble_et_lot
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
api_client.put(
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "surface": 150.0}
)
lignes = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}").json()
assert len(lignes) == 1
assert lignes[0]["caracteristiques"]["surface"] == 150.0
def test_un_champ_vide_efface_la_valeur(api_client, immeuble_et_lot):
"""Corriger une erreur de saisie doit pouvoir revenir a « inconnu »."""
_, lot = immeuble_et_lot
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
fiche = api_client.put(
f"/api/lots/{lot.id}/caracteristiques",
json={**FICHE, "etage": " ", "surface": None},
).json()["caracteristiques"]
assert fiche["etage"] is None
assert fiche["surface"] is None
assert fiche["delta_surface"] is None
def test_refuse_une_classe_dpe_inconnue(api_client, immeuble_et_lot):
_, lot = immeuble_et_lot
response = api_client.put(
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "dpe_classe": "Z"}
)
assert response.status_code == 422
def test_refuse_une_surface_negative(api_client, immeuble_et_lot):
_, lot = immeuble_et_lot
response = api_client.put(
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "surface": -10}
)
assert response.status_code == 422
def test_ecrire_sur_un_lot_inconnu_donne_404(api_client):
assert (
api_client.put("/api/lots/99999/caracteristiques", json=FICHE).status_code
== 404
)
def test_signale_un_desaccord_de_type_sans_effacer_le_pdf(api_client, immeuble_et_lot):
"""Les deux types restent lisibles : la fiche tranche, le PDF reste visible."""
_, lot = immeuble_et_lot
ligne = api_client.put(
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "type": "Appartement T3"}
).json()
assert ligne["type_extrait"] == "Appartement"
assert ligne["type_effectif"] == "Appartement T3"
assert ligne["type_ecart"] is True
def test_le_type_saisi_prime_dans_la_liste_des_lots(api_client, immeuble_et_lot):
"""La priorite du referentiel vaut partout, pas seulement sur sa page."""
_, lot = immeuble_et_lot
api_client.put(
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "type": "Appartement T3"}
)
lots = api_client.get("/api/lots").json()
assert [ligne["type"] for ligne in lots] == ["Appartement T3"]
def test_une_re_extraction_ne_touche_pas_la_fiche(
api_client, immeuble_et_lot, db_session, sample_data
):
"""Le point critique : reimporter le PDF ne doit rien perdre de la saisie."""
immeuble, lot = immeuble_et_lot
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
DatabaseService(db_session).save_document(data=sample_data, overwrite=True)
lignes = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}").json()
fiche = next(ligne for ligne in lignes if ligne["id"] == lot.id)["caracteristiques"]
assert fiche is not None, "la re-extraction a perdu la fiche du lot"
assert fiche["surface"] == 148.0
def test_compte_les_rattachements_du_lot(api_client, immeuble_et_lot):
"""Un lot avec des revenus n'est pas un orphelin : la liste doit le dire."""
immeuble, lot = immeuble_et_lot
ligne = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}").json()[0]
assert ligne["nb_revenus"] == 1
# La depense de la fixture porte sur l'immeuble (lot_id NULL). La compter
# ici rendrait tout lot de l'immeuble faussement non supprimable.
assert ligne["nb_depenses"] == 0
def test_l_enregistrement_renvoie_les_memes_comptes_que_la_liste(
api_client, immeuble_et_lot
):
"""Décrire un lot occupé ne doit pas le faire passer pour inutilisé.
La réponse du PUT remplace la ligne dans le tableau : si elle rapporte zéro
revenu, le lot se pare d'un « inutilisé » que la liste dément au rechargement.
"""
_, lot = immeuble_et_lot
ligne = api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE).json()
assert ligne["nb_revenus"] == 1
def test_compte_les_depenses_propres_au_lot(api_client, immeuble_et_lot, db_session):
"""Une depense rattachee au lot, elle, doit bien remonter sur sa ligne."""
immeuble, lot = immeuble_et_lot
depense = db_session.query(Depense).one()
depense.lot_id = lot.id
db_session.commit()
ligne = api_client.get(f"/api/lots/referentiel?immeuble_id={immeuble.id}").json()[0]
assert ligne["nb_depenses"] == 1
# ============================================================
# Suppression des lots sans rattachement
# ============================================================
def test_supprime_un_lot_inutilise(api_client, immeuble_et_lot, db_session):
"""Les lots d'un ancien format de numerotation doivent pouvoir disparaitre."""
immeuble, _ = immeuble_et_lot
orphelin = Lot(immeuble_id=immeuble.id, numero="0001", type="Appartement T1")
db_session.add(orphelin)
db_session.commit()
response = api_client.delete(f"/api/lots/{orphelin.id}")
assert response.status_code == 204
numeros = [
ligne["numero"]
for ligne in api_client.get(
f"/api/lots/referentiel?immeuble_id={immeuble.id}"
).json()
]
assert numeros == ["01"]
def test_supprimer_un_lot_emporte_sa_fiche(api_client, immeuble_et_lot, db_session):
"""Sans cascade, la fiche resterait en base sans lot pour la porter."""
immeuble, _ = immeuble_et_lot
orphelin = Lot(immeuble_id=immeuble.id, numero="0001")
db_session.add(orphelin)
db_session.commit()
api_client.put(f"/api/lots/{orphelin.id}/caracteristiques", json=FICHE)
api_client.delete(f"/api/lots/{orphelin.id}")
assert db_session.query(LotCaracteristiques).count() == 0
def test_refuse_de_supprimer_un_lot_avec_des_revenus(api_client, immeuble_et_lot):
"""Le refus protege des montants : les perdre fausserait les comptes."""
_, lot = immeuble_et_lot
response = api_client.delete(f"/api/lots/{lot.id}")
assert response.status_code == 409
assert "revenu" in response.json()["detail"]
def test_refuse_de_supprimer_un_lot_avec_des_depenses(
api_client, immeuble_et_lot, db_session
):
"""Une depense seule suffit a retenir le lot, meme sans aucun revenu."""
immeuble, _ = immeuble_et_lot
lot_charge = Lot(immeuble_id=immeuble.id, numero="0002")
db_session.add(lot_charge)
db_session.commit()
depense = db_session.query(Depense).one()
depense.lot_id = lot_charge.id
db_session.commit()
response = api_client.delete(f"/api/lots/{lot_charge.id}")
assert response.status_code == 409
def test_supprimer_un_lot_inconnu_donne_404(api_client):
assert api_client.delete("/api/lots/99999").status_code == 404
# ============================================================
# Nom d'usage de l'immeuble
# ============================================================
def test_nomme_un_immeuble(api_client, immeuble_et_lot):
immeuble, _ = immeuble_et_lot
response = api_client.put(
f"/api/immeubles/{immeuble.id}", json={"denomination": "Servient"}
)
assert response.status_code == 200
assert response.json()["denomination"] == "Servient"
assert api_client.get("/api/immeubles").json()[0]["denomination"] == "Servient"
# La reponse remplace l'immeuble dans les listes du client : des compteurs a
# zero le feraient passer pour un immeuble sans lot.
assert response.json()["nb_lots"] == 1
def test_effacer_le_nom_rend_l_immeuble_a_son_code(api_client, immeuble_et_lot):
immeuble, _ = immeuble_et_lot
api_client.put(f"/api/immeubles/{immeuble.id}", json={"denomination": "Servient"})
response = api_client.put(
f"/api/immeubles/{immeuble.id}", json={"denomination": " "}
)
assert response.json()["denomination"] is None
assert response.json()["code"] == "IMM1"
def test_nommer_un_immeuble_inconnu_donne_404(api_client):
response = api_client.put("/api/immeubles/99999", json={"denomination": "Servient"})
assert response.status_code == 404