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