erp-core-backend-v2/src/modules/inventory/services/warehouses.service.ts
Adrian Flores Cortes 7a957a69c7 refactor: Consolidate duplicate services and normalize module structure
- Partners: Moved services to services/ directory, consolidated duplicates,
  kept singleton pattern version with ranking service
- Products: Moved service to services/, removed duplicate class-based version,
  kept singleton with deletedAt filtering
- Reports: Moved service to services/, kept raw SQL version for active controller
- Warehouses: Moved service to services/, removed duplicate class-based version,
  kept singleton with proper tenant isolation

All modules now follow consistent structure:
- services/*.service.ts for business logic
- services/index.ts for exports
- Controllers import from ./services/index.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 04:40:16 -06:00

300 lines
8.5 KiB
TypeScript

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<Warehouse>;
private locationRepository: Repository<Location>;
private stockQuantRepository: Repository<StockQuant>;
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<WarehouseWithRelations> {
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<Warehouse> {
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<Warehouse> = {
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<Warehouse> {
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<void> {
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<Location[]> {
await this.findById(warehouseId, tenantId);
return this.locationRepository.find({
where: {
warehouseId,
tenantId,
},
order: { name: 'ASC' },
});
}
async getStock(warehouseId: string, tenantId: string): Promise<any[]> {
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();