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

@@ -87,21 +87,31 @@ export const opexPostes = mysqlTable("opex_postes", {
isCustom: boolean("isCustom").default(false),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
}, (t) => ({
/** Empêche une initialisation répétée de créer le même poste pour une année. */
uniqAnneeLibelle: uniqueIndex("opex_postes_annee_libelle_idx").on(t.annee, t.libelle),
}));
export type OpexPoste = typeof opexPostes.$inferSelect;
export type InsertOpexPoste = typeof opexPostes.$inferInsert;
// Overrides manuels par établissement × poste × année
export const opexMontantsEtab = mysqlTable("opex_montants_etab", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
libellePoste: varchar("libellePoste", { length: 255 }).notNull(),
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
libellePoste: varchar("libellePoste", { length: 255 }).notNull(),
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (t) => ({
/** Une seule surcharge manuelle par année, établissement et poste. */
uniqAnneeEtabPoste: uniqueIndex("opex_montants_annee_etab_poste_idx").on(
t.annee,
t.etablissementCode,
t.libellePoste,
),
}));
export type OpexMontantEtab = typeof opexMontantsEtab.$inferSelect;
export type InsertOpexMontantEtab = typeof opexMontantsEtab.$inferInsert;
@@ -148,16 +158,23 @@ export const inventaireMeta = mysqlTable("inventaire_meta", {
// CAPEX — Lignes budgétaires par établissement
// ─────────────────────────────────────────────────────────────────────────────
export const capexLignes = mysqlTable("capex_lignes", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
cle: varchar("cle", { length: 100 }).notNull(),
// cles possibles: renouvellement_2027 | machines_supplementaires | appel_malade
// | telephonie | wifi | video_surveillance | copieurs | visio | autres | commentaires
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
cle: varchar("cle", { length: 100 }).notNull(),
// cles possibles: renouvellement_2027 | machines_supplementaires | appel_malade
// | telephonie | wifi | video_surveillance | copieurs | visio | autres | commentaires
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
}, (t) => ({
/** La sauvegarde CAPEX reste idempotente pour chaque clé métier. */
uniqAnneeEtabCle: uniqueIndex("capex_lignes_annee_etab_cle_idx").on(
t.annee,
t.etablissementCode,
t.cle,
),
}));
export type CapexLigne = typeof capexLignes.$inferSelect;
export type InsertCapexLigne = typeof capexLignes.$inferInsert;