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): 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, 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 { return this.transitAgentsRepository.findAssignable(todayISODate()); } async findById(id: string): Promise { 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 { 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 { 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 { 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 { await this.findById(id); await this.transitAgentsRepository.softDelete(id); } }