Checkpoint: Ajout du module Masse salariale SANTINOVA : tables de salariés, rémunérations et évolutions manuelles sécurisées par rôle administrateur ; import contrôlé des bulletins 2025 et janvier–août 2026 ; écran tableau 2025–2027 ; tests d'autorisations, d'agrégats et des quatre états UI ; documentation des sources et de la politique d'import.

This commit is contained in:
Manus
2026-08-27 13:10:47 +00:00
parent 2f4b1f2e8e
commit da407db4de
21 changed files with 4390 additions and 4 deletions

View File

@@ -71,6 +71,9 @@ vi.mock("./db", async (importOriginal) => {
upsertOpexBaseRepartition: vi.fn(),
setOpexMontantsEtabBatch: vi.fn(),
deleteOpexPoste: vi.fn(),
getMasseSalariale: vi.fn(),
importMasseSalariale: vi.fn(),
setMasseSalarialeEvolution: vi.fn(),
};
});
@@ -342,3 +345,92 @@ describe("opex.deletePoste", () => {
expect(db.deleteOpexPoste).toHaveBeenCalledWith(2027, 42);
});
});
// ─── Tests Masse salariale (données sensibles, administrateur uniquement) ─────
describe("masseSalariale", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("retourne les données uniquement pour un administrateur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const source = { salaries: [], remunerations: [], evolutions: [] };
(db.getMasseSalariale as ReturnType<typeof vi.fn>).mockResolvedValue(source);
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).masseSalariale.get()).resolves.toEqual(source);
});
it("interdit la consultation à un profil standard", async () => {
const { ctx } = createAuthCtx({ role: "standard" });
await expect(appRouter.createCaller(ctx).masseSalariale.get()).rejects.toThrow();
});
it("importe les données validées uniquement pour un administrateur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
(db.importMasseSalariale as ReturnType<typeof vi.fn>).mockResolvedValue({ salaries: 1, remunerations: 1 });
const { ctx } = createAuthCtx({ role: "admin" });
const salaries = [{
matricule: "000001",
nom: "DUPONT",
prenom: "Alice",
poste: "Responsable administratif",
dateEmbauche: "2024-01-01",
}];
const remunerations = [{
matricule: "000001",
annee: 2026,
salaireAnnuelBrutHorsPrimesCents: 36_000_00,
salaireAnnuelBrutAvecPrimesCents: 40_000_00,
salaireMensuelBrutHorsPrimesCents: 3_000_00,
tauxChargeBps: 5_500,
periodeReference: "2026-08",
statut: "cumul_provisoire" as const,
source: "bulletin",
}];
await expect(appRouter.createCaller(ctx).masseSalariale.import({ salaries, remunerations }))
.resolves.toEqual({ salaries: 1, remunerations: 1 });
expect(db.importMasseSalariale).toHaveBeenCalledWith(salaries, remunerations);
});
it("interdit l'import de rémunérations à un profil standard", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "standard" });
await expect(appRouter.createCaller(ctx).masseSalariale.import({ salaries: [], remunerations: [] })).rejects.toThrow();
expect(db.importMasseSalariale).not.toHaveBeenCalled();
});
it("enregistre l'évolution manuelle avec l'identifiant de l'administrateur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ id: 42, role: "admin" });
await expect(appRouter.createCaller(ctx).masseSalariale.setEvolution({
salarieId: 7,
annee: 2027,
evolutionSalarialeBps: 250,
})).resolves.toEqual({ success: true });
expect(db.setMasseSalarialeEvolution).toHaveBeenCalledWith({
salarieId: 7,
annee: 2027,
evolutionSalarialeBps: 250,
createdBy: 42,
});
});
it("rejette un taux d'évolution hors plage avant l'accès à la base", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).masseSalariale.setEvolution({
salarieId: 7,
annee: 2027,
evolutionSalarialeBps: 10_001,
})).rejects.toThrow();
expect(db.setMasseSalarialeEvolution).not.toHaveBeenCalled();
});
});