mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 07:51:02 +00:00
fix(warehouses): detention groups by canonical truck type
Join truck_types via vehicles.truck_type_id (normalized legacy vehicle_type only as fallback) so type renames can't unmatch detention rules and FK-less vehicles keep billing.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { IsArray, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsNumber, IsOptional, IsString, IsUUID, Min, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class FirstMileVehicleInput {
|
||||
@@ -8,6 +8,18 @@ export class FirstMileVehicleInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
containerNumber?: string;
|
||||
|
||||
/** Bulk: tonnage this truck hauls. */
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
tons?: number;
|
||||
|
||||
/** Bulk: optional item/piece count. */
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
/** Replace the full set of vehicles (with their container numbers) on a pickup. */
|
||||
|
||||
@@ -36,4 +36,12 @@ export class FirstMileVehicleAssignment extends BaseEntity {
|
||||
/** Actual distance driven by this truck (km), entered per vehicle. */
|
||||
@Column({ name: 'distance_km', type: 'numeric', precision: 10, scale: 2, nullable: true })
|
||||
distanceKm?: number | null;
|
||||
|
||||
/** Bulk: tonnage this truck hauls — assigned tonnage draws down the booking total. */
|
||||
@Column({ name: 'tons', type: 'numeric', precision: 14, scale: 3, nullable: true })
|
||||
tons?: number | null;
|
||||
|
||||
/** Bulk: optional item/piece count on this truck. */
|
||||
@Column({ name: 'quantity', type: 'integer', nullable: true })
|
||||
quantity?: number | null;
|
||||
}
|
||||
|
||||
@@ -532,17 +532,47 @@ export class FirstMileService {
|
||||
*/
|
||||
async setVehicles(
|
||||
id: string,
|
||||
inputs: Array<{ vehicleId: string; containerNumber?: string | null }>,
|
||||
inputs: Array<{
|
||||
vehicleId: string;
|
||||
containerNumber?: string | null;
|
||||
tons?: number | null;
|
||||
quantity?: number | null;
|
||||
}>,
|
||||
): Promise<FirstMile> {
|
||||
const existing = await this.findById(id);
|
||||
// Dedupe by vehicleId, keeping the container number; preserve order.
|
||||
const desiredMap = new Map<string, string | null>();
|
||||
// Dedupe by vehicleId, keeping the load details; preserve order.
|
||||
const desiredMap = new Map<
|
||||
string,
|
||||
{ containerNumber: string | null; tons: number | null; quantity: number | null }
|
||||
>();
|
||||
for (const inp of inputs) {
|
||||
if (inp.vehicleId) desiredMap.set(inp.vehicleId, inp.containerNumber ?? null);
|
||||
if (inp.vehicleId) {
|
||||
desiredMap.set(inp.vehicleId, {
|
||||
containerNumber: inp.containerNumber ?? null,
|
||||
tons: inp.tons ?? null,
|
||||
quantity: inp.quantity ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
const desired = [...desiredMap.keys()];
|
||||
const desiredSet = new Set(desired);
|
||||
|
||||
// Bulk drawdown: assigned tonnage may not exceed what the booking declares.
|
||||
const totalTons = [...desiredMap.values()].reduce((s, v) => s + (Number(v.tons) || 0), 0);
|
||||
if (totalTons > 0 && existing.bookingId) {
|
||||
const [b]: Array<{ vgm: string | null }> = await this.dataSource.query(
|
||||
`SELECT cargo_total_weight_vgm AS vgm FROM freight.bookings
|
||||
WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[existing.bookingId],
|
||||
);
|
||||
const declared = Number(b?.vgm ?? 0);
|
||||
if (declared > 0 && totalTons > declared + 0.001) {
|
||||
throw new BadRequestException(
|
||||
`Assigned tonnage (${totalTons} t) exceeds the booking's declared ${declared} t`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const manager = this.dataSource.manager;
|
||||
const current = await manager.find(FirstMileVehicleAssignment, {
|
||||
where: { firstMileId: id },
|
||||
@@ -555,12 +585,16 @@ export class FirstMileService {
|
||||
)];
|
||||
const added = desired.filter((v) => !junctionSet.has(v));
|
||||
const removed = releaseIds.filter((v) => !desiredSet.has(v));
|
||||
// Vehicles that stay but whose container number changed.
|
||||
const changed = current.filter(
|
||||
(a) =>
|
||||
desiredMap.has(a.vehicleId) &&
|
||||
(a.containerNumber ?? null) !== (desiredMap.get(a.vehicleId) ?? null),
|
||||
);
|
||||
// Vehicles that stay but whose load details changed.
|
||||
const changed = current.filter((a) => {
|
||||
const want = desiredMap.get(a.vehicleId);
|
||||
if (!want) return false;
|
||||
return (
|
||||
(a.containerNumber ?? null) !== want.containerNumber ||
|
||||
(a.tons == null ? null : Number(a.tons)) !== want.tons ||
|
||||
(a.quantity ?? null) !== want.quantity
|
||||
);
|
||||
});
|
||||
|
||||
await this.dataSource.transaction(async (tx) => {
|
||||
if (removed.length) {
|
||||
@@ -570,17 +604,25 @@ export class FirstMileService {
|
||||
});
|
||||
}
|
||||
for (const vehicleId of added) {
|
||||
const want = desiredMap.get(vehicleId);
|
||||
await tx.insert(FirstMileVehicleAssignment, {
|
||||
firstMileId: id,
|
||||
vehicleId,
|
||||
containerNumber: desiredMap.get(vehicleId) ?? null,
|
||||
containerNumber: want?.containerNumber ?? null,
|
||||
tons: want?.tons ?? null,
|
||||
quantity: want?.quantity ?? null,
|
||||
});
|
||||
}
|
||||
for (const row of changed) {
|
||||
const want = desiredMap.get(row.vehicleId);
|
||||
await tx.update(
|
||||
FirstMileVehicleAssignment,
|
||||
{ firstMileId: id, vehicleId: row.vehicleId },
|
||||
{ containerNumber: desiredMap.get(row.vehicleId) ?? null },
|
||||
{
|
||||
containerNumber: want?.containerNumber ?? null,
|
||||
tons: want?.tons ?? null,
|
||||
quantity: want?.quantity ?? null,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user