fix: recalculer le score après correction manuelle
This commit is contained in:
@@ -72,19 +72,6 @@ export default function InvoiceDetail() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSave = () => {
|
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
|
// Apprentissage : enregistrer les corrections manuelles sur les champs clés
|
||||||
// On compare les valeurs actuelles (invoice) avec les nouvelles valeurs (formData)
|
// On compare les valeurs actuelles (invoice) avec les nouvelles valeurs (formData)
|
||||||
if (invoice && formData.supplierName) {
|
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 { z } from "zod";
|
||||||
import { COOKIE_NAME } from "@shared/const";
|
import { COOKIE_NAME } from "@shared/const";
|
||||||
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||||
|
import { calculateInvoiceQualityScore } from "@shared/invoiceQuality";
|
||||||
|
|
||||||
interface Condition {
|
interface Condition {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -476,13 +477,23 @@ export const appRouter = router({
|
|||||||
if (!invoice || (ctx.user.role !== 'admin' && invoice.userId !== ctx.user.id)) {
|
if (!invoice || (ctx.user.role !== 'admin' && invoice.userId !== ctx.user.id)) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
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, {
|
await updateInvoice(input.id, {
|
||||||
...input.data,
|
...input.data,
|
||||||
|
qualityScore,
|
||||||
manuallyEdited: 1,
|
manuallyEdited: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true, qualityScore };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
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