Checkpoint: Ajout de la projection annuelle prévisionnel-réel dans Masse salariale : import contrôlé du fichier modèle 2025-2027, réel 2026 provenant uniquement des liasses Salaires, ajustements administrateur limités aux primes et évolutions de l'année courante/future, conservation des centimes, modèle de calcul testé et documentation. 72 tests, TypeScript et build validés.

This commit is contained in:
Manus
2026-08-31 10:58:02 +00:00
parent 5c9618b204
commit 28570234d0
17 changed files with 2415 additions and 204 deletions

View File

@@ -74,6 +74,7 @@ vi.mock("./db", async (importOriginal) => {
getMasseSalariale: vi.fn(),
importMasseSalariale: vi.fn(),
setMasseSalarialeEvolution: vi.fn(),
setMasseSalarialeForecastAdjustments: vi.fn(),
getSalairesMensuels: vi.fn(),
};
});
@@ -356,11 +357,16 @@ describe("masseSalariale", () => {
it("retourne les données uniquement pour un administrateur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const source = { salaries: [], remunerations: [], evolutions: [] };
const source = { salaries: [], remunerations: [], evolutions: [], bulletinsMensuels: [] };
(db.getMasseSalariale as ReturnType<typeof vi.fn>).mockResolvedValue(source);
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).masseSalariale.get()).resolves.toEqual(source);
await expect(appRouter.createCaller(ctx).masseSalariale.get()).resolves.toEqual({
salaries: [],
forecasts: [],
actuals: [],
currentYear: new Date().getFullYear(),
});
});
it("interdit la consultation à un profil standard", async () => {
@@ -369,6 +375,43 @@ describe("masseSalariale", () => {
await expect(appRouter.createCaller(ctx).masseSalariale.get()).rejects.toThrow();
});
it("construit le réel annuel uniquement depuis les bulletins mensuels archivés", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
(db.getMasseSalariale as ReturnType<typeof vi.fn>).mockResolvedValue({
salaries: [{ id: 7, matricule: "000001", nom: "NOM", prenom: "PRENOM", poste: "Poste", dateEmbauche: "2024-01-01" }],
remunerations: [{
salarieId: 7,
annee: 2026,
salaireAnnuelBrutHorsPrimesCents: 1_000_000,
salaireAnnuelBrutAvecPrimesCents: 1_200_000,
salaireMensuelBrutHorsPrimesCents: 83_333,
tauxChargeBps: 4_500,
primeAstreinteCents: 200_000,
primeCents: 0,
primeReferenceCents: 0,
evolutionCents: 0,
evolutionReferenceCents: 0,
}],
evolutions: [],
bulletinsMensuels: [
{ annee: 2026, mois: 1, matricule: "000001", brutMensuelCents: 100_000, brutAvecPrimesCents: 120_000, primeAstreinteCents: 20_000 },
{ annee: 2026, mois: 2, matricule: "000001", brutMensuelCents: 100_000, brutAvecPrimesCents: 110_000, primeAstreinteCents: 0 },
],
});
const { ctx } = createAuthCtx({ role: "admin" });
const result = await appRouter.createCaller(ctx).masseSalariale.get();
expect(result.actuals).toEqual([expect.objectContaining({
salarieId: 7,
annee: 2026,
brutAvecPrimesCents: 230_000,
brutHorsAstreinteCents: 210_000,
moisDisponibles: [1, 2],
origine: "liasses",
})]);
});
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 });
@@ -394,7 +437,15 @@ describe("masseSalariale", () => {
await expect(appRouter.createCaller(ctx).masseSalariale.import({ salaries, remunerations }))
.resolves.toEqual({ salaries: 1, remunerations: 1 });
expect(db.importMasseSalariale).toHaveBeenCalledWith(salaries, remunerations);
expect(db.importMasseSalariale).toHaveBeenCalledWith(salaries, [{
...remunerations[0],
nature: "previsionnel",
primeAstreinteCents: 0,
primeCents: 0,
evolutionCents: 0,
primeReferenceCents: 0,
evolutionReferenceCents: 0,
}]);
});
it("interdit l'import de rémunérations à un profil standard", async () => {
@@ -434,6 +485,52 @@ describe("masseSalariale", () => {
})).rejects.toThrow();
expect(db.setMasseSalarialeEvolution).not.toHaveBeenCalled();
});
it("autorise un administrateur à modifier uniquement les primes et évolutions de l'exercice courant", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "admin" });
const anneeCourante = new Date().getFullYear();
await expect(appRouter.createCaller(ctx).masseSalariale.setForecastAdjustments({
salarieId: 7,
annee: anneeCourante,
primeCents: 12_500,
evolutionCents: 20_000,
})).resolves.toEqual({ success: true });
expect(db.setMasseSalarialeForecastAdjustments).toHaveBeenCalledWith({
salarieId: 7,
annee: anneeCourante,
primeCents: 12_500,
evolutionCents: 20_000,
});
});
it("interdit l'édition prévisionnelle d'un exercice antérieur", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "admin" });
await expect(appRouter.createCaller(ctx).masseSalariale.setForecastAdjustments({
salarieId: 7,
annee: new Date().getFullYear() - 1,
primeCents: 12_500,
evolutionCents: 20_000,
})).rejects.toThrow();
expect(db.setMasseSalarialeForecastAdjustments).not.toHaveBeenCalled();
});
it("interdit l'édition prévisionnelle à un profil standard", async () => {
const { db } = await import("./db").then(m => ({ db: m }));
const { ctx } = createAuthCtx({ role: "standard" });
await expect(appRouter.createCaller(ctx).masseSalariale.setForecastAdjustments({
salarieId: 7,
annee: new Date().getFullYear(),
primeCents: 0,
evolutionCents: 0,
})).rejects.toThrow();
expect(db.setMasseSalarialeForecastAdjustments).not.toHaveBeenCalled();
});
});
// ─── Tests Salaires : sonde de chiffrement et confidentialité ─────────────────