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:
@@ -9,6 +9,12 @@ import { systemRouter } from "./_core/systemRouter";
|
||||
import { adminProcedure, protectedProcedure, publicProcedure, router } from "./_core/trpc";
|
||||
import { isOpexEtablissementCode, normalizeOpexEtablissementCode } from "../shared/opexValidation";
|
||||
import { getPayrollEncryptionStatus } from "./payrollCrypto";
|
||||
import {
|
||||
buildAnnualForecasts,
|
||||
buildAnnualPayrollActuals,
|
||||
isForecastAdjustmentEditable,
|
||||
} from "./masseSalarialeProjection";
|
||||
import type { AnnualProjection } from "./masseSalarialeProjection";
|
||||
|
||||
const writeProcedure = protectedProcedure.use(({ ctx, next }) => {
|
||||
if (ctx.user.role === "readonly") {
|
||||
@@ -363,7 +369,74 @@ export const appRouter = router({
|
||||
// Les données de rémunération sont sensibles : aucune procédure de ce
|
||||
// sous-routeur n'est accessible aux profils standard ou lecture seule.
|
||||
masseSalariale: router({
|
||||
get: adminProcedure.query(async () => db.getMasseSalariale()),
|
||||
get: adminProcedure.query(async () => {
|
||||
const source = await db.getMasseSalariale();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const forecastsBySalarie = new Map<number, Map<number, AnnualProjection>>();
|
||||
|
||||
for (const remuneration of source.remunerations) {
|
||||
const projection = buildAnnualForecasts([{
|
||||
annee: remuneration.annee,
|
||||
brutHorsAstreinteReferenceCents: remuneration.salaireAnnuelBrutHorsPrimesCents,
|
||||
brutAvecPrimesReferenceCents: remuneration.salaireAnnuelBrutAvecPrimesCents,
|
||||
primeAstreinteCents: remuneration.primeAstreinteCents,
|
||||
primeCents: remuneration.primeCents,
|
||||
primeReferenceCents: remuneration.primeReferenceCents,
|
||||
evolutionCents: remuneration.evolutionCents,
|
||||
evolutionReferenceCents: remuneration.evolutionReferenceCents,
|
||||
tauxChargeBps: remuneration.tauxChargeBps,
|
||||
}]).get(remuneration.annee);
|
||||
if (!projection) continue;
|
||||
const byYear = forecastsBySalarie.get(remuneration.salarieId) ?? new Map();
|
||||
byYear.set(remuneration.annee, projection);
|
||||
forecastsBySalarie.set(remuneration.salarieId, byYear);
|
||||
}
|
||||
|
||||
const payrollActuals = buildAnnualPayrollActuals(source.bulletinsMensuels ?? []);
|
||||
const forecasts = source.remunerations.flatMap((remuneration) => {
|
||||
const projection = forecastsBySalarie.get(remuneration.salarieId)?.get(remuneration.annee);
|
||||
if (!projection) return [];
|
||||
return [{
|
||||
salarieId: remuneration.salarieId,
|
||||
annee: remuneration.annee,
|
||||
...projection,
|
||||
primeCents: remuneration.primeCents,
|
||||
evolutionCents: remuneration.evolutionCents,
|
||||
editable: isForecastAdjustmentEditable(remuneration.annee, currentYear),
|
||||
}];
|
||||
});
|
||||
const actuals: Array<AnnualProjection & {
|
||||
salarieId: number;
|
||||
annee: number;
|
||||
moisDisponibles: number[];
|
||||
origine: "liasses" | "cloture_2025";
|
||||
}> = [];
|
||||
for (const salarie of source.salaries) {
|
||||
const forecast2025 = forecastsBySalarie.get(salarie.id)?.get(2025);
|
||||
const archivedActuals = payrollActuals.get(salarie.matricule);
|
||||
for (const [annee, actual] of Array.from(archivedActuals?.entries() ?? [])) {
|
||||
actuals.push({
|
||||
salarieId: salarie.id,
|
||||
annee,
|
||||
...actual,
|
||||
origine: "liasses" as const,
|
||||
evolutionCents: 0,
|
||||
tauxChargeBps: null,
|
||||
});
|
||||
}
|
||||
if (forecast2025) {
|
||||
actuals.push({
|
||||
salarieId: salarie.id,
|
||||
annee: 2025,
|
||||
...forecast2025,
|
||||
moisDisponibles: Array.from({ length: 12 }, (_, index) => index + 1),
|
||||
origine: "cloture_2025" as const,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { salaries: source.salaries, forecasts, actuals, currentYear };
|
||||
}),
|
||||
|
||||
import: adminProcedure
|
||||
.input(z.object({
|
||||
@@ -380,7 +453,13 @@ export const appRouter = router({
|
||||
salaireAnnuelBrutHorsPrimesCents: z.number().int().min(0).max(100_000_000),
|
||||
salaireAnnuelBrutAvecPrimesCents: z.number().int().min(0).max(100_000_000),
|
||||
salaireMensuelBrutHorsPrimesCents: z.number().int().min(0).max(10_000_000),
|
||||
tauxChargeBps: z.number().int().min(0).max(100_000),
|
||||
tauxChargeBps: z.number().int().min(0).max(100_000).nullable(),
|
||||
nature: z.literal("previsionnel").default("previsionnel"),
|
||||
primeAstreinteCents: z.number().int().min(-100_000_000).max(100_000_000).default(0),
|
||||
primeCents: z.number().int().min(-100_000_000).max(100_000_000).default(0),
|
||||
evolutionCents: z.number().int().min(-100_000_000).max(100_000_000).default(0),
|
||||
primeReferenceCents: z.number().int().min(-100_000_000).max(100_000_000).default(0),
|
||||
evolutionReferenceCents: z.number().int().min(-100_000_000).max(100_000_000).default(0),
|
||||
periodeReference: z.string().regex(/^\d{4}-\d{2}$/).nullable().optional(),
|
||||
statut: z.enum(["annuel", "cumul_provisoire"]),
|
||||
source: z.string().trim().min(1).max(64),
|
||||
@@ -396,9 +475,27 @@ export const appRouter = router({
|
||||
evolutionSalarialeBps: z.number().int().min(-10_000).max(10_000),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!isForecastAdjustmentEditable(input.annee, new Date().getFullYear())) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Les exercices antérieurs ne sont pas modifiables" });
|
||||
}
|
||||
await db.setMasseSalarialeEvolution({ ...input, createdBy: ctx.user.id });
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
setForecastAdjustments: adminProcedure
|
||||
.input(z.object({
|
||||
salarieId: z.number().int().positive(),
|
||||
annee: z.number().int().min(2000).max(2100),
|
||||
primeCents: z.number().int().min(-100_000_000).max(100_000_000),
|
||||
evolutionCents: z.number().int().min(-100_000_000).max(100_000_000),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
if (!isForecastAdjustmentEditable(input.annee, new Date().getFullYear())) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Les exercices antérieurs ne sont pas modifiables" });
|
||||
}
|
||||
await db.setMasseSalarialeForecastAdjustments(input);
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
|
||||
inventaire: router({
|
||||
|
||||
Reference in New Issue
Block a user