Compare commits

..

4 Commits

6 changed files with 52 additions and 13 deletions

View File

@@ -8,7 +8,7 @@
}, },
"containerName": "demat-facturation-app", "containerName": "demat-facturation-app",
"image": "images/demat-facturation-dsi.jpg", "image": "images/demat-facturation-dsi.jpg",
"giteaRepo": "demat-facturation-dsi", "giteaRepo": "demat-facturation",
"giteaOwner": "manus-admin", "giteaOwner": "manus-admin",
"ci": { "ci": {
"required": true "required": true

View File

@@ -32,6 +32,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { trpc } from "@/lib/trpc"; 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 { 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"; 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 // Déclarée ici (avant filteredInvoices et statusCounts) pour éviter le hoisting error
const isEligibleForBAPValidation = (invoice: any) => { const isEligibleForBAPValidation = (invoice: any) => {
return ( return (
(invoice.qualityScore || 0) === 100 && meetsBapQualityThreshold(invoice.qualityScore) &&
invoice.exportStatus !== "exported" && invoice.exportStatus !== "exported" &&
invoice.isSubscription === 0 && invoice.isSubscription === 0 &&
invoice.serviceConcerne && invoice.serviceConcerne &&
@@ -526,19 +527,19 @@ export default function InvoicesBAP() {
const getQualityBadge = (score: number | null) => { const getQualityBadge = (score: number | null) => {
if (score === null) return <Badge variant="outline">-</Badge>; 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>; 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>; return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
}; };
const isEligibleForExport = (invoice: any) => { const isEligibleForExport = (invoice: any) => {
// Une facture BAP est exportable si : // Une facture BAP est exportable si :
// 1. Score = 100% // 1. Score >= 90 % (seuil BAP)
// 2. Type d'achat rempli // 2. Type d'achat rempli
// 3. Service concerné rempli // 3. Service concerné rempli
// 4. Ventilation comptable remplie // 4. Ventilation comptable remplie
return ( return (
(invoice.qualityScore || 0) === 100 && meetsBapQualityThreshold(invoice.qualityScore) &&
invoice.typeAchat && invoice.typeAchat &&
invoice.serviceConcerne && invoice.serviceConcerne &&
invoice.ventilationComptable invoice.ventilationComptable
@@ -549,7 +550,7 @@ export default function InvoicesBAP() {
const getBAPValidationTooltip = (invoice: any): string => { const getBAPValidationTooltip = (invoice: any): string => {
const reasons: 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.exportStatus === "exported") reasons.push("Déjà exportée");
if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement"); if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement");
if (!invoice.serviceConcerne) reasons.push("Service manquant"); if (!invoice.serviceConcerne) reasons.push("Service manquant");
@@ -655,7 +656,7 @@ export default function InvoicesBAP() {
</Button> </Button>
<Button <Button
onClick={() => { 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(); validateBAPBulkMutation.mutate();
} }
}} }}

View 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);
});
});

View File

@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { COOKIE_NAME } from "@shared/const"; import { COOKIE_NAME } from "@shared/const";
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
interface Condition { interface Condition {
field: string; field: string;
@@ -499,8 +500,8 @@ export const appRouter = router({
const hasService = !!invoice.serviceConcerne; const hasService = !!invoice.serviceConcerne;
const hasTypeAchat = !!invoice.typeAchat; const hasTypeAchat = !!invoice.typeAchat;
const hasVentilation = !!invoice.ventilationComptable; const hasVentilation = !!invoice.ventilationComptable;
if (score < 100) { if (!meetsBapQualityThreshold(score)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Le score de qualité doit être à 100% pour valider" }); throw new TRPCError({ code: "BAD_REQUEST", message: `Le score de qualité doit être au moins de ${BAP_MIN_QUALITY_SCORE}% pour valider` });
} }
if (!isNotSubscription) { if (!isNotSubscription) {
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture est marquée comme abonnement" }); 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); const allInvoices = ctx.user.role === 'admin' ? await getAllInvoices() : await getInvoicesByUser(ctx.user.id);
// Filtrer les factures éligibles (non déjà validées) // Filtrer les factures éligibles (non déjà validées)
const eligible = allInvoices.filter((inv: any) => const eligible = allInvoices.filter((inv: any) =>
(inv.qualityScore || 0) >= 100 && meetsBapQualityThreshold(inv.qualityScore) &&
inv.isSubscription === 0 && inv.isSubscription === 0 &&
!!inv.serviceConcerne && !!inv.serviceConcerne &&
!!inv.typeAchat && !!inv.typeAchat &&

10
shared/bapEligibility.ts Normal file
View 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 linterface 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;
}

17
todo.md
View File

@@ -778,7 +778,18 @@
- [x] Ajouter les tests et valider TypeScript, tests et build - [x] Ajouter les tests et valider TypeScript, tests et build
## Déploiement — filtre par action des automatismes ## Déploiement — filtre par action des automatismes
- [ ] Pousser le filtre par action vers Gitea recette - [x] Pousser le filtre par action vers Gitea recette
- [ ] Déployer et vérifier le filtre par action en recette - [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
## Déploiement — seuil BAP à 90 %
- [ ] Pousser le correctif vers Gitea recette
- [ ] Déployer et vérifier le seuil BAP à 90 % en recette
- [ ] Pousser la version validée vers Gitea production - [ ] Pousser la version validée vers Gitea production
- [ ] Déployer et vérifier le filtre par action en production - [ ] Déployer et vérifier le seuil BAP à 90 % en production