import { Response, NextFunction } from 'express'; import { z } from 'zod'; import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, CreateLocationDto, UpdateLocationDto } from './warehouses.service.js'; import { ApiResponse, AuthenticatedRequest, ValidationError } from '../../shared/types/index.js'; // Validation schemas const createWarehouseSchema = z.object({ code: z.string().min(1, 'Código es requerido').max(20), name: z.string().min(1, 'Nombre es requerido').max(100), address: z.string().max(200).optional(), city: z.string().max(100).optional(), state: z.string().max(100).optional(), country: z.string().max(3).default('MEX'), postalCode: z.string().max(20).optional(), phone: z.string().max(30).optional(), email: z.string().email().max(255).optional(), isActive: z.boolean().default(true), isDefault: z.boolean().default(false), }); const updateWarehouseSchema = z.object({ code: z.string().min(1).max(20).optional(), name: z.string().min(1).max(100).optional(), address: z.string().max(200).optional().nullable(), city: z.string().max(100).optional().nullable(), state: z.string().max(100).optional().nullable(), country: z.string().max(3).optional(), postalCode: z.string().max(20).optional().nullable(), phone: z.string().max(30).optional().nullable(), email: z.string().email().max(255).optional().nullable(), isActive: z.boolean().optional(), isDefault: z.boolean().optional(), }); const querySchema = z.object({ search: z.string().optional(), isActive: z.coerce.boolean().optional(), limit: z.coerce.number().int().positive().max(100).default(50), offset: z.coerce.number().int().min(0).default(0), }); const createLocationSchema = z.object({ warehouseId: z.string().uuid('Warehouse ID inválido'), code: z.string().min(1, 'Código es requerido').max(30), name: z.string().min(1, 'Nombre es requerido').max(100), parentId: z.string().uuid().optional(), locationType: z.enum(['zone', 'aisle', 'rack', 'shelf', 'bin']).default('shelf'), barcode: z.string().max(50).optional(), isActive: z.boolean().default(true), }); const updateLocationSchema = z.object({ code: z.string().min(1).max(30).optional(), name: z.string().min(1).max(100).optional(), parentId: z.string().uuid().optional().nullable(), locationType: z.enum(['zone', 'aisle', 'rack', 'shelf', 'bin']).optional(), barcode: z.string().max(50).optional().nullable(), isActive: z.boolean().optional(), }); const locationQuerySchema = z.object({ warehouseId: z.string().uuid().optional(), parentId: z.string().uuid().optional(), locationType: z.enum(['zone', 'aisle', 'rack', 'shelf', 'bin']).optional(), isActive: z.coerce.boolean().optional(), limit: z.coerce.number().int().positive().max(100).default(50), offset: z.coerce.number().int().min(0).default(0), }); class WarehousesControllerClass { // ========== WAREHOUSES ========== async findAll(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = querySchema.safeParse(req.query); if (!validation.success) { throw new ValidationError('Parámetros de consulta inválidos', validation.error.errors); } const result = await warehousesService.findAll({ tenantId, ...validation.data }); res.json({ success: true, ...result } as ApiResponse); } catch (error) { next(error); } } async findById(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const warehouse = await warehousesService.findOne(req.params.id, tenantId); if (!warehouse) { return res.status(404).json({ success: false, error: 'Almacén no encontrado' } as ApiResponse); } res.json({ success: true, data: warehouse } as ApiResponse); } catch (error) { next(error); } } async findByCode(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const warehouse = await warehousesService.findByCode(req.params.code, tenantId); if (!warehouse) { return res.status(404).json({ success: false, error: 'Almacén no encontrado' } as ApiResponse); } res.json({ success: true, data: warehouse } as ApiResponse); } catch (error) { next(error); } } async getDefault(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const warehouse = await warehousesService.getDefault(tenantId); if (!warehouse) { return res.status(404).json({ success: false, error: 'No hay almacén por defecto' } as ApiResponse); } res.json({ success: true, data: warehouse } as ApiResponse); } catch (error) { next(error); } } async getActive(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const warehouses = await warehousesService.getActive(tenantId); res.json({ success: true, data: warehouses, total: warehouses.length } as ApiResponse); } catch (error) { next(error); } } async create(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; const userId = req.user?.userId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = createWarehouseSchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos de almacén inválidos', validation.error.errors); } const warehouse = await warehousesService.create(tenantId, validation.data as CreateWarehouseDto, userId); res.status(201).json({ success: true, data: warehouse } as ApiResponse); } catch (error) { next(error); } } async update(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; const userId = req.user?.userId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = updateWarehouseSchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos de actualización inválidos', validation.error.errors); } const warehouse = await warehousesService.update(req.params.id, tenantId, validation.data as UpdateWarehouseDto, userId); if (!warehouse) { return res.status(404).json({ success: false, error: 'Almacén no encontrado' } as ApiResponse); } res.json({ success: true, data: warehouse } as ApiResponse); } catch (error) { next(error); } } async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const deleted = await warehousesService.delete(req.params.id, tenantId); if (!deleted) { return res.status(404).json({ success: false, error: 'Almacén no encontrado' } as ApiResponse); } res.status(204).send(); } catch (error) { next(error); } } // ========== LOCATIONS ========== async findAllLocations(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = locationQuerySchema.safeParse(req.query); if (!validation.success) { throw new ValidationError('Parámetros de consulta inválidos', validation.error.errors); } const result = await warehousesService.findAllLocations({ tenantId, ...validation.data }); res.json({ success: true, ...result } as ApiResponse); } catch (error) { next(error); } } async findLocationById(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const location = await warehousesService.findLocation(req.params.id, tenantId); if (!location) { return res.status(404).json({ success: false, error: 'Ubicación no encontrada' } as ApiResponse); } res.json({ success: true, data: location } as ApiResponse); } catch (error) { next(error); } } async getWarehouseLocations(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const locations = await warehousesService.getLocationsByWarehouse(req.params.warehouseId, tenantId); res.json({ success: true, data: locations, total: locations.length } as ApiResponse); } catch (error) { next(error); } } async createLocation(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; const userId = req.user?.userId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = createLocationSchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos de ubicación inválidos', validation.error.errors); } const location = await warehousesService.createLocation(tenantId, validation.data as CreateLocationDto, userId); res.status(201).json({ success: true, data: location } as ApiResponse); } catch (error) { next(error); } } async updateLocation(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; const userId = req.user?.userId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const validation = updateLocationSchema.safeParse(req.body); if (!validation.success) { throw new ValidationError('Datos de actualización inválidos', validation.error.errors); } const location = await warehousesService.updateLocation(req.params.id, tenantId, validation.data as UpdateLocationDto, userId); if (!location) { return res.status(404).json({ success: false, error: 'Ubicación no encontrada' } as ApiResponse); } res.json({ success: true, data: location } as ApiResponse); } catch (error) { next(error); } } async deleteLocation(req: AuthenticatedRequest, res: Response, next: NextFunction) { try { const tenantId = req.user?.tenantId; if (!tenantId) { return res.status(400).json({ success: false, error: 'Tenant ID required' } as ApiResponse); } const deleted = await warehousesService.deleteLocation(req.params.id, tenantId); if (!deleted) { return res.status(404).json({ success: false, error: 'Ubicación no encontrada' } as ApiResponse); } res.status(204).send(); } catch (error) { next(error); } } } export const warehousesController = new WarehousesControllerClass();