erp-core-backend-v2/src/modules/warehouses/warehouses.service.ts
rckrdmrd a127a4a424 feat(routes): Add independent routes for Invoices, Products, Warehouses modules
- Create invoices.controller.ts and invoices.routes.ts with singleton service
- Create products.service.ts, products.controller.ts, products.routes.ts
- Create warehouses.service.ts, warehouses.controller.ts, warehouses.routes.ts
- Register all routes in app.ts
- Use Zod validation schemas in all controllers
- Apply multi-tenant isolation via tenantId
- Update invoices.module.ts to use singleton pattern

All business modules now have API routes registered and build passes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 03:43:43 -06:00

263 lines
8.0 KiB
TypeScript

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<Warehouse> = { 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<Warehouse | null> {
return this.warehouseRepository.findOne({
where: { id, tenantId, deletedAt: IsNull() },
});
}
async findByCode(code: string, tenantId: string): Promise<Warehouse | null> {
return this.warehouseRepository.findOne({
where: { code, tenantId, deletedAt: IsNull() },
});
}
async getDefault(tenantId: string): Promise<Warehouse | null> {
return this.warehouseRepository.findOne({
where: { tenantId, isDefault: true, deletedAt: IsNull() },
});
}
async getActive(tenantId: string): Promise<Warehouse[]> {
return this.warehouseRepository.find({
where: { tenantId, isActive: true, deletedAt: IsNull() },
order: { isDefault: 'DESC', name: 'ASC' },
});
}
async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise<Warehouse> {
// 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<Warehouse | null> {
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<boolean> {
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<WarehouseLocation | null> {
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<WarehouseLocation> {
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<WarehouseLocation | null> {
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<boolean> {
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<WarehouseLocation[]> {
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();