- Fix api keys controller scope type handling - Fix billing-usage invoices/subscriptions undefined assignments - Fix branches service type casting - Fix financial controller Zod schemas (accounts camelCase, journals snake_case) - Fix inventory controller with type assertions for enums - Fix valuation controller meta type - Fix notifications service channel type casting - Fix payment-terminals transactions service undefined assignments - Fix CircuitBreaker constructor signature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
876 lines
30 KiB
TypeScript
876 lines
30 KiB
TypeScript
import { Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import { productsService, CreateProductDto, UpdateProductDto, ProductFilters } from './products.service.js';
|
|
import { warehousesService, CreateWarehouseDto, UpdateWarehouseDto, WarehouseFilters } from './warehouses.service.js';
|
|
import { locationsService, CreateLocationDto, UpdateLocationDto, LocationFilters } from './locations.service.js';
|
|
import { pickingsService, CreatePickingDto, PickingFilters } from './pickings.service.js';
|
|
import { lotsService, CreateLotDto, UpdateLotDto, LotFilters } from './lots.service.js';
|
|
import { adjustmentsService, CreateAdjustmentDto, UpdateAdjustmentDto, CreateAdjustmentLineDto, UpdateAdjustmentLineDto, AdjustmentFilters } from './adjustments.service.js';
|
|
import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js';
|
|
import { ValidationError } from '../../shared/errors/index.js';
|
|
|
|
// Product schemas
|
|
const createProductSchema = z.object({
|
|
name: z.string().min(1, 'El nombre es requerido').max(255),
|
|
code: z.string().max(100).optional(),
|
|
barcode: z.string().max(100).optional(),
|
|
description: z.string().optional(),
|
|
productType: z.enum(['storable', 'consumable', 'service']).default('storable'),
|
|
tracking: z.enum(['none', 'lot', 'serial']).default('none'),
|
|
categoryId: z.string().uuid().optional(),
|
|
uomId: z.string().uuid({ message: 'La unidad de medida es requerida' }),
|
|
purchaseUomId: z.string().uuid().optional(),
|
|
costPrice: z.number().min(0).default(0),
|
|
listPrice: z.number().min(0).default(0),
|
|
valuationMethod: z.enum(['standard', 'fifo', 'average']).default('fifo'),
|
|
weight: z.number().min(0).optional(),
|
|
volume: z.number().min(0).optional(),
|
|
canBeSold: z.boolean().default(true),
|
|
canBePurchased: z.boolean().default(true),
|
|
imageUrl: z.string().url().max(500).optional(),
|
|
});
|
|
|
|
const updateProductSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
barcode: z.string().max(100).optional().nullable(),
|
|
description: z.string().optional().nullable(),
|
|
tracking: z.enum(['none', 'lot', 'serial']).optional(),
|
|
categoryId: z.string().uuid().optional().nullable(),
|
|
uomId: z.string().uuid().optional(),
|
|
purchaseUomId: z.string().uuid().optional().nullable(),
|
|
costPrice: z.number().min(0).optional(),
|
|
listPrice: z.number().min(0).optional(),
|
|
valuationMethod: z.enum(['standard', 'fifo', 'average']).optional(),
|
|
weight: z.number().min(0).optional().nullable(),
|
|
volume: z.number().min(0).optional().nullable(),
|
|
canBeSold: z.boolean().optional(),
|
|
canBePurchased: z.boolean().optional(),
|
|
imageUrl: z.string().url().max(500).optional().nullable(),
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
const productQuerySchema = z.object({
|
|
search: z.string().optional(),
|
|
categoryId: z.string().uuid().optional(),
|
|
productType: z.enum(['storable', 'consumable', 'service']).optional(),
|
|
canBeSold: z.coerce.boolean().optional(),
|
|
canBePurchased: z.coerce.boolean().optional(),
|
|
active: z.coerce.boolean().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
});
|
|
|
|
// Warehouse schemas
|
|
const createWarehouseSchema = z.object({
|
|
companyId: z.string().uuid({ message: 'La empresa es requerida' }),
|
|
name: z.string().min(1, 'El nombre es requerido').max(255),
|
|
code: z.string().min(1).max(20),
|
|
addressId: z.string().uuid().optional(),
|
|
isDefault: z.boolean().default(false),
|
|
});
|
|
|
|
const updateWarehouseSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
addressId: z.string().uuid().optional().nullable(),
|
|
isDefault: z.boolean().optional(),
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
const warehouseQuerySchema = z.object({
|
|
companyId: z.string().uuid().optional(),
|
|
active: z.coerce.boolean().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(50),
|
|
});
|
|
|
|
// Location schemas
|
|
const createLocationSchema = z.object({
|
|
warehouse_id: z.string().uuid().optional(),
|
|
name: z.string().min(1, 'El nombre es requerido').max(255),
|
|
location_type: z.enum(['internal', 'supplier', 'customer', 'inventory', 'production', 'transit']),
|
|
parent_id: z.string().uuid().optional(),
|
|
is_scrap_location: z.boolean().default(false),
|
|
is_return_location: z.boolean().default(false),
|
|
});
|
|
|
|
const updateLocationSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
parent_id: z.string().uuid().optional().nullable(),
|
|
is_scrap_location: z.boolean().optional(),
|
|
is_return_location: z.boolean().optional(),
|
|
active: z.boolean().optional(),
|
|
});
|
|
|
|
const locationQuerySchema = z.object({
|
|
warehouse_id: z.string().uuid().optional(),
|
|
location_type: z.enum(['internal', 'supplier', 'customer', 'inventory', 'production', 'transit']).optional(),
|
|
active: z.coerce.boolean().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(50),
|
|
});
|
|
|
|
// Picking schemas
|
|
const stockMoveLineSchema = z.object({
|
|
product_id: z.string().uuid({ message: 'El producto es requerido' }),
|
|
product_uom_id: z.string().uuid({ message: 'La UdM es requerida' }),
|
|
product_qty: z.number().positive({ message: 'La cantidad debe ser mayor a 0' }),
|
|
lot_id: z.string().uuid().optional(),
|
|
location_id: z.string().uuid({ message: 'La ubicación origen es requerida' }),
|
|
location_dest_id: z.string().uuid({ message: 'La ubicación destino es requerida' }),
|
|
});
|
|
|
|
const createPickingSchema = z.object({
|
|
company_id: z.string().uuid({ message: 'La empresa es requerida' }),
|
|
name: z.string().min(1, 'El nombre es requerido').max(100),
|
|
picking_type: z.enum(['incoming', 'outgoing', 'internal']),
|
|
location_id: z.string().uuid({ message: 'La ubicación origen es requerida' }),
|
|
location_dest_id: z.string().uuid({ message: 'La ubicación destino es requerida' }),
|
|
partner_id: z.string().uuid().optional(),
|
|
scheduled_date: z.string().optional(),
|
|
origin: z.string().max(255).optional(),
|
|
notes: z.string().optional(),
|
|
moves: z.array(stockMoveLineSchema).min(1, 'Debe incluir al menos un movimiento'),
|
|
});
|
|
|
|
const pickingQuerySchema = z.object({
|
|
company_id: z.string().uuid().optional(),
|
|
picking_type: z.enum(['incoming', 'outgoing', 'internal']).optional(),
|
|
status: z.enum(['draft', 'waiting', 'confirmed', 'assigned', 'done', 'cancelled']).optional(),
|
|
partner_id: z.string().uuid().optional(),
|
|
date_from: z.string().optional(),
|
|
date_to: z.string().optional(),
|
|
search: z.string().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
});
|
|
|
|
// Lot schemas
|
|
const createLotSchema = z.object({
|
|
product_id: z.string().uuid({ message: 'El producto es requerido' }),
|
|
name: z.string().min(1, 'El nombre del lote es requerido').max(100),
|
|
ref: z.string().max(100).optional(),
|
|
manufacture_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
expiration_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
removal_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
alert_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
const updateLotSchema = z.object({
|
|
ref: z.string().max(100).optional().nullable(),
|
|
manufacture_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
|
expiration_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
|
removal_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
|
alert_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
|
notes: z.string().optional().nullable(),
|
|
});
|
|
|
|
const lotQuerySchema = z.object({
|
|
product_id: z.string().uuid().optional(),
|
|
expiring_soon: z.coerce.boolean().optional(),
|
|
expired: z.coerce.boolean().optional(),
|
|
search: z.string().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(50),
|
|
});
|
|
|
|
// Adjustment schemas
|
|
const adjustmentLineSchema = z.object({
|
|
product_id: z.string().uuid({ message: 'El producto es requerido' }),
|
|
location_id: z.string().uuid({ message: 'La ubicación es requerida' }),
|
|
lot_id: z.string().uuid().optional(),
|
|
counted_qty: z.number().min(0),
|
|
uom_id: z.string().uuid({ message: 'La UdM es requerida' }),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
const createAdjustmentSchema = z.object({
|
|
company_id: z.string().uuid({ message: 'La empresa es requerida' }),
|
|
location_id: z.string().uuid({ message: 'La ubicación es requerida' }),
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
notes: z.string().optional(),
|
|
lines: z.array(adjustmentLineSchema).min(1, 'Debe incluir al menos una línea'),
|
|
});
|
|
|
|
const updateAdjustmentSchema = z.object({
|
|
location_id: z.string().uuid().optional(),
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
notes: z.string().optional().nullable(),
|
|
});
|
|
|
|
const createAdjustmentLineSchema = z.object({
|
|
product_id: z.string().uuid({ message: 'El producto es requerido' }),
|
|
location_id: z.string().uuid({ message: 'La ubicación es requerida' }),
|
|
lot_id: z.string().uuid().optional(),
|
|
counted_qty: z.number().min(0),
|
|
uom_id: z.string().uuid({ message: 'La UdM es requerida' }),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
const updateAdjustmentLineSchema = z.object({
|
|
counted_qty: z.number().min(0).optional(),
|
|
notes: z.string().optional().nullable(),
|
|
});
|
|
|
|
const adjustmentQuerySchema = z.object({
|
|
company_id: z.string().uuid().optional(),
|
|
location_id: z.string().uuid().optional(),
|
|
status: z.enum(['draft', 'confirmed', 'done', 'cancelled']).optional(),
|
|
date_from: z.string().optional(),
|
|
date_to: z.string().optional(),
|
|
search: z.string().optional(),
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
});
|
|
|
|
class InventoryController {
|
|
// ========== PRODUCTS ==========
|
|
async getProducts(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = productQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters = queryResult.data as ProductFilters;
|
|
const result = await productsService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 20)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getProduct(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const product = await productsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: product });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createProduct(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createProductSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de producto inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto = parseResult.data as CreateProductDto;
|
|
const product = await productsService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: product,
|
|
message: 'Producto creado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateProduct(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateProductSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de producto inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto = parseResult.data as UpdateProductDto;
|
|
const product = await productsService.update(req.params.id, dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: product,
|
|
message: 'Producto actualizado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteProduct(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await productsService.delete(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({ success: true, message: 'Producto eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getProductStock(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const stock = await productsService.getStock(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: stock });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== WAREHOUSES ==========
|
|
async getWarehouses(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = warehouseQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters: WarehouseFilters = queryResult.data;
|
|
const result = await warehousesService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 50)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getWarehouse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const warehouse = await warehousesService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: warehouse });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createWarehouse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createWarehouseSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de almacén inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreateWarehouseDto = parseResult.data;
|
|
const warehouse = await warehousesService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: warehouse,
|
|
message: 'Almacén creado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateWarehouse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateWarehouseSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de almacén inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: UpdateWarehouseDto = parseResult.data;
|
|
const warehouse = await warehousesService.update(req.params.id, dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: warehouse,
|
|
message: 'Almacén actualizado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteWarehouse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await warehousesService.delete(req.params.id, req.tenantId!);
|
|
res.json({ success: true, message: 'Almacén eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getWarehouseLocations(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const locations = await warehousesService.getLocations(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: locations });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getWarehouseStock(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const stock = await warehousesService.getStock(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: stock });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== LOCATIONS ==========
|
|
async getLocations(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = locationQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters: LocationFilters = queryResult.data;
|
|
const result = await locationsService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 50)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getLocation(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const location = await locationsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: location });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createLocation(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createLocationSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de ubicación inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreateLocationDto = parseResult.data;
|
|
const location = await locationsService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: location,
|
|
message: 'Ubicación creada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateLocation(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateLocationSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de ubicación inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: UpdateLocationDto = parseResult.data;
|
|
const location = await locationsService.update(req.params.id, dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: location,
|
|
message: 'Ubicación actualizada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getLocationStock(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const stock = await locationsService.getStock(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: stock });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== PICKINGS ==========
|
|
async getPickings(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = pickingQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters: PickingFilters = queryResult.data;
|
|
const result = await pickingsService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 20)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getPicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const picking = await pickingsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: picking });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createPicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createPickingSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de picking inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreatePickingDto = parseResult.data;
|
|
const picking = await pickingsService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: picking,
|
|
message: 'Picking creado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async confirmPicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const picking = await pickingsService.confirm(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: picking,
|
|
message: 'Picking confirmado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async validatePicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const picking = await pickingsService.validate(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: picking,
|
|
message: 'Picking validado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async cancelPicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const picking = await pickingsService.cancel(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: picking,
|
|
message: 'Picking cancelado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deletePicking(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await pickingsService.delete(req.params.id, req.tenantId!);
|
|
res.json({ success: true, message: 'Picking eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== LOTS ==========
|
|
async getLots(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = lotQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters: LotFilters = queryResult.data;
|
|
const result = await lotsService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 50)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getLot(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const lot = await lotsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: lot });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createLot(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createLotSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de lote inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreateLotDto = parseResult.data;
|
|
const lot = await lotsService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: lot,
|
|
message: 'Lote creado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateLot(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateLotSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de lote inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: UpdateLotDto = parseResult.data;
|
|
const lot = await lotsService.update(req.params.id, dto, req.tenantId!);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: lot,
|
|
message: 'Lote actualizado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getLotMovements(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const movements = await lotsService.getMovements(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: movements });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteLot(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await lotsService.delete(req.params.id, req.tenantId!);
|
|
res.json({ success: true, message: 'Lote eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
// ========== ADJUSTMENTS ==========
|
|
async getAdjustments(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const queryResult = adjustmentQuerySchema.safeParse(req.query);
|
|
if (!queryResult.success) {
|
|
throw new ValidationError('Parámetros de consulta inválidos', queryResult.error.errors);
|
|
}
|
|
|
|
const filters: AdjustmentFilters = queryResult.data;
|
|
const result = await adjustmentsService.findAll(req.tenantId!, filters);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
meta: {
|
|
total: result.total,
|
|
page: filters.page,
|
|
limit: filters.limit,
|
|
totalPages: Math.ceil(result.total / (filters.limit || 20)),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async getAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const adjustment = await adjustmentsService.findById(req.params.id, req.tenantId!);
|
|
res.json({ success: true, data: adjustment });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async createAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createAdjustmentSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de ajuste inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreateAdjustmentDto = parseResult.data;
|
|
const adjustment = await adjustmentsService.create(dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: adjustment,
|
|
message: 'Ajuste de inventario creado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateAdjustmentSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de ajuste inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: UpdateAdjustmentDto = parseResult.data;
|
|
const adjustment = await adjustmentsService.update(req.params.id, dto, req.tenantId!, req.user!.userId);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: adjustment,
|
|
message: 'Ajuste de inventario actualizado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async addAdjustmentLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = createAdjustmentLineSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de línea inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: CreateAdjustmentLineDto = parseResult.data;
|
|
const line = await adjustmentsService.addLine(req.params.id, dto, req.tenantId!);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: line,
|
|
message: 'Línea agregada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async updateAdjustmentLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const parseResult = updateAdjustmentLineSchema.safeParse(req.body);
|
|
if (!parseResult.success) {
|
|
throw new ValidationError('Datos de línea inválidos', parseResult.error.errors);
|
|
}
|
|
|
|
const dto: UpdateAdjustmentLineDto = parseResult.data;
|
|
const line = await adjustmentsService.updateLine(req.params.id, req.params.lineId, dto, req.tenantId!);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: line,
|
|
message: 'Línea actualizada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async removeAdjustmentLine(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await adjustmentsService.removeLine(req.params.id, req.params.lineId, req.tenantId!);
|
|
res.json({ success: true, message: 'Línea eliminada exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async confirmAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const adjustment = await adjustmentsService.confirm(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: adjustment,
|
|
message: 'Ajuste confirmado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async validateAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const adjustment = await adjustmentsService.validate(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: adjustment,
|
|
message: 'Ajuste validado exitosamente. Stock actualizado.',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async cancelAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const adjustment = await adjustmentsService.cancel(req.params.id, req.tenantId!, req.user!.userId);
|
|
res.json({
|
|
success: true,
|
|
data: adjustment,
|
|
message: 'Ajuste cancelado exitosamente',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
async deleteAdjustment(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
await adjustmentsService.delete(req.params.id, req.tenantId!);
|
|
res.json({ success: true, message: 'Ajuste eliminado exitosamente' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const inventoryController = new InventoryController();
|