import { and, eq, isNotNull, isNull, or, sql } from "drizzle-orm"; import { cartoes, lancamentos, pagadores } from "@/db/schema"; import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE, } from "@/lib/accounts/constants"; import { toNumber } from "@/lib/dashboard/common"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; // Calcula a data de vencimento baseada no período e dia de vencimento do cartão function calculateDueDate(period: string, dueDay: string | null): Date | null { if (!dueDay) return null; try { const [year, month] = period.split("-"); if (!year || !month) return null; const day = parseInt(dueDay, 10); if (Number.isNaN(day)) return null; // Criar data ao meio-dia para evitar problemas de timezone return new Date(parseInt(year, 10), parseInt(month, 10) - 1, day, 12, 0, 0); } catch { return null; } } export type InstallmentDetail = { id: string; currentInstallment: number; amount: number; dueDate: Date | null; period: string; isAnticipated: boolean; purchaseDate: Date; isSettled: boolean; }; export type InstallmentGroup = { seriesId: string; name: string; paymentMethod: string; cartaoId: string | null; cartaoName: string | null; cartaoDueDay: string | null; cartaoLogo: string | null; totalInstallments: number; paidInstallments: number; pendingInstallments: InstallmentDetail[]; totalPendingAmount: number; firstPurchaseDate: Date; }; export type InstallmentAnalysisData = { installmentGroups: InstallmentGroup[]; totalPendingInstallments: number; }; export async function fetchInstallmentAnalysis( userId: string, ): Promise { // 1. Buscar todos os lançamentos parcelados não antecipados do pagador admin const installmentRows = await db .select({ id: lancamentos.id, seriesId: lancamentos.seriesId, name: lancamentos.name, amount: lancamentos.amount, paymentMethod: lancamentos.paymentMethod, currentInstallment: lancamentos.currentInstallment, installmentCount: lancamentos.installmentCount, dueDate: lancamentos.dueDate, period: lancamentos.period, isAnticipated: lancamentos.isAnticipated, isSettled: lancamentos.isSettled, purchaseDate: lancamentos.purchaseDate, cartaoId: lancamentos.cartaoId, cartaoName: cartoes.name, cartaoDueDay: cartoes.dueDay, cartaoLogo: cartoes.logo, }) .from(lancamentos) .leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id)) .leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.transactionType, "Despesa"), eq(lancamentos.condition, "Parcelado"), eq(lancamentos.isAnticipated, false), isNotNull(lancamentos.seriesId), eq(pagadores.role, PAGADOR_ROLE_ADMIN), or( isNull(lancamentos.note), and( sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`, sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, ), ), ), ) .orderBy(lancamentos.purchaseDate, lancamentos.currentInstallment); // Agrupar por seriesId const seriesMap = new Map(); for (const row of installmentRows) { if (!row.seriesId) continue; const amount = Math.abs(toNumber(row.amount)); // Calcular vencimento correto baseado no período e dia de vencimento do cartão const calculatedDueDate = row.cartaoDueDay ? calculateDueDate(row.period, row.cartaoDueDay) : row.dueDate; const installmentDetail: InstallmentDetail = { id: row.id, currentInstallment: row.currentInstallment ?? 1, amount, dueDate: calculatedDueDate, period: row.period, isAnticipated: row.isAnticipated ?? false, purchaseDate: row.purchaseDate, isSettled: row.isSettled ?? false, }; if (seriesMap.has(row.seriesId)) { const group = seriesMap.get(row.seriesId); group?.pendingInstallments.push(installmentDetail); if (group) group.totalPendingAmount += amount; } else { seriesMap.set(row.seriesId, { seriesId: row.seriesId, name: row.name, paymentMethod: row.paymentMethod, cartaoId: row.cartaoId, cartaoName: row.cartaoName, cartaoDueDay: row.cartaoDueDay, cartaoLogo: row.cartaoLogo, totalInstallments: row.installmentCount ?? 0, paidInstallments: 0, pendingInstallments: [installmentDetail], totalPendingAmount: amount, firstPurchaseDate: row.purchaseDate, }); } } // Calcular quantas parcelas já foram pagas para cada grupo const installmentGroups = Array.from(seriesMap.values()) .map((group) => { // Contar quantas parcelas estão marcadas como pagas (settled) const paidCount = group.pendingInstallments.filter( (i) => i.isSettled, ).length; group.paidInstallments = paidCount; return group; }) // Filtrar apenas séries que têm pelo menos uma parcela em aberto (não paga) .filter((group) => { const hasUnpaidInstallments = group.pendingInstallments.some( (i) => !i.isSettled, ); return hasUnpaidInstallments; }); // Calcular totais const totalPendingInstallments = installmentGroups.reduce( (sum, group) => sum + group.totalPendingAmount, 0, ); return { installmentGroups, totalPendingInstallments, }; }