diff --git a/apps/backend/src/modules/quotes/handlers/getMonthlyLast.ts b/apps/backend/src/modules/quotes/handlers/getMonthlyLast.ts index 4c952df..2490bb7 100644 --- a/apps/backend/src/modules/quotes/handlers/getMonthlyLast.ts +++ b/apps/backend/src/modules/quotes/handlers/getMonthlyLast.ts @@ -2,12 +2,56 @@ import type { Context } from "hono"; import { cache } from "../../../lib/cache"; import { prisma } from "../../../lib/prisma"; +type MonthlyLastRow = { + month: string; + type: string; + buy: number; +}; + +type ExternalQuote = { + casa: string; + fecha: string; + compra: number; + venta: number; +}; + +const CASA_MAP: Record = { + BELO: "cripto", + BLUE: "blue", + BNA: "oficial", +}; + +const TYPES = ["BELO", "BLUE", "BNA"] as const; + +function getMonthKey(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + return `${y}-${m}`; +} + +function subtractMonths(date: Date, months: number): Date { + const result = new Date(date); + result.setMonth(result.getMonth() - months); + return result; +} + export async function getMonthlyLast(c: Context) { - const cacheKey = "quotes:monthly-last"; + const cacheKey = "quotes:monthly-last-v2"; const cached = cache.get(cacheKey); if (cached) return c.json(cached); - const rows = await prisma.$queryRaw< + const today = new Date(); + const currentMonthKey = getMonthKey(today); + const startMonth = subtractMonths(today, 11); + + const startDate = new Date( + startMonth.getFullYear(), + startMonth.getMonth(), + 1, + ); + const endDate = new Date(today.getFullYear(), today.getMonth() + 1, 1); + + const dbRows = await prisma.$queryRaw< Array<{ month: string; type: string; @@ -19,10 +63,106 @@ export async function getMonthlyLast(c: Context) { "type"::text, (ARRAY_AGG(buy ORDER BY "timeStamp" DESC))[1]::numeric::float8 AS buy FROM "quotes_history" + WHERE "timeStamp" >= ${startDate}::timestamptz + AND "timeStamp" < ${endDate}::timestamptz GROUP BY DATE_TRUNC('month', "timeStamp"), "type" ORDER BY month ASC `; - cache.set(cacheKey, rows, 300); - return c.json(rows); + const dbMap = new Map>(); + for (const row of dbRows) { + if (!dbMap.has(row.month)) dbMap.set(row.month, new Map()); + dbMap.get(row.month)?.set(row.type, row.buy); + } + + const expectedMonths: string[] = []; + let cursor = new Date(startMonth.getFullYear(), startMonth.getMonth(), 1); + while (getMonthKey(cursor) <= currentMonthKey) { + expectedMonths.push(getMonthKey(cursor)); + cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1); + } + + const missingByType = new Map(); + for (const type of TYPES) { + const missing = expectedMonths.filter((m) => !dbMap.get(m)?.has(type)); + if (missing.length > 0) missingByType.set(type, missing); + } + + if (missingByType.size === 0) { + const result: MonthlyLastRow[] = []; + for (const month of expectedMonths) { + for (const type of TYPES) { + const buy = dbMap.get(month)?.get(type); + if (buy !== undefined) { + result.push({ month, type, buy }); + } + } + } + cache.set(cacheKey, result, 300); + return c.json(result); + } + + const neededMonths = new Set(); + for (const months of missingByType.values()) { + for (const m of months) neededMonths.add(m); + } + const sortedNeeded = [...neededMonths].sort(); + const minNeeded = sortedNeeded[0]; + const maxNeeded = sortedNeeded[sortedNeeded.length - 1]; + + const externalData = new Map>(); + + for (const [type, casa] of Object.entries(CASA_MAP)) { + if (!missingByType.has(type)) continue; + + try { + const res = await fetch( + `https://api.argentinadatos.com/v1/cotizaciones/dolares/${casa}`, + ); + if (!res.ok) continue; + + const quotes: ExternalQuote[] = await res.json(); + const monthLast = new Map(); + + for (const q of quotes) { + const fecha = q.fecha.slice(0, 10); + if (fecha < minNeeded || fecha > maxNeeded) continue; + + const monthKey = fecha.slice(0, 7); + if (!neededMonths.has(monthKey)) continue; + + const existing = monthLast.get(monthKey); + if (!existing || fecha > existing.fecha) { + monthLast.set(monthKey, { fecha, compra: q.compra }); + } + } + + const typeMap = new Map(); + for (const [month, data] of monthLast) { + typeMap.set(month, data.compra); + } + externalData.set(type, typeMap); + } catch { + // If external API fails, continue without those values + } + } + + const result: MonthlyLastRow[] = []; + for (const month of expectedMonths) { + for (const type of TYPES) { + const dbBuy = dbMap.get(month)?.get(type); + if (dbBuy !== undefined) { + result.push({ month, type, buy: dbBuy }); + continue; + } + + const extBuy = externalData.get(type)?.get(month); + if (extBuy !== undefined) { + result.push({ month, type, buy: extBuy }); + } + } + } + + cache.set(cacheKey, result, 300); + return c.json(result); }