From 9729b00dc7eca60885ff5f0ae9efbf95197b71cd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 20:32:45 +0000 Subject: [PATCH] =?UTF-8?q?Diagnostic=20:=20d'o=C3=B9=20vient=20(ou=20ne?= =?UTF-8?q?=20vient=20pas)=20le=20prix=20de=20vente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La colonne « PV central » s'affiche vide sur toutes les lignes. Le chemin du code est pourtant intact : `getProductRows` renseigne `prixVente` depuis `article_infosup.prix_vente_mini` (phase 3), aucune phase suivante ne l'efface, et la route de streaming diffuse la ligne entière sans filtrer ses champs. La valeur est donc absente à la source. Deux causes possibles, que seule la base peut départager : la colonne existe mais n'est pas renseignée, ou le prix de vente vit ailleurs sous un autre nom. `article_infosup.prix_vente_mini` est la seule source de prix de vente câblée dans l'application — la Grille et la fiche produit la partagent, si bien qu'une fiche produit affichant « — » sur PV central confirmerait la première hypothèse. Cette route de diagnostic répond aux deux questions : remplissage réel de la colonne actuelle (absente / à zéro / renseignée), globalement puis pour un fournisseur donné, et énumération des colonnes candidates du schéma AVEC leur remplissage — une colonne bien nommée mais vide ne servirait à rien. À supprimer une fois la source établie. Elle est derrière l'authentification et ne renvoie que des comptages et quelques exemples. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EYGgoQCG1VCe42HzbzhAXR --- src/app/api/diag/prix-vente/route.ts | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/app/api/diag/prix-vente/route.ts diff --git a/src/app/api/diag/prix-vente/route.ts b/src/app/api/diag/prix-vente/route.ts new file mode 100644 index 0000000..eeef0b7 --- /dev/null +++ b/src/app/api/diag/prix-vente/route.ts @@ -0,0 +1,118 @@ +import { NextRequest } from "next/server"; +import { db } from "@/db"; +import { sql } from "drizzle-orm"; + +export const dynamic = "force-dynamic"; + +/** + * CollectFlow — Diagnostic « d'où vient le prix de vente ? » + * + * La colonne « PV central » de la Grille lit `article_infosup.prix_vente_mini`, + * seule source de prix de vente câblée dans l'application (Grille et fiche + * produit la partagent). Quand elle s'affiche vide de bout en bout, deux causes + * possibles, que seule la base peut départager : + * + * 1. la colonne existe mais n'est pas renseignée (ou vaut 0) ; + * 2. le prix de vente vit ailleurs, sous un autre nom. + * + * Cette route répond aux deux : elle compte le remplissage de la colonne + * actuelle, et énumère les colonnes candidates du schéma avec leur remplissage + * réel — de quoi rebrancher la Grille sur la bonne source sans deviner. + * + * Route de diagnostic, à supprimer une fois la source établie. Elle est derrière + * l'authentification (cf. `middleware.ts`) et ne renvoie que des comptages et + * quelques exemples. + */ + +/** Identifiant SQL sûr : les noms viennent d'`information_schema`, on le vérifie quand même. */ +const identifiantValide = (nom: string) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(nom); + +export async function GET(req: NextRequest) { + const codefou = req.nextUrl.searchParams.get("fournisseur")?.trim() || null; + const diagnostic: Record = { + lecture: "La Grille (« PV central ») et la fiche produit lisent article_infosup.prix_vente_mini", + fournisseur: codefou ?? "(non précisé — passez ?fournisseur=CODE pour le détail par fournisseur)", + }; + + // ── 1. Remplissage de la colonne actuellement câblée ──────────────────── + try { + const r = await db.execute(sql` + SELECT + COUNT(*)::int AS lignes, + COUNT(*) FILTER (WHERE prix_vente_mini IS NULL)::int AS sans_valeur, + COUNT(*) FILTER (WHERE prix_vente_mini = 0)::int AS a_zero, + COUNT(*) FILTER (WHERE prix_vente_mini > 0)::int AS renseigne, + MAX(prix_vente_mini)::float AS maximum + FROM article_infosup + `); + diagnostic.prix_vente_mini_global = r.rows[0]; + } catch (e) { + diagnostic.prix_vente_mini_global = { erreur: (e as Error).message?.slice(0, 200) }; + } + + // ── 2. Le même comptage sur les articles d'un fournisseur ─────────────── + if (codefou) { + try { + const r = await db.execute(sql` + SELECT + COUNT(*)::int AS articles, + COUNT(ai.artnoid)::int AS avec_fiche_infosup, + COUNT(*) FILTER (WHERE ai.prix_vente_mini > 0)::int AS avec_prix, + COUNT(*) FILTER (WHERE ai.prix_vente_mini = 0)::int AS prix_a_zero, + COUNT(*) FILTER (WHERE ai.prix_vente_mini IS NULL)::int AS prix_absent + FROM artfou1 af + JOIN articles a ON a.no_id = af.art_no_id + LEFT JOIN article_infosup ai ON ai.artnoid = a.no_id + WHERE af.code = ${codefou} AND a.codein IS NOT NULL + `); + diagnostic.pour_ce_fournisseur = r.rows[0]; + + const ex = await db.execute(sql` + SELECT a.codein, a.libelle1, ai.prix_vente_mini::float AS prix_vente_mini + FROM artfou1 af + JOIN articles a ON a.no_id = af.art_no_id + LEFT JOIN article_infosup ai ON ai.artnoid = a.no_id + WHERE af.code = ${codefou} AND a.codein IS NOT NULL + LIMIT 8 + `); + diagnostic.exemples = ex.rows; + } catch (e) { + diagnostic.pour_ce_fournisseur = { erreur: (e as Error).message?.slice(0, 200) }; + } + } + + // ── 3. Colonnes candidates du schéma, et leur remplissage réel ────────── + // C'est la réponse à « le prix est peut-être ailleurs » : une colonne bien + // nommée mais vide ne sert à rien, seul le comptage tranche. + try { + const candidates = await db.execute(sql` + SELECT table_name, column_name, data_type + FROM information_schema.columns + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + AND data_type IN ('numeric', 'double precision', 'real', 'integer', 'bigint') + AND (column_name ILIKE '%prix%' OR column_name ILIKE 'pv%' OR column_name ILIKE '%vente%' OR column_name ILIKE '%tarif%') + ORDER BY table_name, column_name + LIMIT 60 + `); + const lignes = candidates.rows as { table_name: string; column_name: string; data_type: string }[]; + diagnostic.colonnes_candidates = lignes.map((l) => `${l.table_name}.${l.column_name} (${l.data_type})`); + + // Remplissage des colonnes candidates portées par article_infosup : + // c'est la table déjà jointe, donc la moins coûteuse à rebrancher. + const surInfosup = lignes.filter((l) => l.table_name === "article_infosup" && identifiantValide(l.column_name)); + if (surInfosup.length > 0) { + const morceaux = surInfosup.map((l) => + sql`COUNT(*) FILTER (WHERE ${sql.raw(`"${l.column_name}"`)} > 0)::int AS ${sql.raw(`"${l.column_name}"`)}` + ); + const r = await db.execute(sql`SELECT ${sql.join(morceaux, sql`, `)} FROM article_infosup`); + diagnostic.remplissage_article_infosup = { + commentaire: "nombre de lignes où la colonne est > 0", + ...(r.rows[0] as Record), + }; + } + } catch (e) { + diagnostic.colonnes_candidates = { erreur: (e as Error).message?.slice(0, 200) }; + } + + return Response.json(diagnostic, { headers: { "Cache-Control": "no-store" } }); +}