refactor: pagina transações e modulariza ações

This commit is contained in:
Felipe Coutinho
2026-03-20 18:39:49 +00:00
parent 41fd8226cb
commit 3c31ee5d90
20 changed files with 4261 additions and 1782 deletions

View File

@@ -1,4 +1,15 @@
import { and, desc, eq, gte, isNull, ne, or, type SQL } from "drizzle-orm";
import {
and,
count,
desc,
eq,
gte,
isNull,
ne,
or,
type SQL,
sql,
} from "drizzle-orm";
import {
cards,
categories,
@@ -9,6 +20,109 @@ import {
import { INITIAL_BALANCE_NOTE } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db";
type BaseTransactionQueryInput = {
filters: SQL[];
extraFilters?: SQL[];
excludeInitialBalanceFromIncome?: boolean;
};
type TransactionQueryInput = BaseTransactionQueryInput & {
limit?: number;
offset?: number;
};
export type PaginatedTransactionsResult = {
rows: Awaited<ReturnType<typeof fetchTransactions>>;
totalItems: number;
page: number;
pageSize: number;
totalPages: number;
};
const DEFAULT_EXCLUDE_INITIAL_BALANCE = true;
const buildInitialBalanceVisibilityFilter = () =>
or(
ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
);
const buildTransactionsWhere = ({
filters,
extraFilters = [],
excludeInitialBalanceFromIncome = DEFAULT_EXCLUDE_INITIAL_BALANCE,
}: BaseTransactionQueryInput) => {
const whereFilters = [...filters, ...extraFilters];
if (excludeInitialBalanceFromIncome) {
const initialBalanceFilter = buildInitialBalanceVisibilityFilter();
if (initialBalanceFilter) {
whereFilters.push(initialBalanceFilter);
}
}
return and(...whereFilters);
};
const mapTransactionRows = (
transactionRows: {
transaction: typeof transactions.$inferSelect;
payer: typeof payers.$inferSelect | null;
financialAccount: typeof financialAccounts.$inferSelect | null;
card: typeof cards.$inferSelect | null;
category: typeof categories.$inferSelect | null;
}[],
) =>
transactionRows.map((row) => ({
...row.transaction,
payer: row.payer,
financialAccount: row.financialAccount,
card: row.card,
category: row.category,
}));
async function selectTransactionsWithRelations({
filters,
extraFilters = [],
excludeInitialBalanceFromIncome = DEFAULT_EXCLUDE_INITIAL_BALANCE,
limit,
offset,
}: TransactionQueryInput) {
const baseQuery = db
.select({
transaction: transactions,
payer: payers,
financialAccount: financialAccounts,
card: cards,
category: categories,
})
.from(transactions)
.leftJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(categories, eq(transactions.categoryId, categories.id))
.where(
buildTransactionsWhere({
filters,
extraFilters,
excludeInitialBalanceFromIncome,
}),
)
.orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
const transactionRows =
typeof limit === "number"
? await baseQuery.limit(limit).offset(offset ?? 0)
: await baseQuery;
return mapTransactionRows(transactionRows);
}
export async function fetchTransactionFilterSources(userId: string) {
const [payerRows, accountRows, cardRows, categoryRows] = await Promise.all([
db.query.payers.findMany({
@@ -31,44 +145,98 @@ export async function fetchTransactionFilterSources(userId: string) {
return { payerRows, accountRows, cardRows, categoryRows };
}
export async function fetchTransactionsWithRelations(
input: BaseTransactionQueryInput,
) {
return selectTransactionsWithRelations(input);
}
export async function fetchTransactions(filters: SQL[]) {
const transactionRows = await db
.select({
transaction: transactions,
payer: payers,
financialAccount: financialAccounts,
card: cards,
category: categories,
})
return fetchTransactionsWithRelations({ filters });
}
export async function fetchTransactionsPage(
filters: SQL[],
{
page,
pageSize,
}: {
page: number;
pageSize: number;
},
): Promise<PaginatedTransactionsResult> {
const [countRow] = await db
.select({ total: count() })
.from(transactions)
.leftJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(categories, eq(transactions.categoryId, categories.id))
.where(
and(
...filters,
// Excluir saldos iniciais de financialAccounts que têm excludeInitialBalanceFromIncome = true
or(
ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
),
),
)
.orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
.where(buildTransactionsWhere({ filters }));
// Transformar resultado para o formato esperado
return transactionRows.map((row) => ({
...row.transaction,
payer: row.payer,
financialAccount: row.financialAccount,
card: row.card,
category: row.category,
}));
const totalItems = Number(countRow?.total ?? 0);
const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
const currentPage = Math.min(page, totalPages);
const rows = await selectTransactionsWithRelations({
filters,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
});
return {
rows,
totalItems,
page: currentPage,
pageSize,
totalPages,
};
}
export async function fetchTransactionsPageWithRelations({
filters,
page,
pageSize,
extraFilters = [],
excludeInitialBalanceFromIncome = DEFAULT_EXCLUDE_INITIAL_BALANCE,
}: BaseTransactionQueryInput & {
page: number;
pageSize: number;
}): Promise<PaginatedTransactionsResult> {
const [countRow] = await db
.select({ total: count() })
.from(transactions)
.leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.leftJoin(cards, eq(transactions.cardId, cards.id))
.where(
buildTransactionsWhere({
filters,
extraFilters,
excludeInitialBalanceFromIncome,
}),
);
const totalItems = Number(countRow?.total ?? 0);
const totalPages = Math.max(Math.ceil(totalItems / pageSize), 1);
const currentPage = Math.min(page, totalPages);
const rows = await selectTransactionsWithRelations({
filters,
extraFilters,
excludeInitialBalanceFromIncome,
limit: pageSize,
offset: (currentPage - 1) * pageSize,
});
return {
rows,
totalItems,
page: currentPage,
pageSize,
totalPages,
};
}
export async function fetchRecentEstablishments(
@@ -84,22 +252,15 @@ export async function fetchRecentEstablishments(
and(
eq(transactions.userId, userId),
gte(transactions.purchaseDate, threeMonthsAgo),
sql`TRIM(${transactions.name}) <> ''`,
sql`LOWER(${transactions.name}) NOT LIKE 'pagamento fatura%'`,
),
)
.orderBy(desc(transactions.purchaseDate));
.groupBy(transactions.name)
.orderBy(sql`MAX(${transactions.purchaseDate}) DESC`)
.limit(100);
const uniqueNames = Array.from(
new Set<string>(
results
.map((row) => row.name)
.filter(
(name: string | null): name is string =>
name != null &&
name.trim().length > 0 &&
!name.toLowerCase().startsWith("pagamento fatura"),
),
),
);
return uniqueNames.slice(0, 100);
return results
.map((row) => row.name)
.filter((name): name is string => name !== null);
}