diff options
| author | Lucas Faria Mendes <lucas.fariamo08@gmail.com> | 2026-08-16 23:17:42 +0000 |
|---|---|---|
| committer | Lucas Faria Mendes <lucas.fariamo08@gmail.com> | 2026-08-16 23:17:42 +0000 |
| commit | 375482e654a01615683867e732ac9e458c7cbe74 (patch) | |
| tree | 12d1b31b8deb6bf8590bcdbc2cfb52dcc553e006 /api/src/modules/incomes | |
| parent | 2c2dfba45357bbd205a59145ae55a2779c911ae0 (diff) | |
| download | yaba-375482e654a01615683867e732ac9e458c7cbe74.tar.gz yaba-375482e654a01615683867e732ac9e458c7cbe74.zip | |
Signed-off-by: Lucas Faria Mendes <lucas.fariamo08@gmail.com>
Diffstat (limited to 'api/src/modules/incomes')
| -rw-r--r-- | api/src/modules/incomes/incomes.routes.ts | 97 | ||||
| -rw-r--r-- | api/src/modules/incomes/incomes.schema.ts | 38 | ||||
| -rw-r--r-- | api/src/modules/incomes/incomes.service.ts | 83 |
3 files changed, 218 insertions, 0 deletions
diff --git a/api/src/modules/incomes/incomes.routes.ts b/api/src/modules/incomes/incomes.routes.ts new file mode 100644 index 0000000..7932d18 --- /dev/null +++ b/api/src/modules/incomes/incomes.routes.ts @@ -0,0 +1,97 @@ +import type { FastifyPluginAsync } from 'fastify'; +import { ZodError } from 'zod'; +import { + createIncomeSchema, + incomeParamsSchema, + incomeQuerySchema, + incomeUserParamsSchema, + updateIncomeSchema, +} from './incomes.schema'; +import * as incomesService from './incomes.service'; + +const incomesRoutes: FastifyPluginAsync = async (fastify) => { + /** + * GET /users/:userId/incomes + * Lista todas as rendas de um usuário. + * Query params: ?month=6&year=2025 + */ + fastify.get<{ Params: { userId: string }; Querystring: Record<string, string> }>( + '/users/:userId/incomes', + async (request, reply) => { + const { userId } = incomeUserParamsSchema.parse(request.params); + const query = incomeQuerySchema.parse(request.query); + const result = await incomesService.findIncomesByUser(userId, query); + return reply.send({ data: result }); + }, + ); + + /** GET /users/:userId/incomes/:id — Busca uma renda específica */ + fastify.get<{ Params: { userId: string; id: string } }>( + '/users/:userId/incomes/:id', + async (request, reply) => { + const { userId, id } = incomeParamsSchema.parse(request.params); + const income = await incomesService.findIncomeById(userId, id); + + if (!income) { + return reply.status(404).send({ error: 'Renda não encontrada.' }); + } + return reply.send({ data: income }); + }, + ); + + /** POST /users/:userId/incomes — Registra a renda de um mês */ + fastify.post<{ Params: { userId: string } }>( + '/users/:userId/incomes', + async (request, reply) => { + try { + const { userId } = incomeUserParamsSchema.parse(request.params); + const body = createIncomeSchema.parse(request.body); + const income = await incomesService.createIncome(userId, body); + return reply.status(201).send({ data: income }); + } catch (err) { + if (err instanceof ZodError) { + return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors }); + } + throw err; + } + }, + ); + + /** PUT /users/:userId/incomes/:id — Atualiza uma renda */ + fastify.put<{ Params: { userId: string; id: string } }>( + '/users/:userId/incomes/:id', + async (request, reply) => { + try { + const { userId, id } = incomeParamsSchema.parse(request.params); + const body = updateIncomeSchema.parse(request.body); + const income = await incomesService.updateIncome(userId, id, body); + + if (!income) { + return reply.status(404).send({ error: 'Renda não encontrada.' }); + } + return reply.send({ data: income }); + } catch (err) { + if (err instanceof ZodError) { + return reply.status(400).send({ error: 'Dados inválidos.', details: err.errors }); + } + throw err; + } + }, + ); + + /** DELETE /users/:userId/incomes/:id — Remove uma renda */ + fastify.delete<{ Params: { userId: string; id: string } }>( + '/users/:userId/incomes/:id', + async (request, reply) => { + const { userId, id } = incomeParamsSchema.parse(request.params); + const deleted = await incomesService.deleteIncome(userId, id); + + if (!deleted) { + return reply.status(404).send({ error: 'Renda não encontrada.' }); + } + return reply.status(200).send({ data: deleted, message: 'Renda removida com sucesso.' }); + }, + ); +}; + +export default incomesRoutes; diff --git a/api/src/modules/incomes/incomes.schema.ts b/api/src/modules/incomes/incomes.schema.ts new file mode 100644 index 0000000..cc661fd --- /dev/null +++ b/api/src/modules/incomes/incomes.schema.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; + +export const createIncomeSchema = z.object({ + gross_salary: z.number().positive('Salário bruto deve ser positivo'), + /** + * Salário líquido efetivo (após override do usuário). + * Deve ser enviado pelo app após o cálculo de INSS/IRRF. + */ + calculated_net_salary: z.number().nonnegative(), + effective_net_salary: z.number().nonnegative(), + reference_month: z.number().int().min(1).max(12), + reference_year: z.number().int().min(2020).max(2100), + description: z.string().max(255).nullable().optional(), +}); + +export const updateIncomeSchema = z.object({ + gross_salary: z.number().positive().optional(), + calculated_net_salary: z.number().nonnegative().optional(), + effective_net_salary: z.number().nonnegative().optional(), + description: z.string().max(255).nullable().optional(), +}); + +export const incomeParamsSchema = z.object({ + userId: z.coerce.number().int().positive(), + id: z.coerce.number().int().positive(), +}); + +export const incomeUserParamsSchema = z.object({ + userId: z.coerce.number().int().positive(), +}); + +export const incomeQuerySchema = z.object({ + month: z.coerce.number().int().min(1).max(12).optional(), + year: z.coerce.number().int().min(2020).optional(), +}); + +export type CreateIncomeInput = z.infer<typeof createIncomeSchema>; +export type UpdateIncomeInput = z.infer<typeof updateIncomeSchema>; diff --git a/api/src/modules/incomes/incomes.service.ts b/api/src/modules/incomes/incomes.service.ts new file mode 100644 index 0000000..0a9ea84 --- /dev/null +++ b/api/src/modules/incomes/incomes.service.ts @@ -0,0 +1,83 @@ +import { and, eq } from 'drizzle-orm'; +import { db } from '@/db/client'; +import { incomes } from '@/db/schema'; +import type { CreateIncomeInput, UpdateIncomeInput } from './incomes.schema'; + +export async function findIncomesByUser( + userId: number, + filters?: { month?: number; year?: number }, +) { + const conditions = [eq(incomes.user_id, userId)]; + + if (filters?.month !== undefined) { + conditions.push(eq(incomes.reference_month, filters.month)); + } + if (filters?.year !== undefined) { + conditions.push(eq(incomes.reference_year, filters.year)); + } + + return db + .select() + .from(incomes) + .where(and(...conditions)) + .orderBy(incomes.reference_year, incomes.reference_month); +} + +export async function findIncomeById(userId: number, id: number) { + const result = await db + .select() + .from(incomes) + .where(and(eq(incomes.user_id, userId), eq(incomes.id, id))) + .limit(1); + return result[0] ?? null; +} + +export async function createIncome(userId: number, data: CreateIncomeInput) { + const [created] = await db + .insert(incomes) + .values({ + user_id: userId, + gross_salary: data.gross_salary, + calculated_net_salary: data.calculated_net_salary, + effective_net_salary: data.effective_net_salary, + reference_month: data.reference_month, + reference_year: data.reference_year, + description: data.description ?? null, + }) + .returning(); + return created; +} + +export async function updateIncome( + userId: number, + id: number, + data: UpdateIncomeInput, +) { + const now = new Date().toISOString(); + + const [updated] = await db + .update(incomes) + .set({ + ...(data.gross_salary !== undefined && { gross_salary: data.gross_salary }), + ...(data.calculated_net_salary !== undefined && { + calculated_net_salary: data.calculated_net_salary, + }), + ...(data.effective_net_salary !== undefined && { + effective_net_salary: data.effective_net_salary, + }), + ...(data.description !== undefined && { description: data.description }), + updated_at: now, + }) + .where(and(eq(incomes.user_id, userId), eq(incomes.id, id))) + .returning(); + + return updated ?? null; +} + +export async function deleteIncome(userId: number, id: number) { + const [deleted] = await db + .delete(incomes) + .where(and(eq(incomes.user_id, userId), eq(incomes.id, id))) + .returning(); + return deleted ?? null; +} |