"""Tests du chapitre 7 — servitude par ventes appariées, quotes-parts et charges de copropriété, fonds de prévoyance, exercices. Tolérances : 1 $ sur les montants cités arrondis au dollar ; 1e-9 sur les identités comptables ; 0,0001 sur les proportions.""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch07_servitudes_copro as cas from traite import comparables as cp from traite import finance as fin @pytest.fixture(scope="module") def res(): return cas.main() class TestServitude: def test_paires_citees(self, res): p = res["paires"] assert p["n"] == 12 assert p["moyenne"] == pytest.approx(-20333, abs=1) assert p["mediane"] == pytest.approx(-19000, abs=1) assert p["ecart_type"] == pytest.approx(10026, abs=1) assert p["se"] == pytest.approx(2894, abs=1) assert p["t"] == pytest.approx(-7.03, abs=0.01) assert p["ic95"][0] == pytest.approx(-26703, abs=1) and p["ic95"][1] == pytest.approx(-13963, abs=1) assert p["effet_relatif_moyen"] == pytest.approx(-0.0451, abs=0.0001) assert p["effet_relatif_ratio"] == pytest.approx(-0.0446, abs=0.0001) assert p["effet_semilog"] == pytest.approx(-0.0454, abs=0.0001) assert p["sans"].mean() == pytest.approx(455792, abs=1) assert all(a < s for a, s in zip(p["avec"], p["sans"])) def test_identites_paires(self, res): p = res["paires"] # la moyenne des différences est la différence des moyennes ; l'écart-type est celui de l'échantillon (n−1) assert p["moyenne"] == pytest.approx(p["avec"].mean() - p["sans"].mean(), abs=1e-9) assert p["ecart_type"] == pytest.approx(float(np.std(p["diffs"], ddof=1)), abs=1e-9) assert p["se"] == pytest.approx(p["ecart_type"] / math.sqrt(12), abs=1e-9) # cohérence avec traite.comparables va = cp.ventes_appariees(list(p["avec"]), list(p["sans"])) assert va["moyenne"] == pytest.approx(p["moyenne"]) and va["mediane"] == pytest.approx(p["mediane"]) def test_modele_bande(self, res): b = res["bande"] assert b["superficie_bande_m2"] == 120 and b["valeur_bande"] == 21600 and b["effet"] == 10800 assert b["part_terrain"] == pytest.approx(120 / 700) class TestCopropriete: def test_quotes_parts(self, res): qp = res["qp"] assert list(qp["q_dixmilliemes"]) == [935, 1352, 974, 1408, 1196, 1644, 1186, 1305] assert int(qp["q_dixmilliemes"].sum()) == 10000 and qp["q"].sum() == pytest.approx(1.0, abs=1e-12) assert qp["superficies"].sum() == 710 assert qp["valeur_relative"].sum() == pytest.approx(716.91, abs=0.01) # méthode déclarée : F6 = 110 × 1,04 × 1,03 assert qp["valeur_relative"][5] == pytest.approx(110 * 1.04 * 1.03) def test_repartition(self, res): rep = res["rep"]; qp = res["qp"] assert rep["courantes"] == 40000 and rep["auto_assurance"] == 5000 assert rep["general"] == pytest.approx(40000 + res["A_star"] + 5000) assert rep["par_fraction_general"].sum() == pytest.approx(rep["general"], abs=1e-6) assert rep["par_fraction_restreint"].sum() == pytest.approx(3200) and int(rep["n_stationnements"]) == 4 assert rep["par_fraction_total"].sum() == pytest.approx(rep["total"], abs=1e-6) assert rep["total"] == pytest.approx(70091, abs=1) # chiffres cités : F4 et F6 assert rep["par_fraction_total"][3] == pytest.approx(10218, abs=1) and rep["par_fraction_total"][5] == pytest.approx(11797, abs=1) assert rep["par_fraction_total"][0] == pytest.approx(6254, abs=1) assert rep["par_fraction_restreint"][3] == 800 and rep["par_fraction_restreint"][0] == 0 assert rep["par_fraction_fonds"][3] == pytest.approx(qp["q"][3] * res["A_star"]) def test_auto_assurance(self): assert cas.contribution_auto_assurance(10000, 0) == 5000 assert cas.contribution_auto_assurance(10000, 7000) == 3000 assert cas.contribution_auto_assurance(10000, 12000) == 0 class TestFondsPrevoyance: def test_depenses(self, res): D = res["tr_star"]["D"] assert sum(c for _, c, a in cas.COMPOSANTES for _ in a) == 479000 assert D.sum() == pytest.approx(651959, abs=1) assert D[14] == pytest.approx(140000 * 1.025 ** 14, abs=1e-6) and D[14] == pytest.approx(197816, abs=1) assert D[7] == pytest.approx(89151, abs=1) def test_cotisation_seuil(self, res): A = res["A_star"] assert A == pytest.approx(21891, abs=1) tr = res["tr_star"] assert tr["min"] == pytest.approx(0.0, abs=0.01) and tr["annee_min"] == 14 assert tr["F"][-1] == pytest.approx(239241, abs=1) assert tr["A"][-1] == pytest.approx(A * 1.025 ** 24, abs=1e-6) # identité de récurrence F_t = F_{t-1}(1+r) + A_t − D_t for t in range(1, cas.HORIZON + 1): assert tr["F"][t] == pytest.approx(tr["F"][t - 1] * 1.02 + tr["A"][t] - tr["D"][t], abs=1e-6) assert res["A_floor"] == pytest.approx(23228, abs=1) def test_scenarios(self, res): act, regle = res["tr_act"], res["tr_regle"] assert res["regle_0_5"] == 21000 assert act["premiere_annee_negative"] == 13 and act["min"] == pytest.approx(-233300, abs=1) and act["annee_min"] == 22 assert act["F"][-1] == pytest.approx(-182783, abs=1) assert regle["premiere_annee_negative"] == 14 and regle["min"] == pytest.approx(-16660, abs=1) def test_toiture_sff(self, res): fc = res["toiture"] assert fc["cout_indexe"] == pytest.approx(89151, abs=1) assert fc["sff"] == pytest.approx(fin.facteur_fonds_amortissement(0.02, 7)) and fc["sff"] == pytest.approx(0.13451, abs=1e-5) assert fc["versement"] == pytest.approx(11992, abs=1) # le versement accumule exactement le coût indexé en 7 ans à 2 % acc = sum(fc["versement"] * 1.02 ** (7 - k) for k in range(1, 8)) assert acc == pytest.approx(fc["cout_indexe"], abs=1e-6) def test_sensibilite(self, res): s = res["sens"] g = s["grille"] assert g[0.02][2] == pytest.approx(res["A_star"], abs=1) assert g[0.01][0] == pytest.approx(21683, abs=1) and g[0.01][4] == pytest.approx(24442, abs=1) assert g[0.03][0] == pytest.approx(19342, abs=1) and g[0.03][4] == pytest.approx(22099, abs=1) # monotonie : A* croît avec π et décroît avec r for r in s["rendements"]: assert all(np.diff(g[r]) > 0) for j in range(len(s["inflations"])): assert g[0.01][j] > g[0.02][j] > g[0.03][j] def test_incidence_valeur(self, res): inc = res["incidence"]; qp = res["qp"] assert inc["solde_requis"] == pytest.approx(235908, abs=1) and inc["deficit"] == pytest.approx(150908, abs=1) assert inc["ajustement_par_fraction"][3] == pytest.approx(-21248, abs=1) assert inc["ajustement_par_fraction"][5] == pytest.approx(-24809, abs=1) assert inc["ajustement_par_fraction"].sum() == pytest.approx(-inc["deficit"], abs=1e-6) assert cas.trajectoire(cas.COTISATION_ACTUELLE, inc["solde_requis"])["min"] == pytest.approx(0.0, abs=0.01) class TestExercices: def test_ex1_tarifs(self): # 1 fiche + 6 actes + 1 plan à 1 $, état certifié 16 $ + 16 $ pour 1 réquisition, copie certifiée 42 $ assert 8 * 1 + (16 + 16) + 42 == 82 def test_ex2_quotes_parts(self): s = np.array([65.0, 80.0, 95.0, 120.0]); q = s / s.sum() assert q.sum() == pytest.approx(1) and q[3] == pytest.approx(1 / 3) charges = q * 54000 assert charges[0] == pytest.approx(9750) and charges[3] == pytest.approx(18000) # variante avec coefficient d'étage 1,05 pour la fraction de 120 m² v = s * np.array([1, 1, 1, 1.05]); q2 = v / v.sum() assert q2[3] == pytest.approx(126 / 366, abs=1e-9) and (q2 * 54000)[3] == pytest.approx(18590, abs=1) def test_ex3_paires(self): sans = np.array([398000, 412500, 430000, 445000, 462000, 489500]); avec = np.array([384000, 395500, 416000, 423000, 448000, 466500]) va = cp.ventes_appariees(list(avec), list(sans)) assert va["moyenne"] == pytest.approx(-17333, abs=1) and va["mediane"] == pytest.approx(-15500, abs=1) assert va["ecart_type"] == pytest.approx(4179, abs=1) se = va["ecart_type"] / math.sqrt(6); t = 2.571 assert se == pytest.approx(1706, abs=1) assert va["moyenne"] - t * se == pytest.approx(-21720, abs=2) and va["moyenne"] + t * se == pytest.approx(-12947, abs=2) assert va["moyenne"] / sans.mean() == pytest.approx(-0.0394, abs=0.0001) def test_ex4_bande(self): effet = 4 * 35 * 165 * 0.6 assert effet == pytest.approx(13860) assert effet / 20000 == pytest.approx(0.693, abs=0.001) def test_ex5_toiture(self): cT = 75000 * 1.025 ** 7; sff = fin.facteur_fonds_amortissement(0.02, 7) assert cT == pytest.approx(89151, abs=1) and cT * sff == pytest.approx(11992, abs=1) # sans indexation, versement 10 088 $ ; déficit à l'an 7 a0 = 75000 * sff assert a0 == pytest.approx(10088, abs=1) assert cT - 75000 == pytest.approx(14151, abs=1) # variante r = 3 % assert cT * fin.facteur_fonds_amortissement(0.03, 7) == pytest.approx(11635, abs=1) def test_ex6_script(self): # l'exercice demande : A* avec r = 3 % et π = 3 % ; A* avec plancher 25 000 $ ; solde initial requis assert cas.cotisation_seuil(inflation=0.03, rendement=0.03) == pytest.approx(21409, abs=1) assert cas.cotisation_seuil(plancher=25000) == pytest.approx(23228, abs=1) assert cas.cotisation_seuil(solde0=150000) == pytest.approx(17304, abs=1) assert cas.cotisation_seuil(indexer=False) == pytest.approx(25625, abs=1) assert cas.trajectoire(cas.cotisation_seuil(indexer=False), indexer=False)["F"][-1] == pytest.approx(125996, abs=1) # F4 : ajustement −q4 D0 vs valeur actualisée (5 %) de la hausse de cotisation indexée sur 10 ans q4 = cas.quotes_parts()["q"][3]; h = q4 * (cas.cotisation_seuil() - cas.COTISATION_ACTUELLE) assert h == pytest.approx(1393, abs=1) pv = sum(h * 1.025 ** (t - 1) / 1.05 ** t for t in range(1, 11)) assert pv == pytest.approx(11929, abs=1) and sum(h * 1.025 ** (t - 1) for t in range(1, 11)) == pytest.approx(15602, abs=1) def test_ex7_indivision(self): V = 1050000.0 part = V / 3 assert part == pytest.approx(350000) assert part * (1 - 0.08) == pytest.approx(322000) and part * (1 - 0.15) == pytest.approx(297500) assert 350000 - 322000 == 28000 and 350000 - 297500 == 52500