mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 06:40:57 +00:00
fix: the invoice status syncing with booking payment window
This commit is contained in:
@@ -4,24 +4,24 @@ import {
|
||||
Logger,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Freight } from '@edr/types';
|
||||
} from "@nestjs/common";
|
||||
import { InjectDataSource } from "@nestjs/typeorm";
|
||||
import { Cron, SchedulerRegistry } from "@nestjs/schedule";
|
||||
import { DataSource } from "typeorm";
|
||||
import { Freight } from "@edr/types";
|
||||
|
||||
import { BillingService } from '../billing/billing.service';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { Locomotive } from '../locomotives/entities/locomotive.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
||||
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
|
||||
import { TrainScheduleBookingsRepository } from '../train-schedules/train-schedule-bookings.repository';
|
||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||
import { BookingNotifierService } from './booking-notifier.service';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
import { eatDay, groupBookingsIntoBoardWindows } from './batch-window.util';
|
||||
import { BillingService } from "../billing/billing.service";
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import { BookingsRepository } from "../bookings/bookings.repository";
|
||||
import { Locomotive } from "../locomotives/entities/locomotive.entity";
|
||||
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
||||
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
|
||||
import { TrainSchedulesRepository } from "../train-schedules/train-schedules.repository";
|
||||
import { TrainScheduleBookingsRepository } from "../train-schedules/train-schedule-bookings.repository";
|
||||
import { TrainSchedulingGlobalRules } from "./entities/train-scheduling-global-rules.entity";
|
||||
import { BookingNotifierService } from "./booking-notifier.service";
|
||||
import { TrainSchedulingService } from "./train-scheduling.service";
|
||||
import { eatDay, groupBookingsIntoBoardWindows } from "./batch-window.util";
|
||||
import {
|
||||
BATCH_CRON,
|
||||
BATCH_TIMEZONE,
|
||||
@@ -29,13 +29,13 @@ import {
|
||||
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||
DEFAULT_WAGONS_PER_BOOKING,
|
||||
PAYMENT_WINDOW_MS,
|
||||
} from './booking-batch.constants';
|
||||
} from "./booking-batch.constants";
|
||||
import {
|
||||
bookingTrainLengthMeters,
|
||||
deriveTrainCapacityFromLocomotive,
|
||||
wagonTypeDimensionsFromEntity,
|
||||
} from './train-capacity.util';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
} from "./train-capacity.util";
|
||||
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
|
||||
|
||||
/** A train's remaining capacity along the three physical limits the batch enforces. */
|
||||
interface Capacity {
|
||||
@@ -55,12 +55,12 @@ interface RouteDayGroup {
|
||||
type WagonLengths = { container: number; bulk: number };
|
||||
|
||||
export type BatchBoardBookingState =
|
||||
| 'ALLOCATED'
|
||||
| 'SELECTED_FOR_BATCH'
|
||||
| 'READY'
|
||||
| 'WAITING'
|
||||
| 'PENDING_CONTRACT'
|
||||
| 'EXPIRED';
|
||||
| "ALLOCATED"
|
||||
| "SELECTED_FOR_BATCH"
|
||||
| "READY"
|
||||
| "WAITING"
|
||||
| "PENDING_CONTRACT"
|
||||
| "EXPIRED";
|
||||
|
||||
export interface BatchBoardBooking {
|
||||
id: string;
|
||||
@@ -75,10 +75,10 @@ export interface BatchBoardBooking {
|
||||
}
|
||||
|
||||
export type BookingAllocationStatus =
|
||||
| 'NOT_ATTEMPTED'
|
||||
| 'ASSIGNED'
|
||||
| 'DEFERRED'
|
||||
| 'FAILED';
|
||||
| "NOT_ATTEMPTED"
|
||||
| "ASSIGNED"
|
||||
| "DEFERRED"
|
||||
| "FAILED";
|
||||
|
||||
export interface BatchBoardBookingDetail extends BatchBoardBooking {
|
||||
fullyExecutedAt: string | null;
|
||||
@@ -116,9 +116,9 @@ export interface BatchBoardScheduleDetail {
|
||||
scheduleDate: string | null;
|
||||
status: string;
|
||||
bookingWindowStatus: string;
|
||||
locomotive: BatchBoardSchedule['locomotive'];
|
||||
capacity: BatchBoardSchedule['capacity'];
|
||||
counts: BatchBoardSchedule['counts'];
|
||||
locomotive: BatchBoardSchedule["locomotive"];
|
||||
capacity: BatchBoardSchedule["capacity"];
|
||||
counts: BatchBoardSchedule["counts"];
|
||||
windows: BatchWindowGroup[];
|
||||
pendingContract: BatchWindowGroup;
|
||||
allocationViolations: string[];
|
||||
@@ -182,7 +182,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private readonly scheduler: SchedulerRegistry,
|
||||
private readonly trainSchedulingService: TrainSchedulingService,
|
||||
private readonly billing: BillingService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/** On boot, reconcile OPEN route-days and re-arm settle timers. */
|
||||
async onModuleInit(): Promise<void> {
|
||||
@@ -198,10 +198,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
const reserved = await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.createQueryBuilder('b')
|
||||
.select('DISTINCT b.train_schedule_id', 'scheduleId')
|
||||
.createQueryBuilder("b")
|
||||
.select("DISTINCT b.train_schedule_id", "scheduleId")
|
||||
.where(`b.status IN ('SELECTED_FOR_BATCH', 'AWAITING_PAYMENT')`)
|
||||
.andWhere('b.train_schedule_id IS NOT NULL')
|
||||
.andWhere("b.train_schedule_id IS NOT NULL")
|
||||
.getRawMany<{ scheduleId: string }>();
|
||||
for (const { scheduleId } of reserved) this.armSettle(scheduleId);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
/** Distinct (origin, destination, EAT day) groups across all OPEN schedules. */
|
||||
private async openRouteDayGroups(): Promise<RouteDayGroup[]> {
|
||||
const open = await this.trainSchedulesRepository.findAll({
|
||||
where: { bookingWindowStatus: 'OPEN' },
|
||||
where: { bookingWindowStatus: "OPEN" },
|
||||
});
|
||||
const groups = new Map<string, RouteDayGroup>();
|
||||
for (const s of open) {
|
||||
@@ -313,25 +313,29 @@ export class BookingBatchService implements OnModuleInit {
|
||||
if (!booking?.trainScheduleId) return;
|
||||
|
||||
const isBatchPaid =
|
||||
booking.status === 'SELECTED_FOR_BATCH' ||
|
||||
booking.status === 'AWAITING_PAYMENT' ||
|
||||
booking.status === 'PAID' ||
|
||||
booking.paymentStatus === 'PAID';
|
||||
booking.status === "SELECTED_FOR_BATCH" ||
|
||||
booking.status === "AWAITING_PAYMENT" ||
|
||||
booking.status === "PAID" ||
|
||||
booking.paymentStatus === "PAID";
|
||||
if (!isBatchPaid) return;
|
||||
|
||||
if (booking.status === 'SELECTED_FOR_BATCH' || booking.status === 'AWAITING_PAYMENT') {
|
||||
if (
|
||||
booking.status === "SELECTED_FOR_BATCH" ||
|
||||
booking.status === "AWAITING_PAYMENT"
|
||||
) {
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { paymentStatus: 'PAID', status: 'PAID' });
|
||||
} else if (booking.paymentStatus !== 'PAID') {
|
||||
.update(bookingId, { paymentStatus: "PAID", status: "PAID" });
|
||||
} else if (booking.paymentStatus !== "PAID") {
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { paymentStatus: 'PAID' });
|
||||
.update(bookingId, { paymentStatus: "PAID" });
|
||||
}
|
||||
|
||||
const linked = await this.trainScheduleBookingsRepository.existsForBooking(bookingId);
|
||||
const linked =
|
||||
await this.trainScheduleBookingsRepository.existsForBooking(bookingId);
|
||||
if (!linked) {
|
||||
await this.allocate(booking.trainScheduleId, booking, 'paid');
|
||||
await this.allocate(booking.trainScheduleId, booking, "paid");
|
||||
this.logger.log(
|
||||
`Linked PAID booking ${booking.reference ?? bookingId} to schedule ${booking.trainScheduleId}`,
|
||||
);
|
||||
@@ -341,7 +345,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
booking.trainScheduleId,
|
||||
);
|
||||
if (schedule && (await this.remainingWagons(schedule)) <= 0) {
|
||||
await this.setWindow(booking.trainScheduleId, 'FULL');
|
||||
await this.setWindow(booking.trainScheduleId, "FULL");
|
||||
}
|
||||
|
||||
const result = await this.trainSchedulingService.tryAutoWagonAllocation(
|
||||
@@ -352,7 +356,11 @@ export class BookingBatchService implements OnModuleInit {
|
||||
`Wagon allocation for ${booking.reference ?? bookingId}: ${result.assignedBookingIds.length} assigned`,
|
||||
);
|
||||
}
|
||||
if (result.issues.some((i) => i.bookingId === bookingId && i.status !== 'ASSIGNED')) {
|
||||
if (
|
||||
result.issues.some(
|
||||
(i) => i.bookingId === bookingId && i.status !== "ASSIGNED",
|
||||
)
|
||||
) {
|
||||
const issue = result.issues.find((i) => i.bookingId === bookingId);
|
||||
this.logger.warn(
|
||||
`Wagon allocation issue for ${booking.reference ?? bookingId}: ${issue?.issue ?? issue?.status}`,
|
||||
@@ -367,9 +375,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
/** Link PAID bookings that have no train_schedule_bookings row (cron backstop). */
|
||||
async reconcilePaidUnlinked(scheduleId: string): Promise<void> {
|
||||
const unlinked = await this.bookingsRepository.findPaidUnlinkedForSchedule(scheduleId);
|
||||
const unlinked =
|
||||
await this.bookingsRepository.findPaidUnlinkedForSchedule(scheduleId);
|
||||
for (const booking of unlinked) {
|
||||
await this.allocate(scheduleId, booking, 'paid');
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
this.logger.log(
|
||||
`Reconciled PAID booking ${booking.reference ?? booking.id} → schedule ${scheduleId}`,
|
||||
);
|
||||
@@ -378,7 +387,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
// ---- cron entry point -----------------------------------------------------
|
||||
|
||||
@Cron(BATCH_CRON, { name: 'booking-batch-fill', timeZone: BATCH_TIMEZONE })
|
||||
@Cron(BATCH_CRON, { name: "booking-batch-fill", timeZone: BATCH_TIMEZONE })
|
||||
async runBatchFill(): Promise<void> {
|
||||
const groups = await this.openRouteDayGroups();
|
||||
this.logger.log(`Batch fill: ${groups.length} OPEN route-day group(s).`);
|
||||
@@ -408,7 +417,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
destinationStation: true,
|
||||
route: true,
|
||||
},
|
||||
order: { scheduledDepartureDate: 'ASC' },
|
||||
order: { scheduledDepartureDate: "ASC" },
|
||||
});
|
||||
|
||||
const wagonLengths = await this.loadWagonLengths();
|
||||
@@ -416,7 +425,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
const board: BatchBoardSchedule[] = [];
|
||||
for (const s of schedules) {
|
||||
if (s.status === 'ARRIVED' || s.status === 'CANCELLED') continue;
|
||||
if (s.status === "ARRIVED" || s.status === "CANCELLED") continue;
|
||||
|
||||
const links = await linkRepo.find({ where: { trainScheduleId: s.id } });
|
||||
const linkedIds = new Set(links.map((l) => l.bookingId));
|
||||
@@ -428,13 +437,15 @@ export class BookingBatchService implements OnModuleInit {
|
||||
id: b.id,
|
||||
reference: b.reference ?? b.id.slice(0, 8),
|
||||
company: b.isGovernment
|
||||
? (b.governmentInstitution ?? 'Government')
|
||||
: (b.company?.name ?? '—'),
|
||||
? (b.governmentInstitution ?? "Government")
|
||||
: (b.company?.name ?? "—"),
|
||||
isGovernment: Boolean(b.isGovernment),
|
||||
wagons: need.wagons,
|
||||
weightTons: need.weightTons,
|
||||
lengthMeters: need.lengthMeters,
|
||||
paymentDeadline: b.paymentDeadline ? b.paymentDeadline.toISOString() : null,
|
||||
paymentDeadline: b.paymentDeadline
|
||||
? b.paymentDeadline.toISOString()
|
||||
: null,
|
||||
state: this.boardState(b, linkedIds.has(b.id)),
|
||||
};
|
||||
});
|
||||
@@ -445,11 +456,15 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
/** Schedule-level batch board with EAT 3h windows grouped by fullyExecutedAt. */
|
||||
async getBatchBoardDetail(scheduleId: string): Promise<BatchBoardScheduleDetail> {
|
||||
const s = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!s) throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
if (s.status === 'ARRIVED' || s.status === 'CANCELLED') {
|
||||
throw new BadRequestException('Schedule is no longer active');
|
||||
async getBatchBoardDetail(
|
||||
scheduleId: string,
|
||||
): Promise<BatchBoardScheduleDetail> {
|
||||
const s =
|
||||
await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!s)
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
if (s.status === "ARRIVED" || s.status === "CANCELLED") {
|
||||
throw new BadRequestException("Schedule is no longer active");
|
||||
}
|
||||
|
||||
const wagonLengths = await this.loadWagonLengths();
|
||||
@@ -459,12 +474,18 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const bookings = await this.bookingsRepository.findAllBySchedule(s.id);
|
||||
|
||||
let allocationPreview: Awaited<
|
||||
ReturnType<TrainSchedulingService['previewAllocationForSchedule']>
|
||||
ReturnType<TrainSchedulingService["previewAllocationForSchedule"]>
|
||||
>;
|
||||
try {
|
||||
allocationPreview = await this.trainSchedulingService.previewAllocationForSchedule(s.id);
|
||||
allocationPreview =
|
||||
await this.trainSchedulingService.previewAllocationForSchedule(s.id);
|
||||
} catch {
|
||||
allocationPreview = { assignedBookingIds: [], deferred: [], issues: [], violations: [] };
|
||||
allocationPreview = {
|
||||
assignedBookingIds: [],
|
||||
deferred: [],
|
||||
issues: [],
|
||||
violations: [],
|
||||
};
|
||||
}
|
||||
const allocationByBooking = new Map(
|
||||
allocationPreview.issues.map((i) => [i.bookingId, i]),
|
||||
@@ -477,17 +498,23 @@ export class BookingBatchService implements OnModuleInit {
|
||||
id: b.id,
|
||||
reference: b.reference ?? b.id.slice(0, 8),
|
||||
company: b.isGovernment
|
||||
? (b.governmentInstitution ?? 'Government')
|
||||
: (b.company?.name ?? '—'),
|
||||
? (b.governmentInstitution ?? "Government")
|
||||
: (b.company?.name ?? "—"),
|
||||
isGovernment: Boolean(b.isGovernment),
|
||||
wagons: need.wagons,
|
||||
weightTons: need.weightTons,
|
||||
lengthMeters: need.lengthMeters,
|
||||
paymentDeadline: b.paymentDeadline ? b.paymentDeadline.toISOString() : null,
|
||||
paymentDeadline: b.paymentDeadline
|
||||
? b.paymentDeadline.toISOString()
|
||||
: null,
|
||||
state: this.boardState(b, linkedIds.has(b.id)),
|
||||
fullyExecutedAt: b.fullyExecutedAt ? b.fullyExecutedAt.toISOString() : null,
|
||||
selectedForBatchAt: b.selectedForBatchAt ? b.selectedForBatchAt.toISOString() : null,
|
||||
allocationStatus: alloc?.status ?? 'NOT_ATTEMPTED',
|
||||
fullyExecutedAt: b.fullyExecutedAt
|
||||
? b.fullyExecutedAt.toISOString()
|
||||
: null,
|
||||
selectedForBatchAt: b.selectedForBatchAt
|
||||
? b.selectedForBatchAt.toISOString()
|
||||
: null,
|
||||
allocationStatus: alloc?.status ?? "NOT_ATTEMPTED",
|
||||
allocationIssue: alloc?.issue ?? null,
|
||||
};
|
||||
});
|
||||
@@ -517,11 +544,11 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const countFor = (bookingsInWindow: BatchBoardBookingDetail[]) => {
|
||||
const counts = emptyCounts();
|
||||
for (const b of bookingsInWindow) {
|
||||
if (b.state === 'ALLOCATED') counts.allocated += 1;
|
||||
else if (b.state === 'SELECTED_FOR_BATCH') counts.selectedForBatch += 1;
|
||||
else if (b.state === 'READY') counts.ready += 1;
|
||||
else if (b.state === 'WAITING') counts.waiting += 1;
|
||||
else if (b.state === 'EXPIRED') counts.expired += 1;
|
||||
if (b.state === "ALLOCATED") counts.allocated += 1;
|
||||
else if (b.state === "SELECTED_FOR_BATCH") counts.selectedForBatch += 1;
|
||||
else if (b.state === "READY") counts.ready += 1;
|
||||
else if (b.state === "WAITING") counts.waiting += 1;
|
||||
else if (b.state === "EXPIRED") counts.expired += 1;
|
||||
else counts.pendingContract += 1;
|
||||
}
|
||||
return counts;
|
||||
@@ -529,7 +556,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
const windows: BatchWindowGroup[] = [];
|
||||
for (const [key, bucket] of windowBuckets) {
|
||||
if (key === 'pending-contract' || !bucket.window) continue;
|
||||
if (key === "pending-contract" || !bucket.window) continue;
|
||||
const w = bucket.window;
|
||||
windows.push({
|
||||
key: w.key,
|
||||
@@ -542,44 +569,51 @@ export class BookingBatchService implements OnModuleInit {
|
||||
bookings: bucket.items,
|
||||
});
|
||||
}
|
||||
windows.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
windows.sort(
|
||||
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(),
|
||||
);
|
||||
|
||||
const pendingBookings = windowBuckets.get('pending-contract')?.items ?? [];
|
||||
const pendingBookings = windowBuckets.get("pending-contract")?.items ?? [];
|
||||
|
||||
return {
|
||||
scheduleId: s.id,
|
||||
trainNumber: s.trainNumber ?? null,
|
||||
routeName: s.route?.name ?? null,
|
||||
origin: s.originStation?.label ?? s.originStation?.code ?? null,
|
||||
destination: s.destinationStation?.label ?? s.destinationStation?.code ?? null,
|
||||
scheduleDate: s.scheduledDepartureDate ? s.scheduledDepartureDate.toISOString() : null,
|
||||
destination:
|
||||
s.destinationStation?.label ?? s.destinationStation?.code ?? null,
|
||||
scheduleDate: s.scheduledDepartureDate
|
||||
? s.scheduledDepartureDate.toISOString()
|
||||
: null,
|
||||
status: s.status,
|
||||
bookingWindowStatus: s.bookingWindowStatus,
|
||||
locomotive: loco
|
||||
? {
|
||||
code: loco.code,
|
||||
name: loco.name ?? null,
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
code: loco.code,
|
||||
name: loco.name ?? null,
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
: null,
|
||||
capacity: this.computeBoardCapacity(items, loco),
|
||||
counts: {
|
||||
allocated: items.filter((i) => i.state === 'ALLOCATED').length,
|
||||
selectedForBatch: items.filter((i) => i.state === 'SELECTED_FOR_BATCH').length,
|
||||
ready: items.filter((i) => i.state === 'READY').length,
|
||||
waiting: items.filter((i) => i.state === 'WAITING').length,
|
||||
pendingContract: items.filter((i) => i.state === 'PENDING_CONTRACT').length,
|
||||
expired: items.filter((i) => i.state === 'EXPIRED').length,
|
||||
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
||||
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
||||
.length,
|
||||
ready: items.filter((i) => i.state === "READY").length,
|
||||
waiting: items.filter((i) => i.state === "WAITING").length,
|
||||
pendingContract: items.filter((i) => i.state === "PENDING_CONTRACT")
|
||||
.length,
|
||||
expired: items.filter((i) => i.state === "EXPIRED").length,
|
||||
},
|
||||
windows,
|
||||
pendingContract: {
|
||||
key: 'pending-contract',
|
||||
label: 'Pending contract',
|
||||
date: '',
|
||||
dateLabel: '',
|
||||
start: '',
|
||||
end: '',
|
||||
key: "pending-contract",
|
||||
label: "Pending contract",
|
||||
date: "",
|
||||
dateLabel: "",
|
||||
start: "",
|
||||
end: "",
|
||||
counts: countFor(pendingBookings),
|
||||
bookings: pendingBookings,
|
||||
},
|
||||
@@ -600,17 +634,21 @@ export class BookingBatchService implements OnModuleInit {
|
||||
lengthMeters: number;
|
||||
}>,
|
||||
loco: Locomotive | null,
|
||||
): BatchBoardSchedule['capacity'] {
|
||||
const allocated = items.filter((i) => i.state === 'ALLOCATED');
|
||||
): BatchBoardSchedule["capacity"] {
|
||||
const allocated = items.filter((i) => i.state === "ALLOCATED");
|
||||
const committed = items.filter(
|
||||
(i) => i.state === 'ALLOCATED' || i.state === 'SELECTED_FOR_BATCH',
|
||||
(i) => i.state === "ALLOCATED" || i.state === "SELECTED_FOR_BATCH",
|
||||
);
|
||||
return {
|
||||
allocatedWagons: allocated.reduce((sum, i) => sum + i.wagons, 0),
|
||||
allocatedLengthMeters:
|
||||
Math.round(allocated.reduce((sum, i) => sum + i.lengthMeters, 0) * 100) / 100,
|
||||
Math.round(
|
||||
allocated.reduce((sum, i) => sum + i.lengthMeters, 0) * 100,
|
||||
) / 100,
|
||||
maxLengthMeters: loco ? Number(loco.maxTrainLengthMeters) : null,
|
||||
usedWeightTons: Math.round(committed.reduce((sum, i) => sum + i.weightTons, 0) * 100) / 100,
|
||||
usedWeightTons:
|
||||
Math.round(committed.reduce((sum, i) => sum + i.weightTons, 0) * 100) /
|
||||
100,
|
||||
maxWeightTons: loco ? Number(loco.maxPullWeightTons) : null,
|
||||
};
|
||||
}
|
||||
@@ -626,51 +664,66 @@ export class BookingBatchService implements OnModuleInit {
|
||||
trainNumber: s.trainNumber ?? null,
|
||||
routeName: s.route?.name ?? null,
|
||||
origin: s.originStation?.label ?? s.originStation?.code ?? null,
|
||||
destination: s.destinationStation?.label ?? s.destinationStation?.code ?? null,
|
||||
scheduleDate: s.scheduledDepartureDate ? s.scheduledDepartureDate.toISOString() : null,
|
||||
destination:
|
||||
s.destinationStation?.label ?? s.destinationStation?.code ?? null,
|
||||
scheduleDate: s.scheduledDepartureDate
|
||||
? s.scheduledDepartureDate.toISOString()
|
||||
: null,
|
||||
status: s.status,
|
||||
bookingWindowStatus: s.bookingWindowStatus,
|
||||
locomotive: loco
|
||||
? {
|
||||
code: loco.code,
|
||||
name: loco.name ?? null,
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
code: loco.code,
|
||||
name: loco.name ?? null,
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
: null,
|
||||
capacity: this.computeBoardCapacity(items, loco),
|
||||
counts: {
|
||||
allocated: items.filter((i) => i.state === 'ALLOCATED').length,
|
||||
selectedForBatch: items.filter((i) => i.state === 'SELECTED_FOR_BATCH').length,
|
||||
ready: items.filter((i) => i.state === 'READY').length,
|
||||
waiting: items.filter((i) => i.state === 'WAITING').length,
|
||||
pendingContract: items.filter((i) => i.state === 'PENDING_CONTRACT').length,
|
||||
expired: items.filter((i) => i.state === 'EXPIRED').length,
|
||||
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
||||
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
||||
.length,
|
||||
ready: items.filter((i) => i.state === "READY").length,
|
||||
waiting: items.filter((i) => i.state === "WAITING").length,
|
||||
pendingContract: items.filter((i) => i.state === "PENDING_CONTRACT")
|
||||
.length,
|
||||
expired: items.filter((i) => i.state === "EXPIRED").length,
|
||||
},
|
||||
bookings: items.slice(0, 3),
|
||||
};
|
||||
}
|
||||
|
||||
private boardState(booking: Booking, linked: boolean): BatchBoardBookingState {
|
||||
if (linked) return 'ALLOCATED';
|
||||
if (booking.status === 'SELECTED_FOR_BATCH' || booking.status === 'AWAITING_PAYMENT') {
|
||||
return 'SELECTED_FOR_BATCH';
|
||||
private boardState(
|
||||
booking: Booking,
|
||||
linked: boolean,
|
||||
): BatchBoardBookingState {
|
||||
if (linked) return "ALLOCATED";
|
||||
if (
|
||||
booking.status === "SELECTED_FOR_BATCH" ||
|
||||
booking.status === "AWAITING_PAYMENT"
|
||||
) {
|
||||
return "SELECTED_FOR_BATCH";
|
||||
}
|
||||
if (booking.status === 'EXPIRED') return 'EXPIRED';
|
||||
if (booking.status === 'FULLY_EXECUTED' && booking.fullyExecutedAt) return 'READY';
|
||||
if (booking.status === 'PAID') return 'WAITING';
|
||||
return 'PENDING_CONTRACT';
|
||||
if (booking.status === "EXPIRED") return "EXPIRED";
|
||||
if (booking.status === "FULLY_EXECUTED" && booking.fullyExecutedAt)
|
||||
return "READY";
|
||||
if (booking.status === "PAID") return "WAITING";
|
||||
return "PENDING_CONTRACT";
|
||||
}
|
||||
|
||||
// ---- core fill ------------------------------------------------------------
|
||||
|
||||
/** Fill one schedule from its priority-ordered pool until full. */
|
||||
async fillSchedule(scheduleId: string): Promise<void> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule || schedule.bookingWindowStatus !== 'OPEN') return;
|
||||
const schedule =
|
||||
await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule || schedule.bookingWindowStatus !== "OPEN") return;
|
||||
const locomotive = schedule.trainSet?.locomotive;
|
||||
if (!schedule.trainSetId || !locomotive) {
|
||||
this.logger.warn(`Schedule ${scheduleId} has no locomotive/train set — skipped.`);
|
||||
this.logger.warn(
|
||||
`Schedule ${scheduleId} has no locomotive/train set — skipped.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -680,7 +733,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
||||
let budget = await this.remainingCapacity(schedule, limits, wagonLengths);
|
||||
if (budget.wagons <= 0) {
|
||||
await this.setWindow(scheduleId, 'FULL');
|
||||
await this.setWindow(scheduleId, "FULL");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -692,7 +745,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
if (!this.fits(need, budget)) {
|
||||
if (booking.isGovernment) {
|
||||
budget = await this.preemptForGovernment(scheduleId, need, budget, wagonLengths);
|
||||
budget = await this.preemptForGovernment(
|
||||
scheduleId,
|
||||
need,
|
||||
budget,
|
||||
wagonLengths,
|
||||
);
|
||||
if (!this.fits(need, budget)) continue; // still doesn't fit even after preempt
|
||||
} else {
|
||||
continue; // skip a booking that exceeds weight/length/wagons, try the next
|
||||
@@ -700,7 +758,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (booking.isGovernment) {
|
||||
await this.allocate(scheduleId, booking, 'gov');
|
||||
await this.allocate(scheduleId, booking, "gov");
|
||||
} else {
|
||||
await this.reserve(booking, scheduleId);
|
||||
armed = true;
|
||||
@@ -709,7 +767,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
if (budget.wagons <= 0) break; // no wagon slots left — nothing more can board
|
||||
}
|
||||
|
||||
if (budget.wagons <= 0) await this.setWindow(scheduleId, 'FULL');
|
||||
if (budget.wagons <= 0) await this.setWindow(scheduleId, "FULL");
|
||||
if (armed) this.armSettle(scheduleId);
|
||||
void this.triggerWagonAllocation(scheduleId);
|
||||
}
|
||||
@@ -735,13 +793,14 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const scheduleIds = bookable
|
||||
.filter(
|
||||
(s) =>
|
||||
s.bookingWindowStatus === 'OPEN' &&
|
||||
s.bookingWindowStatus === "OPEN" &&
|
||||
s.scheduleDate != null &&
|
||||
eatDay(new Date(s.scheduleDate)) === day,
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.scheduleDate).getTime() - new Date(b.scheduleDate).getTime(),
|
||||
new Date(a.scheduleDate).getTime() -
|
||||
new Date(b.scheduleDate).getTime(),
|
||||
)
|
||||
.map((s) => s.id);
|
||||
|
||||
@@ -753,15 +812,22 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// Live per-schedule budget + arm flag, in departure order.
|
||||
const trains: Array<{ id: string; budget: Capacity; armed: boolean }> = [];
|
||||
for (const id of scheduleIds) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(id);
|
||||
const schedule =
|
||||
await this.trainSchedulesRepository.findByIdWithFullGraph(id);
|
||||
const locomotive = schedule?.trainSet?.locomotive;
|
||||
if (!schedule || !schedule.trainSetId || !locomotive) {
|
||||
this.logger.warn(`Schedule ${id} has no locomotive/train set — skipped.`);
|
||||
this.logger.warn(
|
||||
`Schedule ${id} has no locomotive/train set — skipped.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
||||
const budget = await this.remainingCapacity(schedule, limits, wagonLengths);
|
||||
const budget = await this.remainingCapacity(
|
||||
schedule,
|
||||
limits,
|
||||
wagonLengths,
|
||||
);
|
||||
trains.push({ id, budget, armed: false });
|
||||
}
|
||||
if (trains.length === 0) return [];
|
||||
@@ -782,7 +848,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// Government booking fits nowhere on its own — try to preempt commercial
|
||||
// on each train (earliest first) until one frees enough room.
|
||||
for (const t of trains) {
|
||||
t.budget = await this.preemptForGovernment(t.id, need, t.budget, wagonLengths);
|
||||
t.budget = await this.preemptForGovernment(
|
||||
t.id,
|
||||
need,
|
||||
t.budget,
|
||||
wagonLengths,
|
||||
);
|
||||
if (this.fits(need, t.budget)) {
|
||||
target = t;
|
||||
break;
|
||||
@@ -797,7 +868,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
if (booking.isGovernment) {
|
||||
await this.allocate(target.id, booking, 'gov');
|
||||
await this.allocate(target.id, booking, "gov");
|
||||
} else {
|
||||
await this.reserve(booking, target.id);
|
||||
target.armed = true;
|
||||
@@ -806,7 +877,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
for (const t of trains) {
|
||||
if (t.budget.wagons <= 0) await this.setWindow(t.id, 'FULL');
|
||||
if (t.budget.wagons <= 0) await this.setWindow(t.id, "FULL");
|
||||
if (t.armed) this.armSettle(t.id);
|
||||
void this.triggerWagonAllocation(t.id);
|
||||
}
|
||||
@@ -816,18 +887,20 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
/** Durable settle: allocate paid / expire overdue reservations, then top up. */
|
||||
async settleDueReservations(scheduleId: string): Promise<void> {
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(scheduleId);
|
||||
const reserved =
|
||||
await this.bookingsRepository.findReservedForSchedule(scheduleId);
|
||||
const now = Date.now();
|
||||
let anySettled = false;
|
||||
|
||||
for (const booking of reserved) {
|
||||
const paid = booking.paymentStatus === 'PAID' || booking.status === 'PAID';
|
||||
const paid =
|
||||
booking.paymentStatus === "PAID" || booking.status === "PAID";
|
||||
const expired = booking.paymentDeadline
|
||||
? booking.paymentDeadline.getTime() <= now
|
||||
: false;
|
||||
|
||||
if (paid) {
|
||||
await this.allocate(scheduleId, booking, 'paid');
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
anySettled = true;
|
||||
} else if (expired) {
|
||||
await this.expire(booking);
|
||||
@@ -843,17 +916,19 @@ export class BookingBatchService implements OnModuleInit {
|
||||
/** Allocate paid reservations, expire the rest, then top up. */
|
||||
async settleBatch(scheduleId: string): Promise<void> {
|
||||
this.removeTimeout(scheduleId);
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(scheduleId);
|
||||
const reserved =
|
||||
await this.bookingsRepository.findReservedForSchedule(scheduleId);
|
||||
const now = Date.now();
|
||||
|
||||
for (const booking of reserved) {
|
||||
const paid = booking.paymentStatus === 'PAID' || booking.status === 'PAID';
|
||||
const paid =
|
||||
booking.paymentStatus === "PAID" || booking.status === "PAID";
|
||||
const expired = booking.paymentDeadline
|
||||
? booking.paymentDeadline.getTime() <= now
|
||||
: true;
|
||||
|
||||
if (paid) {
|
||||
await this.allocate(scheduleId, booking, 'paid');
|
||||
await this.allocate(scheduleId, booking, "paid");
|
||||
} else if (expired) {
|
||||
await this.expire(booking);
|
||||
}
|
||||
@@ -865,11 +940,13 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
private triggerWagonAllocation(scheduleId: string): void {
|
||||
void this.trainSchedulingService.tryAutoWagonAllocation(scheduleId).catch((err) =>
|
||||
this.logger.warn(
|
||||
`Auto wagon allocation failed for ${scheduleId}: ${(err as Error).message}`,
|
||||
),
|
||||
);
|
||||
void this.trainSchedulingService
|
||||
.tryAutoWagonAllocation(scheduleId)
|
||||
.catch((err) =>
|
||||
this.logger.warn(
|
||||
`Auto wagon allocation failed for ${scheduleId}: ${(err as Error).message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- staff override actions ----------------------------------------------
|
||||
@@ -881,18 +958,20 @@ export class BookingBatchService implements OnModuleInit {
|
||||
.findOne({ where: { id: bookingId } });
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
if (!booking.trainScheduleId) {
|
||||
throw new BadRequestException('Booking has no target schedule to allocate to');
|
||||
throw new BadRequestException(
|
||||
"Booking has no target schedule to allocate to",
|
||||
);
|
||||
}
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { paymentStatus: 'PAID' });
|
||||
await this.allocate(booking.trainScheduleId, booking, 'paid');
|
||||
.update(bookingId, { paymentStatus: "PAID" });
|
||||
await this.allocate(booking.trainScheduleId, booking, "paid");
|
||||
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(
|
||||
booking.trainScheduleId,
|
||||
);
|
||||
if (schedule && (await this.remainingWagons(schedule)) <= 0) {
|
||||
await this.setWindow(booking.trainScheduleId, 'FULL');
|
||||
await this.setWindow(booking.trainScheduleId, "FULL");
|
||||
}
|
||||
void this.triggerWagonAllocation(booking.trainScheduleId!);
|
||||
}
|
||||
@@ -901,7 +980,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* Re-point a booking to another OPEN same-route schedule (keeps approval/contract + priority).
|
||||
* Used for EXPIRED or full-schedule bookings — no re-approval.
|
||||
*/
|
||||
async moveToSchedule(bookingId: string, newScheduleId: string): Promise<void> {
|
||||
async moveToSchedule(
|
||||
bookingId: string,
|
||||
newScheduleId: string,
|
||||
): Promise<void> {
|
||||
const booking = await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.findOne({ where: { id: bookingId } });
|
||||
@@ -910,15 +992,20 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const schedule = await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
.findOne({ where: { id: newScheduleId } });
|
||||
if (!schedule) throw new NotFoundException(`Train schedule ${newScheduleId} not found`);
|
||||
if (schedule.bookingWindowStatus !== 'OPEN') {
|
||||
throw new BadRequestException('Target schedule is not accepting bookings');
|
||||
if (!schedule)
|
||||
throw new NotFoundException(`Train schedule ${newScheduleId} not found`);
|
||||
if (schedule.bookingWindowStatus !== "OPEN") {
|
||||
throw new BadRequestException(
|
||||
"Target schedule is not accepting bookings",
|
||||
);
|
||||
}
|
||||
if (
|
||||
schedule.originStationId !== booking.originYardId ||
|
||||
schedule.destinationStationId !== booking.destinationYardId
|
||||
) {
|
||||
throw new BadRequestException('Target schedule is not on the booking route');
|
||||
throw new BadRequestException(
|
||||
"Target schedule is not on the booking route",
|
||||
);
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
@@ -930,15 +1017,15 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
const restoredStatus =
|
||||
booking.status === 'EXPIRED'
|
||||
booking.status === "EXPIRED"
|
||||
? booking.isGovernment
|
||||
? 'APPROVED'
|
||||
: 'FULLY_EXECUTED'
|
||||
? "APPROVED"
|
||||
: "FULLY_EXECUTED"
|
||||
: booking.status;
|
||||
await manager.getRepository(Booking).update(bookingId, {
|
||||
trainScheduleId: newScheduleId,
|
||||
status: restoredStatus,
|
||||
schedulingStatus: 'ELIGIBLE',
|
||||
schedulingStatus: "ELIGIBLE",
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
} as never);
|
||||
@@ -952,7 +1039,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
.findOne({ where: { id: bookingId } });
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
await this.expire(booking);
|
||||
if (booking.trainScheduleId) await this.fillSchedule(booking.trainScheduleId);
|
||||
if (booking.trainScheduleId)
|
||||
await this.fillSchedule(booking.trainScheduleId);
|
||||
}
|
||||
|
||||
// ---- mutations ------------------------------------------------------------
|
||||
@@ -970,7 +1058,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const deadline = new Date(now.getTime() + PAYMENT_WINDOW_MS);
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
trainScheduleId: scheduleId,
|
||||
status: 'SELECTED_FOR_BATCH',
|
||||
status: "SELECTED_FOR_BATCH",
|
||||
selectedForBatchAt: now,
|
||||
paymentDeadline: deadline,
|
||||
} as never);
|
||||
@@ -989,13 +1077,14 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private async allocate(
|
||||
scheduleId: string,
|
||||
booking: Booking,
|
||||
reason: 'paid' | 'gov',
|
||||
reason: "paid" | "gov",
|
||||
): Promise<void> {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const exists = await this.trainScheduleBookingsRepository.existsForBooking(
|
||||
booking.id,
|
||||
manager,
|
||||
);
|
||||
const exists =
|
||||
await this.trainScheduleBookingsRepository.existsForBooking(
|
||||
booking.id,
|
||||
manager,
|
||||
);
|
||||
if (!exists) {
|
||||
await this.trainScheduleBookingsRepository.createMany(
|
||||
[{ trainScheduleId: scheduleId, bookingId: booking.id }],
|
||||
@@ -1003,8 +1092,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
await manager.getRepository(Booking).update(booking.id, {
|
||||
status: reason === 'paid' ? 'PAID' : booking.status,
|
||||
schedulingStatus: 'SCHEDULED',
|
||||
status: reason === "paid" ? "PAID" : booking.status,
|
||||
schedulingStatus: "SCHEDULED",
|
||||
scheduledAt: new Date(),
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
@@ -1022,8 +1111,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private async expire(booking: Booking): Promise<void> {
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
trainScheduleId: null,
|
||||
status: 'EXPIRED',
|
||||
schedulingStatus: 'ELIGIBLE',
|
||||
status: "EXPIRED",
|
||||
schedulingStatus: "ELIGIBLE",
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
} as never);
|
||||
@@ -1049,7 +1138,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
await this.bookingsRepository.findReservedForSchedule(scheduleId)
|
||||
).filter((b) => !b.isGovernment);
|
||||
const allocatedCommercial =
|
||||
await this.bookingsRepository.findAllocatedCommercialForSchedule(scheduleId);
|
||||
await this.bookingsRepository.findAllocatedCommercialForSchedule(
|
||||
scheduleId,
|
||||
);
|
||||
|
||||
// lowest priority first; reserved are cheaper to free than allocated
|
||||
const candidates = [...reservedCommercial, ...allocatedCommercial].sort(
|
||||
@@ -1066,8 +1157,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
manager,
|
||||
);
|
||||
await manager.getRepository(Booking).update(victim.id, {
|
||||
status: 'EXPIRED',
|
||||
schedulingStatus: 'ELIGIBLE',
|
||||
status: "EXPIRED",
|
||||
schedulingStatus: "ELIGIBLE",
|
||||
paymentDeadline: null,
|
||||
selectedForBatchAt: null,
|
||||
} as never);
|
||||
@@ -1095,7 +1186,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
(sum, c) => sum + Number(c.quantity ?? 0),
|
||||
0,
|
||||
);
|
||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, fromContainers || DEFAULT_WAGONS_PER_BOOKING);
|
||||
return Math.max(
|
||||
DEFAULT_WAGONS_PER_BOOKING,
|
||||
fromContainers || DEFAULT_WAGONS_PER_BOOKING,
|
||||
);
|
||||
}
|
||||
|
||||
/** What one booking consumes along all three capacity axes. */
|
||||
@@ -1182,7 +1276,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
Array<{ lengthMeters: number; capacityTons: number }>
|
||||
> {
|
||||
const types = await this.dataSource.getRepository(WagonType).find({
|
||||
where: [{ code: 'NW5' }, { code: 'CW3' }],
|
||||
where: [{ code: "NW5" }, { code: "CW3" }],
|
||||
});
|
||||
if (types.length) return types.map(wagonTypeDimensionsFromEntity);
|
||||
return [
|
||||
@@ -1193,17 +1287,23 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
private async loadWagonLengths(): Promise<WagonLengths> {
|
||||
const types = await this.dataSource.getRepository(WagonType).find({
|
||||
where: [{ code: 'NW5' }, { code: 'CW3' }],
|
||||
where: [{ code: "NW5" }, { code: "CW3" }],
|
||||
});
|
||||
const byCode = new Map(types.map((t) => [t.code, wagonTypeDimensionsFromEntity(t)]));
|
||||
const byCode = new Map(
|
||||
types.map((t) => [t.code, wagonTypeDimensionsFromEntity(t)]),
|
||||
);
|
||||
return {
|
||||
container: byCode.get('NW5')?.lengthMeters ?? DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||
bulk: byCode.get('CW3')?.lengthMeters ?? DEFAULT_BULK_WAGON_LENGTH_METERS,
|
||||
container:
|
||||
byCode.get("NW5")?.lengthMeters ??
|
||||
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
|
||||
bulk: byCode.get("CW3")?.lengthMeters ?? DEFAULT_BULK_WAGON_LENGTH_METERS,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadGlobalRules(): Promise<TrainSchedulingGlobalRules | null> {
|
||||
return this.dataSource.getRepository(TrainSchedulingGlobalRules).findOne({ where: {} });
|
||||
return this.dataSource
|
||||
.getRepository(TrainSchedulingGlobalRules)
|
||||
.findOne({ where: {} });
|
||||
}
|
||||
|
||||
/** Remaining capacity = hard caps minus what allocated + reserved bookings already use. */
|
||||
@@ -1215,7 +1315,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const allocated = (schedule.scheduleBookings ?? [])
|
||||
.map((sb) => sb.booking)
|
||||
.filter((b): b is Booking => Boolean(b));
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(schedule.id);
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(
|
||||
schedule.id,
|
||||
);
|
||||
const used = [...allocated, ...reserved].reduce<Capacity>(
|
||||
(acc, b) => this.add(acc, this.needFor(b, wagonLengths)),
|
||||
{ wagons: 0, weightTons: 0, lengthMeters: 0 },
|
||||
@@ -1228,7 +1330,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
const allocated = (schedule.scheduleBookings ?? [])
|
||||
.map((sb) => sb.booking)
|
||||
.filter((b): b is Booking => Boolean(b));
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(schedule.id);
|
||||
const reserved = await this.bookingsRepository.findReservedForSchedule(
|
||||
schedule.id,
|
||||
);
|
||||
const used =
|
||||
allocated.reduce((s, b) => s + this.wagonsFor(b), 0) +
|
||||
reserved.reduce((s, b) => s + this.wagonsFor(b), 0);
|
||||
@@ -1237,7 +1341,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
|
||||
private async setWindow(
|
||||
scheduleId: string,
|
||||
status: 'OPEN' | 'FULL' | 'CLOSED',
|
||||
status: "OPEN" | "FULL" | "CLOSED",
|
||||
): Promise<void> {
|
||||
await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
@@ -1254,7 +1358,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
this.removeTimeout(scheduleId);
|
||||
const handle = setTimeout(() => {
|
||||
void this.settleBatch(scheduleId).catch((err) =>
|
||||
this.logger.error(`settleBatch ${scheduleId} failed: ${(err as Error).message}`),
|
||||
this.logger.error(
|
||||
`settleBatch ${scheduleId} failed: ${(err as Error).message}`,
|
||||
),
|
||||
);
|
||||
}, PAYMENT_WINDOW_MS);
|
||||
this.scheduler.addTimeout(this.timeoutName(scheduleId), handle);
|
||||
@@ -1263,7 +1369,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private removeTimeout(scheduleId: string): void {
|
||||
const name = this.timeoutName(scheduleId);
|
||||
try {
|
||||
if (this.scheduler.doesExist('timeout', name)) {
|
||||
if (this.scheduler.doesExist("timeout", name)) {
|
||||
this.scheduler.deleteTimeout(name);
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user