// ----------------------------------------------------------------------------- // Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/synthese.ts : synthèse DÉTERMINISTE de la fiche véhicule (aucun LLM, // aucune donnée inventée) : // · comparaisonPrix : position du prix face à la médiane des comparables // (analyse de marché calculée côté API : autoka/web.py _market_analysis) ; // · enBref : 4 à 6 constats priorisés (marché, kilométrage vs âge, baisse // de prix, même véhicule ailleurs, rappels, fraîcheur, Carfax) ; // · ligneResume : « 2021 · 45 000 km · Automatique · Essence · VUS ». // ----------------------------------------------------------------------------- import { Recall, VehicleDetail, fmtKm, fmtPrice } from "../api"; import { Tone, NBSP } from "./ui"; export interface ComparaisonPrix { tone: "good" | "ok" | "high"; pct: number; // écart signé en % vs médiane des comparables label: string; court: string; ref: number; // médiane ($) n: number; percentile: number; // % de comparables moins chers } export function comparaisonPrix(v: VehicleDetail): ComparaisonPrix | null { const m = v.market; if (!m || v.price == null || m.n < 3) return null; const pct = Math.round(m.delta_pct); const tone = m.badge === "excellent" || m.badge === "good" ? "good" : m.badge === "fair" ? "ok" : "high"; return { tone, pct, ref: m.median, n: m.n, percentile: m.percentile, label: m.label || (tone === "good" ? "Bon prix" : tone === "ok" ? "Dans le marché" : "Au-dessus du marché"), court: `${pct < 0 ? "↓" : pct > 0 ? "↑" : "≈"} ${Math.abs(pct)}${NBSP}% vs médiane`, }; } export interface Constat { tone: Tone; titre: string; detail: string; cle: string; } export function enBref(v: VehicleDetail, recalls: Recall[] | null): Constat[] { const out: Constat[] = []; const cmp = comparaisonPrix(v); // 1. marché if (cmp) { out.push({ cle: "prix", tone: cmp.tone === "good" ? "good" : cmp.tone === "high" ? "warn" : "neutral", titre: cmp.tone === "good" ? `${cmp.label} — ${Math.abs(cmp.pct)}${NBSP}% sous la médiane` : cmp.tone === "high" ? `Prix au-dessus du marché (+${Math.abs(cmp.pct)}${NBSP}%)` : "Prix dans le marché", detail: `Médiane ${fmtPrice(cmp.ref)} sur ${cmp.n} ${v.make} ${v.model.split(" ")[0]} comparables · ${cmp.percentile}${NBSP}% sont moins chers` }); } else if (v.price != null) { out.push({ cle: "prix", tone: "neutral", titre: "Prix non comparé", detail: "Pas assez de véhicules comparables en vente au Québec pour situer ce prix." }); } // 2. kilométrage vs âge (composante du KA Score) const km = v.ka_score?.parts.find((p) => p.key === "km"); if (km && v.mileage_km != null && v.year) { const age = Math.max(1, new Date().getFullYear() - v.year); const parAn = Math.round(v.mileage_km / age / 1000) * 1000; out.push({ cle: "km", tone: km.score >= 70 ? "good" : km.score >= 45 ? "neutral" : "warn", titre: km.score >= 70 ? "Kilométrage bas pour l'âge" : km.score >= 45 ? "Kilométrage dans la norme" : "Kilométrage élevé pour l'âge", detail: `${fmtKm(v.mileage_km)} en ${age} an${age > 1 ? "s" : ""} ≈ ${parAn.toLocaleString("fr-CA")} km/an (référence ≈ 20 000 km/an)` }); } // 3. baisse de prix const hist = (v.price_history ?? []).filter((h) => h.price != null); if (hist.length >= 2 && hist[0].price! < hist[1].price!) out.push({ cle: "baisse", tone: "good", titre: "Prix en baisse", detail: `${fmtPrice(hist[1].price!)} → ${fmtPrice(hist[0].price!)} (−${(hist[1].price! - hist[0].price!).toLocaleString("fr-CA")} $) — observé par Auto-Ka` }); // 4. même véhicule ailleurs const dups = v.dup_sources ?? []; if (dups.length > 0) { const moins = dups.filter((d) => d.price != null && v.price != null && d.price < v.price); out.push({ cle: "dups", tone: moins.length ? "warn" : "neutral", titre: moins.length ? `Le même véhicule est affiché moins cher ailleurs` : `Le même véhicule est affiché sur ${dups.length} autre${dups.length > 1 ? "s" : ""} site${dups.length > 1 ? "s" : ""}`, detail: moins.length ? `${fmtPrice(Math.min(...moins.map((d) => d.price!)))} chez ${moins[0].dealer_name || moins[0].source}` : "Même NIV repéré (dédoublonnage Auto-Ka) — voir le dossier" }); } // 5. rappels if (recalls && recalls.length > 0) out.push({ cle: "rappels", tone: "warn", titre: `${recalls.length} rappel${recalls.length > 1 ? "s" : ""} Transports Canada pour ce modèle`, detail: `Le plus récent : ${recalls[0].date} · ${recalls[0].component} — vérifier auprès du concessionnaire s'ils ont été effectués` }); else if (recalls && recalls.length === 0) out.push({ cle: "rappels", tone: "good", titre: "Aucun rappel Transports Canada répertorié", detail: `${v.make} ${v.model}${v.year ? ` ${v.year}` : ""} — base des rappels de sécurité` }); // 6. fraîcheur / temps en ligne const jours = v.first_seen ? Math.max(0, Math.round((Date.now() / 1000 - v.first_seen) / 86400)) : null; if (jours != null && jours >= 45) out.push({ cle: "age", tone: "neutral", titre: `En vente depuis ${jours}${NBSP}jours`, detail: "Observé par Auto-Ka depuis la première synchronisation — marge de négociation possible" }); else if (jours != null && jours <= 3) out.push({ cle: "age", tone: "info", titre: jours === 0 ? "Arrivé aujourd'hui" : `Nouvel arrivage (${jours}${NBSP}j)`, detail: "Repéré récemment par Auto-Ka chez le concessionnaire" }); // 7. Carfax if (v.carfax_url) out.push({ cle: "carfax", tone: "info", titre: "Rapport d'historique Carfax fourni", detail: "Lien du concessionnaire — accidents, réclamations, entretien" }); return out.slice(0, 6); } export function ligneResume(v: VehicleDetail): string[] { const p: string[] = []; if (v.year) p.push(String(v.year)); if (v.mileage_km != null) p.push(fmtKm(v.mileage_km)); if (v.transmission) p.push(v.transmission); if (v.fuel) p.push(v.fuel); if (v.body_type) p.push(v.body_type); return p; } /** Jours depuis la première observation. */ export const joursEnLigne = (v: VehicleDetail) => v.first_seen ? Math.max(0, Math.round((Date.now() / 1000 - v.first_seen) / 86400)) : null;