feat(wallets): add isDefault field to wallet model and implement setDefaultWallet functionality

This commit is contained in:
Jose Selesan
2026-06-17 10:15:44 -03:00
parent 798d657869
commit 5671b4a14b
11 changed files with 459 additions and 43 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "wallets" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -134,6 +134,7 @@ model Wallet {
name String @db.VarChar(100)
currency String @db.VarChar(10)
balance Decimal @db.Decimal(14, 4)
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -1,5 +1,5 @@
import { z } from "zod";
import { PaymentStatus, QuoteType, Prisma } from "../../generated/prisma/client";
import { PaymentStatus, QuoteType, type Prisma } from "../../generated/prisma/client";
import { cache } from "../../lib/cache";
import { prisma } from "../../lib/prisma";
import { roundTo } from "../../lib/utils";
@@ -17,11 +17,15 @@ export const createNonPeriodicExpenseSchema = z.object({
description: z.string().min(1).max(50),
amount: z.number().positive(),
dueDate: z.string().datetime(),
walletId: z.number().int().positive(),
usdcConversionRate: z.number().positive().optional(),
});
export const payExpenseSchema = z.object({
amountPayed: z.number().positive(),
paymentDate: z.string().datetime().optional(),
walletId: z.number().int().positive(),
usdcConversionRate: z.number().positive().optional(),
});
export const updateExpenseSchema = z.object({
@@ -226,23 +230,64 @@ export async function createNonPeriodicExpense(
const year = dueDate.getUTCFullYear();
const month = dueDate.getUTCMonth() + 1;
const belo = await getBeloSellPriceForDate(dueDate);
const result = await prisma.expense.create({
data: {
description: data.description,
amount: data.amount,
amountPayed: data.amount,
dueDate,
paymentDate: dueDate,
year,
month,
status: PaymentStatus.PAYED,
...(belo !== null
? { beloPrice: belo, usdcEquivalent: roundTo(data.amount / belo, 6) }
: {}),
},
const wallet = await prisma.wallet.findUnique({
where: { id: data.walletId },
});
if (!wallet) {
throw new Error("Billetera no encontrada");
}
const isUSDC = wallet.currency === "USDC";
if (isUSDC && !data.usdcConversionRate) {
throw new Error("La tasa de conversión es obligatoria para billeteras USDC");
}
const belo = isUSDC ? null : await getBeloSellPriceForDate(dueDate);
const movementAmount =
isUSDC && data.usdcConversionRate
? -roundTo(data.amount / data.usdcConversionRate, 4)
: -data.amount;
const expenseData: Prisma.ExpenseUncheckedCreateInput = {
description: data.description,
amount: data.amount,
amountPayed: data.amount,
dueDate,
paymentDate: dueDate,
year,
month,
status: PaymentStatus.PAYED,
};
if (isUSDC && data.usdcConversionRate) {
expenseData.beloPrice = data.usdcConversionRate;
expenseData.usdcEquivalent = roundTo(
data.amount / data.usdcConversionRate,
6,
);
} else if (belo !== null) {
expenseData.beloPrice = belo;
expenseData.usdcEquivalent = roundTo(data.amount / belo, 6);
}
const [result] = await prisma.$transaction([
prisma.expense.create({ data: expenseData }),
prisma.wallet.update({
where: { id: data.walletId },
data: { balance: { increment: movementAmount } },
}),
prisma.walletMovement.create({
data: {
walletId: data.walletId,
type: "WITHDRAWAL",
amount: movementAmount,
description: `Pago: ${data.description}`,
},
}),
]);
cache.invalidateByPrefix("expenses:");
return result;
}
@@ -251,23 +296,61 @@ export async function payExpense(
id: number,
data: z.infer<typeof payExpenseSchema>,
) {
const expense = await prisma.expense.findUniqueOrThrow({ where: { id } });
const paymentDate = data.paymentDate
? new Date(data.paymentDate)
: new Date();
const belo = await getBeloSellPriceForDate(paymentDate);
const result = await prisma.expense.update({
where: { id },
data: {
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate,
...(belo !== null
? { beloPrice: belo, usdcEquivalent: roundTo(data.amountPayed / belo, 6) }
: {}),
},
const wallet = await prisma.wallet.findUnique({
where: { id: data.walletId },
});
if (!wallet) {
throw new Error("Billetera no encontrada");
}
const isUSDC = wallet.currency === "USDC";
if (isUSDC && !data.usdcConversionRate) {
throw new Error("La tasa de conversión es obligatoria para billeteras USDC");
}
const belo = isUSDC ? null : await getBeloSellPriceForDate(paymentDate);
const movementAmount = isUSDC && data.usdcConversionRate
? -roundTo(data.amountPayed / data.usdcConversionRate, 4)
: -data.amountPayed;
const expenseUpdateData: Prisma.ExpenseUncheckedUpdateInput = {
status: PaymentStatus.PAYED,
amountPayed: data.amountPayed,
paymentDate,
};
if (isUSDC && data.usdcConversionRate) {
expenseUpdateData.beloPrice = data.usdcConversionRate;
expenseUpdateData.usdcEquivalent = roundTo(data.amountPayed / data.usdcConversionRate, 6);
} else if (belo !== null) {
expenseUpdateData.beloPrice = belo;
expenseUpdateData.usdcEquivalent = roundTo(data.amountPayed / belo, 6);
}
const [result] = await prisma.$transaction([
prisma.expense.update({ where: { id }, data: expenseUpdateData }),
prisma.wallet.update({
where: { id: data.walletId },
data: { balance: { increment: movementAmount } },
}),
prisma.walletMovement.create({
data: {
walletId: data.walletId,
type: "WITHDRAWAL",
amount: movementAmount,
description: `Pago: ${expense.description}`,
},
}),
]);
cache.invalidateByPrefix("expenses:");
return result;
}

View File

@@ -0,0 +1,8 @@
import type { Context } from "hono";
import { setDefaultWallet } from "../wallets.service";
export async function setDefaultWalletHandler(c: Context) {
const id = Number(c.req.param("id"));
const result = await setDefaultWallet(id);
return c.json(result);
}

View File

@@ -7,6 +7,7 @@ import { listMovementsHandler } from "./handlers/listMovements";
import { listWalletsHandler } from "./handlers/listWallets";
import { transferWalletHandler } from "./handlers/transferWallet";
import { updateWalletHandler } from "./handlers/updateWallet";
import { setDefaultWalletHandler } from "./handlers/setDefaultWallet";
const app = new Hono();
@@ -17,6 +18,7 @@ app.delete("/:id", deleteWalletHandler);
app.post("/:id/deposit", depositWalletHandler);
app.post("/:id/adjust", adjustBalanceHandler);
app.post("/transfer", transferWalletHandler);
app.put("/:id/default", setDefaultWalletHandler);
app.get("/:id/movements", listMovementsHandler);
export default app;

View File

@@ -6,6 +6,7 @@ export const createWalletSchema = z.object({
name: z.string().min(1).max(100),
currency: z.string().min(1).max(10),
initialBalance: z.number().optional().default(0),
isDefault: z.boolean().optional().default(false),
});
export const updateWalletSchema = z.object({
@@ -13,6 +14,10 @@ export const updateWalletSchema = z.object({
currency: z.string().min(1).max(10),
});
export const setDefaultWalletSchema = z.object({
walletId: z.number().int().positive(),
});
export const depositSchema = z.object({
amount: z.number().positive(),
description: z.string().max(255).optional(),
@@ -36,9 +41,12 @@ export type UpdateWalletInput = z.infer<typeof updateWalletSchema>;
export type DepositInput = z.infer<typeof depositSchema>;
export type AdjustBalanceInput = z.infer<typeof adjustBalanceSchema>;
export type TransferInput = z.infer<typeof transferSchema>;
export type SetDefaultWalletInput = z.infer<typeof setDefaultWalletSchema>;
export async function listWallets() {
const wallets = await prisma.wallet.findMany({ orderBy: { createdAt: "desc" } });
const wallets = await prisma.wallet.findMany({
orderBy: [{ isDefault: "desc" }, { createdAt: "desc" }],
});
return wallets.map((w) => ({ ...w, balance: Number(w.balance) }));
}
@@ -48,8 +56,15 @@ export async function createWallet(input: CreateWalletInput) {
name: input.name,
currency: input.currency,
balance: input.initialBalance ?? 0,
isDefault: input.isDefault ?? false,
},
});
if (wallet.isDefault) {
await prisma.wallet.updateMany({
where: { id: { not: wallet.id }, isDefault: true },
data: { isDefault: false },
});
}
return { ...wallet, balance: Number(wallet.balance) };
}
@@ -155,6 +170,21 @@ export async function transferWallet(input: TransferInput) {
};
}
export async function setDefaultWallet(walletId: number) {
await prisma.$transaction([
prisma.wallet.updateMany({
where: { isDefault: true },
data: { isDefault: false },
}),
prisma.wallet.update({
where: { id: walletId },
data: { isDefault: true },
}),
]);
const wallet = await prisma.wallet.findUniqueOrThrow({ where: { id: walletId } });
return { ...wallet, balance: Number(wallet.balance) };
}
export async function listMovements(walletId: number) {
const movements = await prisma.walletMovement.findMany({
where: { walletId },