fix: recalculer le score après correction manuelle
This commit is contained in:
@@ -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) {
|
||||
|
||||
57
server/invoiceQuality.test.ts
Normal file
57
server/invoiceQuality.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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
42
shared/invoiceQuality.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user