import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { accountsService, CreateAccountDto, UpdateAccountDto, AccountFilters } from './accounts.service.js'; import { journalsService, CreateJournalDto, UpdateJournalDto, JournalFilters } from './journals.service.js'; import { journalEntriesService, CreateJournalEntryDto, UpdateJournalEntryDto, JournalEntryFilters } from './journal-entries.service.js'; import { invoicesService, CreateInvoiceDto, UpdateInvoiceDto, CreateInvoiceLineDto, UpdateInvoiceLineDto, InvoiceFilters } from './invoices.service.js'; import { paymentsService, CreatePaymentDto, UpdatePaymentDto, ReconcileDto, PaymentFilters } from './payments.service.js'; import { taxesService, CreateTaxDto, UpdateTaxDto, TaxFilters } from './taxes.service.js'; import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; import { ValidationError } from '../../shared/errors/index.js'; // Schemas - Accounts use camelCase DTOs const createAccountSchema = z.object({ companyId: z.string().uuid(), code: z.string().min(1).max(50), name: z.string().min(1).max(255), accountTypeId: z.string().uuid(), parentId: z.string().uuid().optional(), currencyId: z.string().uuid().optional(), isReconcilable: z.boolean().default(false), notes: z.string().optional(), }); const updateAccountSchema = z.object({ name: z.string().min(1).max(255).optional(), parentId: z.string().uuid().optional().nullable(), currencyId: z.string().uuid().optional().nullable(), isReconcilable: z.boolean().optional(), isDeprecated: z.boolean().optional(), notes: z.string().optional().nullable(), }); const accountQuerySchema = z.object({ companyId: z.string().uuid().optional(), accountTypeId: z.string().uuid().optional(), parentId: z.string().optional(), isDeprecated: z.coerce.boolean().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(50), }); // Journals and Journal Entries use snake_case DTOs const createJournalSchema = z.object({ company_id: z.string().uuid(), name: z.string().min(1).max(255), code: z.string().min(1).max(20), journal_type: z.enum(['sale', 'purchase', 'cash', 'bank', 'general']), default_account_id: z.string().uuid().optional(), sequence_id: z.string().uuid().optional(), currency_id: z.string().uuid().optional(), }); const updateJournalSchema = z.object({ name: z.string().min(1).max(255).optional(), default_account_id: z.string().uuid().optional().nullable(), sequence_id: z.string().uuid().optional().nullable(), currency_id: z.string().uuid().optional().nullable(), active: z.boolean().optional(), }); const journalQuerySchema = z.object({ company_id: z.string().uuid().optional(), journal_type: z.enum(['sale', 'purchase', 'cash', 'bank', 'general']).optional(), active: z.coerce.boolean().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(50), }); const journalEntryLineSchema = z.object({ account_id: z.string().uuid(), partner_id: z.string().uuid().optional(), debit: z.number().min(0).default(0), credit: z.number().min(0).default(0), description: z.string().optional(), ref: z.string().optional(), }); const createJournalEntrySchema = z.object({ company_id: z.string().uuid(), journal_id: z.string().uuid(), name: z.string().min(1).max(100), ref: z.string().max(255).optional(), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), notes: z.string().optional(), lines: z.array(journalEntryLineSchema).min(2), }); const updateJournalEntrySchema = z.object({ ref: z.string().max(255).optional().nullable(), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), notes: z.string().optional().nullable(), lines: z.array(journalEntryLineSchema).min(2).optional(), }); const journalEntryQuerySchema = z.object({ company_id: z.string().uuid().optional(), journal_id: z.string().uuid().optional(), status: z.enum(['draft', 'posted', 'cancelled']).optional(), date_from: z.string().optional(), date_to: z.string().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); // ========== INVOICE SCHEMAS ========== const createInvoiceSchema = z.object({ company_id: z.string().uuid(), partner_id: z.string().uuid(), invoice_type: z.enum(['customer', 'supplier']), currency_id: z.string().uuid(), invoice_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), due_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), payment_term_id: z.string().uuid().optional(), journal_id: z.string().uuid().optional(), ref: z.string().optional(), notes: z.string().optional(), }); const updateInvoiceSchema = z.object({ partner_id: z.string().uuid().optional(), currency_id: z.string().uuid().optional(), invoice_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), due_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(), payment_term_id: z.string().uuid().optional().nullable(), journal_id: z.string().uuid().optional().nullable(), ref: z.string().optional().nullable(), notes: z.string().optional().nullable(), }); const invoiceQuerySchema = z.object({ company_id: z.string().uuid().optional(), partner_id: z.string().uuid().optional(), invoice_type: z.enum(['customer', 'supplier']).optional(), status: z.enum(['draft', 'open', 'paid', 'cancelled']).optional(), date_from: z.string().optional(), date_to: z.string().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); const createInvoiceLineSchema = z.object({ product_id: z.string().uuid().optional(), description: z.string().min(1), quantity: z.number().positive(), uom_id: z.string().uuid().optional(), price_unit: z.number().min(0), tax_ids: z.array(z.string().uuid()).optional(), account_id: z.string().uuid().optional(), }); const updateInvoiceLineSchema = z.object({ product_id: z.string().uuid().optional().nullable(), description: z.string().min(1).optional(), quantity: z.number().positive().optional(), uom_id: z.string().uuid().optional().nullable(), price_unit: z.number().min(0).optional(), tax_ids: z.array(z.string().uuid()).optional(), account_id: z.string().uuid().optional().nullable(), }); // ========== PAYMENT SCHEMAS ========== const createPaymentSchema = z.object({ company_id: z.string().uuid(), partner_id: z.string().uuid(), payment_type: z.enum(['inbound', 'outbound']), payment_method: z.enum(['cash', 'bank_transfer', 'check', 'card', 'other']), amount: z.number().positive(), currency_id: z.string().uuid(), payment_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), ref: z.string().optional(), journal_id: z.string().uuid(), notes: z.string().optional(), }); const updatePaymentSchema = z.object({ partner_id: z.string().uuid().optional(), payment_method: z.enum(['cash', 'bank_transfer', 'check', 'card', 'other']).optional(), amount: z.number().positive().optional(), currency_id: z.string().uuid().optional(), payment_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), ref: z.string().optional().nullable(), journal_id: z.string().uuid().optional(), notes: z.string().optional().nullable(), }); const reconcilePaymentSchema = z.object({ invoices: z.array(z.object({ invoice_id: z.string().uuid(), amount: z.number().positive(), })).min(1), }); const paymentQuerySchema = z.object({ company_id: z.string().uuid().optional(), partner_id: z.string().uuid().optional(), payment_type: z.enum(['inbound', 'outbound']).optional(), payment_method: z.enum(['cash', 'bank_transfer', 'check', 'card', 'other']).optional(), status: z.enum(['draft', 'posted', 'reconciled', 'cancelled']).optional(), date_from: z.string().optional(), date_to: z.string().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); // ========== TAX SCHEMAS ========== const createTaxSchema = z.object({ company_id: z.string().uuid(), name: z.string().min(1).max(100), code: z.string().min(1).max(20), tax_type: z.enum(['sales', 'purchase', 'all']), amount: z.number().min(0).max(100), included_in_price: z.boolean().default(false), }); const updateTaxSchema = z.object({ name: z.string().min(1).max(100).optional(), code: z.string().min(1).max(20).optional(), tax_type: z.enum(['sales', 'purchase', 'all']).optional(), amount: z.number().min(0).max(100).optional(), included_in_price: z.boolean().optional(), active: z.boolean().optional(), }); const taxQuerySchema = z.object({ company_id: z.string().uuid().optional(), tax_type: z.enum(['sales', 'purchase', 'all']).optional(), active: z.coerce.boolean().optional(), search: z.string().optional(), page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().positive().max(100).default(20), }); class FinancialController { // ========== ACCOUNT TYPES ========== async getAccountTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const accountTypes = await accountsService.findAllAccountTypes(); res.json({ success: true, data: accountTypes }); } catch (error) { next(error); } } // ========== ACCOUNTS ========== async getAccounts(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = accountQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: AccountFilters = queryResult.data; const result = await accountsService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, }); } catch (error) { next(error); } } async getAccount(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const account = await accountsService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: account }); } catch (error) { next(error); } } async createAccount(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createAccountSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de cuenta inválidos', parseResult.error.errors); } const dto: CreateAccountDto = parseResult.data; const account = await accountsService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: account, message: 'Cuenta creada exitosamente' }); } catch (error) { next(error); } } async updateAccount(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateAccountSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de cuenta inválidos', parseResult.error.errors); } const dto: UpdateAccountDto = parseResult.data; const account = await accountsService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: account, message: 'Cuenta actualizada exitosamente' }); } catch (error) { next(error); } } async deleteAccount(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await accountsService.delete(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, message: 'Cuenta eliminada exitosamente' }); } catch (error) { next(error); } } async getAccountBalance(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const balance = await accountsService.getBalance(req.params.id, req.tenantId!); res.json({ success: true, data: balance }); } catch (error) { next(error); } } // ========== JOURNALS ========== async getJournals(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = journalQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: JournalFilters = queryResult.data; const result = await journalsService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 50)) }, }); } catch (error) { next(error); } } async getJournal(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const journal = await journalsService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: journal }); } catch (error) { next(error); } } async createJournal(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createJournalSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de diario inválidos', parseResult.error.errors); } const dto: CreateJournalDto = parseResult.data; const journal = await journalsService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: journal, message: 'Diario creado exitosamente' }); } catch (error) { next(error); } } async updateJournal(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateJournalSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de diario inválidos', parseResult.error.errors); } const dto: UpdateJournalDto = parseResult.data; const journal = await journalsService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: journal, message: 'Diario actualizado exitosamente' }); } catch (error) { next(error); } } async deleteJournal(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await journalsService.delete(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, message: 'Diario eliminado exitosamente' }); } catch (error) { next(error); } } // ========== JOURNAL ENTRIES ========== async getJournalEntries(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = journalEntryQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: JournalEntryFilters = queryResult.data; const result = await journalEntriesService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const entry = await journalEntriesService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: entry }); } catch (error) { next(error); } } async createJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createJournalEntrySchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de póliza inválidos', parseResult.error.errors); } const dto: CreateJournalEntryDto = parseResult.data; const entry = await journalEntriesService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: entry, message: 'Póliza creada exitosamente' }); } catch (error) { next(error); } } async updateJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateJournalEntrySchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de póliza inválidos', parseResult.error.errors); } const dto: UpdateJournalEntryDto = parseResult.data; const entry = await journalEntriesService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: entry, message: 'Póliza actualizada exitosamente' }); } catch (error) { next(error); } } async postJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const entry = await journalEntriesService.post(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: entry, message: 'Póliza publicada exitosamente' }); } catch (error) { next(error); } } async cancelJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const entry = await journalEntriesService.cancel(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: entry, message: 'Póliza cancelada exitosamente' }); } catch (error) { next(error); } } async deleteJournalEntry(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await journalEntriesService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Póliza eliminada exitosamente' }); } catch (error) { next(error); } } // ========== INVOICES ========== async getInvoices(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = invoiceQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: InvoiceFilters = queryResult.data; const result = await invoicesService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const invoice = await invoicesService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: invoice }); } catch (error) { next(error); } } async createInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createInvoiceSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de factura inválidos', parseResult.error.errors); } const dto: CreateInvoiceDto = parseResult.data; const invoice = await invoicesService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: invoice, message: 'Factura creada exitosamente' }); } catch (error) { next(error); } } async updateInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateInvoiceSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de factura inválidos', parseResult.error.errors); } const dto: UpdateInvoiceDto = parseResult.data; const invoice = await invoicesService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: invoice, message: 'Factura actualizada exitosamente' }); } catch (error) { next(error); } } async validateInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const invoice = await invoicesService.validate(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: invoice, message: 'Factura validada exitosamente' }); } catch (error) { next(error); } } async cancelInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const invoice = await invoicesService.cancel(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: invoice, message: 'Factura cancelada exitosamente' }); } catch (error) { next(error); } } async deleteInvoice(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await invoicesService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Factura eliminada exitosamente' }); } catch (error) { next(error); } } // ========== INVOICE LINES ========== async addInvoiceLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createInvoiceLineSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); } const dto: CreateInvoiceLineDto = parseResult.data; const line = await invoicesService.addLine(req.params.id, dto, req.tenantId!); res.status(201).json({ success: true, data: line, message: 'Línea agregada exitosamente' }); } catch (error) { next(error); } } async updateInvoiceLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateInvoiceLineSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de línea inválidos', parseResult.error.errors); } const dto: UpdateInvoiceLineDto = parseResult.data; const line = await invoicesService.updateLine(req.params.id, req.params.lineId, dto, req.tenantId!); res.json({ success: true, data: line, message: 'Línea actualizada exitosamente' }); } catch (error) { next(error); } } async removeInvoiceLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await invoicesService.removeLine(req.params.id, req.params.lineId, req.tenantId!); res.json({ success: true, message: 'Línea eliminada exitosamente' }); } catch (error) { next(error); } } // ========== PAYMENTS ========== async getPayments(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = paymentQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: PaymentFilters = queryResult.data; const result = await paymentsService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getPayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const payment = await paymentsService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: payment }); } catch (error) { next(error); } } async createPayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createPaymentSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de pago inválidos', parseResult.error.errors); } const dto: CreatePaymentDto = parseResult.data; const payment = await paymentsService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: payment, message: 'Pago creado exitosamente' }); } catch (error) { next(error); } } async updatePayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updatePaymentSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de pago inválidos', parseResult.error.errors); } const dto: UpdatePaymentDto = parseResult.data; const payment = await paymentsService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: payment, message: 'Pago actualizado exitosamente' }); } catch (error) { next(error); } } async postPayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const payment = await paymentsService.post(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: payment, message: 'Pago publicado exitosamente' }); } catch (error) { next(error); } } async reconcilePayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = reconcilePaymentSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de conciliación inválidos', parseResult.error.errors); } const dto: ReconcileDto = parseResult.data; const payment = await paymentsService.reconcile(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: payment, message: 'Pago conciliado exitosamente' }); } catch (error) { next(error); } } async cancelPayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const payment = await paymentsService.cancel(req.params.id, req.tenantId!, req.user!.userId); res.json({ success: true, data: payment, message: 'Pago cancelado exitosamente' }); } catch (error) { next(error); } } async deletePayment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await paymentsService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Pago eliminado exitosamente' }); } catch (error) { next(error); } } // ========== TAXES ========== async getTaxes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const queryResult = taxQuerySchema.safeParse(req.query); if (!queryResult.success) { throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors); } const filters: TaxFilters = queryResult.data; const result = await taxesService.findAll(req.tenantId!, filters); res.json({ success: true, data: result.data, meta: { total: result.total, page: filters.page, limit: filters.limit, totalPages: Math.ceil(result.total / (filters.limit || 20)) }, }); } catch (error) { next(error); } } async getTax(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const tax = await taxesService.findById(req.params.id, req.tenantId!); res.json({ success: true, data: tax }); } catch (error) { next(error); } } async createTax(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = createTaxSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de impuesto inválidos', parseResult.error.errors); } const dto: CreateTaxDto = parseResult.data; const tax = await taxesService.create(dto, req.tenantId!, req.user!.userId); res.status(201).json({ success: true, data: tax, message: 'Impuesto creado exitosamente' }); } catch (error) { next(error); } } async updateTax(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { const parseResult = updateTaxSchema.safeParse(req.body); if (!parseResult.success) { throw new ValidationError('Datos de impuesto inválidos', parseResult.error.errors); } const dto: UpdateTaxDto = parseResult.data; const tax = await taxesService.update(req.params.id, dto, req.tenantId!, req.user!.userId); res.json({ success: true, data: tax, message: 'Impuesto actualizado exitosamente' }); } catch (error) { next(error); } } async deleteTax(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { try { await taxesService.delete(req.params.id, req.tenantId!); res.json({ success: true, message: 'Impuesto eliminado exitosamente' }); } catch (error) { next(error); } } } export const financialController = new FinancialController();