Checkpoint: Le score de qualité est désormais recalculé et persisté par le serveur après chaque modification manuelle de facture. Il s’appuie sur les champs obligatoires configurés pour le propriétaire de la facture, conserve un référentiel par défaut en l’absence de configuration et accepte explicitement un compte ayant rendu tous ses champs optionnels. Le calcul local non persisté de la page détail est supprimé. 64 tests, TypeScript et build validés.
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);
|
||||
}
|
||||
5
todo.md
5
todo.md
@@ -5,6 +5,11 @@
|
||||
- [x] Corriger la cause sans lancer d’import ni créer de planification non sollicitée, avec sélection explicite du compte source pour les administrateurs
|
||||
- [x] Valider les contrôles, les tests et le comportement de démarrage/arrêt
|
||||
|
||||
## Diagnostic démarrage OAuth2 après test IMAP concluant — 24/09/2026
|
||||
- [x] Reproduire le démarrage OAuth2 sans importer de facture et tracer le résultat API : la réception automatique est désactivée en production pour le compte configuré
|
||||
- [ ] Corriger la cause du message générique de démarrage
|
||||
- [ ] Vérifier le démarrage et l’arrêt contrôlés avec les tests de non-régression
|
||||
|
||||
## Phase 1: Base de données et schéma
|
||||
- [x] Créer table users avec support authentification locale + Azure AD
|
||||
- [x] Créer table sourceFiles pour fichiers PDF uploadés
|
||||
|
||||
Reference in New Issue
Block a user