- 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>
339 lines
12 KiB
TypeScript
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();
|