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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user