Files
edr-platform/apps/edr-freight-api/src/modules/transit-agents/transit-agents.service.ts

139 lines
4.7 KiB
TypeScript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { FindOptionsOrder } from 'typeorm';
import { CreateTransitAgentDto } from './dto/create-transit-agent.dto';
import { UpdateTransitAgentDto } from './dto/update-transit-agent.dto';
import { TransitAgent } from './entities/transit-agent.entity';
import { TransitAgentsRepository } from './transit-agents.repository';
export type TransitAgentValidityStatus = 'VALID' | 'NOT_STARTED' | 'EXPIRED';
export type TransitAgentView = TransitAgent & {
validityStatus: TransitAgentValidityStatus;
};
type TransitAgentListFilter = {
isActive?: boolean;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: string;
};
/** Today as `yyyy-MM-dd`, matching the `date`-typed validity columns. */
function todayISODate(): string {
return new Date().toISOString().slice(0, 10);
}
function validityStatus(agent: Pick<TransitAgent, 'validFrom' | 'validTo'>): TransitAgentValidityStatus {
const today = todayISODate();
if (today < agent.validFrom) return 'NOT_STARTED';
if (today > agent.validTo) return 'EXPIRED';
return 'VALID';
}
function withValidityStatus(agent: TransitAgent): TransitAgentView {
return { ...agent, validityStatus: validityStatus(agent) };
}
@Injectable()
export class TransitAgentsService {
constructor(private readonly transitAgentsRepository: TransitAgentsRepository) {}
async findAll(filter: TransitAgentListFilter = {}): Promise<{
data: TransitAgentView[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
}> {
const page = filter.page ?? 1;
const pageSize = filter.pageSize ?? 500;
const sortBy = ['name', 'validFrom', 'validTo', 'isActive'].includes(filter.sortBy ?? '')
? (filter.sortBy as keyof TransitAgent)
: 'name';
const sortOrder = filter.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
const [data, total] = await this.transitAgentsRepository.findAndCount({
where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
order: { [sortBy]: sortOrder } as FindOptionsOrder<TransitAgent>,
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
data: data.map(withValidityStatus),
meta: {
total,
page,
pageSize,
totalPages: Math.max(1, Math.ceil(total / pageSize)),
},
};
}
/** Active and currently inside its validity window — the DJ assignment dropdown. */
async findAssignable(): Promise<TransitAgent[]> {
return this.transitAgentsRepository.findAssignable(todayISODate());
}
async findById(id: string): Promise<TransitAgentView> {
const agent = await this.transitAgentsRepository.findById(id);
if (!agent) {
throw new NotFoundException(`Transit agent ${id} not found`);
}
return withValidityStatus(agent);
}
/** Used by the assignment flow — rejects a suspended or out-of-window officer. */
async getAssignable(id: string): Promise<TransitAgent> {
const agent = await this.transitAgentsRepository.findById(id);
if (!agent) {
throw new BadRequestException('Selected transit officer was not found.');
}
if (!agent.isActive) {
throw new BadRequestException(`${agent.name} is suspended — pick another transit officer.`);
}
if (validityStatus(agent) !== 'VALID') {
throw new BadRequestException(
`${agent.name}'s validity window has expired — pick another transit officer or extend their dates.`,
);
}
return agent;
}
async create(dto: CreateTransitAgentDto): Promise<TransitAgentView> {
if (dto.validTo < dto.validFrom) {
throw new BadRequestException('Valid-to date must be on or after valid-from date.');
}
const agent = await this.transitAgentsRepository.create({
name: dto.name.trim(),
validFrom: dto.validFrom,
validTo: dto.validTo,
isActive: dto.isActive ?? true,
});
return withValidityStatus(agent);
}
async update(id: string, dto: UpdateTransitAgentDto): Promise<TransitAgentView> {
const current = await this.findById(id);
const nextValidFrom = dto.validFrom ?? current.validFrom;
const nextValidTo = dto.validTo ?? current.validTo;
if (nextValidTo < nextValidFrom) {
throw new BadRequestException('Valid-to date must be on or after valid-from date.');
}
const updated = await this.transitAgentsRepository.update(id, {
...dto,
...(dto.name ? { name: dto.name.trim() } : {}),
});
if (!updated) {
throw new NotFoundException(`Transit agent ${id} not found`);
}
return withValidityStatus(updated);
}
async remove(id: string): Promise<void> {
await this.findById(id);
await this.transitAgentsRepository.softDelete(id);
}
}