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

@@ -7,6 +7,7 @@ import { getSessionCookieOptions } from "./_core/cookies";
import { sdk } from "./_core/sdk";
import { systemRouter } from "./_core/systemRouter";
import { adminProcedure, protectedProcedure, publicProcedure, router } from "./_core/trpc";
import { isOpexEtablissementCode, normalizeOpexEtablissementCode } from "../shared/opexValidation";
const writeProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.user.role === "readonly") {
@@ -15,6 +16,12 @@ const writeProcedure = protectedProcedure.use(({ ctx, next }) => {
return next({ ctx });
});
/** Normalise et valide la clé métier d'une base de répartition OPEX. */
const opexEtablissementCodeSchema = z
.string()
.transform(normalizeOpexEtablissementCode)
.refine(isOpexEtablissementCode, { message: "Code établissement OPEX invalide" });
export const appRouter = router({
system: systemRouter,
@@ -132,6 +139,10 @@ export const appRouter = router({
.input(z.object({ id: z.number().optional(), annee: z.number(), colIdx: z.number(), libelle: z.string(), libelleCourt: z.string().optional().nullable(), libelleDetail: z.string().optional().nullable(), fournisseur: z.string().optional().nullable(), categorie: z.string().optional().nullable(), type: z.string().optional().nullable(), facturation: z.string().optional().nullable(), modeVentilation: z.string().optional().nullable(), compte: z.string().optional().nullable(), detail: z.string().optional().nullable(), budgetN1: z.string().optional().nullable(), montant: z.string().optional().nullable(), isCustom: z.boolean().optional() }))
.mutation(async ({ input }) => { await db.upsertOpexPoste(input as Parameters<typeof db.upsertOpexPoste>[0]); return { success: true }; }),
deletePoste: writeProcedure
.input(z.object({ annee: z.number(), id: z.number().int().positive() }))
.mutation(async ({ input }) => ({ success: await db.deleteOpexPoste(input.annee, input.id) })),
getMontantsEtab: protectedProcedure
.input(z.object({ annee: z.number() }))
.query(async ({ input }) => db.getOpexMontantsEtab(input.annee)),
@@ -151,14 +162,12 @@ export const appRouter = router({
}))
}))
.mutation(async ({ input }) => {
for (const m of input.montants) {
await db.setOpexMontantEtab({
await db.setOpexMontantsEtabBatch(input.montants.map((m) => ({
annee: input.annee,
etablissementCode: input.etablissementCode,
libellePoste: m.libellePoste,
montant: m.montant,
});
}
})));
return { success: true, count: input.montants.length };
}),
@@ -205,7 +214,7 @@ export const appRouter = router({
setBaseRepartition: writeProcedure
.input(z.object({
annee: z.number(),
etablissementCode: z.string(),
etablissementCode: opexEtablissementCodeSchema,
etablissementNom: z.string().optional().nullable(),
baseRepartition: z.number(),
baseRepartitionHep: z.number().optional().default(0),
@@ -218,7 +227,7 @@ export const appRouter = router({
.input(z.object({
annee: z.number(),
rows: z.array(z.object({
etablissementCode: z.string(),
etablissementCode: opexEtablissementCodeSchema,
etablissementNom: z.string().optional().nullable(),
baseRepartition: z.number(),
baseRepartitionHep: z.number().optional().default(0),