This commit is contained in:
natib21
2026-07-03 21:21:42 +00:00
parent 7155ec7e01
commit 6bf8da2934
5 changed files with 51 additions and 431 deletions

View File

@@ -1,17 +0,0 @@
import { IsArray, IsUUID, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
export class LastMileContainerAllocationDto {
@IsUUID()
containerId!: string;
@IsUUID()
vehicleId!: string;
}
export class AllocateLastMileContainersDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => LastMileContainerAllocationDto)
allocations!: LastMileContainerAllocationDto[];
}

View File

@@ -17,7 +17,6 @@ import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking
import { CreateLastMileDto } from './dto/create-last-mile.dto';
import { UpdateLastMileDto } from './dto/update-last-mile.dto';
import { AllocateLastMileContainersDto } from './dto/allocate-containers.dto';
import { SetVehiclesDto } from './dto/set-vehicles.dto';
import { SetDistancesDto } from './dto/set-distances.dto';
import { LastMileStatus } from './entities/last-mile.entity';
@@ -93,15 +92,6 @@ export class LastMileController {
return this.lastMileService.remove(id);
}
@Post(':id/allocate-containers')
@TrainSchedulingManage()
@ApiOperation({ summary: 'Allocate containers to vehicles' })
async allocateContainers(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: AllocateLastMileContainersDto,
) {
return this.lastMileService.allocateContainers(id, dto.allocations);
}
@Post(':id/vehicles')
@TrainSchedulingManage()

View File

@@ -243,14 +243,16 @@ export class LastMileService {
return record;
}
@OnEvent("lastmile.invoice.paid")
@OnEvent("last_mile.invoice.paid")
async onBookingInvoicePaid(payload: InvoiceEventPayload): Promise<void> {
try {
await this.lastMileRepository.update(payload.sourceId, { paid: true } as any);
this.logger.log(`Marked last-mile record ${payload.sourceId} as paid (invoice ${payload.invoiceId})`);
// Invoice paid → the delivery is complete. Route through update() so it
// also frees the trucks + records history (same as "Mark Delivered").
await this.update(payload.sourceId, { status: 'DELIVERED', paid: true } as unknown as UpdateLastMileDto);
this.logger.log(`Last-mile ${payload.sourceId} marked DELIVERED on invoice ${payload.invoiceId} payment`);
} catch (err) {
this.logger.error(
`Failed to update last-mile payment status for record ${payload.sourceId}: ${String(err)}`,
`Failed to deliver last-mile ${payload.sourceId} on payment: ${String(err)}`,
);
}
}
@@ -484,6 +486,15 @@ export class LastMileService {
remainingPayment?: number,
): Promise<LastMile> {
await this.findById(id);
// Distances are locked once the invoice exists.
const invoices = await this.billing.findBySourceIds('last_mile', [id]);
if (invoices.length) {
throw new BadRequestException(
'Distances cannot be changed after the invoice is generated',
);
}
for (const d of distances) {
await this.dataSource.manager.update(
LastMileVehicleAssignment,
@@ -541,6 +552,14 @@ export class LastMileService {
async remove(id: string): Promise<void> {
const existing = await this.findById(id);
// Can't delete once billed.
const invoices = await this.billing.findBySourceIds('last_mile', [id]);
if (invoices.length) {
throw new BadRequestException(
'Cannot delete a last-mile delivery after its invoice is generated',
);
}
// Every vehicle this delivery holds — junction + legacy + container rows.
const assignments = await this.dataSource.manager.find(LastMileVehicleAssignment, {
where: { lastMileId: id },
@@ -581,89 +600,4 @@ export class LastMileService {
}
}
async allocateContainers(
lastMileId: string,
allocations: Array<{ containerId: string; vehicleId: string }>,
) {
const lastMile = await this.findById(lastMileId);
if (!lastMile) {
throw new NotFoundException(`Last-mile record ${lastMileId} not found`);
}
// Capture the vehicles currently on these containers so a reallocation can
// be diffed into assigned/released history events below.
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, {
lastMileId,
containerId: allocation.containerId,
});
await manager.insert(LastMileContainerAllocation, {
lastMileId,
containerId: allocation.containerId,
vehicleId: allocation.vehicleId,
containerType: 'CONTAINER',
quantity: 1,
});
}
});
// Keep vehicle availability in sync: newly-allocated cars go BUSY, cars no
// longer on any of these containers are freed if unused elsewhere.
const vehicleIds = new Set(allocations.map((a) => a.vehicleId));
await Promise.all(
[...vehicleIds].map((id) =>
this.vehiclesService.setAvailability(id, VehicleAvailability.BUSY),
),
);
await this.vehiclesService.releaseIfUnused(
previousVehicleIds.filter((id) => !vehicleIds.has(id)),
);
// History: one event per vehicle actually added or removed by this
// multi-car (re)allocation, so reassignments show on every timeline.
const prevSet = new Set(previousVehicleIds);
const bookingRef = await this.resolveBookingRef(lastMile);
for (const vehicleId of vehicleIds) {
if (prevSet.has(vehicleId)) continue;
const info = await this.vehicleInfo(vehicleId);
await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId,
lastMileId,
driverId: info.driverId,
label: lastMile.status,
metadata: { mile: 'LAST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName },
});
}
for (const vehicleId of previousVehicleIds) {
if (vehicleIds.has(vehicleId)) continue;
const info = await this.vehicleInfo(vehicleId);
await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_RELEASED,
vehicleId,
lastMileId,
driverId: info.driverId,
metadata: { mile: 'LAST', bookingRef, vehiclePlate: info.plate, driverName: info.driverName },
});
}
return {
success: true,
allocated: allocations.length,
};
}
}