--- /dev/null
+// 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
+++ /dev/null
-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