Checkpoint: Le seuil d’éligibilité et de validation BAP est centralisé à 90 % dans le serveur et l’interface. Les validations unitaires, TypeScript, build et rendu de la page Factures BAP sont réussis en sandbox.
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown, ChevronDown, ChevronRight, ChevronsUpDown, CalendarDays, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
@@ -394,7 +395,7 @@ export default function InvoicesBAP() {
|
||||
// Déclarée ici (avant filteredInvoices et statusCounts) pour éviter le hoisting error
|
||||
const isEligibleForBAPValidation = (invoice: any) => {
|
||||
return (
|
||||
(invoice.qualityScore || 0) === 100 &&
|
||||
meetsBapQualityThreshold(invoice.qualityScore) &&
|
||||
invoice.exportStatus !== "exported" &&
|
||||
invoice.isSubscription === 0 &&
|
||||
invoice.serviceConcerne &&
|
||||
@@ -526,19 +527,19 @@ export default function InvoicesBAP() {
|
||||
|
||||
const getQualityBadge = (score: number | null) => {
|
||||
if (score === null) return <Badge variant="outline">-</Badge>;
|
||||
if (score === 100) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
|
||||
if (meetsBapQualityThreshold(score)) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
|
||||
if (score >= 80) return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">{score}%</Badge>;
|
||||
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
|
||||
};
|
||||
|
||||
const isEligibleForExport = (invoice: any) => {
|
||||
// Une facture BAP est exportable si :
|
||||
// 1. Score = 100%
|
||||
// 1. Score >= 90 % (seuil BAP)
|
||||
// 2. Type d'achat rempli
|
||||
// 3. Service concerné rempli
|
||||
// 4. Ventilation comptable remplie
|
||||
return (
|
||||
(invoice.qualityScore || 0) === 100 &&
|
||||
meetsBapQualityThreshold(invoice.qualityScore) &&
|
||||
invoice.typeAchat &&
|
||||
invoice.serviceConcerne &&
|
||||
invoice.ventilationComptable
|
||||
@@ -549,7 +550,7 @@ export default function InvoicesBAP() {
|
||||
|
||||
const getBAPValidationTooltip = (invoice: any): string => {
|
||||
const reasons: string[] = [];
|
||||
if ((invoice.qualityScore || 0) < 100) reasons.push("Score < 100%");
|
||||
if (!meetsBapQualityThreshold(invoice.qualityScore)) reasons.push(`Score < ${BAP_MIN_QUALITY_SCORE}%`);
|
||||
if (invoice.exportStatus === "exported") reasons.push("Déjà exportée");
|
||||
if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement");
|
||||
if (!invoice.serviceConcerne) reasons.push("Service manquant");
|
||||
@@ -655,7 +656,7 @@ export default function InvoicesBAP() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (confirm("Valider en BAP toutes les factures éligibles (score 100%, champs remplis, non abonnement) ? Les PDFs annotés seront générés automatiquement.")) {
|
||||
if (confirm(`Valider en BAP toutes les factures éligibles (score ≥ ${BAP_MIN_QUALITY_SCORE}%, champs remplis, non abonnement) ? Les PDFs annotés seront générés automatiquement.`)) {
|
||||
validateBAPBulkMutation.mutate();
|
||||
}
|
||||
}}
|
||||
|
||||
16
server/bapEligibility.test.ts
Normal file
16
server/bapEligibility.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||
|
||||
describe("meetsBapQualityThreshold", () => {
|
||||
it("autorise exactement le seuil de 90 %", () => {
|
||||
expect(BAP_MIN_QUALITY_SCORE).toBe(90);
|
||||
expect(meetsBapQualityThreshold(90)).toBe(true);
|
||||
expect(meetsBapQualityThreshold(100)).toBe(true);
|
||||
});
|
||||
|
||||
it("refuse les scores inférieurs ou absents", () => {
|
||||
expect(meetsBapQualityThreshold(89)).toBe(false);
|
||||
expect(meetsBapQualityThreshold(null)).toBe(false);
|
||||
expect(meetsBapQualityThreshold(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { COOKIE_NAME } from "@shared/const";
|
||||
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||
|
||||
interface Condition {
|
||||
field: string;
|
||||
@@ -499,8 +500,8 @@ export const appRouter = router({
|
||||
const hasService = !!invoice.serviceConcerne;
|
||||
const hasTypeAchat = !!invoice.typeAchat;
|
||||
const hasVentilation = !!invoice.ventilationComptable;
|
||||
if (score < 100) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Le score de qualité doit être à 100% pour valider" });
|
||||
if (!meetsBapQualityThreshold(score)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Le score de qualité doit être au moins de ${BAP_MIN_QUALITY_SCORE}% pour valider` });
|
||||
}
|
||||
if (!isNotSubscription) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture est marquée comme abonnement" });
|
||||
@@ -720,7 +721,7 @@ export const appRouter = router({
|
||||
const allInvoices = ctx.user.role === 'admin' ? await getAllInvoices() : await getInvoicesByUser(ctx.user.id);
|
||||
// Filtrer les factures éligibles (non déjà validées)
|
||||
const eligible = allInvoices.filter((inv: any) =>
|
||||
(inv.qualityScore || 0) >= 100 &&
|
||||
meetsBapQualityThreshold(inv.qualityScore) &&
|
||||
inv.isSubscription === 0 &&
|
||||
!!inv.serviceConcerne &&
|
||||
!!inv.typeAchat &&
|
||||
|
||||
10
shared/bapEligibility.ts
Normal file
10
shared/bapEligibility.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Score minimal requis pour générer et valider un BAP. */
|
||||
export const BAP_MIN_QUALITY_SCORE = 90;
|
||||
|
||||
/**
|
||||
* Centralise le seuil BAP afin que l’interface et le serveur appliquent la
|
||||
* même règle métier, y compris pour les scores nuls ou absents.
|
||||
*/
|
||||
export function meetsBapQualityThreshold(score: number | null | undefined): boolean {
|
||||
return typeof score === "number" && Number.isFinite(score) && score >= BAP_MIN_QUALITY_SCORE;
|
||||
}
|
||||
5
todo.md
5
todo.md
@@ -782,3 +782,8 @@
|
||||
- [x] Déployer et vérifier le filtre par action en recette
|
||||
- [x] Pousser la version validée vers Gitea production
|
||||
- [x] Déployer et vérifier le filtre par action en production
|
||||
|
||||
## Seuil d’éligibilité BAP à 90 %
|
||||
- [x] Identifier les validations BAP fondées sur le score de qualité
|
||||
- [x] Abaisser le seuil de 100 % à 90 % pour les opérations BAP
|
||||
- [x] Ajouter les tests de seuil et valider TypeScript, tests et build
|
||||
|
||||
Reference in New Issue
Block a user