mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-21 14:08:08 +02:00
feat: Implement AI analysis engine with simplified v5 logic for product recommendations based on relative supplier performance.
This commit is contained in:
1 parent
1f38f487b1
commit
9faea9df07
1 file changed
+61
-167
@@ -1,11 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* CollectFlow — Analysis Engine (v4 — Anti sur-classement)
|
* CollectFlow — Analysis Engine (v5 — SIMPLIFIÉ)
|
||||||
*
|
*
|
||||||
* v4 — Correctifs :
|
* Version simplifiée basée sur la performance relative au fournisseur.
|
||||||
* - Suppression des prescriptions directes "→ A" dans le guide des quadrants.
|
* Principe: Tout est relatif à la moyenne du lot fournisseur.
|
||||||
* - Ajout du seuil plancher absolu : contribution insignifiante + score faible → Z.
|
|
||||||
* - Affichage du signal isLowContribution en rouge dans le message.
|
|
||||||
* - Les quadrants sont des INDICES, pas des verdicts. Mary doit croiser avec les poids.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ProductAnalysisInput } from "../models/ai-analysis.types";
|
import type { ProductAnalysisInput } from "../models/ai-analysis.types";
|
||||||
@@ -13,114 +10,54 @@ import type { ProductContextProfile } from "./context-profiler";
|
|||||||
|
|
||||||
export class AnalysisEngine {
|
export class AnalysisEngine {
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// SYSTEM PROMPT v4
|
// SYSTEM PROMPT v5 — SIMPLIFIÉ
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
static generateSystemPrompt(): string {
|
static generateSystemPrompt(): string {
|
||||||
return `Tu es Mary, Senior Retail Strategist pour une enseigne discount d'équipement de la maison (type La Foir'Fouille). Ton modèle économique repose sur la rotation rapide, le volume, et la rentabilité de l'espace en rayon. Tu analyses des produits pour recommander A (garder) ou Z (sortir).
|
return `Tu es Mary, Senior Retail Strategist pour une enseigne discount d'équipement de la maison (type La Foir'Fouille).
|
||||||
|
Ton modèle économique repose sur la rotation rapide, le volume, et la rentabilité de l'espace en rayon.
|
||||||
|
Tu analyses des produits pour recommander A (garder) ou Z (sortir).
|
||||||
|
|
||||||
IMPORTANT : La gamme C est RÉSERVÉE aux produits saisonniers et gérée MANUELLEMENT par l'acheteur. Tu ne dois JAMAIS recommander C.
|
IMPORTANT : La gamme C est RÉSERVÉE aux produits saisonniers et gérée MANUELLEMENT par l'acheteur. Tu ne dois JAMAIS recommander C.
|
||||||
|
|
||||||
--- ORDRE DE PRIORITÉ (STRICT) ---
|
--- LOGIQUE SIMPLIFIÉE (3 RÈGLES SEULEMENT) ---
|
||||||
|
|
||||||
⛔ VÉRIFICATION OBLIGATOIRE EN PREMIER ⛔
|
⛔ RÈGLE 1 — STOCK MORT ABSOLU (priorité absolue):
|
||||||
Avant toute autre analyse, VÉRIFIER systématiquement si le produit est un STOCK MORT ABSOLU:
|
SI CA réseau < 100€ ET Quantité réseau < 30 unités SIMULTANÉMENT → Z IMMÉDIAT, STOP.
|
||||||
SI CA réseau < 100€ ET Quantité réseau < 30 unités → Z IMMÉDIAT, ne pas continuer l'analyse, STOP.
|
Les DEUX conditions doivent être vraies en même temps.
|
||||||
Cette règle PRIME sur toutes les autres (protection, star, haute valeur, locomotive, complémentarité, etc.).
|
Un produit à 30€ de CA sur 2 magasins (15€/magasin/an) coûte plus en gestion qu'il ne rapporte.
|
||||||
|
|
||||||
1. GARDE-FOU PROTECTION FORTE : Si isProtected = true (Nouveauté / Dernier Produit / Top30), c'est un signal FORT en faveur de A.
|
📊 RÈGLE 2 — PERFORMANCE RELATIVE AU FOURNISSEUR:
|
||||||
Cependant, si les performances réelles du produit sont clairement insuffisantes (CA dérisoire, marge nulle ou négative), tu PEUX passer outre la protection et recommander Z avec justification.
|
Le produit doit être évalué PAR RAPPORT à la moyenne de son lot fournisseur.
|
||||||
|
|
||||||
2. QUADRANT STAR — PROTECTION ABSOLUE : Si le produit est en quadrant STAR (volume élevé ET marge élevée), c'est un produit STRATÉGIQUE qui génère à la fois du volume et de la rentabilité.
|
ÉTAPE 1: Calculer la moyenne du lot
|
||||||
→ GARDE en A automatiquement.
|
CA Moyen Fournisseur = CA Total Fournisseur ÷ Nombre de produits
|
||||||
|
|
||||||
3. PROTECTION PRODUITS HAUTE VALEUR FACIALE (PMV élevé) :
|
ÉTAPE 2: Calculer le score relatif du produit
|
||||||
Si le produit génère un CA significatif MALGRÉ un faible volume, c'est un produit à forte valeur faciale (PMV élevé = CA/Quantité élevé).
|
Score Relatif = (CA Produit ÷ CA Moyen) × 100
|
||||||
Ces produits optimisent la rentabilité par m² car ils génèrent du CA et de la marge avec peu d'espace rayon.
|
|
||||||
→ Si CA total réseau ≥ 200€ → GARDE en A, même si quantité < 50 unités.
|
|
||||||
RATIONALE : Un produit qui vend 40 unités à 5€ (200€ CA) est MEILLEUR qu'un produit qui vend 200 unités à 0.80€ (160€ CA).
|
|
||||||
Le PMV élevé = optimisation espace rayon + meilleure rentabilité.
|
|
||||||
|
|
||||||
4. SEUIL PLANCHER ABSOLU ET CONTEXTE DISCOUNT (règle critique) :
|
ÉTAPE 3: Décider selon le score
|
||||||
- Règle A : Si isLowContribution = true [percentile CA <= 30 ET percentile QTÉ <= 30] ET score composite < 35 ET scoreCritique = true → Z DIRECT.
|
• Si Score ≥ 100 → Le produit fait AU MOINS sa part → A
|
||||||
- Règle B (⛔ STOCK MORT ABSOLU — RÈGLE NON-NÉGOCIABLE ⛔) :
|
• Si Score ≥ 50 ET < 100 → Le produit fait entre 50% et 100% de sa part → Comparer au percentile médian (P50)
|
||||||
SI CA réseau < 100€ ET Quantité réseau < 30 unités SIMULTANÉMENT:
|
- Si percentile CA ≥ 50 → A (médiane ou mieux)
|
||||||
→ Z OBLIGATOIRE — AUCUNE EXCEPTION POSSIBLE
|
- Si percentile CA < 50 → Z (sous la médiane)
|
||||||
→ Ne PAS considérer: complémentarité, assortiment, choix, rupture d'offre, pilier, locomotive, ou toute autre raison
|
• Si Score < 50 → Le produit fait moins de la moitié de sa part → Z
|
||||||
→ RATIONALE ÉCONOMIQUE: Un produit à 30€ de CA sur 2 magasins (15€/magasin/an = 1.25€/mois) coûte PLUS en gestion qu'il ne rapporte
|
|
||||||
→ Un tel produit ne peut JAMAIS être "indispensable" ou "structurant" — c'est un gaspillage d'espace rayon
|
|
||||||
IMPORTANT : Les DEUX conditions doivent être vraies en même temps. Si CA < 100€ MAIS Quantité ≥ 30 → NE PAS appliquer cette règle. Si Quantité < 30 MAIS CA ≥ 100€ → NE PAS appliquer cette règle.
|
|
||||||
- Règle C (Locomotive de Rayon — PROTECTION CONDITIONNELLE) : Si isLocomotiveRayon = true [poids CA rayon > 15% OU poids QTÉ rayon > 15%]:
|
|
||||||
VÉRIFIER EN PREMIER : Est-ce un stock mort absolu ?
|
|
||||||
→ SI CA total réseau < 100€ ET Quantité réseau < 30 unités → Z OBLIGATOIRE (rayon mort, fermer la catégorie)
|
|
||||||
→ SI taille rayon < 3 produits ET CA total rayon < 500€ → Pas de protection locomotive, Z si performances insuffisantes
|
|
||||||
SI AUCUNE des conditions ci-dessus :
|
|
||||||
→ ALORS c'est un VRAI PILIER structurant → GARDE en A
|
|
||||||
RATIONALE : Un produit qui fait 20€ de CA même avec 100% du poids n'est pas une locomotive, c'est un rayon à fermer. Une vraie locomotive pèse lourd dans un rayon ACTIF avec plusieurs produits.
|
|
||||||
|
|
||||||
5. RÈGLE MANAGER : Si le manager a défini une consigne ET que le produit est concerné → Appliquer la consigne À LA LETTRE.
|
RATIONALE : Cette logique s'adapte automatiquement à TOUS les fournisseurs:
|
||||||
Si la règle ordonne explicitement une Gamme B, C ou D, TU DOIS SORTIR "B", "C" ou "D". "rule_applies" doit être 'true'.
|
- Petit fournisseur (5 produits) : moyenne élevée, mais comparaison équitable
|
||||||
IMPORTANT : Si AUCUNE règle manager n'est fournie dans le contexte ci-dessous, IGNORER complètement cette section et passer directement à l'analyse contextuelle.
|
- Gros fournisseur (50 produits) : moyenne faible, mais comparaison équitable
|
||||||
|
- Même score relatif = même décision, indépendamment de la taille du fournisseur
|
||||||
|
|
||||||
6. ANALYSE CONTEXTUELLE (si aucune règle ci-dessus ne s'applique) :
|
📝 RÈGLE 3 — RÈGLE MANAGER (optionnel):
|
||||||
Les quadrants sont des INDICES pour guider la décision.
|
Si le manager a défini une consigne ET que le produit est concerné → Appliquer la consigne À LA LETTRE.
|
||||||
— Quadrant STAR ⭐ : Volume élevé + Marge élevée = Produit stratégique.
|
Si AUCUNE règle manager n'est fournie → IGNORER cette section.
|
||||||
→ A AUTOMATIQUE (déjà traité en priorité 2 - cette ligne est informative seulement).
|
|
||||||
— Quadrant TRAFIC 🚶 : Générateur de flux (volume élevé, marge faible).
|
|
||||||
Si percentileQty >= 60 → A.
|
|
||||||
Si percentileQty < 40 ET score < 35 → Z.
|
|
||||||
Si CA total réseau ≥ 250€ → A (génère du CA suffisant).
|
|
||||||
Si quantité totale réseau ≥ 100 unités → A (volume élevé = générateur de trafic).
|
|
||||||
Sinon → A (générateur de trafic utile pour l'assortiment).
|
|
||||||
— Quadrant MARGE 💎 : Contributeur rentabilité (volume faible, marge élevée).
|
|
||||||
Si percentileMarge >= 70 → A.
|
|
||||||
Si percentileMarge < 70 ET score < 35 → Z.
|
|
||||||
Si CA total réseau ≥ 300€ OU marge totale ≥ 150€ → A (contribution rentabilité suffisante).
|
|
||||||
Sinon → A (produit de marge utile pour la rentabilité globale).
|
|
||||||
— Quadrant WATCH ⚠️ : Sous-performant relatif (qty/marge sous médiane).
|
|
||||||
Si poids CA fournisseur ≥ 3% OU poids CA rayon ≥ 2% OU poids QTÉ rayon ≥ 2% → A.
|
|
||||||
Si CA total réseau ≥ 250€ → A (génère du CA suffisant, peu importe la quantité).
|
|
||||||
Si quantité totale réseau ≥ 100 unités ET CA ≥ 150€ → A (volume suffisant avec CA raisonnable).
|
|
||||||
Si percentileCa ≥ 50 OU percentileQty ≥ 50 → A (médiane ou mieux dans le lot).
|
|
||||||
Sinon → Z pour purger les vrais sous-performeurs.
|
|
||||||
|
|
||||||
--- COHÉRENCE INTER-PRODUITS ---
|
|
||||||
Ne mets jamais Z un produit si son percentile CA ET son percentile QTÉ sont tous les deux supérieurs à un autre produit classé A.
|
|
||||||
EXCEPTION : Cette règle de cohérence s'annule totalement si le produit est considéré comme un [⛔ STOCK MORT] (chiffres absolus dérisoires). Un stock mort doit toujours sortir en Z.
|
|
||||||
|
|
||||||
--- COMPLÉMENTARITÉ ET CHOIX MINIMAL (règle d'assortiment) ---
|
|
||||||
⛔ IMPORTANT: Cette règle NE S'APPLIQUE PAS si le produit est un STOCK MORT ABSOLU (CA < 100€ ET Qty < 30).
|
|
||||||
Un produit sous ces seuils est TOUJOURS Z, sans exception, même s'il est le seul de sa catégorie.
|
|
||||||
Un rayon à 30€/an doit être FERMÉ, pas maintenu pour la complémentarité.
|
|
||||||
|
|
||||||
La Foir'Fouille doit offrir un CHOIX COMPLET dans chaque nomenclature pour attirer et fidéliser la clientèle. Certaines catégories génèrent peu de CA mais sont INDISPENSABLES pour l'image "tout sous un même toit".
|
|
||||||
MAIS: Ceci s'applique UNIQUEMENT aux produits au-dessus des seuils de rentabilité minimale (CA ≥ 100€ OU Qty ≥ 30).
|
|
||||||
|
|
||||||
RÈGLE DE COMPLÉMENTARITÉ :
|
|
||||||
- Si le rayon compte MOINS de 8 produits actifs (non-Z) dans la nomenclature
|
|
||||||
ET que le produit analysé n'est PAS un stock mort (CA > 150€ OU QTÉ > 30)
|
|
||||||
→ GARDE en A pour maintenir un assortiment minimal et éviter une rupture d'offre.
|
|
||||||
|
|
||||||
- Si le rayon compte PLUS de 15 produits actifs → Tu peux être sévère et purger les sous-performants sans risquer une rupture de gamme.
|
|
||||||
|
|
||||||
GUIDE NOMENCLATURES (à adapter selon contexte) :
|
|
||||||
• Nomenclatures ESSENTIELLES (CA faible accepté si complémentarité) :
|
|
||||||
Arts de la table, Cuisine de base, Rangement, Papeterie, Hygiène, Petit électroménager
|
|
||||||
→ Objectif : Offrir un choix complet même si rotation faible. Accepte des produits avec percentile CA < 50 si nécessaires pour couvrir les sous-familles.
|
|
||||||
|
|
||||||
• Nomenclatures GÉNÉRATRICES DE TRAFIC (volume prioritaire) :
|
|
||||||
Décoration, Textile maison, Jardin, Jouets, Loisirs créatifs
|
|
||||||
→ Objectif : Maximiser le volume et la fréquentation. Privilégie forte rotation même avec marge faible.
|
|
||||||
|
|
||||||
• Nomenclatures OPPORTUNISTES (exigence maximale) :
|
|
||||||
Saisonnalité forte (Noël, Halloween, Pâques, Barbecue, Piscine)
|
|
||||||
→ Objectif : Rentabilité pure. Pas de sentimentalisme. Si le produit sous-performe → Z sans hésitation.
|
|
||||||
|
|
||||||
--- FORMAT OBLIGATOIRE ---
|
--- FORMAT OBLIGATOIRE ---
|
||||||
JSON uniquement, sans markdown.
|
JSON uniquement, sans markdown.
|
||||||
{
|
{
|
||||||
"rule_applies": boolean,
|
"rule_applies": boolean,
|
||||||
"recommendation": "A" | "B" | "C" | "D" | "Z",
|
"recommendation": "A" | "Z",
|
||||||
"justification": "2 phrases max. Cite les chiffres absolus si ridicules, ou les percentiles/quadrant."
|
"justification": "2-3 phrases max. Cite le score relatif et/ou les seuils absolus."
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +73,7 @@ JSON uniquement, sans markdown.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Message contextuel enrichi (v4)
|
// Message contextuel enrichi (v5 simplifié)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
private static buildContextualMessage(
|
private static buildContextualMessage(
|
||||||
@@ -146,73 +83,40 @@ JSON uniquement, sans markdown.
|
|||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const storeLabel = ctx.storeCount > 1 ? `${ctx.storeCount} magasins` : `1 magasin`;
|
const storeLabel = ctx.storeCount > 1 ? `${ctx.storeCount} magasins` : `1 magasin`;
|
||||||
|
|
||||||
|
// En-tête produit
|
||||||
lines.push(`PRODUIT : ${ctx.libelle1} (${ctx.codein})`);
|
lines.push(`PRODUIT : ${ctx.libelle1} (${ctx.codein})`);
|
||||||
lines.push(`CATÉGORIE : ${ctx.libelleNiveau2}`);
|
lines.push(`CATÉGORIE : ${ctx.libelleNiveau2}`);
|
||||||
lines.push(`CONTEXTE RAYON : ${ctx.rayonSize} produits dans cette nomenclature | Lot fournisseur total: ${ctx.lotSize} produits`);
|
lines.push(`DISTRIBUTION : ${storeLabel}`);
|
||||||
lines.push(`DISTRIBUTION : ${storeLabel} référençant ce produit`);
|
|
||||||
|
|
||||||
// Alerte complémentarité si petit rayon
|
|
||||||
if (ctx.rayonSize < 8) {
|
|
||||||
lines.push(`⚠️ PETIT RAYON : Seulement ${ctx.rayonSize} produits dans cette nomenclature → Risque de rupture d'offre si purge excessive`);
|
|
||||||
}
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// KPIs bruts réseau + valeurs normalisées par magasin
|
// KPIs bruts
|
||||||
lines.push(`--- PERFORMANCE (brute réseau / par magasin) ---`);
|
lines.push(`--- PERFORMANCE ABSOLUE ---`);
|
||||||
lines.push(`• CA réseau : ${ctx.totalCaRaw.toLocaleString('fr-FR')}€ | CA par magasin : ${ctx.caPerStore.toFixed(0)}€`);
|
lines.push(`• CA réseau : ${ctx.totalCaRaw.toLocaleString('fr-FR')}€`);
|
||||||
lines.push(`• Quantité réseau : ${ctx.totalQtyRaw} unités | QTÉ par magasin : ${ctx.qtyPerStore.toFixed(0)} unités`);
|
lines.push(`• Quantité réseau : ${ctx.totalQtyRaw} unités`);
|
||||||
lines.push(`• Marge : ${ctx.tauxMarge.toFixed(1)}%`);
|
lines.push(`• Marge : ${ctx.tauxMarge.toFixed(1)}%`);
|
||||||
lines.push(`⚠️ Les percentiles ci-dessous sont calculés sur les valeurs PAR MAGASIN pour comparer équitablement les produits 1-magasin et 2-magasins.`);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Quadrant — présenté comme un indice, sans prescription
|
// Contexte fournisseur
|
||||||
lines.push(`--- PROFIL QUADRANT (indice, pas un verdict) ---`);
|
// Calcul du CA total fournisseur à partir du poids
|
||||||
lines.push(`${ctx.quadrantEmoji} ${ctx.quadrantLabel}`);
|
const totalFournisseurCa = ctx.weightCaFournisseur > 0
|
||||||
lines.push(`Santé : ${ctx.regularityScore}/12 mois actifs | ${ctx.inactivityMonths} mois sans vente | Marge : ${ctx.tauxMarge.toFixed(1)}%`);
|
? ctx.totalCaRaw / (ctx.weightCaFournisseur / 100)
|
||||||
|
: 0;
|
||||||
|
const caMoyenAttendu = ctx.lotSize > 0 ? totalFournisseurCa / ctx.lotSize : 0;
|
||||||
|
|
||||||
|
lines.push(`--- CONTEXTE FOURNISSEUR ---`);
|
||||||
|
lines.push(`• Lot fournisseur : ${ctx.lotSize} produits`);
|
||||||
|
lines.push(`• CA moyen attendu : ${caMoyenAttendu.toFixed(0)}€ par produit`);
|
||||||
|
lines.push(`• Poids CA de ce produit : ${ctx.weightCaFournisseur.toFixed(1)}% du CA total fournisseur`);
|
||||||
|
lines.push(`• Percentile CA : ${ctx.percentileCa}e percentile (position dans le lot)`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Position / poids — données brutes
|
// Score relatif calculé
|
||||||
lines.push(`--- POSITION DANS LE LOT FOURNISSEUR (percentiles sur valeurs par magasin) ---`);
|
const scoreRelatif = ctx.weightCaFournisseur * ctx.lotSize; // weight% × nb produits = score relatif
|
||||||
lines.push(`• CA/magasin : ${ctx.percentileCa}e percentile | Poids CA réseau fournisseur : ${ctx.weightCaFournisseur}% | Poids CA réseau rayon N2 : ${ctx.weightCaRayon}%`);
|
lines.push(`--- SCORE RELATIF ---`);
|
||||||
lines.push(`• QTÉ/magasin : ${ctx.percentileQty}e percentile | Poids QTÉ réseau fournisseur : ${ctx.weightQtyFournisseur}% | Poids QTÉ réseau rayon N2 : ${ctx.weightQtyRayon}%`);
|
lines.push(`• Score Relatif : ${scoreRelatif.toFixed(0)} (100 = fait exactement sa part)`);
|
||||||
lines.push(`• Marge : ${ctx.percentileMarge}e percentile`);
|
lines.push(` → ${scoreRelatif >= 100 ? "✓ Au-dessus de la moyenne" : scoreRelatif >= 50 ? "⚠️ Entre 50% et 100% de la moyenne" : "✗ Très en dessous de la moyenne"}`);
|
||||||
lines.push(`• Score composite : ${ctx.percentileComposite}/100`);
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Signaux — tous présentés factuellement, sans recommandation inline
|
|
||||||
lines.push(`--- SIGNAUX ---`);
|
|
||||||
|
|
||||||
// Signal rouge prioritaire
|
|
||||||
if (ctx.isDeadStock) {
|
|
||||||
lines.push(`[⛔ STOCK MORT] Statistiques absolues dérisoires (CA < 100€ ou QTÉ < 20 unités sur le réseau) → non rentable pour notre modèle volume`);
|
|
||||||
}
|
|
||||||
if (ctx.isLowContribution) {
|
|
||||||
lines.push(`[⛔ CONTRIBUTION FAIBLE] Percentiles CA (${ctx.percentileCa}) et QTÉ (${ctx.percentileQty}) dans les 30% inférieurs → produit marginal`);
|
|
||||||
}
|
|
||||||
if (ctx.scoreCritique) {
|
|
||||||
lines.push(`[⛔ SCORE CRITIQUE] Score brut < 20 → sous-seuil absolu`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signaux positifs
|
|
||||||
if (ctx.isLocomotiveRayon) {
|
|
||||||
lines.push(`[🚂 LOCOMOTIVE RAYON] Poids CA rayon: ${ctx.weightCaRayon}% | Poids QTÉ rayon: ${ctx.weightQtyRayon}% → Pilier structurant de la nomenclature`);
|
|
||||||
}
|
|
||||||
lines.push(`${ctx.isTop20Ca ? "[✓]" : "[ ]"} Top 20% CA/magasin fournisseur`);
|
|
||||||
lines.push(`${ctx.isTop20Qty ? "[✓]" : "[ ]"} Top 20% Quantités/magasin fournisseur`);
|
|
||||||
lines.push(`${ctx.isHighVolumeWithLowMargin ? "[✓]" : "[ ]"} Fort volume/magasin (>P60 lot) avec marge faible`);
|
|
||||||
lines.push(`${ctx.isMargePure ? "[✓]" : "[ ]"} Forte marge (>P70 lot) malgré volume/magasin faible`);
|
|
||||||
lines.push(`${ctx.isAboveMedianComposite ? "[✓]" : "[ ]"} Au-dessus de la médiane composite`);
|
|
||||||
if (!ctx.isHighVolumeWithLowMargin && !ctx.isMargePure) {
|
|
||||||
lines.push(`[i] Signaux Trafic/Marge non activés (rayon de ${ctx.rayonSize} produits${ctx.rayonSize < 6 ? " — trop petit pour stats fiables" : ""})`);
|
|
||||||
}
|
|
||||||
lines.push("");
|
|
||||||
|
|
||||||
// Protection
|
|
||||||
if (ctx.isProtected) {
|
|
||||||
lines.push(`🛡️ GARDE-FOU : ${ctx.protectionReason} → A obligatoire`);
|
|
||||||
lines.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Règle manager
|
// Règle manager
|
||||||
if (p.supplierContext) {
|
if (p.supplierContext) {
|
||||||
lines.push(`--- RÈGLE MANAGER ---`);
|
lines.push(`--- RÈGLE MANAGER ---`);
|
||||||
@@ -235,31 +139,21 @@ JSON uniquement, sans markdown.
|
|||||||
let contextStats = "";
|
let contextStats = "";
|
||||||
if (p.shareCa !== undefined && p.shareQty !== undefined) {
|
if (p.shareCa !== undefined && p.shareQty !== undefined) {
|
||||||
contextStats += `\nPOIDS DU PRODUIT :
|
contextStats += `\nPOIDS DU PRODUIT :
|
||||||
- Poids Fournisseur (CA) : ${p.shareCa.toFixed(1)}%
|
- Poids CA Fournisseur : ${p.shareCa.toFixed(1)}%`;
|
||||||
- Poids Secteur/Rayon (Quantité) : ${p.shareQty.toFixed(1)}%`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scoringInfo = p.scoring
|
|
||||||
? `\n--- RÉSULTATS DÉCISION RAYON ---
|
|
||||||
INDICE RAYON: ${p.scoring.compositeScore}/100 (Seuil Z : ${p.scoring.threshold})
|
|
||||||
PROFIL: ${p.scoring.labelProfil}
|
|
||||||
GARDES-FOUS : Top30: ${p.scoring.isTop30Supplier ? "Oui" : "Non"} | Récent: ${p.scoring.isRecent ? "Oui" : "Non"} | Dernier: ${p.scoring.isLastProduct ? "Oui" : "Non"}
|
|
||||||
`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const contextRules = p.supplierContext
|
const contextRules = p.supplierContext
|
||||||
? `\n--- RÈGLE MANAGER ---\n"${p.supplierContext}"\n→ Évalue si le produit ("${p.libelle1}") est concerné. rule_applies = true/false.\n`
|
? `\n--- RÈGLE MANAGER ---\n"${p.supplierContext}"\n→ Évalue si le produit ("${p.libelle1}") est concerné. rule_applies = true/false.\n`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `PRODUIT : ${p.libelle1} (${p.codein})
|
return `PRODUIT : ${p.libelle1} (${p.codein})
|
||||||
Famille / Rayon : ${p.libelleNiveau2}
|
Famille / Rayon : ${p.libelleNiveau2}
|
||||||
Score Algorithmique : ${p.score.toFixed(1)}/100
|
KPIs : CA: ${p.totalCa.toFixed(2)}€ | Qté: ${p.totalQuantite} | Marge: ${p.tauxMarge.toFixed(1)}% | PMV: ${pmv.toFixed(2)}€${contextStats}${contextRules}
|
||||||
KPIs : CA: ${p.totalCa.toFixed(2)}€ | Qté: ${p.totalQuantite} | Marge: ${p.tauxMarge.toFixed(1)}% | PMV: ${pmv.toFixed(2)}€${contextStats}${scoringInfo}${contextRules}
|
|
||||||
Génère UNIQUEMENT le JSON :`;
|
Génère UNIQUEMENT le JSON :`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Utilitaires de parsing (inchangés)
|
// Utilitaires de parsing
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
static extractRecommendation(content: string): "A" | "B" | "C" | "D" | "Z" | null {
|
static extractRecommendation(content: string): "A" | "B" | "C" | "D" | "Z" | null {
|
||||||
|
|||||||
Reference in new issue
Block a user