import { Injectable, NotFoundException } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { CreateAllocationRuleDto, UpdateAllocationRuleDto } from './dto/allocation-rule.dto'; import { WarehouseAllocationRule } from './entities/warehouse-allocation-rule.entity'; import { WarehouseAllocationRuleRepository } from './warehouse-allocation-rule.repository'; export interface AllocationCriteria { freightType?: string | null; // CONTAINER | BULK tradeDirection?: string | null; // IMPORT | EXPORT | DOMESTIC | BOTH cargoTypeCode?: string | null; containerStatus?: string | null; // EMPTY | MAINTENANCE | ... requiresInspection?: boolean | null; } export interface AllocationResult { warehouseId: string; yardId: string; zoneId: string; facilityId: string | null; rule: { id: string; name: string; storageType: string | null } | null; /** Human-readable path: Facility → Warehouse → Yard → Zone. */ path: string; } /** * Batch 5 — deterministic warehouse/yard allocation driven by configurable rules. * Never assigns randomly: matches criteria against active rules by priority and * resolves the target Yard/Warehouse/Zone by code. */ @Injectable() export class WarehouseAllocationService { constructor( private readonly dataSource: DataSource, private readonly ruleRepository: WarehouseAllocationRuleRepository, ) {} // ── Rule CRUD ────────────────────────────────────────────────────────────── listRules(): Promise { return this.ruleRepository.findAll({ order: { priority: 'ASC' } }); } createRule(dto: CreateAllocationRuleDto): Promise { return this.ruleRepository.create({ isActive: true, priority: 100, ...dto }); } async updateRule(id: string, dto: UpdateAllocationRuleDto): Promise { const updated = await this.ruleRepository.update(id, dto); if (!updated) throw new NotFoundException(`Allocation rule ${id} not found`); return updated; } deleteRule(id: string): Promise { return this.ruleRepository.softDelete(id); } private matches(rule: WarehouseAllocationRule, c: AllocationCriteria): boolean { const eq = (ruleVal?: string | null, inVal?: string | null) => ruleVal == null || (inVal != null && ruleVal.toUpperCase() === inVal.toUpperCase()); return ( eq(rule.freightType, c.freightType) && eq(rule.tradeDirection, c.tradeDirection) && eq(rule.cargoTypeCode, c.cargoTypeCode) && eq(rule.containerStatus, c.containerStatus) && (rule.requiresInspection == null || rule.requiresInspection === Boolean(c.requiresInspection)) ); } /** First active rule (by priority) whose criteria match. */ async findMatchingRule(criteria: AllocationCriteria): Promise { const rules = await this.ruleRepository.findAll({ where: { isActive: true }, order: { priority: 'ASC' }, }); return rules.find((r) => this.matches(r, criteria)) ?? null; } /** Resolve a concrete warehouse/yard/zone for the given criteria, or null if none configured. */ async resolveLocation(criteria: AllocationCriteria): Promise { const rule = await this.findMatchingRule(criteria); if (!rule) return null; // Resolve yard by rule code. const [yard] = await this.dataSource.query( `SELECT y.id, y.warehouse_id AS "warehouseId", y.name FROM freight.warehouse_yards y WHERE y.code = $1 AND y.deleted_at IS NULL LIMIT 1`, [rule.targetYardCode], ); if (!yard) return null; // Zone: rule code if given, else first zone in the yard. const [zone] = await this.dataSource.query( rule?.targetZoneCode ? `SELECT z.id, z.name FROM freight.warehouse_zones z WHERE z.code = $1 AND z.deleted_at IS NULL LIMIT 1` : `SELECT z.id, z.name FROM freight.warehouse_zones z WHERE z.yard_id = $1 AND z.deleted_at IS NULL ORDER BY z.created_at ASC LIMIT 1`, rule?.targetZoneCode ? [rule.targetZoneCode] : [yard.id], ); if (!zone) return null; const [wh] = await this.dataSource.query( `SELECT w.id, w.name, w.facility_id AS "facilityId", (SELECT name FROM freight.facilities f WHERE f.id = w.facility_id) AS "facilityName" FROM freight.warehouses w WHERE w.id = $1 AND w.deleted_at IS NULL LIMIT 1`, [yard.warehouseId], ); return { warehouseId: yard.warehouseId, yardId: yard.id, zoneId: zone.id, facilityId: wh?.facilityId ?? null, rule: rule ? { id: rule.id, name: rule.name, storageType: rule.storageType ?? null } : null, path: [wh?.facilityName, wh?.name, yard.name, zone.name].filter(Boolean).join(' → '), }; } }