mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
L'instantané grid_rows fige la ligne au moment du calcul, mais deux données évoluent indépendamment et doivent refléter l'état courant : - Métriques réseau Qlik : qlik_network_metrics est alimentée par les syncs et les recherches réseau, hors du calcul de grille. Relues à chaque appel et exposées dans `network` (null quand le produit n'en a pas). Les colonnes caReseau/qteReseau/... de la ligne sont réalignées dessus. - Gamme serveur : codeGamme peut être surchargée par un snapshot de session. L'API expose `codeGammeServeur`, la gamme NON modifiée telle qu'elle est en base PostgreSQL, relue à chaque appel. codeGammeInit est gardé aligné. Ajout de pgGetGammesByCodeins() : variante sans jointure artfou1, nécessaire car la recherche de l'API est transversale (pas de fournisseur connu). Ce n'est pas un recalcul : deux lectures indexées bornées à la page courante (500 lignes max). Mesuré à ~20 ms, soit le même coût que sans enrichissement. Paramètre enrich=0 pour servir l'instantané brut. Paramètres → nouvelle section « API CollectFlow — Connexion » : URL de base déduite de l'origine, en-têtes d'authentification, liste des endpoints et des paramètres, exemples curl copiables, lien vers openapi.json. Vérifié sur PostgreSQL local avec un instantané volontairement périmé : gamme snapshot A → serveur C, caReseau 111 → 990000, produit sans données réseau → network null, produit sans gamme → codeGammeServeur null, enrich=0 redonnant bien les valeurs figées, et toujours zéro recalcul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
211 lines
11 KiB
TypeScript
211 lines
11 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { requireApiAuth } from "@/lib/api-auth";
|
|
import { GRID_SORT_KEYS } from "@/lib/grid-store";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* GET /api/v1/openapi.json
|
|
*
|
|
* Spécification lisible par machine de l'API CollectFlow. Permet aux outils externes
|
|
* — et à l'assistant IA — de découvrir les endpoints sans documentation manuscrite.
|
|
* Authentifiée comme le reste de l'API : la spec décrit une surface interne.
|
|
*/
|
|
export async function GET(req: NextRequest) {
|
|
const authCtx = await requireApiAuth(req);
|
|
if (authCtx instanceof Response) return authCtx;
|
|
|
|
const paginationParams = [
|
|
{ name: "page", in: "query", schema: { type: "integer", minimum: 1, default: 1 } },
|
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
|
|
];
|
|
const sortParams = [
|
|
{ name: "sort", in: "query", schema: { type: "string", enum: GRID_SORT_KEYS }, description: "Colonne de tri." },
|
|
{ name: "order", in: "query", schema: { type: "string", enum: ["asc", "desc"], default: "desc" } },
|
|
{
|
|
name: "fields",
|
|
in: "query",
|
|
schema: { type: "string" },
|
|
description: "Champs de ProductRow à conserver, séparés par des virgules (allège fortement la réponse). `codein` est toujours inclus.",
|
|
},
|
|
{
|
|
name: "enrich",
|
|
in: "query",
|
|
schema: { type: "string", enum: ["0", "1"], default: "1" },
|
|
description:
|
|
"1 (défaut) : relit les métriques réseau Qlik (`network`) et la gamme serveur (`codeGammeServeur`) au moment de l'appel. "
|
|
+ "0 : sert l'instantané brut, légèrement plus rapide.",
|
|
},
|
|
];
|
|
|
|
const spec = {
|
|
openapi: "3.1.0",
|
|
info: {
|
|
title: "CollectFlow API",
|
|
version: "1.0.0",
|
|
description:
|
|
"Lecture de la grille CollectFlow (ventes 12 mois, stock, marges, gammes, métriques réseau Qlik) "
|
|
+ "et recherche de produits.\n\n"
|
|
+ "**Aucun endpoint ne déclenche de recalcul ni d'appel à Qlik.** Toutes les réponses proviennent "
|
|
+ "de données déjà persistées : l'instantané de grille (`grid_rows`), rempli quand la Grille est "
|
|
+ "ouverte dans l'application, et le cache des métriques réseau (`qlik_network_metrics`). "
|
|
+ "Un fournisseur jamais ouvert renvoie donc `202 not_ready` au lieu d'imposer une attente.\n\n"
|
|
+ "Deux informations sont relues à chaque appel car elles évoluent indépendamment du calcul de "
|
|
+ "la grille : les **métriques réseau Qlik** (`network`, `null` quand il n'y en a pas) et la "
|
|
+ "**gamme serveur non modifiée** (`codeGammeServeur`). Passer `enrich=0` pour s'en dispenser.",
|
|
},
|
|
servers: [{ url: "/api/v1" }],
|
|
security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }],
|
|
components: {
|
|
securitySchemes: {
|
|
ApiKeyAuth: { type: "apiKey", in: "header", name: "X-API-Key" },
|
|
BearerAuth: { type: "http", scheme: "bearer" },
|
|
},
|
|
schemas: {
|
|
Error: {
|
|
type: "object",
|
|
properties: {
|
|
error: {
|
|
type: "object",
|
|
properties: {
|
|
code: {
|
|
type: "string",
|
|
enum: ["unauthorized", "forbidden", "bad_request", "not_found", "not_ready", "internal_error"],
|
|
},
|
|
message: { type: "string" },
|
|
details: {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Pagination: {
|
|
type: "object",
|
|
properties: {
|
|
page: { type: "integer" },
|
|
limit: { type: "integer" },
|
|
total: { type: "integer" },
|
|
totalPages: { type: "integer" },
|
|
},
|
|
},
|
|
ProductRow: {
|
|
type: "object",
|
|
description: "Ligne de grille. Voir src/types/grid.ts pour la liste complète des champs.",
|
|
properties: {
|
|
codein: { type: "string", description: "Identifiant interne FF Nancy." },
|
|
codeFournisseur: { type: "string" },
|
|
nomFournisseur: { type: "string" },
|
|
libelle1: { type: "string" },
|
|
gtin: { type: "string" },
|
|
codeCentrale: { type: "string", description: "Clé de jointure avec Qlik (format 10000XXXXXX)." },
|
|
codeGamme: { type: ["string", "null"], description: "Gamme courante — peut être surchargée par un snapshot de session." },
|
|
codeGammeServeur: {
|
|
type: ["string", "null"],
|
|
description: "Gamme **non modifiée**, telle qu'elle existe sur le serveur PostgreSQL. Relue à chaque appel (sauf `enrich=0`). `null` = aucune gamme en base.",
|
|
},
|
|
codeGammeInit: { type: ["string", "null"], description: "Alias historique de `codeGammeServeur`, maintenu aligné." },
|
|
network: {
|
|
type: ["object", "null"],
|
|
description: "Métriques réseau Qlik en cache, `null` si le produit n'en a pas.",
|
|
properties: {
|
|
caReseau: { type: "number" },
|
|
qteReseau: { type: "number" },
|
|
nbMagasinsReseau: { type: "integer" },
|
|
caParMagasinReseau: { type: "number" },
|
|
margePctReseau: { type: "number", description: "Ratio brut Qlik (0.32 = 32 %)." },
|
|
qteByMonth: { type: ["object", "null"], description: "Quantités par mois, clés YYYY-MM." },
|
|
fetchedAt: { type: ["string", "null"], format: "date-time" },
|
|
},
|
|
},
|
|
totalCa: { type: "number" },
|
|
totalQuantite: { type: "number" },
|
|
totalMarge: { type: "number" },
|
|
tauxMarge: { type: "number" },
|
|
stockActuel: { type: "number" },
|
|
sales12m: { type: "object", description: "Quantités vendues par mois, clés YYYYMM." },
|
|
stock12m: { type: "object", description: "Stock fin de mois, clés YYYYMM." },
|
|
sales12mByStore: { type: "object", description: "Ventes par magasin puis par mois." },
|
|
caReseau: { type: "number" },
|
|
qteReseau: { type: "number" },
|
|
nbMagasinsReseau: { type: "integer" },
|
|
caParMagasinReseau: { type: "number" },
|
|
margePctReseau: { type: "number", description: "Ratio brut Qlik (0.32 = 32 %)." },
|
|
qteReseauByMonth: { type: ["object", "null"], description: "Quantités réseau par mois, clés YYYY-MM." },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
paths: {
|
|
"/fournisseurs": {
|
|
get: {
|
|
summary: "Liste des fournisseurs, avec la fraîcheur de leur instantané de grille",
|
|
parameters: [
|
|
{ name: "search", in: "query", schema: { type: "string" } },
|
|
{
|
|
name: "withData",
|
|
in: "query",
|
|
schema: { type: "string", enum: ["0", "1"] },
|
|
description: "1 = uniquement les fournisseurs déjà présents dans l'instantané.",
|
|
},
|
|
],
|
|
responses: { "200": { description: "OK" }, "401": { description: "Non authentifié" } },
|
|
},
|
|
},
|
|
"/grid": {
|
|
get: {
|
|
summary: "Lignes de grille d'un fournisseur",
|
|
description: "Répond `202 not_ready` si le fournisseur n'a pas encore d'instantané.",
|
|
parameters: [
|
|
{ name: "fournisseur", in: "query", required: true, schema: { type: "string" } },
|
|
{ name: "gamme", in: "query", schema: { type: "string" } },
|
|
{ name: "code1", in: "query", schema: { type: "string" } },
|
|
{ name: "code2", in: "query", schema: { type: "string" } },
|
|
{ name: "code3", in: "query", schema: { type: "string" } },
|
|
{ name: "search", in: "query", schema: { type: "string" }, description: "Libellé, codein, GTIN, référence ou code centrale." },
|
|
...sortParams,
|
|
...paginationParams,
|
|
],
|
|
responses: {
|
|
"200": { description: "OK" },
|
|
"202": { description: "Instantané pas encore calculé" },
|
|
"400": { description: "Paramètres invalides" },
|
|
"401": { description: "Non authentifié" },
|
|
},
|
|
},
|
|
},
|
|
"/products/search": {
|
|
get: {
|
|
summary: "Recherche transversale de produits, tous fournisseurs confondus",
|
|
parameters: [
|
|
{ name: "q", in: "query", required: true, schema: { type: "string", maxLength: 120 } },
|
|
{ name: "fournisseur", in: "query", schema: { type: "string" }, description: "Restreint la recherche à un fournisseur." },
|
|
{ name: "gamme", in: "query", schema: { type: "string" } },
|
|
...sortParams,
|
|
...paginationParams,
|
|
],
|
|
responses: { "200": { description: "OK" }, "401": { description: "Non authentifié" } },
|
|
},
|
|
},
|
|
"/products/{codein}": {
|
|
get: {
|
|
summary: "Fiche complète d'un produit",
|
|
parameters: [
|
|
{ name: "codein", in: "path", required: true, schema: { type: "string" } },
|
|
{ name: "fournisseur", in: "query", schema: { type: "string" }, description: "Lève l'ambiguïté d'un article multi-fournisseurs." },
|
|
],
|
|
responses: { "200": { description: "OK" }, "404": { description: "Inconnu de l'instantané" } },
|
|
},
|
|
},
|
|
"/network/{codeCentrale}": {
|
|
get: {
|
|
summary: "Métriques réseau Qlik en cache + courbe 12 mois",
|
|
parameters: [{ name: "codeCentrale", in: "path", required: true, schema: { type: "string" } }],
|
|
responses: { "200": { description: "OK" }, "404": { description: "Absent du cache réseau" } },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return Response.json(spec);
|
|
}
|