erp-core-backend-v2/src/modules/inventory/inventory.controller.ts
rckrdmrd ffd5ffe56a fix: Resolve remaining TypeScript build errors (19→0)
- 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>
2026-01-18 02:33:43 -06:00

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