]> git.digitality.be Git - pdw25-26/commitdiff
Mise à jour price demande de christophe
authorValentin Hulin <e23743@eps-marche.be>
Fri, 27 Feb 2026 17:22:07 +0000 (18:22 +0100)
committerValentin Hulin <e23743@eps-marche.be>
Fri, 27 Feb 2026 17:22:07 +0000 (18:22 +0100)
Wallette/server/modules/price/.env.example [new file with mode: 0644]
Wallette/server/modules/price/app.js [deleted file]
Wallette/server/modules/price/db.js
Wallette/server/modules/price/index.js [new file with mode: 0644]
Wallette/server/modules/price/routes/api.router.js [new file with mode: 0644]
Wallette/server/modules/price/server.js [deleted file]

diff --git a/Wallette/server/modules/price/.env.example b/Wallette/server/modules/price/.env.example
new file mode 100644 (file)
index 0000000..629fb25
--- /dev/null
@@ -0,0 +1,7 @@
+# Configuration DB (à placer dans l'environnement de ton app)
+DB_HOST=localhost
+DB_USER=root
+DB_PASSWORD=
+DB_NAME=crypto
+DB_PORT=3306
+DB_CONNECTION_LIMIT=10
diff --git a/Wallette/server/modules/price/app.js b/Wallette/server/modules/price/app.js
deleted file mode 100644 (file)
index 4bb257a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-import "./server.js";
index ee443a32ba97ee7c1d1ae49ecc7f4a3c33919d39..73023b7efea5158246b0c1dec806ee1fe9bd7579 100644 (file)
@@ -1,9 +1,50 @@
 import mysql from "mysql2/promise";
 
-export const db = mysql.createPool({
-  host: "localhost",
-  user: "root",
-  password: "vava",
-  database: "crypto",
-  port: 3306
-});
+function readEnv(name, { required = true, fallback = undefined } = {}) {
+  const v = process.env[name];
+  if (v == null || v === "") {
+    if (required) throw new Error(`Missing environment variable: ${name}`);
+    return fallback;
+  }
+  return v;
+}
+
+/**
+ * Crée un pool MySQL à partir des variables d'environnement.
+ *
+ * Variables attendues:
+ * - DB_HOST (ex: localhost)
+ * - DB_USER (ex: root)
+ * - DB_PASSWORD (peut être vide)
+ * - DB_NAME (ex: crypto)
+ * - DB_PORT (optionnel, défaut 3306)
+ * - DB_CONNECTION_LIMIT (optionnel, défaut 10)
+ */
+export function createDbPool(overrides = {}) {
+  const host = readEnv("DB_HOST");
+  const user = readEnv("DB_USER");
+  const password = readEnv("DB_PASSWORD", { required: false, fallback: "" });
+  const database = readEnv("DB_NAME");
+  const port = Number(readEnv("DB_PORT", { required: false, fallback: "3306" }));
+  const connectionLimit = Number(
+    readEnv("DB_CONNECTION_LIMIT", { required: false, fallback: "10" })
+  );
+
+  if (!Number.isFinite(port)) throw new Error("DB_PORT must be a number");
+  if (!Number.isFinite(connectionLimit)) throw new Error("DB_CONNECTION_LIMIT must be a number");
+
+  return mysql.createPool({
+    host,
+    user,
+    password,
+    database,
+    port,
+    connectionLimit,
+    // mysql2: recommandé pour éviter les erreurs de timezone (optionnel)
+    // timezone: 'Z',
+    ...overrides
+  });
+}
+
+// Export compat: les repositories importent `db`.
+export const db = createDbPool();
diff --git a/Wallette/server/modules/price/index.js b/Wallette/server/modules/price/index.js
new file mode 100644 (file)
index 0000000..c1e0735
--- /dev/null
@@ -0,0 +1,4 @@
+// Point d'entrée du module: on exporte uniquement un Router Express.
+// À monter côté app principale avec: app.use('/api', priceApiRouter)
+
+export { default as priceApiRouter } from "./routes/api.router.js";
diff --git a/Wallette/server/modules/price/routes/api.router.js b/Wallette/server/modules/price/routes/api.router.js
new file mode 100644 (file)
index 0000000..e0e16ae
--- /dev/null
@@ -0,0 +1,145 @@
+// price/routes/api.router.js
+import { Router } from "express";
+
+import { formatProviderError, getPriceWithFallback } from "../services/price.service.js";
+import { getActivePairIdByCode } from "../repositories/pair.repository.js";
+import { insertPricePoint, getCurrentPrice, getPriceHistory } from "../repositories/price.repository.js";
+
+const router = Router();
+
+function ok(res, data) {
+  return res.json({ ok: true, data });
+}
+
+function fail(res, status, code, message, details = null) {
+  return res.status(status).json({ ok: false, error: { code, message, details } });
+}
+
+function normalizePairCode(value) {
+  return String(value || "").trim().toUpperCase();
+}
+
+/**
+ * Convertit:
+ * - BTC/EUR, BTC_EUR, BTC-EUR, "BTC EUR"
+ * en { base:"BTC", quote:"EUR", dbCode:"BTC_EUR" }
+ */
+function parsePairInput(pairInput) {
+  const raw = normalizePairCode(pairInput);
+  const dbCode = raw.replace(/\s+/g, "").replace(/[-/]/g, "_");
+  const parts = dbCode.split("_");
+  if (parts.length !== 2 || !parts[0] || !parts[1]) {
+    const err = new Error("PAIR_INVALID");
+    err.code = "PAIR_INVALID";
+    throw err;
+  }
+  return { base: parts[0], quote: parts[1], dbCode };
+}
+
+// NOTE:
+// - Ce Router est prévu pour être monté sur `/api` (ex: app.use('/api', router)).
+// - Il expose UNIQUEMENT les endpoints du contrat:
+//   GET  /api/price/current
+//   GET  /api/price/history
+//   POST /api/price/refresh (optionnel)
+
+/**
+ * Prix courant
+ * GET /api/price/current?pair=BTC/EUR
+ */
+router.get("/price/current", async (req, res) => {
+  const pair = normalizePairCode(req.query.pair);
+
+  if (!pair) return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
+
+  let pair_id;
+  try {
+    pair_id = await getActivePairIdByCode(pair);
+  } catch (err) {
+    if (err.code === "PAIR_NOT_FOUND") return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
+    return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`);
+  }
+
+  try {
+    const row = await getCurrentPrice(pair_id);
+    if (!row) return fail(res, 404, "NO_PRICE", `Aucun prix en base pour ${pair}`);
+
+    return ok(res, {
+      pair,
+      pair_id,
+      source: row.source,
+      timestamp_ms: row.timestamp_ms,
+      current_price: row.current_price,
+      open_price: row.open_price,
+      high_price: row.high_price,
+      low_price: row.low_price,
+      close_price: row.close_price,
+      volume_24h: row.volume_24h ?? null
+    });
+  } catch (err) {
+    return fail(res, 500, "DB_ERROR", "Erreur lecture prix courant", err.message);
+  }
+});
+
+/**
+ * Historique
+ * GET /api/price/history?pair=BTC/EUR&limit=200
+ */
+router.get("/price/history", async (req, res) => {
+  const pair = normalizePairCode(req.query.pair);
+  const limit = req.query.limit;
+
+  if (!pair) return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
+
+  let pair_id;
+  try {
+    pair_id = await getActivePairIdByCode(pair);
+  } catch (err) {
+    if (err.code === "PAIR_NOT_FOUND") return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
+    return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`);
+  }
+
+  try {
+    const points = await getPriceHistory(pair_id, limit);
+    return ok(res, { pair, pair_id, points });
+  } catch (err) {
+    return fail(res, 500, "DB_ERROR", "Erreur lecture historique", err.message);
+  }
+});
+
+/**
+ * Refresh manuel
+ * POST /api/price/refresh?pair=BTC/EUR
+ */
+router.post("/price/refresh", async (req, res) => {
+  const pair = normalizePairCode(req.query.pair);
+  if (!pair) return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
+
+  try {
+    const { base, quote } = parsePairInput(pair);
+    const pair_id = await getActivePairIdByCode(pair);
+
+    const data = await getPriceWithFallback("BINANCE", { base, quote });
+
+    await insertPricePoint({
+      pair_id,
+      timestamp_ms: data.timestamp_ms,
+      current_price: data.price,
+      source: data.source
+    });
+
+    return ok(res, {
+      pair,
+      pair_id,
+      source: data.source,
+      timestamp_ms: data.timestamp_ms,
+      current_price: data.price,
+      used_fallback: Boolean(data.used_fallback)
+    });
+  } catch (err) {
+    if (err.code === "PAIR_NOT_FOUND") return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
+    return fail(res, 503, "PROVIDER_ERROR", `Refresh impossible pour ${pair}`, formatProviderError(err));
+  }
+});
+
+export default router;
\ No newline at end of file
diff --git a/Wallette/server/modules/price/server.js b/Wallette/server/modules/price/server.js
deleted file mode 100644 (file)
index f652eec..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-import express from "express";
-import { db } from "./db.js";
-
-import { formatProviderError, getPriceWithFallback } from "./services/price.service.js";
-import { getActivePairIdByCode, getPairById, listActivePairs } from "./repositories/pair.repository.js";
-import { insertPricePoint, getCurrentPrice, getPriceHistory } from "./repositories/price.repository.js";
-
-const app = express();
-const PORT = 3000;
-
-// On fetch souvent pour construire OHLC (bougie 5 min)
-const SAMPLE_INTERVAL_MS = 10 * 1000; // 10s
-
-// Toutes les paires actives (chargées au démarrage)
-let PAIR_IDS_TO_SAMPLE = [];
-
-app.use(express.json());
-app.use(express.static("public"));
-
-function ok(res, data) {
-  return res.json({ ok: true, data });
-}
-
-function fail(res, status, code, message, details = null) {
-  return res.status(status).json({
-    ok: false,
-    error: { code, message, details }
-  });
-}
-
-function normalizePairCode(value) {
-  return String(value || "").trim().toUpperCase();
-}
-
-/**
- * Convertit:
- * - BTC/EUR, BTC_EUR, BTC-EUR, "BTC EUR"
- * en { base:"BTC", quote:"EUR", dbCode:"BTC_EUR" }
- */
-function parsePairInput(pairInput) {
-  const raw = normalizePairCode(pairInput);
-  const dbCode = raw.replace(/\s+/g, "").replace(/[-/]/g, "_");
-  const parts = dbCode.split("_");
-  if (parts.length !== 2 || !parts[0] || !parts[1]) {
-    const err = new Error("PAIR_INVALID");
-    err.code = "PAIR_INVALID";
-    throw err;
-  }
-  return { base: parts[0], quote: parts[1], dbCode };
-}
-
-/**
- * Reconstruit base/quote depuis pair_code DB (ex: BTC_EUR)
- * -> { base:"BTC", quote:"EUR" }
- */
-function parsePairCodeFromDb(pairCodeDb) {
-  const code = String(pairCodeDb || "").trim().toUpperCase();
-  const parts = code.split("_");
-  if (parts.length !== 2) throw new Error(`PAIR_CODE_DB_INVALID: ${code}`);
-  return { base: parts[0], quote: parts[1] };
-}
-
-/**
- * Charge toutes les paires actives (is_active=1) depuis la DB
- */
-async function loadAllActivePairIds() {
-  const pairs = await listActivePairs(); // doit être ORDER BY pair_id ASC dans le repo
-  PAIR_IDS_TO_SAMPLE = pairs.map((p) => p.pair_id);
-  console.log("PAIR_IDS_TO_SAMPLE (actives):", PAIR_IDS_TO_SAMPLE);
-}
-
-/**
- * ✅ Liste des paires (pour ton interface)
- * GET /api/pairs
- */
-app.get("/api/pairs", async (req, res) => {
-  try {
-    const pairs = await listActivePairs();
-    return ok(res, { count: pairs.length, pairs });
-  } catch (e) {
-    return fail(res, 500, "DB_ERROR", "Erreur lecture des paires", e.message);
-  }
-});
-
-/**
- * DEBUG: Voir les paires en DB (tout)
- * GET /api/debug/pairs
- */
-app.get("/api/debug/pairs", async (req, res) => {
-  try {
-    const [rows] = await db.execute(
-      "SELECT pair_id, base_symbol, quote_symbol, pair_code, is_active FROM pairs ORDER BY pair_id"
-    );
-    return ok(res, { count: rows.length, rows });
-  } catch (e) {
-    return fail(res, 500, "DB_ERROR", "Erreur lecture table pairs", e.message);
-  }
-});
-
-/**
- * Fetch + insert sur une paire ID
- */
-async function fetchAndInsertTickById(pairId, preferredSource = "BINANCE") {
-  const pairRow = await getPairById(pairId);
-  const { base, quote } = parsePairCodeFromDb(pairRow.pair_code);
-
-  const data = await getPriceWithFallback(preferredSource, { base, quote });
-
-  await insertPricePoint({
-    pair_id: pairRow.pair_id,
-    timestamp_ms: data.timestamp_ms,
-    current_price: data.price,
-    source: data.source
-  });
-
-  return {
-    pair_id: pairRow.pair_id,
-    pair_code: pairRow.pair_code,
-    base,
-    quote,
-    source: data.source,
-    timestamp_ms: data.timestamp_ms,
-    current_price: data.price,
-    used_fallback: Boolean(data.used_fallback)
-  };
-}
-
-/**
- * Prix courant
- * GET /api/price/current?pair=BTC/EUR
- */
-app.get("/api/price/current", async (req, res) => {
-  const pair = normalizePairCode(req.query.pair);
-
-  if (!pair) {
-    return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
-  }
-
-  let pair_id;
-  try {
-    pair_id = await getActivePairIdByCode(pair);
-  } catch (err) {
-    if (err.code === "PAIR_NOT_FOUND") {
-      return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
-    }
-    return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`);
-  }
-
-  try {
-    const row = await getCurrentPrice(pair_id);
-    if (!row) {
-      return fail(res, 404, "NO_PRICE", `Aucun prix en base pour ${pair}`);
-    }
-
-    return ok(res, {
-      pair,
-      pair_id,
-      source: row.source,
-      timestamp_ms: row.timestamp_ms,
-      current_price: row.current_price,
-      open_price: row.open_price,
-      high_price: row.high_price,
-      low_price: row.low_price,
-      close_price: row.close_price,
-      volume_24h: row.volume_24h ?? null
-    });
-  } catch (err) {
-    return fail(res, 500, "DB_ERROR", "Erreur lecture prix courant", err.message);
-  }
-});
-
-/**
- * Historique
- * GET /api/price/history?pair=BTC/EUR&limit=200
- */
-app.get("/api/price/history", async (req, res) => {
-  const pair = normalizePairCode(req.query.pair);
-  const limit = req.query.limit;
-
-  if (!pair) {
-    return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
-  }
-
-  let pair_id;
-  try {
-    pair_id = await getActivePairIdByCode(pair);
-  } catch (err) {
-    if (err.code === "PAIR_NOT_FOUND") {
-      return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
-    }
-    return fail(res, 400, "PAIR_INVALID", `Paire invalide: ${pair}`);
-  }
-
-  try {
-    const points = await getPriceHistory(pair_id, limit);
-    return ok(res, { pair, pair_id, points });
-  } catch (err) {
-    return fail(res, 500, "DB_ERROR", "Erreur lecture historique", err.message);
-  }
-});
-
-/**
- * Refresh manuel
- * POST /api/price/refresh?pair=BTC/EUR
- */
-app.post("/api/price/refresh", async (req, res) => {
-  const pair = normalizePairCode(req.query.pair);
-  if (!pair) {
-    return fail(res, 400, "PAIR_REQUIRED", "Parametre 'pair' obligatoire (ex: BTC/EUR)");
-  }
-
-  try {
-    const { base, quote } = parsePairInput(pair);
-    const pair_id = await getActivePairIdByCode(pair);
-
-    const data = await getPriceWithFallback("BINANCE", { base, quote });
-
-    await insertPricePoint({
-      pair_id,
-      timestamp_ms: data.timestamp_ms,
-      current_price: data.price,
-      source: data.source
-    });
-
-    return ok(res, {
-      pair,
-      pair_id,
-      source: data.source,
-      timestamp_ms: data.timestamp_ms,
-      current_price: data.price,
-      used_fallback: Boolean(data.used_fallback)
-    });
-  } catch (err) {
-    if (err.code === "PAIR_NOT_FOUND") {
-      return fail(res, 404, "PAIR_NOT_FOUND", `Paire inconnue: ${pair}`);
-    }
-    return fail(res, 503, "PROVIDER_ERROR", `Refresh impossible pour ${pair}`, formatProviderError(err));
-  }
-});
-
-/**
- * Scheduler: toutes les paires actives à chaque tick
- * => limite de concurrence pour éviter rate-limit
- */
-async function schedulerTick() {
-  if (PAIR_IDS_TO_SAMPLE.length === 0) {
-    console.warn("Aucune paire active à sampler (PAIR_IDS_TO_SAMPLE vide).");
-    return;
-  }
-
-  const preferred = "BINANCE";
-  const CONCURRENCY = 5; // ajuste à 3/5/10 selon le nombre de paires
-
-  let i = 0;
-
-  async function worker() {
-    while (i < PAIR_IDS_TO_SAMPLE.length) {
-      const pairId = PAIR_IDS_TO_SAMPLE[i++];
-      try {
-        const tick = await fetchAndInsertTickById(pairId, preferred);
-        console.log(
-          `Tick OK pair_id=${tick.pair_id} (${tick.pair_code}) ${tick.current_price} (${tick.source}${tick.used_fallback ? " fallback" : ""})`
-        );
-      } catch (err) {
-        console.error(`Erreur scheduler pair_id=${pairId}: ${formatProviderError(err)}`);
-      }
-    }
-  }
-
-  await Promise.all(Array.from({ length: CONCURRENCY }, worker));
-}
-
-/**
- * ✅ Démarrage du serveur
- */
-app.listen(PORT, async () => {
-  console.log(`Serveur lance : http://localhost:${PORT}`);
-
-  try {
-    await loadAllActivePairIds(); // charge toutes les paires actives
-    await schedulerTick();        // 1 tick immédiat
-    setInterval(schedulerTick, SAMPLE_INTERVAL_MS);
-  } catch (e) {
-    console.error("Erreur démarrage serveur:", e.message);
-  }
-});
\ No newline at end of file