erp-core-backend-v2/src/modules/warehouses/warehouses.controller.ts
rckrdmrd a127a4a424 feat(routes): Add independent routes for Invoices, Products, Warehouses modules
- Create invoices.controller.ts and invoices.routes.ts with singleton service
- Create products.service.ts, products.controller.ts, products.routes.ts
- Create warehouses.service.ts, warehouses.controller.ts, warehouses.routes.ts
- Register all routes in app.ts
- Use Zod validation schemas in all controllers
- Apply multi-tenant isolation via tenantId
- Update invoices.module.ts to use singleton pattern

All business modules now have API routes registered and build passes.

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

339 lines
12 KiB
TypeScript

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();