fix: recalculer le score après correction manuelle

This commit is contained in:
Manus
2026-09-30 10:19:33 +00:00
committed by manus-admin
parent d85c975fb1
commit daa5fb8463
4 changed files with 111 additions and 14 deletions

View File

@@ -72,19 +72,6 @@ export default function InvoiceDetail() {
});
const handleSave = () => {
// Calculate new quality score based on filled fields
let filledFields = 0;
let totalFields = 6;
if (formData.supplierName) filledFields++;
if (formData.invoiceNumber) filledFields++;
if (formData.invoiceDate) filledFields++;
if (formData.deliveryNoteNumber) filledFields++;
if (formData.orderNumber) filledFields++;
if (formData.totalAmount) filledFields++;
const newQualityScore = Math.round((filledFields / totalFields) * 100);
// Apprentissage : enregistrer les corrections manuelles sur les champs clés
// On compare les valeurs actuelles (invoice) avec les nouvelles valeurs (formData)
if (invoice && formData.supplierName) {

View File

@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { calculateInvoiceQualityScore } from "@shared/invoiceQuality";
const defaultRequiredFields = [
{ fieldName: "supplierName", isRequired: 1 },
{ fieldName: "invoiceNumber", isRequired: 1 },
{ fieldName: "invoiceDate", isRequired: 1 },
{ fieldName: "totalAmount", isRequired: 1 },
{ fieldName: "deliveryNoteNumber", isRequired: 0 },
];
describe("calculateInvoiceQualityScore", () => {
it("passe à 100 % lorsque tous les champs obligatoires sont complétés manuellement", () => {
expect(calculateInvoiceQualityScore({
supplierName: "Microsoft France",
invoiceNumber: "FAC-2026-001",
invoiceDate: new Date("2026-09-30"),
totalAmount: "0.00",
}, defaultRequiredFields)).toBe(100);
});
it("recalcule proportionnellement lorsque seuls certains champs obligatoires sont remplis", () => {
expect(calculateInvoiceQualityScore({
supplierName: "Microsoft France",
invoiceNumber: "FAC-2026-001",
invoiceDate: new Date("2026-09-30"),
totalAmount: " ",
}, defaultRequiredFields)).toBe(75);
});
it("utilise la configuration personnalisée des champs obligatoires", () => {
const configuration = [
{ fieldName: "recipientName", isRequired: 1 },
{ fieldName: "supplierName", isRequired: 0 },
];
expect(calculateInvoiceQualityScore({
recipientName: "ITINOVA",
supplierName: "",
}, configuration)).toBe(100);
});
it("utilise les champs obligatoires par défaut lorsqu’aucune configuration n’existe encore", () => {
expect(calculateInvoiceQualityScore({
supplierName: "Fournisseur",
invoiceNumber: "F-1",
invoiceDate: new Date("2026-09-30"),
totalAmount: "120.00",
}, [])).toBe(100);
});
it("retourne 100 % lorsque le compte a explicitement rendu tous ses champs optionnels", () => {
expect(calculateInvoiceQualityScore({}, [
{ fieldName: "supplierName", isRequired: 0 },
])).toBe(100);
});
});

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { COOKIE_NAME } from "@shared/const";
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
import { calculateInvoiceQualityScore } from "@shared/invoiceQuality";
interface Condition {
field: string;
@@ -476,13 +477,23 @@ export const appRouter = router({
if (!invoice || (ctx.user.role !== 'admin' && invoice.userId !== ctx.user.id)) {
throw new TRPCError({ code: "NOT_FOUND" });
}
// Le score reflète toujours les valeurs actuellement enregistrées, y compris
// celles corrigées manuellement. La configuration est celle du propriétaire
// de la facture afin de conserver la même règle que lors de son extraction.
const fieldsConfiguration = await getLlmFieldsConfigByUser(invoice.userId);
const qualityScore = calculateInvoiceQualityScore(
{ ...invoice, ...input.data },
fieldsConfiguration,
);
await updateInvoice(input.id, {
...input.data,
qualityScore,
manuallyEdited: 1,
});
return { success: true };
return { success: true, qualityScore };
}),
delete: protectedProcedure

42
shared/invoiceQuality.ts Normal file
View File

@@ -0,0 +1,42 @@
/** Champ requis configuré pour le calcul du score de qualité d’une facture. */
export type QualityFieldConfig = {
fieldName: string;
isRequired: number;
};
/** Référentiel utilisé tant qu’un compte ne possède pas encore sa configuration en base. */
export const DEFAULT_REQUIRED_INVOICE_FIELDS = [
"supplierName",
"invoiceNumber",
"invoiceDate",
"totalAmount",
] as const;
function hasValue(value: unknown): boolean {
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim().length > 0;
if (value instanceof Date) return Number.isFinite(value.getTime());
return true;
}
/**
* Recalcule le score selon les champs obligatoires configurés pour le compte.
* Les champs optionnels n'affectent jamais le résultat. Le calcul est purement
* déterministe afin d'être identique après import et après une correction manuelle.
*/
export function calculateInvoiceQualityScore(
invoice: Record<string, unknown>,
fieldsConfiguration: readonly QualityFieldConfig[],
): number {
const configuredRequiredFields = fieldsConfiguration
.filter((field) => field.isRequired === 1)
.map((field) => field.fieldName);
const requiredFields = fieldsConfiguration.length > 0
? configuredRequiredFields
: DEFAULT_REQUIRED_INVOICE_FIELDS;
if (requiredFields.length === 0) return 100;
const completedFields = requiredFields.filter((fieldName) => hasValue(invoice[fieldName])).length;
return Math.round((completedFields / requiredFields.length) * 100);
}