import { Repository, IsNull } from 'typeorm'; import { AppDataSource } from '../../../config/typeorm.js'; import { Warehouse } from '../../warehouses/entities/warehouse.entity.js'; import { Location } from '../entities/location.entity.js'; import { StockQuant } from '../entities/stock-quant.entity.js'; import { NotFoundError, ValidationError, ConflictError } from '../../../shared/types/index.js'; import { logger } from '../../../shared/utils/logger.js'; // ===== Interfaces ===== export interface CreateWarehouseDto { companyId?: string; name: string; code: string; description?: string; addressLine1?: string; city?: string; state?: string; postalCode?: string; isDefault?: boolean; } export interface UpdateWarehouseDto { name?: string; description?: string; addressLine1?: string; city?: string; state?: string; postalCode?: string; isDefault?: boolean; isActive?: boolean; } export interface WarehouseFilters { companyId?: string; isActive?: boolean; page?: number; limit?: number; } export interface WarehouseWithRelations extends Warehouse { companyName?: string; } // ===== Service Class ===== class WarehousesService { private warehouseRepository: Repository; private locationRepository: Repository; private stockQuantRepository: Repository; constructor() { this.warehouseRepository = AppDataSource.getRepository(Warehouse); this.locationRepository = AppDataSource.getRepository(Location); this.stockQuantRepository = AppDataSource.getRepository(StockQuant); } async findAll( tenantId: string, filters: WarehouseFilters = {} ): Promise<{ data: WarehouseWithRelations[]; total: number }> { try { const { companyId, isActive, page = 1, limit = 50 } = filters; const skip = (page - 1) * limit; const queryBuilder = this.warehouseRepository .createQueryBuilder('warehouse') .leftJoinAndSelect('warehouse.company', 'company') .where('warehouse.tenantId = :tenantId', { tenantId }); if (companyId) { queryBuilder.andWhere('warehouse.companyId = :companyId', { companyId }); } if (isActive !== undefined) { queryBuilder.andWhere('warehouse.isActive = :isActive', { isActive }); } const total = await queryBuilder.getCount(); const warehouses = await queryBuilder .orderBy('warehouse.name', 'ASC') .skip(skip) .take(limit) .getMany(); const data: WarehouseWithRelations[] = warehouses.map(w => ({ ...w, companyName: w.company?.name, })); logger.debug('Warehouses retrieved', { tenantId, count: data.length, total }); return { data, total }; } catch (error) { logger.error('Error retrieving warehouses', { error: (error as Error).message, tenantId, }); throw error; } } async findById(id: string, tenantId: string): Promise { try { const warehouse = await this.warehouseRepository .createQueryBuilder('warehouse') .leftJoinAndSelect('warehouse.company', 'company') .where('warehouse.id = :id', { id }) .andWhere('warehouse.tenantId = :tenantId', { tenantId }) .getOne(); if (!warehouse) { throw new NotFoundError('Almacén no encontrado'); } return { ...warehouse, companyName: warehouse.company?.name, }; } catch (error) { logger.error('Error finding warehouse', { error: (error as Error).message, id, tenantId, }); throw error; } } async create(dto: CreateWarehouseDto, tenantId: string, userId: string): Promise { try { // Check unique code within company const existing = await this.warehouseRepository.findOne({ where: { companyId: dto.companyId, code: dto.code, }, }); if (existing) { throw new ConflictError(`Ya existe un almacén con código ${dto.code} en esta empresa`); } // If is_default, clear other defaults for company if (dto.isDefault) { await this.warehouseRepository.update( { companyId: dto.companyId, tenantId }, { isDefault: false } ); } const warehouseData: Partial = { tenantId, companyId: dto.companyId, name: dto.name, code: dto.code, description: dto.description, addressLine1: dto.addressLine1, city: dto.city, state: dto.state, postalCode: dto.postalCode, isDefault: dto.isDefault || false, createdBy: userId, }; const warehouse = this.warehouseRepository.create(warehouseData as Warehouse); await this.warehouseRepository.save(warehouse); logger.info('Warehouse created', { warehouseId: warehouse.id, tenantId, name: warehouse.name, createdBy: userId, }); return warehouse; } catch (error) { logger.error('Error creating warehouse', { error: (error as Error).message, tenantId, dto, }); throw error; } } async update(id: string, dto: UpdateWarehouseDto, tenantId: string, userId: string): Promise { try { const existing = await this.findById(id, tenantId); // If setting as default, clear other defaults if (dto.isDefault) { await this.warehouseRepository .createQueryBuilder() .update(Warehouse) .set({ isDefault: false }) .where('companyId = :companyId', { companyId: existing.companyId }) .andWhere('tenantId = :tenantId', { tenantId }) .andWhere('id != :id', { id }) .execute(); } if (dto.name !== undefined) existing.name = dto.name; if (dto.description !== undefined) existing.description = dto.description; if (dto.addressLine1 !== undefined) existing.addressLine1 = dto.addressLine1; if (dto.city !== undefined) existing.city = dto.city; if (dto.state !== undefined) existing.state = dto.state; if (dto.postalCode !== undefined) existing.postalCode = dto.postalCode; if (dto.isDefault !== undefined) existing.isDefault = dto.isDefault; if (dto.isActive !== undefined) existing.isActive = dto.isActive; existing.updatedBy = userId; await this.warehouseRepository.save(existing); logger.info('Warehouse updated', { warehouseId: id, tenantId, updatedBy: userId, }); return await this.findById(id, tenantId); } catch (error) { logger.error('Error updating warehouse', { error: (error as Error).message, id, tenantId, }); throw error; } } async delete(id: string, tenantId: string): Promise { try { await this.findById(id, tenantId); // Check if warehouse has locations with stock const hasStock = await this.stockQuantRepository .createQueryBuilder('sq') .innerJoin('sq.location', 'location') .where('location.warehouseId = :warehouseId', { warehouseId: id }) .andWhere('sq.quantity > 0') .getCount(); if (hasStock > 0) { throw new ConflictError('No se puede eliminar un almacén que tiene stock'); } await this.warehouseRepository.delete({ id, tenantId }); logger.info('Warehouse deleted', { warehouseId: id, tenantId, }); } catch (error) { logger.error('Error deleting warehouse', { error: (error as Error).message, id, tenantId, }); throw error; } } async getLocations(warehouseId: string, tenantId: string): Promise { await this.findById(warehouseId, tenantId); return this.locationRepository.find({ where: { warehouseId, tenantId, }, order: { name: 'ASC' }, }); } async getStock(warehouseId: string, tenantId: string): Promise { await this.findById(warehouseId, tenantId); const stock = await this.stockQuantRepository .createQueryBuilder('sq') .innerJoinAndSelect('sq.product', 'product') .innerJoinAndSelect('sq.location', 'location') .where('location.warehouseId = :warehouseId', { warehouseId }) .orderBy('product.name', 'ASC') .addOrderBy('location.name', 'ASC') .getMany(); return stock.map(sq => ({ ...sq, productName: sq.product?.name, productCode: sq.product?.code, locationName: sq.location?.name, })); } } export const warehousesService = new WarehousesService();