SPB Git forge

spb/fabri-ka

Public

Agrégateur de produits québécois — www.fabri-ka.com

217commits 1branches 0releases
66.1 MBsize
maindefault branch
7 h agolast push
Python 38.4% HTML 30.3% TypeScript 17.2% CSS 11% JavaScript 3.2%
5.3 KB · 77 lines typescript
Raw Blame History
1// ---------------------------------------------------------------------------2// Fabri-Ka — Agrégateur de produits fabriqués au Québec3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/synthese.ts : synthèse DÉTERMINISTE de la fiche produit (aucun LLM,5//   aucune donnée inventée) : rabais, origine (classe A–E), disponibilité,6//   note moyenne, déclinaisons, devise ; constats « En bref » ; résumé.7// ---------------------------------------------------------------------------8import { CATEGORY_LABELS, ORIGIN_LABELS, Product, ProductDetail, ProductVariant, formatPrice } from '../api'9import { NBSP, Tone } from './ui'1011export interface Rabais { pct: number; avant: number; apres: number }1213/** Rabais réel (prix barré plausible : ≤ 10× le prix, sinon saisie en cents). */14export function rabais(p: Product): Rabais | null {15  if (p.compare_at_price == null || p.price == null || p.compare_at_price <= p.price || p.compare_at_price > p.price * 10) return null16  const pct = Math.round((1 - p.price / p.compare_at_price) * 100)17  return pct >= 3 ? { pct, avant: p.compare_at_price, apres: p.price } : null18}1920export function variantsOf(p: Product): ProductVariant[] {21  return (p.details?.variants ?? []).filter((v) => v.title || v.sku)22}2324export interface Constat { tone: Tone; titre: string; detail: string; cle: string }2526export function enBref(p: ProductDetail): Constat[] {27  const out: Constat[] = []28  const det = p.details ?? {}2930  // 1. origine31  const o = p.origin_class32  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)` })33  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' })34  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' })35  else if (o === 'D') out.push({ cle: 'origine', tone: 'neutral', titre: 'Origine mixte', detail: 'Classe D — assortiment québécois et importé' })36  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' })3738  // 2. prix / rabais39  const r = rabais(p)40  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}` })41  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' })42  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' })4344  // 3. disponibilité45  if (!p.available) out.push({ cle: 'dispo', tone: 'bad', titre: 'Non disponible actuellement', detail: 'Selon la boutique lors de la dernière synchronisation' })46  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' })47  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' })48  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` })4950  // 4. avis51  if (det.average_rating && det.average_rating > 0)52    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' })5354  // 5. déclinaisons55  const vars = variantsOf(p)56  if (vars.length > 1) {57    const prix = vars.map((v) => v.price).filter((x): x is number => x != null)58    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' })59  }6061  // 6. devise62  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' })6364  return out.slice(0, 6)65}6667/** Ligne résumé : « Marque · Catégorie · Type » */68export function ligneResume(p: Product): string[] {69  const out: string[] = []70  if (p.vendor && p.vendor !== p.store_name) out.push(p.vendor)71  if (p.category) out.push(CATEGORY_LABELS[p.category] ?? p.category)72  if (p.product_type) out.push(p.product_type)73  return out74}7576export const origineLabel = (p: Product) => ORIGIN_LABELS[p.origin_class] ?? ''77