import { PaginatedResponse } from '@edr/types'; import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { CreatePriorityConfigDto } from '../dto/create-priority-config.dto'; import { ListPriorityConfigsQueryDto } from '../dto/list-rule-engine-query.dto'; import { UpdatePriorityConfigDto } from '../dto/update-priority-config.dto'; import { PriorityConfig } from '../entities/priority-config.entity'; import { IPriorityConfigsRepository, PRIORITY_CONFIGS_REPOSITORY, } from '../interfaces/priority-configs.repository.interface'; import { DisplayOrderService } from './display-order.service'; @Injectable() export class PriorityConfigsService { constructor( @Inject(PRIORITY_CONFIGS_REPOSITORY) private readonly repository: IPriorityConfigsRepository, private readonly displayOrder: DisplayOrderService, ) {} /** List priority configs — standard paginated envelope with server-side search. */ async findAll(query: ListPriorityConfigsQueryDto): Promise> { return this.repository.findPaged(query); } async findById(id: string): Promise { const entity = await this.repository.findById(id); if (!entity) throw new NotFoundException(`Priority config ${id} not found`); return entity; } async create(dto: CreatePriorityConfigDto): Promise { this.validateCurrencyField(dto.type, dto.currency); await this.assertNoRangeCollision({ type: dto.type, currency: dto.currency ?? null, minWagonCount: dto.minWagonCount, maxWagonCount: dto.maxWagonCount, }); const displayOrder = await this.displayOrder.resolveCreateOrder(PriorityConfig, 'displayOrder', {}); return this.repository.create({ type: dto.type, label: dto.label, currency: dto.currency ?? null, minWagonCount: dto.minWagonCount, maxWagonCount: dto.maxWagonCount, scorePoints: dto.scorePoints ?? 0, isActive: dto.isActive ?? false, displayOrder, }); } async update(id: string, dto: UpdatePriorityConfigDto): Promise { const existing = await this.findById(id); const type = dto.type ?? existing.type; const currency = dto.currency !== undefined ? dto.currency : existing.currency; this.validateCurrencyField(type, currency); await this.assertNoRangeCollision({ type, currency: currency ?? null, minWagonCount: dto.minWagonCount ?? existing.minWagonCount, maxWagonCount: dto.maxWagonCount ?? existing.maxWagonCount, excludeId: id, }); const { ...patch } = dto; const updated = await this.repository.update(id, patch); if (!updated) throw new NotFoundException(`Priority config ${id} not found`); return updated; } /** * No two rules of the same type (and, for CURRENCY rules, the same currency) * may cover overlapping wagon-count ranges — a booking must match at most one * rule per type. Rejects an exact duplicate (1–5 vs 1–5) and any partial * overlap (1–5 vs 4–7). Ranges are inclusive on both ends. */ async assertNoRangeCollision(input: { type: 'WAGON' | 'CURRENCY' | 'CUSTOMS'; currency?: string | null; minWagonCount: number; maxWagonCount: number; excludeId?: string; }): Promise { if (input.minWagonCount > input.maxWagonCount) { throw new BadRequestException( 'Min wagon count cannot be greater than max wagon count', ); } const siblings = await this.repository.findAll({ where: { type: input.type }, }); const clash = siblings.find( (s) => s.id !== input.excludeId && (input.type !== 'CURRENCY' || (s.currency ?? null) === (input.currency ?? null)) && input.minWagonCount <= s.maxWagonCount && input.maxWagonCount >= s.minWagonCount, ); if (clash) { throw new BadRequestException( `Wagon range ${input.minWagonCount}–${input.maxWagonCount} overlaps existing rule ` + `"${clash.label}" (${clash.minWagonCount}–${clash.maxWagonCount}). ` + 'Adjust the range so rules do not collide.', ); } } async remove(id: string): Promise { await this.findById(id); await this.repository.softDelete(id); } async reorder(ids: string[]): Promise { await this.displayOrder.reorderByIds(PriorityConfig, 'displayOrder', ids); } async moveOrder(id: string, direction: 'up' | 'down'): Promise { await this.findById(id); await this.displayOrder.moveOne(PriorityConfig, 'displayOrder', id, direction); } private validateCurrencyField( type: 'WAGON' | 'CURRENCY' | 'CUSTOMS', currency: string | undefined | null, ): void { if (type === 'CURRENCY' && !currency) { throw new BadRequestException('currency field is required when type is CURRENCY'); } if (type !== 'CURRENCY' && currency) { throw new BadRequestException(`currency field must be null when type is ${type}`); } } }