monregistraire/tests/test_api.py

637 lines
26 KiB
Python
Raw Permalink Normal View History

Lancement v1.0 — Gestionnaire d'immobilisations fiscales (Chezlepro Inc.) Application web locale (FastAPI + SQLite + React SPA) pour la gestion des immobilisations fiscales d'une PME québécoise (T2 / CO-17). ─── Modèle de données ─────────────────────────────────────────────────────── • Deux niveaux : PARENT (immobilisation fiscale) + PIÈCE (inventaire physique) • Trois vues par actif : Propriétaire, CPA, Auditeur • Porteur d'étiquette d'assemblage (unique par immobilisation) • Piste d'audit journalisée sur chaque modification ─── Mécanique fiscale ─────────────────────────────────────────────────────── • Pool DPA par catégorie (Cat. 8, 10, 12, 46, 50) — amortissement dégressif • Règle de la demi-année paramétrable (coeff. 0..1 par exercice) • Perte finale et récupération d'amortissement calculées automatiquement • Annexe 8 / Schedule 8 multi-catégories (JSON + CSV téléchargeable) • Rapport fiscal HTML imprimable — lignes Schedule 1 (T2 : 104/107/410/414 ; CO-17 : 130/135/150/155) • Export immobilisations par catégorie (JSON + CSV) ─── Fonctionnalités ───────────────────────────────────────────────────────── • CRUD complet : parents et pièces, rattachement/détachement (libre/stock/rebut) • Écriture comptable générée au détachement • Registre de pool DPA multi-exercices avec override manuel • Sauvegarde/restauration (SQLite native backup, horodatée) • Factures jointes aux pièces (PDF/JPG/PNG, max 20 Mo, UUID + regex) • UI mobile-first : bottom nav, FAB, bottom sheet, safe-area iPhone • Guide utilisateur complet in-app (aide.html, 11 sections) • Données de démonstration riches (6 immos, 5 catégories, perte finale + récupération incluses) ─── Qualité ───────────────────────────────────────────────────────────────── • 50 tests pytest — 100 % verts (calculs DPA, export, backup, factures, seed) • Lancement en 1 commande : ./lancer.sh (Mac/Linux) ou lancer.bat (Windows) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 10:30:53 -04:00
"""Tests de régression — API du gestionnaire d'immobilisations.
Couvre : CRUD actifs, rattachement/détachement, porteur d'étiquette,
mécanique DPA (demi-année, perte finale, récupération), export CSV,
rapport fiscal, sauvegardes, factures jointes, seed-demo.
Exécuter : .venv/bin/pytest tests/ -v
"""
import io
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
@pytest.fixture(autouse=True)
def reset_db():
"""Base et registre vides avant chaque test."""
client.post("/api/reset")
yield
client.post("/api/reset")
# ─── Helpers ────────────────────────────────────────────────────────────────
def _newest(assets: list) -> dict:
"""Retourne l'actif avec le plus grand id (le dernier créé)."""
return max(assets, key=lambda a: a["id"])
def _make_parent(name="Parent", cca=50) -> dict:
r = client.post("/api/assets", json={"type": "parent", "name": name, "cca_class_id": cca})
return _newest(r.json()["assets"])
def _make_piece(name="Pièce", cost=500.0, acq=None, cca=50) -> dict:
body = {"type": "piece", "name": name, "cost_cad": cost, "cca_class_id": cca}
if acq:
body["acquisition_date"] = acq
r = client.post("/api/assets", json=body)
return _newest(r.json()["assets"])
# ═══════════════════════════════════════════════════════════
# CRUD — actifs
# ═══════════════════════════════════════════════════════════
def test_create_parent():
r = client.post("/api/assets", json={"type": "parent", "name": "Srv Test", "cca_class_id": 50})
assert r.status_code == 200
d = _newest(r.json()["assets"])
assert d["type"] == "parent"
assert d["name"] == "Srv Test"
assert d["view_cpa"]["cca_class"]["id"] == 50
def test_create_piece():
r = client.post("/api/assets", json={"type": "piece", "name": "RAM Test", "cost_cad": 420.0})
assert r.status_code == 200
d = _newest(r.json()["assets"])
assert d["type"] == "piece"
assert float(d["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(420.0)
def test_list_assets():
client.post("/api/assets", json={"type": "parent", "name": "P1"})
client.post("/api/assets", json={"type": "piece", "name": "PC1", "cost_cad": 100.0})
r = client.get("/api/assets")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_asset():
aid = _make_piece()["id"]
r = client.get(f"/api/assets/{aid}")
assert r.status_code == 200
assert r.json()["id"] == aid
def test_get_asset_not_found():
r = client.get("/api/assets/99999")
assert r.status_code == 404
def test_patch_name():
aid = _make_piece(name="Ancien")["id"]
r = client.patch(f"/api/assets/{aid}", json={"changes": {"name": "Nouveau"}})
assert r.status_code == 200
updated = next(a for a in r.json()["assets"] if a["id"] == aid)
assert updated["name"] == "Nouveau"
def test_patch_cost():
aid = _make_piece(cost=100.0)["id"]
r = client.patch(f"/api/assets/{aid}", json={"changes": {"view_cpa.capitalizable_base_cad": 999.0}})
assert r.status_code == 200
updated = next(a for a in r.json()["assets"] if a["id"] == aid)
assert float(updated["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(999.0)
def test_delete_piece():
aid = _make_piece()["id"]
r = client.delete(f"/api/assets/{aid}")
assert r.status_code == 200
assert not any(a["id"] == aid for a in r.json()["assets"])
def test_delete_parent_frees_pieces():
"""Supprimer un parent libère ses pièces (elles deviennent libres)."""
p = _make_parent()
pc = _make_piece(cost=100.0)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.delete(f"/api/assets/{p['id']}")
assert r.status_code == 200
assets = r.json()["assets"]
child = next(a for a in assets if a["id"] == pc["id"])
assert child["parent_id"] is None
# ═══════════════════════════════════════════════════════════
# Rattachement / détachement
# ═══════════════════════════════════════════════════════════
def test_attach_updates_parent_base():
"""La base amortissable du parent = somme exacte des coûts de ses pièces."""
p = _make_parent()
pc1 = _make_piece(cost=1000.0)
pc2 = _make_piece(cost=500.0)
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
r = client.get(f"/api/assets/{p['id']}")
assert float(r.json()["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(1500.0)
def test_detach_free():
p = _make_parent()
pc = _make_piece()
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/detach", json={"disposition": "free"})
assert r.status_code == 200
detached = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert detached["parent_id"] is None
def test_detach_scrap():
p = _make_parent()
pc = _make_piece()
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/detach", json={"disposition": "scrap"})
assert r.status_code == 200
scrapped = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert scrapped["view_admin"]["operational_status"] == "Rebut"
assert scrapped["parent_id"] is None
def test_detach_reduces_parent_base():
p = _make_parent()
pc1 = _make_piece(cost=1000.0)
pc2 = _make_piece(cost=400.0)
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/detach", json={"disposition": "free"})
r = client.get(f"/api/assets/{p['id']}")
assert float(r.json()["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(1000.0)
def test_set_tag_bearer():
p = _make_parent()
pc = _newest(client.post("/api/assets", json={
"type": "piece", "name": "Porteur", "cost_cad": 500.0, "inventory_tag": "TAG-001"
}).json()["assets"])
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/set-tag-bearer")
assert r.status_code == 200
bearer = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert bearer["view_admin"]["is_tag_bearer"] is True
def test_tag_bearer_unique_per_parent():
"""Désigner un nouveau porteur retire le statut à l'ancien."""
p = _make_parent()
pc1 = _newest(client.post("/api/assets", json={
"type": "piece", "name": "A", "cost_cad": 100.0, "inventory_tag": "T1"
}).json()["assets"])
pc2 = _newest(client.post("/api/assets", json={
"type": "piece", "name": "B", "cost_cad": 200.0, "inventory_tag": "T2"
}).json()["assets"])
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc1['id']}/set-tag-bearer")
r = client.post(f"/api/assets/{pc2['id']}/set-tag-bearer")
assets = r.json()["assets"]
assert next(a for a in assets if a["id"] == pc1["id"])["view_admin"]["is_tag_bearer"] is False
assert next(a for a in assets if a["id"] == pc2["id"])["view_admin"]["is_tag_bearer"] is True
# ═══════════════════════════════════════════════════════════
# Catégories DPA
# ═══════════════════════════════════════════════════════════
def test_cca_categories():
r = client.get("/api/cca-categories")
assert r.status_code == 200
ids = [c["id"] for c in r.json()]
for expected in (50, 8, 10, 12, 46):
assert expected in ids
def test_cca_categories_have_rate():
cats = client.get("/api/cca-categories").json()
for c in cats:
assert 0 < c["dpa_rate"] <= 1.0
# ═══════════════════════════════════════════════════════════
# Registre pool DPA — mécanique fiscale
# ═══════════════════════════════════════════════════════════
def test_pool_demi_annee_standard():
"""Première année, ajouts = 10 000 $, coeff 0,5 → DPA = 2 750 $ (Cat. 50)."""
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50,
"additions_override": 10_000.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 0,
})
assert r.status_code == 200
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
assert float(y["additions"]) == pytest.approx(10_000.0)
assert float(y["adjustment"]) == pytest.approx(5_000.0)
assert float(y["base_for_cca"]) == pytest.approx(5_000.0)
assert float(y["cca_claimed"]) == pytest.approx(2_750.0) # 5 000 × 0.55
assert float(y["closing_ucc"]) == pytest.approx(7_250.0)
def test_pool_annee_pleine():
"""Deuxième année sans acquisitions → DPA pleine sur le solde résiduel."""
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 50,
"additions_override": 10_000.0, "adjustment_coeff": 0.5,
"opening_ucc_override": 0,
})
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50,
"additions_override": 0, "adjustment_coeff": 1.0,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
# Ouverture 2024 = 7 250 $ (clôture 2023) → DPA = 7 250 × 0.55 = 3 987.50
assert float(y["opening_ucc"]) == pytest.approx(7_250.0)
assert float(y["cca_claimed"]) == pytest.approx(3_987.50)
def test_pool_recapture():
"""Produit de cession > FNACC résiduelle → récupération d'amortissement."""
client.post("/api/cca-registry/year", json={
"year": 2022, "cca_class_id": 10,
"additions_override": 0.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 3_000.0,
})
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 10,
"additions_override": 0, "dispositions": 0, "adjustment_coeff": 1.0,
})
# Vente à 2 500 $, FNACC < 2 500 → récupération
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 10,
"additions_override": 0, "dispositions": 2_500.0,
"adjustment_coeff": 1.0, "pool_physically_empty": True,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
assert float(y["recapture"]) > 0, "Récupération attendue"
assert float(y["terminal_loss"]) == 0
assert float(y["closing_ucc"]) == 0
def test_pool_perte_finale():
"""Catégorie physiquement vide avec FNACC positive → perte finale."""
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 12,
"additions_override": 3_000.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 0,
})
r = client.post("/api/cca-registry/year", json={
"year": 2025, "cca_class_id": 12,
"additions_override": 0, "dispositions": 0,
"adjustment_coeff": 1.0, "pool_physically_empty": True,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2025)
assert float(y["terminal_loss"]) > 0, "Perte finale attendue"
assert float(y["cca_claimed"]) == 0
assert float(y["closing_ucc"]) == 0
def test_pool_progression_continue():
"""La FNACC de clôture d'une année est l'ouverture de la suivante."""
for yr, coeff in [(2022, 0.5), (2023, 1.0), (2024, 1.0)]:
client.post("/api/cca-registry/year", json={
"year": yr, "cca_class_id": 50,
"additions_override": 0, "dispositions": 0,
"adjustment_coeff": coeff,
**({"opening_ucc_override": 20_000.0} if yr == 2022 else {}),
})
r = client.get("/api/cca-registry?cca_class_id=50")
years = {y["year"]: y for y in r.json()["years"]}
assert float(years[2022]["closing_ucc"]) == pytest.approx(float(years[2023]["opening_ucc"]))
assert float(years[2023]["closing_ucc"]) == pytest.approx(float(years[2024]["opening_ucc"]))
def test_delete_registry_year():
client.post("/api/cca-registry/year", json={"year": 2024, "cca_class_id": 50})
r = client.delete("/api/cca-registry/year/2024?cca_class_id=50")
assert r.status_code == 200
assert not any(y["year"] == 2024 for y in r.json()["years"])
# ═══════════════════════════════════════════════════════════
# Annexe 8 / Schedule 8
# ═══════════════════════════════════════════════════════════
def test_annexe8_empty():
r = client.get("/api/annexe8?year=2024")
assert r.status_code == 200
assert r.json()["classes"] == []
def test_annexe8_calcul_dpa_cat8():
"""Annexe 8 utilise le bon taux de la catégorie (Cat. 8 = 20 %, pas 55 %)."""
p = _make_parent(cca=8)
pc = _make_piece(cost=10_000.0, acq="2024-01-01", cca=8)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 8, "dispositions": 0, "adjustment_coeff": 0.5,
})
r = client.get("/api/annexe8?year=2024")
cls8 = next(c for c in r.json()["classes"] if c["cca_class_id"] == 8)
y = cls8["year_data"]
# adj = 0.5 × 10 000 = 5 000 ; base = 5 000 ; DPA = 5 000 × 0.20 = 1 000
assert float(y["cca_claimed"]) == pytest.approx(1_000.0)
assert float(y["closing_ucc"]) == pytest.approx(9_000.0)
def test_annexe8_cat50_calcul():
p = _make_parent(cca=50)
pc = _make_piece(cost=20_000.0, acq="2023-03-01", cca=50)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 50, "dispositions": 0, "adjustment_coeff": 0.5,
})
r = client.get("/api/annexe8?year=2023")
cls50 = next(c for c in r.json()["classes"] if c["cca_class_id"] == 50)
y = cls50["year_data"]
assert float(y["additions"]) == pytest.approx(20_000.0)
assert float(y["cca_claimed"]) == pytest.approx(5_500.0) # 10 000 × 0.55
# ═══════════════════════════════════════════════════════════
# Export fiscal
# ═══════════════════════════════════════════════════════════
def test_fiscal_export_base_correcte():
p = _make_parent(cca=50)
for cost in (5_000.0, 1_500.0, 300.0):
pc = _make_piece(cost=cost)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.get("/api/fiscal-export")
assert r.status_code == 200
cls = next(c for c in r.json()["classes"] if c["cca_class_id"] == 50)
assert float(cls["total_base_cad"]) == pytest.approx(6_800.0)
def test_fiscal_export_plusieurs_categories():
for cca in (50, 8, 46):
p = _make_parent(cca=cca)
pc = _make_piece(cost=1_000.0, cca=cca)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.get("/api/fiscal-export")
ids = [c["cca_class_id"] for c in r.json()["classes"]]
assert 50 in ids and 8 in ids and 46 in ids
def test_fiscal_export_csv():
r = client.get("/api/fiscal-export.csv")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
def test_annexe8_csv():
r = client.get("/api/annexe8.csv?year=2024")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
def test_cca_registry_csv():
r = client.get("/api/cca-registry.csv?cca_class_id=50")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
# ═══════════════════════════════════════════════════════════
# Rapport fiscal (Schedule 1 / Annexe 1)
# ═══════════════════════════════════════════════════════════
def test_rapport_fiscal_structure():
r = client.get("/api/rapport-fiscal?year=2024")
assert r.status_code == 200
d = r.json()
assert d["year"] == 2024
s1 = d["schedule1"]
for key in ("book_deprec_cad", "cca_claimed_cad", "recapture_cad", "terminal_loss_cad"):
assert key in s1
def test_rapport_fiscal_html():
r = client.get("/api/rapport-fiscal.html?year=2024")
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert "Chezlepro" in r.text
def test_rapport_schedule1_cca_coherent():
"""DPA du Schedule 1 = DPA totale de l'Annexe 8."""
p = _make_parent(cca=50)
pc = _make_piece(cost=10_000.0, acq="2024-01-01", cca=50)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50, "dispositions": 0, "adjustment_coeff": 0.5,
})
rapport = client.get("/api/rapport-fiscal?year=2024").json()
annexe8 = client.get("/api/annexe8?year=2024").json()
total_dpa_a8 = sum(
float(c["year_data"]["cca_claimed"])
for c in annexe8["classes"] if c["year_data"]
)
assert rapport["schedule1"]["cca_claimed_cad"] == pytest.approx(total_dpa_a8)
# ═══════════════════════════════════════════════════════════
# Sauvegardes et restauration
# ═══════════════════════════════════════════════════════════
def test_create_and_list_backup():
_make_piece(name="Avant backup", cost=999.0)
r = client.post("/api/backup")
assert r.status_code == 200
filename = r.json()["filename"]
assert filename.startswith("immos_") and filename.endswith(".db")
r2 = client.get("/api/backups")
assert any(b["filename"] == filename for b in r2.json())
def test_restore_backup():
_make_piece(name="Avant backup", cost=999.0)
filename = client.post("/api/backup").json()["filename"]
client.post("/api/reset")
assert client.get("/api/assets").json() == []
r = client.post("/api/restore", json={"filename": filename})
assert r.status_code == 200
restored = client.get("/api/assets").json()
assert any(a["name"] == "Avant backup" for a in restored)
def test_restore_invalide():
r = client.post("/api/restore", json={"filename": "../../etc/passwd"})
assert r.status_code == 400
r2 = client.post("/api/restore", json={"filename": "immos_1234-99-99_99-99-99.db"})
assert r2.status_code == 404
# ═══════════════════════════════════════════════════════════
# Reset — vide actifs ET registre
# ═══════════════════════════════════════════════════════════
def test_reset_vide_actifs():
_make_piece()
client.post("/api/reset")
assert client.get("/api/assets").json() == []
def test_reset_vide_registre():
"""Après reset, le registre ne conserve aucune année."""
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50, "additions_override": 1_000.0
})
client.post("/api/reset")
r = client.get("/api/cca-registry?cca_class_id=50")
assert r.json()["years"] == []
# ═══════════════════════════════════════════════════════════
# Factures jointes
# ═══════════════════════════════════════════════════════════
def test_invoice_upload_et_suppression():
contenu = b"%PDF-1.4 fichier test"
files = {"file": ("facture.pdf", io.BytesIO(contenu), "application/pdf")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 200
file_id = r.json()["file_id"]
assert file_id.endswith(".pdf")
assert len(file_id) == 36 # 32 hex + ".pdf"
r2 = client.get(f"/api/invoices/{file_id}")
assert r2.status_code == 200
assert r2.content == contenu
client.delete(f"/api/invoices/{file_id}")
assert client.get(f"/api/invoices/{file_id}").status_code == 404
def test_invoice_upload_image():
contenu = b"\xff\xd8\xff\xe0" # en-tête JPEG minimal
files = {"file": ("photo.jpg", io.BytesIO(contenu), "image/jpeg")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 200
assert r.json()["file_id"].endswith(".jpg")
def test_invoice_extension_invalide():
files = {"file": ("script.exe", io.BytesIO(b"MZ"), "application/octet-stream")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 400
def test_invoice_path_traversal():
# file_id avec des caractères invalides (non-hex) → rejeté par la validation ou le routage
r = client.get("/api/invoices/INVALID_FILE_ID_WITH_UPPER_CASE.pdf")
assert r.status_code in (400, 404, 422)
def test_invoice_inexistant():
r = client.get("/api/invoices/" + "a" * 32 + ".pdf")
assert r.status_code == 404
# ═══════════════════════════════════════════════════════════
# Données de démonstration — seed-demo
# ═══════════════════════════════════════════════════════════
def test_seed_demo_actifs():
r = client.post("/api/seed-demo")
assert r.status_code == 200
assets = r.json()["assets"]
parents = [a for a in assets if a["type"] == "parent"]
pieces = [a for a in assets if a["type"] == "piece"]
assert len(parents) >= 6, "Au moins 6 immobilisations attendues"
assert len(pieces) >= 10, "Au moins 10 pièces attendues"
# Plusieurs catégories DPA
cat_ids = {a["view_cpa"]["cca_class"]["id"] for a in parents}
assert len(cat_ids) >= 4, f"Au moins 4 catégories attendues, obtenu : {cat_ids}"
# Pièces au rebut
rebuts = [a for a in pieces if a["view_admin"]["operational_status"] == "Rebut"]
assert len(rebuts) >= 2, "Au moins 2 pièces au rebut (Veeam + Broadcom)"
# Porteurs d'étiquette
bearers = [a for a in pieces if a["view_admin"].get("is_tag_bearer")]
assert len(bearers) >= 4, "Un porteur par immobilisation active"
# Pièces libres (stock)
libres = [a for a in pieces if a["parent_id"] is None
and a["view_admin"]["operational_status"] != "Rebut"]
assert len(libres) >= 2, "Des pièces de rechange en stock"
def test_seed_demo_registre_cat50():
client.post("/api/seed-demo")
r = client.get("/api/cca-registry?cca_class_id=50")
assert r.status_code == 200
assert len(r.json()["years"]) >= 3, "3 exercices pour Cat. 50 (2023-2025)"
def test_seed_demo_perte_finale_cat12():
"""Cat. 12 — perte finale en 2025 : licence Veeam non renouvelée."""
client.post("/api/seed-demo")
r = client.get("/api/annexe8?year=2025")
cls12 = next((c for c in r.json()["classes"] if c["cca_class_id"] == 12), None)
assert cls12 is not None, "Cat. 12 doit apparaître dans l'Annexe 8"
y = cls12["year_data"]
assert y is not None
assert float(y["terminal_loss"]) > 0, "Perte finale attendue en 2025 (Cat. 12)"
def test_seed_demo_recuperation_cat10():
"""Cat. 10 — récupération en 2024 : IBM vendu pour plus que la FNACC."""
client.post("/api/seed-demo")
r = client.get("/api/annexe8?year=2024")
cls10 = next((c for c in r.json()["classes"] if c["cca_class_id"] == 10), None)
assert cls10 is not None, "Cat. 10 doit apparaître dans l'Annexe 8"
y = cls10["year_data"]
assert y is not None
assert float(y["recapture"]) > 0, "Récupération attendue en 2024 (Cat. 10)"
def test_seed_demo_bases_coherentes():
"""La base de chaque parent = somme des coûts de ses pièces rattachées."""
client.post("/api/seed-demo")
assets = client.get("/api/assets").json()
pieces_by_parent: dict[int, list[float]] = {}
for a in assets:
if a["type"] == "piece" and a["parent_id"] is not None:
pieces_by_parent.setdefault(a["parent_id"], []).append(
float(a["view_cpa"]["capitalizable_base_cad"])
)
for a in assets:
if a["type"] != "parent" or a["id"] not in pieces_by_parent:
continue
expected = sum(pieces_by_parent[a["id"]])
actual = float(a["view_cpa"]["capitalizable_base_cad"])
assert actual == pytest.approx(expected, rel=1e-4), (
f"Base incohérente pour « {a['name']} » : {actual}{expected}"
)
def test_seed_double_idempotent():
"""Deux appels seed-demo successifs ne provoquent pas d'erreur."""
assert client.post("/api/seed-demo").status_code == 200
assert client.post("/api/seed-demo").status_code == 200