mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
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.
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
import { DataSource } from 'typeorm';
|
|
|
|
type MileRecord = {
|
|
bookingId?: string | null;
|
|
advancedPayment?: number | string | null;
|
|
booking?: {
|
|
cargoTotalWeightVgm?: number | string | null;
|
|
bookingContainers?: Array<{
|
|
units?: Array<{ vgmTons?: number | string | null }> | null;
|
|
}> | null;
|
|
/** Attached here: the train schedule the booking rides, for mile alignment. */
|
|
trainSchedule?: { trainNumber: string | null; departureDate: string | null } | null;
|
|
} | null;
|
|
};
|
|
|
|
/**
|
|
* Display enrichment for first/last-mile lists (Assign Vehicle modal etc.):
|
|
* - Advance payment: mile records are created with advanced_payment 0 — the
|
|
* real advance is the FIRST_MILE/LAST_MILE line the customer already paid
|
|
* on the booking invoice.
|
|
* - Cargo tons: container bookings often carry tonnage on the per-unit VGMs
|
|
* while cargo_total_weight_vgm stays 0 — fall back to the summed units.
|
|
* Fills both in-memory on the loaded records; nothing is persisted.
|
|
*/
|
|
export async function attachMileFinancials(
|
|
dataSource: DataSource,
|
|
records: MileRecord[],
|
|
chargeType: 'FIRST_MILE' | 'LAST_MILE',
|
|
): Promise<void> {
|
|
for (const r of records) {
|
|
const b = r.booking;
|
|
if (!b || Number(b.cargoTotalWeightVgm) > 0) continue;
|
|
const unitTons = (b.bookingContainers ?? []).reduce(
|
|
(sum, bc) =>
|
|
sum + (bc.units ?? []).reduce((s, u) => s + (Number(u.vgmTons) || 0), 0),
|
|
0,
|
|
);
|
|
if (unitTons > 0) b.cargoTotalWeightVgm = Number(unitTons.toFixed(3));
|
|
}
|
|
|
|
// Train alignment: which schedule each booking rides (mile pickups/deliveries
|
|
// are planned against the train's departure).
|
|
const bookingIds = [...new Set(records.map((r) => r.bookingId).filter(Boolean))] as string[];
|
|
if (bookingIds.length) {
|
|
const schedules: Array<{
|
|
bookingId: string;
|
|
trainNumber: string | null;
|
|
departureDate: string | null;
|
|
}> = await dataSource.query(
|
|
`SELECT DISTINCT ON (tsb.booking_id)
|
|
tsb.booking_id AS "bookingId",
|
|
ts.train_number AS "trainNumber",
|
|
COALESCE(ts.actual_departure_at, ts.scheduled_departure_date)::text AS "departureDate"
|
|
FROM freight.train_schedule_bookings tsb
|
|
JOIN freight.train_schedules ts
|
|
ON ts.id = tsb.train_schedule_id AND ts.deleted_at IS NULL
|
|
WHERE tsb.booking_id = ANY($1::uuid[]) AND tsb.deleted_at IS NULL
|
|
ORDER BY tsb.booking_id, tsb.created_at DESC`,
|
|
[bookingIds],
|
|
);
|
|
const byBookingSchedule = new Map(schedules.map((s) => [s.bookingId, s]));
|
|
for (const r of records) {
|
|
const s = r.bookingId ? byBookingSchedule.get(r.bookingId) : undefined;
|
|
if (r.booking && s) {
|
|
r.booking.trainSchedule = {
|
|
trainNumber: s.trainNumber,
|
|
departureDate: s.departureDate,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
const needAdvance = records.filter(
|
|
(r) => r.bookingId && !(Number(r.advancedPayment) > 0),
|
|
);
|
|
if (!needAdvance.length) return;
|
|
|
|
const rows: Array<{ bookingId: string; amount: string }> = await dataSource.query(
|
|
`SELECT i.source_id AS "bookingId", SUM(il.amount) AS amount
|
|
FROM freight.invoice_lines il
|
|
JOIN freight.invoices i ON i.id = il.invoice_id AND i.deleted_at IS NULL
|
|
WHERE i.source = 'booking'
|
|
AND i.status = 'PAID'
|
|
AND i.source_id = ANY($1::text[])
|
|
AND il.charge_type = $2
|
|
AND il.deleted_at IS NULL
|
|
GROUP BY i.source_id`,
|
|
[needAdvance.map((r) => r.bookingId), chargeType],
|
|
);
|
|
const byBooking = new Map(rows.map((r) => [r.bookingId, Number(r.amount)]));
|
|
for (const r of needAdvance) {
|
|
const paid = byBooking.get(r.bookingId as string);
|
|
if (paid) r.advancedPayment = paid;
|
|
}
|
|
}
|