feat: create ScoringEngine for AI copilot product analysis and scoring.

This commit is contained in:
Michael committed 2026-02-27 11:05:25 +01:00
1 parent a053556a64
commit 5773ed9a26
2 files changed
+29 -14

No files matched your search

@@ -182,15 +182,19 @@ export class ContextProfiler {
const targetCaPerStore = normCa(target);
const targetQtyPerStore = normQty(target);
// 1. Totaux fournisseur (valeurs brutes pour les poids de représentativité réseau)
const totalCaFournisseur = allProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
const totalQtyFournisseur = allProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
// 1. Totaux fournisseur (valeurs PONDÉRÉES pour comparaison équitable réseau)
// weightedTotalCa/Qty projettent les 1-magasin sur une base réseau (×2).
const getWeightedCa = (p: ProductAnalysisInput) => p.weightedTotalCa ?? p.totalCa ?? 0;
const getWeightedQty = (p: ProductAnalysisInput) => p.weightedTotalQuantite ?? p.totalQuantite ?? 0;
const totalCaFournisseur = allProds.reduce((s, p) => s + getWeightedCa(p), 0);
const totalQtyFournisseur = allProds.reduce((s, p) => s + getWeightedQty(p), 0);
// 2. Totaux du rayon (Niveau 2 de nomenclature)
const targetRayonKey = getRayonKey(target);
const rayonProds = allProds.filter(p => getRayonKey(p) === targetRayonKey);
const totalCaRayon = rayonProds.reduce((s, p) => s + (p.totalCa ?? 0), 0);
const totalQtyRayon = rayonProds.reduce((s, p) => s + (p.totalQuantite ?? 0), 0);
const totalCaRayon = rayonProds.reduce((s, p) => s + getWeightedCa(p), 0);
const totalQtyRayon = rayonProds.reduce((s, p) => s + getWeightedQty(p), 0);
// 3. Distributions normalisées (PAR MAGASIN) — pour les percentiles et le quadrant
const allCaPerStore = allProds.map(normCa);
@@ -209,14 +213,14 @@ export class ContextProfiler {
const isTop20Qty = targetQtyPerStore >= top20QtyThreshold;
const isAboveMedianComposite = pComposite >= 50;
// 5. Poids bruts (valeurs réseau pour représentativité commerciale réelle)
// 5. Poids pondérés (valeurs réseau projetées pour représentativité réaliste)
const weightCaFournisseur =
totalCaFournisseur > 0
? Math.round(((target.totalCa ?? 0) / totalCaFournisseur) * 1000) / 10
? Math.round((getWeightedCa(target) / totalCaFournisseur) * 1000) / 10
: 0;
const weightQtyFournisseur =
totalQtyFournisseur > 0
? Math.round(((target.totalQuantite ?? 0) / totalQtyFournisseur) * 1000) / 10
? Math.round((getWeightedQty(target) / totalQtyFournisseur) * 1000) / 10
: 0;
const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5;
@@ -289,11 +293,11 @@ export class ContextProfiler {
weightQtyFournisseur,
weightCaRayon:
totalCaRayon > 0
? Math.round(((target.totalCa ?? 0) / totalCaRayon) * 1000) / 10
? Math.round((getWeightedCa(target) / totalCaRayon) * 1000) / 10
: 0,
weightQtyRayon:
totalQtyRayon > 0
? Math.round(((target.totalQuantite ?? 0) / totalQtyRayon) * 1000) / 10
? Math.round((getWeightedQty(target) / totalQtyRayon) * 1000) / 10
: 0,
tauxMarge: target.tauxMarge ?? 0,
@@ -131,12 +131,23 @@ export class ScoringEngine {
};
}
/**
* Normalise les KPIs d'un produit pour comparaison équitable.
*
* Utilise les valeurs pondérées (weightedTotalCa / weightedTotalQuantite)
* qui projettent les produits 1-magasin sur une base réseau comparable
* (×2 si 1 magasin, ×1 si 2 magasins). Cela évite de pénaliser un
* produit vendu uniquement dans 1 magasin face à un produit vendu dans 2.
*
* Applique ensuite la projection Run Rate pour les produits récents
* (\< 12 mois d'historique) afin de comparer sur une base annuelle.
*/
private static preprocess(p: ProductAnalysisInput) {
const coef = (p.totalMagasins || 1) / (p.storeCount || 1);
let caNorm = (p.totalCa || 0) * coef;
let volNorm = (p.totalQuantite || 0) * coef;
// Valeurs pondérées réseau — si absentes, fallback sur les brutes
let caNorm = p.weightedTotalCa ?? p.totalCa ?? 0;
let volNorm = p.weightedTotalQuantite ?? p.totalQuantite ?? 0;
// Run Rate Projection
// Run Rate Projection (produits avec 3-11 mois d'historique)
if ((p.regularityScore || 0) >= 3 && (p.regularityScore || 0) < 12) {
caNorm = (caNorm / (p.regularityScore || 3)) * 12;
volNorm = (volNorm / (p.regularityScore || 3)) * 12;