import { FindOptionsWhere, ILike, IsNull } from 'typeorm'; import { AppDataSource } from '../../config/typeorm.js'; import { Warehouse } from './entities/warehouse.entity.js'; import { WarehouseLocation } from './entities/warehouse-location.entity.js'; export interface WarehouseSearchParams { tenantId: string; search?: string; isActive?: boolean; limit?: number; offset?: number; } export interface LocationSearchParams { warehouseId?: string; parentId?: string; locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; isActive?: boolean; limit?: number; offset?: number; } export interface CreateWarehouseDto { code: string; name: string; address?: string; city?: string; state?: string; country?: string; postalCode?: string; phone?: string; email?: string; isActive?: boolean; isDefault?: boolean; } export interface UpdateWarehouseDto { code?: string; name?: string; address?: string | null; city?: string | null; state?: string | null; country?: string | null; postalCode?: string | null; phone?: string | null; email?: string | null; isActive?: boolean; isDefault?: boolean; } export interface CreateLocationDto { warehouseId: string; code: string; name: string; parentId?: string; locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; barcode?: string; isActive?: boolean; } export interface UpdateLocationDto { code?: string; name?: string; parentId?: string | null; locationType?: 'zone' | 'aisle' | 'rack' | 'shelf' | 'bin'; barcode?: string | null; isActive?: boolean; } class WarehousesServiceClass { private get warehouseRepository() { return AppDataSource.getRepository(Warehouse); } private get locationRepository() { return AppDataSource.getRepository(WarehouseLocation); } // ==================== Warehouses ==================== async findAll(params: WarehouseSearchParams): Promise<{ data: Warehouse[]; total: number }> { const { tenantId, search, isActive, limit = 50, offset = 0 } = params; const where: FindOptionsWhere = { tenantId, deletedAt: IsNull() }; if (isActive !== undefined) { where.isActive = isActive; } if (search) { const [data, total] = await this.warehouseRepository.findAndCount({ where: [ { ...where, name: ILike(`%${search}%`) }, { ...where, code: ILike(`%${search}%`) }, ], take: limit, skip: offset, order: { name: 'ASC' }, }); return { data, total }; } const [data, total] = await this.warehouseRepository.findAndCount({ where, take: limit, skip: offset, order: { isDefault: 'DESC', name: 'ASC' }, }); return { data, total }; } async findOne(id: string, tenantId: string): Promise { return this.warehouseRepository.findOne({ where: { id, tenantId, deletedAt: IsNull() }, }); } async findByCode(code: string, tenantId: string): Promise { return this.warehouseRepository.findOne({ where: { code, tenantId, deletedAt: IsNull() }, }); } async getDefault(tenantId: string): Promise { return this.warehouseRepository.findOne({ where: { tenantId, isDefault: true, deletedAt: IsNull() }, }); } async getActive(tenantId: string): Promise { return this.warehouseRepository.find({ where: { tenantId, isActive: true, deletedAt: IsNull() }, order: { isDefault: 'DESC', name: 'ASC' }, }); } async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise { // If this is set as default, unset other defaults if (dto.isDefault) { await this.warehouseRepository.update( { tenantId, isDefault: true }, { isDefault: false } ); } const warehouse = this.warehouseRepository.create({ ...dto, tenantId, }); return this.warehouseRepository.save(warehouse); } async update(id: string, tenantId: string, dto: UpdateWarehouseDto, _updatedBy?: string): Promise { const warehouse = await this.findOne(id, tenantId); if (!warehouse) return null; // If setting as default, unset other defaults if (dto.isDefault && !warehouse.isDefault) { await this.warehouseRepository.update( { tenantId, isDefault: true }, { isDefault: false } ); } Object.assign(warehouse, dto); return this.warehouseRepository.save(warehouse); } async delete(id: string, tenantId: string): Promise { const result = await this.warehouseRepository.softDelete({ id, tenantId }); return (result.affected ?? 0) > 0; } // ==================== Locations ==================== // Note: Locations don't have tenantId directly, they belong to a Warehouse which has tenantId async findAllLocations(params: LocationSearchParams & { tenantId: string }): Promise<{ data: WarehouseLocation[]; total: number }> { const { tenantId, warehouseId, parentId, locationType, isActive, limit = 50, offset = 0 } = params; // Build query to join with warehouse for tenant filtering const queryBuilder = this.locationRepository .createQueryBuilder('location') .leftJoinAndSelect('location.warehouse', 'warehouse') .where('warehouse.tenantId = :tenantId', { tenantId }) .andWhere('location.deletedAt IS NULL'); if (warehouseId) { queryBuilder.andWhere('location.warehouseId = :warehouseId', { warehouseId }); } if (parentId) { queryBuilder.andWhere('location.parentId = :parentId', { parentId }); } if (locationType) { queryBuilder.andWhere('location.locationType = :locationType', { locationType }); } if (isActive !== undefined) { queryBuilder.andWhere('location.isActive = :isActive', { isActive }); } queryBuilder.orderBy('location.code', 'ASC'); queryBuilder.skip(offset).take(limit); const [data, total] = await queryBuilder.getManyAndCount(); return { data, total }; } async findLocation(id: string, tenantId: string): Promise { return this.locationRepository .createQueryBuilder('location') .leftJoinAndSelect('location.warehouse', 'warehouse') .where('location.id = :id', { id }) .andWhere('warehouse.tenantId = :tenantId', { tenantId }) .andWhere('location.deletedAt IS NULL') .getOne(); } async createLocation(_tenantId: string, dto: CreateLocationDto, _createdBy?: string): Promise { const location = this.locationRepository.create({ warehouseId: dto.warehouseId, code: dto.code, name: dto.name, parentId: dto.parentId, locationType: dto.locationType || 'shelf', barcode: dto.barcode, isActive: dto.isActive ?? true, }); return this.locationRepository.save(location); } async updateLocation(id: string, tenantId: string, dto: UpdateLocationDto, _updatedBy?: string): Promise { const location = await this.findLocation(id, tenantId); if (!location) return null; Object.assign(location, dto); return this.locationRepository.save(location); } async deleteLocation(id: string, tenantId: string): Promise { const location = await this.findLocation(id, tenantId); if (!location) return false; const result = await this.locationRepository.softDelete({ id }); return (result.affected ?? 0) > 0; } async getLocationsByWarehouse(warehouseId: string, tenantId: string): Promise { return this.locationRepository .createQueryBuilder('location') .leftJoin('location.warehouse', 'warehouse') .where('location.warehouseId = :warehouseId', { warehouseId }) .andWhere('warehouse.tenantId = :tenantId', { tenantId }) .andWhere('location.isActive = :isActive', { isActive: true }) .andWhere('location.deletedAt IS NULL') .orderBy('location.code', 'ASC') .getMany(); } } // Export singleton instance export const warehousesService = new WarehousesServiceClass();