Checkpoint: Audit de robustesse et maintenabilité : calcul dynamique des tendances OPEX par véritable année N-1, filtrage/validation centralisés des codes établissement, transactions d'import et d'écriture, contraintes uniques CAPEX/OPEX avec déduplication, suppression effective de postes OPEX, nettoyage des gabarits et scripts morts, chargement différé XLSX, documentation métier et 22 tests passants.

This commit is contained in:
Manus
2026-08-17 20:50:33 +00:00
parent 31cdb257fb
commit 615dd46059
29 changed files with 1449 additions and 3307 deletions

View File

@@ -68,6 +68,9 @@ vi.mock("./db", async (importOriginal) => {
listUsers: vi.fn(),
listEtablissements: vi.fn(),
getParametres: vi.fn(),
upsertOpexBaseRepartition: vi.fn(),
setOpexMontantsEtabBatch: vi.fn(),
deleteOpexPoste: vi.fn(),
};
});
@@ -268,3 +271,74 @@ describe("parametres.get", () => {
expect(result).toEqual({ seuil_fixes_ans: "5", cout_fixe: "850" });
});
});
// ─── Tests robustesse OPEX ────────────────────────────────────────────────────
describe("opex.setBaseRepartition", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("normalise le code établissement avant de l'enregistrer", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "standard" });
const caller = appRouter.createCaller(ctx);
await caller.opex.setBaseRepartition({
annee: 2026,
etablissementCode: " 1001bpt ",
etablissementNom: "FAM Saint Joseph",
baseRepartition: 100,
baseRepartitionHep: 0,
modeManuel: false,
});
expect(db.upsertOpexBaseRepartition).toHaveBeenCalledWith(expect.objectContaining({
etablissementCode: "1001BPT",
}));
});
it("rejette les lignes de synthèse avant tout accès à la base", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "standard" });
const caller = appRouter.createCaller(ctx);
await expect(caller.opex.setBaseRepartition({
annee: 2026,
etablissementCode: "TOTAL",
baseRepartition: 1,
baseRepartitionHep: 0,
modeManuel: false,
})).rejects.toThrow("Code établissement OPEX invalide");
expect(db.upsertOpexBaseRepartition).not.toHaveBeenCalled();
});
it("interdit toute écriture au profil readonly", async () => {
const { ctx } = createAuthCtx({ role: "readonly" });
const caller = appRouter.createCaller(ctx);
await expect(caller.opex.setBaseRepartition({
annee: 2026,
etablissementCode: "1001BPT",
baseRepartition: 1,
baseRepartitionHep: 0,
modeManuel: false,
})).rejects.toThrow("Accès en lecture seule");
});
});
describe("opex.deletePoste", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("supprime un poste et renvoie le résultat de la transaction métier", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
(db.deleteOpexPoste as ReturnType<typeof vi.fn>).mockResolvedValue(true);
const { ctx } = createAuthCtx({ role: "standard" });
const caller = appRouter.createCaller(ctx);
await expect(caller.opex.deletePoste({ annee: 2027, id: 42 })).resolves.toEqual({ success: true });
expect(db.deleteOpexPoste).toHaveBeenCalledWith(2027, 42);
});
});