- Fix api keys controller scope type handling - Fix billing-usage invoices/subscriptions undefined assignments - Fix branches service type casting - Fix financial controller Zod schemas (accounts camelCase, journals snake_case) - Fix inventory controller with type assertions for enums - Fix valuation controller meta type - Fix notifications service channel type casting - Fix payment-terminals transactions service undefined assignments - Fix CircuitBreaker constructor signature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
756 lines
29 KiB
TypeScript
756 lines
29 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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();
|