import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateDriverDto } from './dto/create-driver.dto'; import { UpdateDriverDto } from './dto/update-driver.dto'; import { Driver, DriverStatus } from './entities/driver.entity'; import { FleetHistoryService } from '../fleet-history/fleet-history.service'; import { FleetEventType } from '../fleet-history/entities/fleet-event.entity'; import { FilesService } from '../files/files.service'; /** Resource + code the driver-documents upload area is stored under. */ const DRIVER_DOCS_RESOURCE = 'driver'; const DRIVER_DOCS_CODE = 'driver_docs'; @Injectable() export class DriversService { constructor( @InjectRepository(Driver) private readonly driverRepo: Repository, private readonly history: FleetHistoryService, private readonly filesService: FilesService, ) {} /** Upload one or more driver documents (code "driver_docs"). */ async uploadDocuments(driverId: string, files: Express.Multer.File[]) { const driver = await this.driverRepo.findOneBy({ id: driverId }); if (!driver) throw new NotFoundException(`Driver ${driverId} not found`); if (!files?.length) throw new BadRequestException('No files provided'); return Promise.all( files.map((file) => this.filesService.upload({ resourceId: driverId, resource: DRIVER_DOCS_RESOURCE, code: DRIVER_DOCS_CODE, file, }), ), ); } /** List a driver's uploaded documents (code "driver_docs"). */ async listDocuments(driverId: string) { const all = await this.filesService.findByResource(driverId, DRIVER_DOCS_RESOURCE); return all.filter((f) => f.code === DRIVER_DOCS_CODE); } /** Delete a single driver document by file id. */ async removeDocument(fileId: string): Promise { await this.filesService.remove(fileId); } async create(dto: CreateDriverDto): Promise { if (dto.faydaVerified !== true) { throw new BadRequestException( 'Driver identity must be verified with Fayda before saving', ); } const existing = await this.driverRepo.findOne({ where: [ { licenseNumber: dto.licenseNumber }, { email: dto.email }, { phoneNumber: dto.phoneNumber }, ], }); if (existing) { if (existing.licenseNumber === dto.licenseNumber) { throw new ConflictException(`Driver with license number ${dto.licenseNumber} already exists`); } if (existing.email === dto.email) { throw new ConflictException(`Driver with email ${dto.email} already exists`); } if (existing.phoneNumber === dto.phoneNumber) { throw new ConflictException(`Driver with phone number ${dto.phoneNumber} already exists`); } } if (dto.faydaSub) { const dupe = await this.driverRepo.findOne({ where: { faydaSub: dto.faydaSub }, }); if (dupe) { throw new ConflictException( 'A driver is already registered for this Fayda identity', ); } } const driver = this.driverRepo.create(dto); const saved = await this.driverRepo.save(driver); await this.history.record({ eventType: FleetEventType.DRIVER_REGISTERED, driverId: saved.id, label: `${saved.firstName ?? ''} ${saved.lastName ?? ''}`.trim() || null, toValue: saved.status ?? null, }); return saved; } async findAll(query: { search?: string; status?: DriverStatus | string; page?: number; limit?: number; sortBy?: string; sortOrder?: 'ASC' | 'DESC'; } = {}): Promise { const qb = this.driverRepo.createQueryBuilder('d'); if (query.search) { qb.where( '(d.firstName ILIKE :search OR d.lastName ILIKE :search OR d.email ILIKE :search OR d.licenseNumber ILIKE :search OR d.phoneNumber ILIKE :search)', { search: `%${query.search}%` }, ); } if (query.status) { qb.andWhere('d.status = :status', { status: query.status }); } const sortBy = query.sortBy && ['firstName', 'lastName', 'status', 'createdAt'].includes(query.sortBy) ? query.sortBy : 'createdAt'; const sortOrder = (query.sortOrder ?? 'DESC').toUpperCase(); return qb .orderBy(`d.${sortBy}`, sortOrder as 'ASC' | 'DESC') .getMany(); } async findById(id: string): Promise { const driver = await this.driverRepo.findOne({ where: { id } }); if (!driver) { throw new NotFoundException(`Driver ${id} not found`); } return driver; } async update(id: string, dto: UpdateDriverDto): Promise { const driver = await this.findById(id); if (dto.licenseNumber && dto.licenseNumber !== driver.licenseNumber) { const existing = await this.driverRepo.findOne({ where: { licenseNumber: dto.licenseNumber }, }); if (existing) { throw new ConflictException(`Driver with license number ${dto.licenseNumber} already exists`); } } if (dto.email && dto.email !== driver.email) { const existing = await this.driverRepo.findOne({ where: { email: dto.email }, }); if (existing) { throw new ConflictException(`Driver with email ${dto.email} already exists`); } } if (dto.phoneNumber && dto.phoneNumber !== driver.phoneNumber) { const existing = await this.driverRepo.findOne({ where: { phoneNumber: dto.phoneNumber }, }); if (existing) { throw new ConflictException(`Driver with phone number ${dto.phoneNumber} already exists`); } } if (dto.faydaSub && dto.faydaSub !== driver.faydaSub) { const dupe = await this.driverRepo.findOne({ where: { faydaSub: dto.faydaSub }, }); if (dupe) { throw new ConflictException( 'A driver is already registered for this Fayda identity', ); } } Object.assign(driver, dto); if (driver.faydaVerified !== true) { throw new BadRequestException( 'Driver identity must be verified with Fayda before saving', ); } return this.driverRepo.save(driver); } async remove(id: string): Promise { await this.findById(id); await this.driverRepo.softDelete(id); } }