import { PaginatedResponse } from '@edr/types'; import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateYardDto } from '../dto/create-yard.dto'; import { ListYardsQueryDto } from '../dto/list-rule-engine-query.dto'; import { ReorderItemsDto } from '../dto/reorder-items.dto'; import { UpdateYardDto } from '../dto/update-yard.dto'; import { Yard } from '../entities/yard.entity'; import { IYardsRepository, YARDS_REPOSITORY } from '../interfaces/yards.repository.interface'; import { DisplayOrderService } from './display-order.service'; import { YardFacilitiesService, YardSideFlags } from './yard-facilities.service'; /** Yard rows the config page lists/edits carry the stored per-side facility flags. */ export type YardWithSideFlags = Yard & YardSideFlags; const SIDE_FLAG_KEYS = [ 'hasContainerFacilityOrigin', 'hasBulkFacilityOrigin', 'hasContainerFacilityDestination', 'hasBulkFacilityDestination', ] as const; const NO_FLAGS: YardSideFlags = { hasContainerFacilityOrigin: false, hasBulkFacilityOrigin: false, hasContainerFacilityDestination: false, hasBulkFacilityDestination: false, }; @Injectable() export class YardsService { constructor( @Inject(YARDS_REPOSITORY) private readonly repository: IYardsRepository, private readonly displayOrder: DisplayOrderService, private readonly facilities: YardFacilitiesService, ) {} /** List yards — standard paginated envelope with server-side search. */ async findAll(query: ListYardsQueryDto): Promise> { const page = await this.repository.findPaged(query); const flags = await this.facilities.sideFlagsForYards(page.items.map((y) => y.id)); return { ...page, items: page.items.map((y) => ({ ...y, ...NO_FLAGS, ...flags.get(y.id) })), }; } /** Get a yard by ID. */ async findById(id: string): Promise { const entity = await this.repository.findById(id); if (!entity) throw new NotFoundException(`Yard ${id} not found`); const flags = await this.facilities.sideFlagsForYards([id]); return { ...entity, ...NO_FLAGS, ...flags.get(id) }; } /** The per-side facility flags present in the dto, or null when none were sent. */ private pickSideFlags(dto: Partial): Partial | null { const flags: Partial = {}; for (const key of SIDE_FLAG_KEYS) { if (dto[key] !== undefined) flags[key] = dto[key]; } return Object.keys(flags).length > 0 ? flags : null; } /** Create a yard. */ async create(dto: CreateYardDto): Promise { // Label check first: the code check alone let "sebeta" in next to "Sebeta" // when the existing yard's code didn't match its label (LEGACY_DEST). await this.assertLabelAvailable(dto.label); const code = generateCode(dto.label).slice(0, 40); const existing = await this.repository.findByCode(code); if (existing) throw new ConflictException(`Yard with label "${dto.label}" conflicts with existing code "${code}"`); const displayOrder = await this.displayOrder.resolveCreateOrder(Yard, 'displayOrder', { explicitOrder: dto.displayOrder, insertAfterId: dto.insertAfterId, }); const yard = await this.repository.create({ code, label: dto.label, country: dto.country, isActive: dto.isActive ?? true, hasFacility: dto.hasFacility ?? false, displayOrder, }); const flags = this.pickSideFlags(dto); if (flags) await this.facilities.upsertSideFlags(yard.id, flags); return this.findById(yard.id); } /** Update a yard. */ async update(id: string, dto: UpdateYardDto): Promise { await this.findById(id); if (dto.label !== undefined) await this.assertLabelAvailable(dto.label, id); // Per-side flags live on yard_facilities, not the yards row — split them out. const flags = this.pickSideFlags(dto); const yardDto = { ...dto }; for (const key of SIDE_FLAG_KEYS) delete yardDto[key]; if (Object.keys(yardDto).length > 0) { const updated = await this.repository.update(id, yardDto); if (!updated) throw new NotFoundException(`Yard ${id} not found`); } if (flags) await this.facilities.upsertSideFlags(id, flags); return this.findById(id); } /** No two active yards may share a label (case/whitespace-insensitive). */ private async assertLabelAvailable(label: string, exceptId?: string): Promise { const dupe = await this.repository.findByLabelInsensitive(label); if (dupe && dupe.id !== exceptId) { throw new ConflictException(`A yard named "${dupe.label}" already exists`); } } /** * Soft-delete a yard. The unique `code` (and the label) get a `@` * suffix first — e.g. SEBETA → SEBETA@1755612345678 — so a new yard with the * same name can be created later without tripping UQ_yards_code, which spans * soft-deleted rows too. */ async remove(id: string): Promise { const yard = await this.findById(id); const suffix = `@${Date.now()}`; await this.repository.update(id, { code: `${yard.code.slice(0, 40 - suffix.length)}${suffix}`, label: `${yard.label.slice(0, 100 - suffix.length)}${suffix}`, }); await this.repository.softDelete(id); } async reorder(dto: ReorderItemsDto): Promise { await this.displayOrder.reorderByIds(Yard, 'displayOrder', dto.ids); } async moveOrder(id: string, direction: 'up' | 'down'): Promise { await this.findById(id); await this.displayOrder.moveOne(Yard, 'displayOrder', id, direction); } }