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 { 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; } }