erp-construccion-backend-v2/src/modules/purchase/controllers/supplier-construction.controller.ts
Adrian Flores Cortes 6a71183121 feat(MAI-002,MAI-004,MAI-009): Add missing controllers and services
- construction/torre: Service + Controller for vertical buildings
- construction/nivel: Service + Controller for floor levels
- construction/departamento: Service + Controller for units
- purchase/purchase-order-construction: Controller for PO extensions
- purchase/supplier-construction: Controller for supplier extensions
- quality/checklist: Controller for inspection templates

All endpoints follow existing patterns with multi-tenancy support.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 17:39:51 -06:00

419 lines
13 KiB
TypeScript

/**
* 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;