import { and, asc, eq, gte, lte, ne, sum } from "drizzle-orm"; import { financialAccounts, transactions } from "@/db/schema"; import { buildDashboardAdminFilters, excludeAutoInvoiceEntries, excludeInitialBalanceWhenConfigured, } from "@/features/dashboard/transaction-filters"; import { db } from "@/shared/lib/db"; import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id"; import { safeToNumber } from "@/shared/utils/number"; import { addMonthsToPeriod, buildPeriodRange, comparePeriods, getPreviousPeriod, } from "@/shared/utils/period"; const RECEITA = "Receita"; const DESPESA = "Despesa"; const TRANSFERENCIA = "Transferência"; type MetricPair = { current: number; previous: number; }; export type DashboardCardMetrics = { period: string; previousPeriod: string; receitas: MetricPair; despesas: MetricPair; balanco: MetricPair; previsto: MetricPair; }; type PeriodTotals = { receitas: number; despesas: number; balanco: number; }; const createEmptyTotals = (): PeriodTotals => ({ receitas: 0, despesas: 0, balanco: 0, }); const ensurePeriodTotals = ( store: Map, period: string, ): PeriodTotals => { if (!store.has(period)) { store.set(period, createEmptyTotals()); } const totals = store.get(period); // This should always exist since we just set it above if (!totals) { const emptyTotals = createEmptyTotals(); store.set(period, emptyTotals); return emptyTotals; } return totals; }; // Re-export for backward compatibility export async function fetchDashboardCardMetrics( userId: string, period: string, ): Promise { const previousPeriod = getPreviousPeriod(period); const adminPayerId = await getAdminPayerId(userId); if (!adminPayerId) { return { period, previousPeriod, receitas: { current: 0, previous: 0 }, despesas: { current: 0, previous: 0 }, balanco: { current: 0, previous: 0 }, previsto: { current: 0, previous: 0 }, }; } // Limitar scan histórico a 24 meses para evitar scans progressivamente mais lentos const startPeriod = addMonthsToPeriod(period, -24); const rows = await db .select({ period: transactions.period, transactionType: transactions.transactionType, totalAmount: sum(transactions.amount).as("total"), }) .from(transactions) .leftJoin( financialAccounts, eq(transactions.accountId, financialAccounts.id), ) .where( and( ...buildDashboardAdminFilters({ userId, adminPayerId }), gte(transactions.period, startPeriod), lte(transactions.period, period), ne(transactions.transactionType, TRANSFERENCIA), excludeAutoInvoiceEntries(), excludeInitialBalanceWhenConfigured(), ), ) .groupBy(transactions.period, transactions.transactionType) .orderBy(asc(transactions.period), asc(transactions.transactionType)); const periodTotals = new Map(); for (const row of rows) { if (!row.period) continue; const totals = ensurePeriodTotals(periodTotals, row.period); const total = safeToNumber(row.totalAmount); if (row.transactionType === RECEITA) { totals.receitas += total; } else if (row.transactionType === DESPESA) { totals.despesas += Math.abs(total); } } ensurePeriodTotals(periodTotals, period); ensurePeriodTotals(periodTotals, previousPeriod); const earliestPeriod = periodTotals.size > 0 ? Array.from(periodTotals.keys()).sort()[0] : period; const startRangePeriod = comparePeriods(earliestPeriod, previousPeriod) <= 0 ? earliestPeriod : previousPeriod; const periodRange = buildPeriodRange(startRangePeriod, period); const forecastByPeriod = new Map(); let runningForecast = 0; for (const key of periodRange) { const totals = ensurePeriodTotals(periodTotals, key); totals.balanco = totals.receitas - totals.despesas; runningForecast += totals.balanco; forecastByPeriod.set(key, runningForecast); } const currentTotals = ensurePeriodTotals(periodTotals, period); const previousTotals = ensurePeriodTotals(periodTotals, previousPeriod); return { period, previousPeriod, receitas: { current: currentTotals.receitas, previous: previousTotals.receitas, }, despesas: { current: currentTotals.despesas, previous: previousTotals.despesas, }, balanco: { current: currentTotals.balanco, previous: previousTotals.balanco, }, previsto: { current: forecastByPeriod.get(period) ?? runningForecast, previous: forecastByPeriod.get(previousPeriod) ?? 0, }, }; }