mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
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<PaginatedResponse<PriorityConfig>> {
|
||
return this.repository.findPaged(query);
|
||
}
|
||
|
||
async findById(id: string): Promise<PriorityConfig> {
|
||
const entity = await this.repository.findById(id);
|
||
if (!entity) throw new NotFoundException(`Priority config ${id} not found`);
|
||
return entity;
|
||
}
|
||
|
||
async create(dto: CreatePriorityConfigDto): Promise<PriorityConfig> {
|
||
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<PriorityConfig> {
|
||
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<void> {
|
||
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<void> {
|
||
await this.findById(id);
|
||
await this.repository.softDelete(id);
|
||
}
|
||
|
||
async reorder(ids: string[]): Promise<void> {
|
||
await this.displayOrder.reorderByIds(PriorityConfig, 'displayOrder', ids);
|
||
}
|
||
|
||
async moveOrder(id: string, direction: 'up' | 'down'): Promise<void> {
|
||
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}`);
|
||
}
|
||
}
|
||
}
|