refactor: alinha features financeiras ao novo naming

This commit is contained in:
Felipe Coutinho
2026-03-14 12:50:55 +00:00
parent ef918a3667
commit 67ad4b9d02
51 changed files with 876 additions and 898 deletions

View File

@@ -2,7 +2,7 @@
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { cartoes, contas } from "@/db/schema";
import { cards, financialAccounts } from "@/db/schema";
import {
type ActionResult,
handleActionError,
@@ -40,7 +40,7 @@ const cardBaseSchema = z.object({
.string({ message: "Selecione um logo." })
.trim()
.min(1, "Selecione um logo."),
contaId: uuidSchema("Conta"),
accountId: uuidSchema("FinancialAccount"),
});
const createCardSchema = cardBaseSchema;
@@ -55,14 +55,17 @@ type CardCreateInput = z.infer<typeof createCardSchema>;
type CardUpdateInput = z.infer<typeof updateCardSchema>;
type CardDeleteInput = z.infer<typeof deleteCardSchema>;
async function assertAccountOwnership(userId: string, contaId: string) {
const account = await db.query.contas.findFirst({
async function assertAccountOwnership(userId: string, accountId: string) {
const account = await db.query.financialAccounts.findFirst({
columns: { id: true },
where: and(eq(contas.id, contaId), eq(contas.userId, userId)),
where: and(
eq(financialAccounts.id, accountId),
eq(financialAccounts.userId, userId),
),
});
if (!account) {
throw new Error("Conta vinculada não encontrada.");
throw new Error("FinancialAccount vinculada não encontrada.");
}
}
@@ -73,11 +76,11 @@ export async function createCardAction(
const user = await getUser();
const data = createCardSchema.parse(input);
await assertAccountOwnership(user.id, data.contaId);
await assertAccountOwnership(user.id, data.accountId);
const logoFile = normalizeFilePath(data.logo);
await db.insert(cartoes).values({
await db.insert(cards).values({
name: data.name,
brand: data.brand,
status: data.status,
@@ -86,11 +89,11 @@ export async function createCardAction(
note: data.note ?? null,
limit: formatDecimalForDb(data.limit),
logo: logoFile,
contaId: data.contaId,
accountId: data.accountId,
userId: user.id,
});
revalidateForEntity("cartoes");
revalidateForEntity("cards");
return { success: true, message: "Cartão criado com sucesso." };
} catch (error) {
@@ -105,12 +108,12 @@ export async function updateCardAction(
const user = await getUser();
const data = updateCardSchema.parse(input);
await assertAccountOwnership(user.id, data.contaId);
await assertAccountOwnership(user.id, data.accountId);
const logoFile = normalizeFilePath(data.logo);
const [updated] = await db
.update(cartoes)
.update(cards)
.set({
name: data.name,
brand: data.brand,
@@ -120,9 +123,9 @@ export async function updateCardAction(
note: data.note ?? null,
limit: formatDecimalForDb(data.limit),
logo: logoFile,
contaId: data.contaId,
accountId: data.accountId,
})
.where(and(eq(cartoes.id, data.id), eq(cartoes.userId, user.id)))
.where(and(eq(cards.id, data.id), eq(cards.userId, user.id)))
.returning();
if (!updated) {
@@ -132,7 +135,7 @@ export async function updateCardAction(
};
}
revalidateForEntity("cartoes");
revalidateForEntity("cards");
return { success: true, message: "Cartão atualizado com sucesso." };
} catch (error) {
@@ -148,9 +151,9 @@ export async function deleteCardAction(
const data = deleteCardSchema.parse(input);
const [deleted] = await db
.delete(cartoes)
.where(and(eq(cartoes.id, data.id), eq(cartoes.userId, user.id)))
.returning({ id: cartoes.id });
.delete(cards)
.where(and(eq(cards.id, data.id), eq(cards.userId, user.id)))
.returning({ id: cards.id });
if (!deleted) {
return {
@@ -159,7 +162,7 @@ export async function deleteCardAction(
};
}
revalidateForEntity("cartoes");
revalidateForEntity("cards");
return { success: true, message: "Cartão removido com sucesso." };
} catch (error) {

View File

@@ -70,7 +70,7 @@ const buildInitialValues = ({
limit: formatLimitInput(card?.limit ?? null),
note: card?.note ?? "",
logo: selectedLogo,
contaId: card?.contaId ?? accounts[0]?.id ?? "",
accountId: card?.accountId ?? accounts[0]?.id ?? "",
};
};
@@ -146,7 +146,7 @@ export function CardDialog({
return;
}
if (!formState.contaId) {
if (!formState.accountId) {
const message = "Selecione a conta vinculada.";
setErrorMessage(message);
toast.error(message);
@@ -163,7 +163,7 @@ export function CardDialog({
limit: rawLimit ? Number(rawLimit) : null,
note: formState.note.trim() || null,
logo: formState.logo,
contaId: formState.contaId,
accountId: formState.accountId,
};
if (!payload.logo) {

View File

@@ -160,10 +160,10 @@ export function CardFormFields({
</div>
<div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-account">Conta vinculada</Label>
<Label htmlFor="card-account">FinancialAccount vinculada</Label>
<Select
value={values.contaId}
onValueChange={(value) => onChange("contaId", value)}
value={values.accountId}
onValueChange={(value) => onChange("accountId", value)}
disabled={accountOptions.length === 0}
>
<SelectTrigger id="card-account" className="w-full">
@@ -174,10 +174,10 @@ export function CardFormFields({
: "Selecione a conta"
}
>
{values.contaId &&
{values.accountId &&
(() => {
const selectedAccount = accountOptions.find(
(acc) => acc.id === values.contaId,
(acc) => acc.id === values.accountId,
);
return selectedAccount ? (
<AccountSelectContent

View File

@@ -33,7 +33,7 @@ interface CardItemProps {
limit: number | null;
limitInUse?: number | null;
limitAvailable?: number | null;
contaName: string;
accountName: string;
logo?: string | null;
note?: string | null;
onEdit?: () => void;
@@ -52,14 +52,14 @@ export function CardItem({
limit,
limitInUse,
limitAvailable,
contaName: _contaName,
accountName: _accountName,
logo,
note,
onEdit,
onInvoice,
onRemove,
}: CardItemProps) {
void _contaName;
void _accountName;
const limitTotal = limit ?? null;
const used =

View File

@@ -142,7 +142,7 @@ export function CardsPage({
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName}
accountName={card.accountName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}

View File

@@ -8,10 +8,10 @@ export type Card = {
note: string | null;
logo: string | null;
limit: number | null;
contaId: string;
contaName: string;
limitInUse?: number | null;
limitAvailable?: number | null;
accountId: string;
accountName: string;
limitInUse: number;
limitAvailable: number | null;
};
export type CardFormValues = {
@@ -23,5 +23,5 @@ export type CardFormValues = {
limit: string;
note: string;
logo: string;
contaId: string;
accountId: string;
};

View File

@@ -1,22 +1,22 @@
import { and, eq, ilike, isNull, ne, not, or, sql } from "drizzle-orm";
import { cartoes, contas, lancamentos } from "@/db/schema";
import { cards, financialAccounts, transactions } from "@/db/schema";
import { db } from "@/shared/lib/db";
import { loadLogoOptions } from "@/shared/lib/logo/options";
export type CardData = {
id: string;
name: string;
brand: string | null;
status: string | null;
closingDay: number;
dueDay: number;
brand: string;
status: string;
closingDay: string;
dueDay: string;
note: string | null;
logo: string | null;
limit: number | null;
limitInUse: number;
limitAvailable: number | null;
contaId: string;
contaName: string;
accountId: string;
accountName: string;
};
export type AccountSimple = {
@@ -28,20 +28,14 @@ export type AccountSimple = {
export async function fetchCardsForUser(userId: string): Promise<{
cards: CardData[];
accounts: AccountSimple[];
logoOptions: LogoOption[];
logoOptions: string[];
}> {
const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([
db.query.cartoes.findMany({
orderBy: (
card: typeof cartoes.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(card.name)],
where: and(
eq(cartoes.userId, userId),
not(ilike(cartoes.status, "inativo")),
),
db.query.cards.findMany({
orderBy: (table, { desc }) => [desc(table.name)],
where: and(eq(cards.userId, userId), not(ilike(cards.status, "inativo"))),
with: {
conta: {
financialAccount: {
columns: {
id: true,
name: true,
@@ -49,12 +43,9 @@ export async function fetchCardsForUser(userId: string): Promise<{
},
},
}),
db.query.contas.findMany({
orderBy: (
account: typeof contas.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(account.name)],
where: eq(contas.userId, userId),
db.query.financialAccounts.findMany({
orderBy: (table, { desc }) => [desc(table.name)],
where: eq(financialAccounts.userId, userId),
columns: {
id: true,
name: true,
@@ -64,37 +55,35 @@ export async function fetchCardsForUser(userId: string): Promise<{
loadLogoOptions(),
db
.select({
cartaoId: lancamentos.cartaoId,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
cardId: transactions.cardId,
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
})
.from(lancamentos)
.from(transactions)
.where(
and(
eq(lancamentos.userId, userId),
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)),
eq(transactions.userId, userId),
or(isNull(transactions.isSettled), eq(transactions.isSettled, false)),
// Recorrente no cartão: só consome limite quando a data da ocorrência já passou
or(
ne(lancamentos.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`,
ne(transactions.condition, "Recorrente"),
sql`${transactions.purchaseDate} <= current_date`,
),
),
)
.groupBy(lancamentos.cartaoId),
.groupBy(transactions.cardId),
]);
const usageMap = new Map<string, number>();
usageRows.forEach(
(row: { cartaoId: string | null; total: number | null }) => {
if (!row.cartaoId) return;
usageMap.set(row.cartaoId, Number(row.total ?? 0));
},
);
usageRows.forEach((row: { cardId: string | null; total: number | null }) => {
if (!row.cardId) return;
usageMap.set(row.cardId, Number(row.total ?? 0));
});
const cards = cardRows.map((card) => ({
const cardList = cardRows.map((card) => ({
id: card.id,
name: card.name,
brand: card.brand,
status: card.status,
brand: card.brand ?? "",
status: card.status ?? "",
closingDay: card.closingDay,
dueDay: card.dueDay,
note: card.note,
@@ -112,8 +101,10 @@ export async function fetchCardsForUser(userId: string): Promise<{
const inUse = total < 0 ? Math.abs(total) : 0;
return Math.max(Number(card.limit) - inUse, 0);
})(),
contaId: card.contaId,
contaName: card.conta?.name ?? "Conta não encontrada",
accountId: card.accountId,
accountName:
(card.financialAccount as { name?: string } | null)?.name ??
"Conta não encontrada",
}));
const accounts = accountRows.map((account) => ({
@@ -122,23 +113,20 @@ export async function fetchCardsForUser(userId: string): Promise<{
logo: account.logo,
}));
return { cards, accounts, logoOptions };
return { cards: cardList, accounts, logoOptions };
}
export async function fetchInativosForUser(userId: string): Promise<{
export async function fetchInactiveForUser(userId: string): Promise<{
cards: CardData[];
accounts: AccountSimple[];
logoOptions: LogoOption[];
logoOptions: string[];
}> {
const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([
db.query.cartoes.findMany({
orderBy: (
card: typeof cartoes.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(card.name)],
where: and(eq(cartoes.userId, userId), ilike(cartoes.status, "inativo")),
db.query.cards.findMany({
orderBy: (table, { desc }) => [desc(table.name)],
where: and(eq(cards.userId, userId), ilike(cards.status, "inativo")),
with: {
conta: {
financialAccount: {
columns: {
id: true,
name: true,
@@ -146,12 +134,9 @@ export async function fetchInativosForUser(userId: string): Promise<{
},
},
}),
db.query.contas.findMany({
orderBy: (
account: typeof contas.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(account.name)],
where: eq(contas.userId, userId),
db.query.financialAccounts.findMany({
orderBy: (table, { desc }) => [desc(table.name)],
where: eq(financialAccounts.userId, userId),
columns: {
id: true,
name: true,
@@ -161,37 +146,35 @@ export async function fetchInativosForUser(userId: string): Promise<{
loadLogoOptions(),
db
.select({
cartaoId: lancamentos.cartaoId,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
cardId: transactions.cardId,
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
})
.from(lancamentos)
.from(transactions)
.where(
and(
eq(lancamentos.userId, userId),
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)),
eq(transactions.userId, userId),
or(isNull(transactions.isSettled), eq(transactions.isSettled, false)),
// Recorrente no cartão: só consome limite quando a data da ocorrência já passou
or(
ne(lancamentos.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`,
ne(transactions.condition, "Recorrente"),
sql`${transactions.purchaseDate} <= current_date`,
),
),
)
.groupBy(lancamentos.cartaoId),
.groupBy(transactions.cardId),
]);
const usageMap = new Map<string, number>();
usageRows.forEach(
(row: { cartaoId: string | null; total: number | null }) => {
if (!row.cartaoId) return;
usageMap.set(row.cartaoId, Number(row.total ?? 0));
},
);
usageRows.forEach((row: { cardId: string | null; total: number | null }) => {
if (!row.cardId) return;
usageMap.set(row.cardId, Number(row.total ?? 0));
});
const cards = cardRows.map((card) => ({
const cardList = cardRows.map((card) => ({
id: card.id,
name: card.name,
brand: card.brand,
status: card.status,
brand: card.brand ?? "",
status: card.status ?? "",
closingDay: card.closingDay,
dueDay: card.dueDay,
note: card.note,
@@ -209,8 +192,10 @@ export async function fetchInativosForUser(userId: string): Promise<{
const inUse = total < 0 ? Math.abs(total) : 0;
return Math.max(Number(card.limit) - inUse, 0);
})(),
contaId: card.contaId,
contaName: card.conta?.name ?? "Conta não encontrada",
accountId: card.accountId,
accountName:
(card.financialAccount as { name?: string } | null)?.name ??
"Conta não encontrada",
}));
const accounts = accountRows.map((account) => ({
@@ -219,18 +204,18 @@ export async function fetchInativosForUser(userId: string): Promise<{
logo: account.logo,
}));
return { cards, accounts, logoOptions };
return { cards: cardList, accounts, logoOptions };
}
export async function fetchAllCardsForUser(userId: string): Promise<{
activeCards: CardData[];
archivedCards: CardData[];
accounts: AccountSimple[];
logoOptions: LogoOption[];
logoOptions: string[];
}> {
const [activeData, archivedData] = await Promise.all([
fetchCardsForUser(userId),
fetchInativosForUser(userId),
fetchInactiveForUser(userId),
]);
return {