mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 06:00:55 +00:00
139 lines
4.7 KiB
TypeScript
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);
|
|
}
|
|
}
|