erp-construccion-backend/src/modules/partners/partners.controller.ts
rckrdmrd b3cd6e2e51 [SYNC] feat: Completar integración erp-core - FASE 2A
- Crear capa de compatibilidad config/ (database.ts, typeorm.ts, index.ts)
- Exportar Tenant y User en core/entities/index.ts
- Crear Company entity en auth/entities/
- Crear Warehouse entity y módulo warehouses/
- Corregir ApiKey imports para usar core/entities
- Unificar tipos TokenPayload y JwtPayload
- Corregir ZodError.errors -> .issues en controllers CRM, inventory, sales, partners
- Agregar exports InventoryService e InventoryController
- Deshabilitar temporalmente módulo finance (requiere correcciones estructurales)
- Agregar .gitignore estándar

Build: PASANDO (módulo finance excluido temporalmente)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 11:43:59 -06:00

364 lines
13 KiB
TypeScript

import { Response, NextFunction } from 'express';
import { z } from 'zod';
import { partnersService, CreatePartnerDto, UpdatePartnerDto, PartnerFilters, PartnerType } from './partners.service.js';
import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js';
// Validation schemas (accept both snake_case and camelCase from frontend)
const createPartnerSchema = z.object({
code: z.string().min(1, 'El código es requerido').max(20),
display_name: z.string().min(1).max(200).optional(),
displayName: z.string().min(1, 'El nombre es requerido').max(200).optional(),
legal_name: z.string().max(200).optional(),
legalName: z.string().max(200).optional(),
partner_type: z.enum(['customer', 'supplier', 'both']).default('customer'),
partnerType: z.enum(['customer', 'supplier', 'both']).default('customer'),
email: z.string().email('Email inválido').max(255).optional(),
phone: z.string().max(30).optional(),
mobile: z.string().max(30).optional(),
website: z.string().max(500).optional(),
tax_id: z.string().max(20).optional(),
taxId: z.string().max(20).optional(),
tax_regime: z.string().max(100).optional(),
taxRegime: z.string().max(100).optional(),
cfdi_use: z.string().max(10).optional(),
cfdiUse: z.string().max(10).optional(),
payment_term_days: z.coerce.number().int().default(0),
paymentTermDays: z.coerce.number().int().default(0),
credit_limit: z.coerce.number().default(0),
creditLimit: z.coerce.number().default(0),
price_list_id: z.string().uuid().optional(),
priceListId: z.string().uuid().optional(),
discount_percent: z.coerce.number().default(0),
discountPercent: z.coerce.number().default(0),
category: z.string().max(50).optional(),
tags: z.array(z.string()).optional(),
notes: z.string().optional(),
sales_rep_id: z.string().uuid().optional(),
salesRepId: z.string().uuid().optional(),
});
const updatePartnerSchema = z.object({
display_name: z.string().min(1).max(200).optional(),
displayName: z.string().min(1).max(200).optional(),
legal_name: z.string().max(200).optional().nullable(),
legalName: z.string().max(200).optional().nullable(),
partner_type: z.enum(['customer', 'supplier', 'both']).optional(),
partnerType: z.enum(['customer', 'supplier', 'both']).optional(),
email: z.string().email('Email inválido').max(255).optional().nullable(),
phone: z.string().max(30).optional().nullable(),
mobile: z.string().max(30).optional().nullable(),
website: z.string().max(500).optional().nullable(),
tax_id: z.string().max(20).optional().nullable(),
taxId: z.string().max(20).optional().nullable(),
tax_regime: z.string().max(100).optional().nullable(),
taxRegime: z.string().max(100).optional().nullable(),
cfdi_use: z.string().max(10).optional().nullable(),
cfdiUse: z.string().max(10).optional().nullable(),
payment_term_days: z.coerce.number().int().optional(),
paymentTermDays: z.coerce.number().int().optional(),
credit_limit: z.coerce.number().optional(),
creditLimit: z.coerce.number().optional(),
price_list_id: z.string().uuid().optional().nullable(),
priceListId: z.string().uuid().optional().nullable(),
discount_percent: z.coerce.number().optional(),
discountPercent: z.coerce.number().optional(),
category: z.string().max(50).optional().nullable(),
tags: z.array(z.string()).optional(),
notes: z.string().optional().nullable(),
is_active: z.boolean().optional(),
isActive: z.boolean().optional(),
is_verified: z.boolean().optional(),
isVerified: z.boolean().optional(),
sales_rep_id: z.string().uuid().optional().nullable(),
salesRepId: z.string().uuid().optional().nullable(),
});
const querySchema = z.object({
search: z.string().optional(),
partner_type: z.enum(['customer', 'supplier', 'both']).optional(),
partnerType: z.enum(['customer', 'supplier', 'both']).optional(),
category: z.string().optional(),
is_active: z.coerce.boolean().optional(),
isActive: z.coerce.boolean().optional(),
is_verified: z.coerce.boolean().optional(),
isVerified: z.coerce.boolean().optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
class PartnersController {
async findAll(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const queryResult = querySchema.safeParse(req.query);
if (!queryResult.success) {
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.issues);
}
const data = queryResult.data;
const tenantId = req.user!.tenantId;
const filters: PartnerFilters = {
search: data.search,
partnerType: (data.partnerType || data.partner_type) as PartnerType | undefined,
category: data.category,
isActive: data.isActive ?? data.is_active,
isVerified: data.isVerified ?? data.is_verified,
page: data.page,
limit: data.limit,
};
const result = await partnersService.findAll(tenantId, filters);
const response: ApiResponse = {
success: true,
data: result.data,
meta: {
total: result.total,
page: filters.page || 1,
limit: filters.limit || 20,
totalPages: Math.ceil(result.total / (filters.limit || 20)),
},
};
res.json(response);
} catch (error) {
next(error);
}
}
async findCustomers(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const queryResult = querySchema.safeParse(req.query);
if (!queryResult.success) {
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.issues);
}
const data = queryResult.data;
const tenantId = req.user!.tenantId;
const filters = {
search: data.search,
category: data.category,
isActive: data.isActive ?? data.is_active,
isVerified: data.isVerified ?? data.is_verified,
page: data.page,
limit: data.limit,
};
const result = await partnersService.findCustomers(tenantId, filters);
const response: ApiResponse = {
success: true,
data: result.data,
meta: {
total: result.total,
page: filters.page || 1,
limit: filters.limit || 20,
totalPages: Math.ceil(result.total / (filters.limit || 20)),
},
};
res.json(response);
} catch (error) {
next(error);
}
}
async findSuppliers(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const queryResult = querySchema.safeParse(req.query);
if (!queryResult.success) {
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.issues);
}
const data = queryResult.data;
const tenantId = req.user!.tenantId;
const filters = {
search: data.search,
category: data.category,
isActive: data.isActive ?? data.is_active,
isVerified: data.isVerified ?? data.is_verified,
page: data.page,
limit: data.limit,
};
const result = await partnersService.findSuppliers(tenantId, filters);
const response: ApiResponse = {
success: true,
data: result.data,
meta: {
total: result.total,
page: filters.page || 1,
limit: filters.limit || 20,
totalPages: Math.ceil(result.total / (filters.limit || 20)),
},
};
res.json(response);
} catch (error) {
next(error);
}
}
async findById(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;
const tenantId = req.user!.tenantId;
const partner = await partnersService.findById(id, tenantId);
const response: ApiResponse = {
success: true,
data: partner,
};
res.json(response);
} catch (error) {
next(error);
}
}
async create(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const parseResult = createPartnerSchema.safeParse(req.body);
if (!parseResult.success) {
throw new ValidationError('Datos de contacto inválidos', parseResult.error.issues);
}
const data = parseResult.data;
const tenantId = req.user!.tenantId;
const userId = req.user!.userId;
// Transform to camelCase DTO
const dto: CreatePartnerDto = {
code: data.code,
displayName: data.displayName || data.display_name || data.code,
legalName: data.legalName || data.legal_name,
partnerType: (data.partnerType || data.partner_type) as PartnerType,
email: data.email,
phone: data.phone,
mobile: data.mobile,
website: data.website,
taxId: data.taxId || data.tax_id,
taxRegime: data.taxRegime || data.tax_regime,
cfdiUse: data.cfdiUse || data.cfdi_use,
paymentTermDays: data.paymentTermDays || data.payment_term_days,
creditLimit: data.creditLimit || data.credit_limit,
priceListId: data.priceListId || data.price_list_id,
discountPercent: data.discountPercent || data.discount_percent,
category: data.category,
tags: data.tags,
notes: data.notes,
salesRepId: data.salesRepId || data.sales_rep_id,
};
const partner = await partnersService.create(dto, tenantId, userId);
const response: ApiResponse = {
success: true,
data: partner,
message: 'Contacto creado exitosamente',
};
res.status(201).json(response);
} catch (error) {
next(error);
}
}
async update(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;
const parseResult = updatePartnerSchema.safeParse(req.body);
if (!parseResult.success) {
throw new ValidationError('Datos de contacto inválidos', parseResult.error.issues);
}
const data = parseResult.data;
const tenantId = req.user!.tenantId;
const userId = req.user!.userId;
// Transform to camelCase DTO
const dto: UpdatePartnerDto = {};
if (data.displayName !== undefined || data.display_name !== undefined) {
dto.displayName = data.displayName ?? data.display_name;
}
if (data.legalName !== undefined || data.legal_name !== undefined) {
dto.legalName = data.legalName ?? data.legal_name;
}
if (data.partnerType !== undefined || data.partner_type !== undefined) {
dto.partnerType = (data.partnerType ?? data.partner_type) as PartnerType;
}
if (data.email !== undefined) dto.email = data.email;
if (data.phone !== undefined) dto.phone = data.phone;
if (data.mobile !== undefined) dto.mobile = data.mobile;
if (data.website !== undefined) dto.website = data.website;
if (data.taxId !== undefined || data.tax_id !== undefined) {
dto.taxId = data.taxId ?? data.tax_id;
}
if (data.taxRegime !== undefined || data.tax_regime !== undefined) {
dto.taxRegime = data.taxRegime ?? data.tax_regime;
}
if (data.cfdiUse !== undefined || data.cfdi_use !== undefined) {
dto.cfdiUse = data.cfdiUse ?? data.cfdi_use;
}
if (data.paymentTermDays !== undefined || data.payment_term_days !== undefined) {
dto.paymentTermDays = data.paymentTermDays ?? data.payment_term_days;
}
if (data.creditLimit !== undefined || data.credit_limit !== undefined) {
dto.creditLimit = data.creditLimit ?? data.credit_limit;
}
if (data.priceListId !== undefined || data.price_list_id !== undefined) {
dto.priceListId = data.priceListId ?? data.price_list_id;
}
if (data.discountPercent !== undefined || data.discount_percent !== undefined) {
dto.discountPercent = data.discountPercent ?? data.discount_percent;
}
if (data.category !== undefined) dto.category = data.category;
if (data.tags !== undefined) dto.tags = data.tags;
if (data.notes !== undefined) dto.notes = data.notes;
if (data.isActive !== undefined || data.is_active !== undefined) {
dto.isActive = data.isActive ?? data.is_active;
}
if (data.isVerified !== undefined || data.is_verified !== undefined) {
dto.isVerified = data.isVerified ?? data.is_verified;
}
if (data.salesRepId !== undefined || data.sales_rep_id !== undefined) {
dto.salesRepId = data.salesRepId ?? data.sales_rep_id;
}
const partner = await partnersService.update(id, dto, tenantId, userId);
const response: ApiResponse = {
success: true,
data: partner,
message: 'Contacto actualizado exitosamente',
};
res.json(response);
} catch (error) {
next(error);
}
}
async delete(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
try {
const { id } = req.params;
const tenantId = req.user!.tenantId;
const userId = req.user!.userId;
await partnersService.delete(id, tenantId, userId);
const response: ApiResponse = {
success: true,
message: 'Contacto eliminado exitosamente',
};
res.json(response);
} catch (error) {
next(error);
}
}
}
export const partnersController = new PartnersController();