mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 12:43:43 +00:00
enhance train scheduling and booking management
This commit is contained in:
@@ -114,6 +114,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module";
|
||||
BookingPricingService,
|
||||
BookingInvoiceService,
|
||||
BookingLifecycleNotifierService,
|
||||
ConsolidationService,
|
||||
CustomerTruckService,
|
||||
ContainerReceiptService,
|
||||
],
|
||||
|
||||
@@ -272,26 +272,51 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pair two bookings for consolidation. Both return to SUBMITTED so staff can
|
||||
* accept them into the approval chain; the link itself (consolidationPartnerId)
|
||||
* marks them as consolidated in the UI.
|
||||
* Pair two bookings for consolidation. Each returns to its own resume status —
|
||||
* SUBMITTED for a direct customer booking (so staff can accept it into the
|
||||
* approval chain) or the stored consolidationResumeStatus for a contract
|
||||
* drawdown (OPERATION_REQUEST_PENDING / AWAITING_DOCUMENTS). The link itself
|
||||
* (consolidationPartnerId) marks them as consolidated in the UI. The resume
|
||||
* status is cleared once used, so a later un-pair re-parks cleanly.
|
||||
*/
|
||||
async pairConsolidation(bookingId: string, partnerId: string): Promise<void> {
|
||||
const [booking, partner] = await Promise.all([
|
||||
this.repository.findOne({
|
||||
where: { id: bookingId },
|
||||
select: { id: true, consolidationResumeStatus: true },
|
||||
}),
|
||||
this.repository.findOne({
|
||||
where: { id: partnerId },
|
||||
select: { id: true, consolidationResumeStatus: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
await this.repository.update(bookingId, {
|
||||
consolidationPartnerId: partnerId,
|
||||
status: 'SUBMITTED',
|
||||
status: booking?.consolidationResumeStatus ?? 'SUBMITTED',
|
||||
consolidationResumeStatus: null,
|
||||
} as never);
|
||||
await this.repository.update(partnerId, {
|
||||
consolidationPartnerId: bookingId,
|
||||
status: 'SUBMITTED',
|
||||
status: partner?.consolidationResumeStatus ?? 'SUBMITTED',
|
||||
consolidationResumeStatus: null,
|
||||
} as never);
|
||||
}
|
||||
|
||||
/** Park a booking that needs consolidation but has no partner yet. */
|
||||
async parkForConsolidation(bookingId: string): Promise<void> {
|
||||
/**
|
||||
* Park a booking that needs consolidation but has no partner yet. The optional
|
||||
* resumeStatus is where the booking returns once it pairs — pass it for a
|
||||
* contract drawdown so pairing resumes the contract-booking flow rather than
|
||||
* the direct-booking SUBMITTED default.
|
||||
*/
|
||||
async parkForConsolidation(
|
||||
bookingId: string,
|
||||
resumeStatus?: string | null,
|
||||
): Promise<void> {
|
||||
await this.repository.update(bookingId, {
|
||||
consolidationPartnerId: null,
|
||||
status: 'PENDING_CONSOLIDATION',
|
||||
consolidationResumeStatus: resumeStatus ?? null,
|
||||
} as never);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
RuleEngineService,
|
||||
} from '../rule-engine/rule-engine.service';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
|
||||
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
||||
@@ -99,6 +100,7 @@ export class BookingsService {
|
||||
private readonly consolidationService: ConsolidationService,
|
||||
private readonly vehiclesService: VehiclesService,
|
||||
private readonly contractPdfService: ContractPdfService,
|
||||
private readonly events: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async assignCustomerTruck(
|
||||
@@ -493,6 +495,14 @@ export class BookingsService {
|
||||
messages.push(
|
||||
this.consolidationService.describePaired(partner.reference, slots),
|
||||
);
|
||||
// Let deferred owners (e.g. contract drawdowns whose invoice/milestones
|
||||
// were held while the booking waited) finalize now that a whole wagon
|
||||
// exists. Fire-and-forget: a listener failure must not undo the pairing.
|
||||
this.events
|
||||
.emitAsync('booking.consolidation.paired', {
|
||||
bookingIds: [booking.id, partner.id],
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return { booking: paired, messages };
|
||||
}
|
||||
|
||||
|
||||
@@ -431,6 +431,14 @@ export class Booking extends BaseEntity {
|
||||
@JoinColumn({ name: 'consolidation_partner_id' })
|
||||
consolidationPartner?: Booking | null;
|
||||
|
||||
// Status a booking parked in PENDING_CONSOLIDATION returns to once it pairs.
|
||||
// Null for direct customer bookings (they resume to SUBMITTED, the historical
|
||||
// default); contract-drawdown bookings set it to the status createUnderContract
|
||||
// would otherwise have used (OPERATION_REQUEST_PENDING / AWAITING_DOCUMENTS), so
|
||||
// pairing resumes them into the right flow instead of the direct-booking one.
|
||||
@Column({ name: 'consolidation_resume_status', type: 'varchar', length: 40, nullable: true })
|
||||
consolidationResumeStatus?: string | null;
|
||||
|
||||
@Column({ name: 'wagons_required', type: 'numeric', precision: 6, scale: 2, nullable: true })
|
||||
wagonsRequired?: number | null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user