feat(wallets): add isDefault field to wallet model and implement setDefaultWallet functionality
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "wallets" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user