From 7f6c9c63134beccb3395fffce623cf776ba9611d Mon Sep 17 00:00:00 2001 From: natib21 Date: Thu, 2 Jul 2026 13:55:08 +0000 Subject: [PATCH] fix --- .../modules/first-mile/first-mile.service.ts | 66 ++++++++++++++++++- .../modules/last-mile/last-mile.service.ts | 61 ++++++++++++++++- .../src/modules/vehicles/vehicles.service.ts | 47 ++++++++++++- 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts index bc8801067..6fd1bc618 100644 --- a/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts +++ b/apps/edr-freight-api/src/modules/first-mile/first-mile.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { FindOptionsWhere } from 'typeorm'; +import { FindOptionsWhere, In } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; @@ -7,6 +7,7 @@ import { BookingsRepository } from '../bookings/bookings.repository'; import { DriversService } from '../drivers/drivers.service'; import { SmsClientService } from '../notifications/sms-client.service'; import { VehiclesService } from '../vehicles/vehicles.service'; +import { VehicleStatus } from '../vehicles/entities/vehicle.entity'; import { CreateFirstMileDto } from './dto/create-first-mile.dto'; import { UpdateFirstMileDto } from './dto/update-first-mile.dto'; import { FirstMile, FirstMileStatus } from './entities/first-mile.entity'; @@ -181,7 +182,7 @@ export class FirstMileService { return existing; } - return this.firstMileRepository.create({ + const record = await this.firstMileRepository.create({ bookingId: dto.bookingId, status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, @@ -191,6 +192,12 @@ export class FirstMileService { vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, }); + + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + + return record; } private async findByBookingId(bookingId: string): Promise { @@ -236,24 +243,61 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${id} not found`); } + // Keep vehicle statuses in sync: new vehicle goes BUSY, replaced one goes back to FREE + if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) { + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + if (existing.vehicleId) { + await this.vehiclesService.releaseIfUnused([existing.vehicleId]); + } + } + // Notify assigned driver on every explicit vehicle assignment or reassignment if (dto.vehicleId) { void this.notifyDriverAssignment(dto.vehicleId, existing); } + // Trip finished — release the vehicles it was holding + if (dto.status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') { + await this.releaseVehicles(updated); + } + return updated; } async updateStatus(id: string, status: FirstMileStatus): Promise { + const existing = await this.findById(id); const updated = await this.firstMileRepository.update(id, { status }); if (!updated) { throw new NotFoundException(`First-mile record ${id} not found`); } + if (status === 'RECEIVED_TO_PORT' && existing.status !== 'RECEIVED_TO_PORT') { + await this.releaseVehicles(updated); + } + return updated; } + /** + * Free every vehicle held by this record (direct assignment + container + * allocations), unless still in use by another active trip. + */ + private async releaseVehicles(record: FirstMile): Promise { + const recordAllocations = await this.dataSource.manager.find(FirstMileContainerAllocation, { + where: { firstMileId: record.id }, + }); + const vehicleIds = recordAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + if (record.vehicleId) { + vehicleIds.push(record.vehicleId); + } + await this.vehiclesService.releaseIfUnused(vehicleIds); + } + private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise { try { const vehicle = await this.vehiclesService.findById(vehicleId); @@ -302,6 +346,16 @@ export class FirstMileService { throw new NotFoundException(`First-mile record ${firstMileId} not found`); } + const previousAllocations = await this.dataSource.manager.find(FirstMileContainerAllocation, { + where: { + firstMileId, + containerId: In(allocations.map((a) => a.containerId)), + }, + }); + const previousVehicleIds = previousAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + await this.dataSource.transaction(async (manager) => { for (const allocation of allocations) { await manager.delete(FirstMileContainerAllocation, { @@ -318,6 +372,14 @@ export class FirstMileService { } }); + const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); + await Promise.all( + [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + ); + await this.vehiclesService.releaseIfUnused( + previousVehicleIds.filter((id) => !vehicleIds.has(id)), + ); + return { success: true, allocated: allocations.length, diff --git a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts index 732b1a618..61e732bba 100644 --- a/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile/last-mile.service.ts @@ -1,10 +1,11 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { DataSource, FindOptionsWhere } from 'typeorm'; +import { DataSource, FindOptionsWhere, In } from 'typeorm'; import { BookingsRepository } from '../bookings/bookings.repository'; import { DriversService } from '../drivers/drivers.service'; import { SmsClientService } from '../notifications/sms-client.service'; import { VehiclesService } from '../vehicles/vehicles.service'; +import { VehicleStatus } from '../vehicles/entities/vehicle.entity'; import { CreateLastMileDto } from './dto/create-last-mile.dto'; import { UpdateLastMileDto } from './dto/update-last-mile.dto'; import { LastMile, LastMileStatus } from './entities/last-mile.entity'; @@ -131,7 +132,7 @@ export class LastMileService { } async create(dto: CreateLastMileDto): Promise { - return this.lastMileRepository.create({ + const record = await this.lastMileRepository.create({ bookingId: dto.bookingId, status: dto.status ?? 'READY_TO_TRANSIT', advancedPayment: dto.advancedPayment ?? 0, @@ -141,6 +142,12 @@ export class LastMileService { vehicleId: dto.vehicleId ?? null, paid: (dto as any).paid ?? false, }); + + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + + return record; } @OnEvent("lastmile.invoice.paid") @@ -174,14 +181,46 @@ export class LastMileService { throw new NotFoundException(`Last-mile record ${id} not found`); } + // Keep vehicle statuses in sync: new vehicle goes BUSY, replaced one goes back to FREE + if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) { + if (dto.vehicleId) { + await this.vehiclesService.setStatus(dto.vehicleId, VehicleStatus.BUSY); + } + if (existing.vehicleId) { + await this.vehiclesService.releaseIfUnused([existing.vehicleId]); + } + } + // Notify assigned driver on every explicit vehicle assignment or reassignment if (dto.vehicleId) { void this.notifyDriverAssignment(dto.vehicleId, existing); } + // Trip finished — release the vehicles it was holding + if (dto.status === 'DELIVERED' && existing.status !== 'DELIVERED') { + await this.releaseVehicles(updated); + } + return updated; } + /** + * Free every vehicle held by this record (direct assignment + container + * allocations), unless still in use by another active trip. + */ + private async releaseVehicles(record: LastMile): Promise { + const recordAllocations = await this.dataSource.manager.find(LastMileContainerAllocation, { + where: { lastMileId: record.id }, + }); + const vehicleIds = recordAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + if (record.vehicleId) { + vehicleIds.push(record.vehicleId); + } + await this.vehiclesService.releaseIfUnused(vehicleIds); + } + private async notifyDriverAssignment(vehicleId: string, record: LastMile): Promise { try { const vehicle = await this.vehiclesService.findById(vehicleId); @@ -235,6 +274,16 @@ export class LastMileService { throw new NotFoundException(`Last-mile record ${lastMileId} not found`); } + const previousAllocations = await this.dataSource.manager.find(LastMileContainerAllocation, { + where: { + lastMileId, + containerId: In(allocations.map((a) => a.containerId)), + }, + }); + const previousVehicleIds = previousAllocations + .map((a) => a.vehicleId) + .filter((id): id is string => Boolean(id)); + await this.dataSource.transaction(async (manager) => { for (const allocation of allocations) { await manager.delete(LastMileContainerAllocation, { @@ -251,6 +300,14 @@ export class LastMileService { } }); + const vehicleIds = new Set(allocations.map((a) => a.vehicleId)); + await Promise.all( + [...vehicleIds].map((vehicleId) => this.vehiclesService.setStatus(vehicleId, VehicleStatus.BUSY)), + ); + await this.vehiclesService.releaseIfUnused( + previousVehicleIds.filter((id) => !vehicleIds.has(id)), + ); + return { success: true, allocated: allocations.length, diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts index 2bc882591..61765a88d 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -1,9 +1,13 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Not, Repository } from 'typeorm'; import { CreateVehicleDto } from './dto/create-vehicle.dto'; import { UpdateVehicleDto } from './dto/update-vehicle.dto'; import { Vehicle, VehicleStatus } from './entities/vehicle.entity'; +import { FirstMile, FirstMileStatus } from '../first-mile/entities/first-mile.entity'; +import { FirstMileContainerAllocation } from '../first-mile/entities/first-mile-container-allocation.entity'; +import { LastMile, LastMileStatus } from '../last-mile/entities/last-mile.entity'; +import { LastMileContainerAllocation } from '../last-mile/entities/last-mile-container-allocation.entity'; @Injectable() export class VehiclesService { @@ -91,6 +95,47 @@ export class VehiclesService { return this.vehicleRepo.save(vehicle); } + async setStatus(id: string, status: VehicleStatus): Promise { + await this.vehicleRepo.update(id, { status }); + } + + /** + * Set vehicles back to FREE, but only when no active (non-completed) + * first/last-mile record or container allocation still references them. + * First-mile trips ending in RECEIVED_TO_PORT and last-mile trips ending + * in DELIVERED no longer hold the vehicle. + */ + async releaseIfUnused(vehicleIds: string[]): Promise { + const manager = this.vehicleRepo.manager; + for (const vehicleId of [...new Set(vehicleIds)]) { + const [fmRecords, lmRecords, fmAllocations, lmAllocations] = await Promise.all([ + manager.count(FirstMile, { + where: { vehicleId, status: Not('RECEIVED_TO_PORT') }, + }), + manager.count(LastMile, { + where: { vehicleId, status: Not('DELIVERED') }, + }), + manager + .createQueryBuilder(FirstMileContainerAllocation, 'alloc') + .innerJoin(FirstMile, 'fm', 'fm.id = alloc.firstMileId') + .where('alloc.vehicleId = :vehicleId', { vehicleId }) + .andWhere('fm.status != :done', { done: 'RECEIVED_TO_PORT' }) + .andWhere('fm.deletedAt IS NULL') + .getCount(), + manager + .createQueryBuilder(LastMileContainerAllocation, 'alloc') + .innerJoin(LastMile, 'lm', 'lm.id = alloc.lastMileId') + .where('alloc.vehicleId = :vehicleId', { vehicleId }) + .andWhere('lm.status != :done', { done: 'DELIVERED' }) + .andWhere('lm.deletedAt IS NULL') + .getCount(), + ]); + if (fmRecords + lmRecords + fmAllocations + lmAllocations === 0) { + await this.setStatus(vehicleId, VehicleStatus.FREE); + } + } + } + async remove(id: string): Promise { await this.findById(id); await this.vehicleRepo.softDelete(id);