/** * SupplierConstruction Controller * API endpoints para extensión de proveedores de construcción * * @module Purchase (MAI-004) * @prefix /api/v1/suppliers-construction */ import { Router, Request, Response, NextFunction } from 'express'; import { SupplierConstructionService, CreateSupplierConstructionDto, UpdateSupplierConstructionDto, EvaluateSupplierDto, } from '../services/supplier-construction.service'; const router = Router(); const supplierConstructionService = new SupplierConstructionService(null as any); /** * GET /api/v1/suppliers-construction * Lista todas las extensiones de proveedores del tenant */ router.get('/', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const { isMaterialsSupplier, isServicesSupplier, isEquipmentSupplier, minRating, hasValidDocuments, specialty, page, limit } = req.query; const result = await supplierConstructionService.findAll( { tenantId }, { isMaterialsSupplier: isMaterialsSupplier !== undefined ? isMaterialsSupplier === 'true' : undefined, isServicesSupplier: isServicesSupplier !== undefined ? isServicesSupplier === 'true' : undefined, isEquipmentSupplier: isEquipmentSupplier !== undefined ? isEquipmentSupplier === 'true' : undefined, minRating: minRating ? Number(minRating) : undefined, hasValidDocuments: hasValidDocuments !== undefined ? hasValidDocuments === 'true' : undefined, specialty: specialty as string, page: page ? Number(page) : undefined, limit: limit ? Number(limit) : undefined, }, ); return res.json({ success: true, data: result.data, total: result.total, page: result.page, limit: result.limit, }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/statistics * Estadísticas de proveedores */ router.get('/statistics', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const stats = await supplierConstructionService.getSupplierStatistics({ tenantId }); return res.json({ success: true, data: stats }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/top-rated * Proveedores mejor calificados */ router.get('/top-rated', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const { limit } = req.query; const data = await supplierConstructionService.findTopRated({ tenantId }, limit ? Number(limit) : 10); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/by-specialty/:specialty * Proveedores por especialidad */ router.get('/by-specialty/:specialty', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const data = await supplierConstructionService.findBySpecialty({ tenantId }, req.params.specialty); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/materials * Proveedores de materiales */ router.get('/materials', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const data = await supplierConstructionService.findMaterialsSuppliers({ tenantId }); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/services * Proveedores de servicios */ router.get('/services', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const data = await supplierConstructionService.findServicesSuppliers({ tenantId }); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/equipment * Proveedores de equipos */ router.get('/equipment', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const data = await supplierConstructionService.findEquipmentSuppliers({ tenantId }); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/expiring-documents * Proveedores con documentos por vencer */ router.get('/expiring-documents', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const { daysAhead } = req.query; const data = await supplierConstructionService.findWithExpiringDocuments( { tenantId }, daysAhead ? Number(daysAhead) : 30, ); return res.json({ success: true, data, count: data.length }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/by-supplier/:supplierId * Obtiene extensión por ID de proveedor original */ router.get('/by-supplier/:supplierId', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const extension = await supplierConstructionService.findBySupplierId({ tenantId }, req.params.supplierId); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension }); } catch (error) { return next(error); } }); /** * GET /api/v1/suppliers-construction/:id * Obtiene una extensión de proveedor por ID */ router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const extension = await supplierConstructionService.findById({ tenantId }, req.params.id); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension }); } catch (error) { return next(error); } }); /** * POST /api/v1/suppliers-construction * Crea una nueva extensión de proveedor */ router.post('/', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const dto: CreateSupplierConstructionDto = req.body; if (!dto.supplierId) { return res.status(400).json({ error: 'supplierId es requerido' }); } const extension = await supplierConstructionService.create({ tenantId, userId }, dto); return res.status(201).json({ success: true, data: extension }); } catch (error: any) { if (error.message?.includes('already exists')) { return res.status(409).json({ error: error.message }); } return next(error); } }); /** * PATCH /api/v1/suppliers-construction/:id * Actualiza una extensión de proveedor */ router.patch('/:id', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const dto: UpdateSupplierConstructionDto = req.body; const extension = await supplierConstructionService.update({ tenantId, userId }, req.params.id, dto); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension }); } catch (error) { return next(error); } }); /** * POST /api/v1/suppliers-construction/:id/evaluate * Evalúa un proveedor */ router.post('/:id/evaluate', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const dto: EvaluateSupplierDto = req.body; if (dto.qualityRating === undefined || dto.deliveryRating === undefined || dto.priceRating === undefined) { return res.status(400).json({ error: 'qualityRating, deliveryRating y priceRating son requeridos' }); } const extension = await supplierConstructionService.evaluate({ tenantId, userId }, req.params.id, dto); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension, message: 'Evaluación registrada' }); } catch (error: any) { if (error.message?.includes('between')) { return res.status(400).json({ error: error.message }); } return next(error); } }); /** * PATCH /api/v1/suppliers-construction/:id/documents * Actualiza el estado de documentos de un proveedor */ router.patch('/:id/documents', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const { hasValidDocuments, expiryDate } = req.body; if (hasValidDocuments === undefined) { return res.status(400).json({ error: 'hasValidDocuments es requerido' }); } const extension = await supplierConstructionService.updateDocumentStatus( { tenantId, userId }, req.params.id, hasValidDocuments, expiryDate ? new Date(expiryDate) : undefined, ); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension, message: 'Estado de documentos actualizado' }); } catch (error) { return next(error); } }); /** * PATCH /api/v1/suppliers-construction/:id/credit * Actualiza los términos de crédito de un proveedor */ router.patch('/:id/credit', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const { creditLimit, paymentDays } = req.body; if (creditLimit === undefined || paymentDays === undefined) { return res.status(400).json({ error: 'creditLimit y paymentDays son requeridos' }); } const extension = await supplierConstructionService.updateCreditTerms( { tenantId, userId }, req.params.id, Number(creditLimit), Number(paymentDays), ); if (!extension) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, data: extension, message: 'Términos de crédito actualizados' }); } catch (error: any) { if (error.message?.includes('cannot be negative')) { return res.status(400).json({ error: error.message }); } return next(error); } }); /** * DELETE /api/v1/suppliers-construction/:id * Elimina una extensión de proveedor (soft delete) */ router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => { try { const tenantId = req.headers['x-tenant-id'] as string; if (!tenantId) { return res.status(400).json({ error: 'X-Tenant-Id header required' }); } const userId = (req as any).user?.id; const deleted = await supplierConstructionService.softDelete({ tenantId, userId }, req.params.id); if (!deleted) { return res.status(404).json({ error: 'Extensión de proveedor no encontrada' }); } return res.json({ success: true, message: 'Extensión de proveedor eliminada' }); } catch (error) { return next(error); } }); export default router;