Files
edr-platform/apps/edr-freight-api/src/modules/rule-engine/services/priority-configs.service.ts
2026-07-15 13:29:01 +00:00

138 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (15 vs 15) and any partial
* overlap (15 vs 47). 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}`);
}
}
}