// --------------------------------------------------------------------------- // Fabri-Ka — Agrégateur de produits fabriqués au Québec // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/synthese.ts : synthèse DÉTERMINISTE de la fiche produit (aucun LLM, // aucune donnée inventée) : rabais, origine (classe A–E), disponibilité, // note moyenne, déclinaisons, devise ; constats « En bref » ; résumé. // --------------------------------------------------------------------------- import { CATEGORY_LABELS, ORIGIN_LABELS, Product, ProductDetail, ProductVariant, formatPrice } from '../api' import { NBSP, Tone } from './ui' export interface Rabais { pct: number; avant: number; apres: number } /** Rabais réel (prix barré plausible : ≤ 10× le prix, sinon saisie en cents). */ export function rabais(p: Product): Rabais | null { if (p.compare_at_price == null || p.price == null || p.compare_at_price <= p.price || p.compare_at_price > p.price * 10) return null const pct = Math.round((1 - p.price / p.compare_at_price) * 100) return pct >= 3 ? { pct, avant: p.compare_at_price, apres: p.price } : null } export function variantsOf(p: Product): ProductVariant[] { return (p.details?.variants ?? []).filter((v) => v.title || v.sku) } export interface Constat { tone: Tone; titre: string; detail: string; cle: string } export function enBref(p: ProductDetail): Constat[] { const out: Constat[] = [] const det = p.details ?? {} // 1. origine const o = p.origin_class if (o === 'A') out.push({ cle: 'origine', tone: 'good', titre: 'Fabriqué au Québec', detail: `Boutique ${p.store_name}${p.store_region ? ` · ${p.store_region}` : ''} — classe d'origine A (vérifiée par Fabri-Ka)` }) else if (o === 'B') out.push({ cle: 'origine', tone: 'good', titre: 'Conçu au Québec', detail: 'Classe d\'origine B — conception québécoise, fabrication partielle ou externe' }) else if (o === 'C') out.push({ cle: 'origine', tone: 'neutral', titre: 'Détaillant québécois', detail: 'Classe d\'origine C — boutique d\'ici, produit non nécessairement fabriqué au Québec' }) else if (o === 'D') out.push({ cle: 'origine', tone: 'neutral', titre: 'Origine mixte', detail: 'Classe D — assortiment québécois et importé' }) else if (o === 'E') out.push({ cle: 'origine', tone: 'warn', titre: 'Origine à vérifier', detail: 'Classe E — Fabri-Ka n\'a pas encore confirmé la provenance' }) // 2. prix / rabais const r = rabais(p) if (r) out.push({ cle: 'rabais', tone: 'good', titre: `En solde — ${r.pct}${NBSP}% de rabais`, detail: `${formatPrice(r.avant, p.currency)} → ${formatPrice(r.apres, p.currency)} chez ${p.store_name}` }) else if (p.price == null) out.push({ cle: 'rabais', tone: 'neutral', titre: p.price_on_request ? 'Prix sur devis' : 'Prix affiché en boutique', detail: 'La boutique ne publie pas de prix en ligne pour ce produit' }) else if (p.price_max != null && p.price_max > p.price) out.push({ cle: 'rabais', tone: 'info', titre: `De ${formatPrice(p.price, p.currency)} à ${formatPrice(p.price_max, p.currency)}`, detail: 'Le prix dépend de la déclinaison choisie' }) // 3. disponibilité if (!p.available) out.push({ cle: 'dispo', tone: 'bad', titre: 'Non disponible actuellement', detail: 'Selon la boutique lors de la dernière synchronisation' }) else if (det.low_stock_remaining) out.push({ cle: 'dispo', tone: 'warn', titre: `Plus que ${det.low_stock_remaining} en stock`, detail: 'Inventaire publié par la boutique' }) else if (det.is_on_backorder) out.push({ cle: 'dispo', tone: 'warn', titre: 'En précommande', detail: 'Délai de fabrication ou de réapprovisionnement à confirmer avec la boutique' }) else if (det.inventory_quantity != null && det.inventory_quantity > 0) out.push({ cle: 'dispo', tone: 'good', titre: 'En stock', detail: `${det.inventory_quantity} unité${det.inventory_quantity > 1 ? 's' : ''} publiées par la boutique` }) // 4. avis if (det.average_rating && det.average_rating > 0) out.push({ cle: 'avis', tone: det.average_rating >= 4 ? 'good' : 'neutral', titre: `Noté ${det.average_rating.toFixed(1).replace('.', ',')} / 5`, detail: det.review_count ? `${det.review_count} avis publiés sur la boutique` : 'Note publiée par la boutique' }) // 5. déclinaisons const vars = variantsOf(p) if (vars.length > 1) { const prix = vars.map((v) => v.price).filter((x): x is number => x != null) out.push({ cle: 'var', tone: 'info', titre: `${vars.length} déclinaisons`, detail: prix.length > 1 ? `de ${formatPrice(Math.min(...prix), p.currency)} à ${formatPrice(Math.max(...prix), p.currency)}` : (det.options ?? []).map((o) => o.name || o.title).filter(Boolean).join(' · ') || 'Formats, tailles ou couleurs au choix' }) } // 6. devise if (p.currency && p.currency !== 'CAD') out.push({ cle: 'devise', tone: 'warn', titre: `Prix affiché en ${p.currency}`, detail: 'Boutique facturant dans une autre devise — conversion et frais possibles' }) return out.slice(0, 6) } /** Ligne résumé : « Marque · Catégorie · Type » */ export function ligneResume(p: Product): string[] { const out: string[] = [] if (p.vendor && p.vendor !== p.store_name) out.push(p.vendor) if (p.category) out.push(CATEGORY_LABELS[p.category] ?? p.category) if (p.product_type) out.push(p.product_type) return out } export const origineLabel = (p: Product) => ORIGIN_LABELS[p.origin_class] ?? ''