mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 00:10:57 +00:00
feat(freight): surface scheduling clock and gate pay during drain
This commit is contained in:
@@ -34,6 +34,7 @@ import { ServiceType } from '../rule-engine/entities/service-type.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { Contract } from '../contracts/entities/contract.entity';
|
||||
import { BookingBatchService } from '../train-scheduling/booking-batch.service';
|
||||
import { paymentDrainEndsAtIso } from '../train-scheduling/booking-batch.constants';
|
||||
import { BookingContractService } from './booking-contract.service';
|
||||
import { BookingsRepository } from './bookings.repository';
|
||||
import { ConsolidationService } from './consolidation.service';
|
||||
@@ -57,6 +58,24 @@ import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto'
|
||||
import { PdfRenderService } from '../billing/documents/pdf-render.service';
|
||||
import { buildTabularFallbackPdf } from '../billing/documents/styled-pdf.util';
|
||||
|
||||
/**
|
||||
* The allocated train as the backoffice booking detail page needs it: which
|
||||
* train, its window phase, and both the planned and actual clock. Attached by
|
||||
* `findById` only when the booking is on a schedule.
|
||||
*/
|
||||
export interface TrainScheduleSummary {
|
||||
id: string;
|
||||
reference: string | null;
|
||||
trainNumber: string | null;
|
||||
status: string | null;
|
||||
scheduledDepartureDate: string | null;
|
||||
scheduledArrivalDate: string | null;
|
||||
actualDepartureAt: string | null;
|
||||
actualArrivalAt: string | null;
|
||||
windowPhase: string | null;
|
||||
paymentPhaseEndsAt: string | null;
|
||||
}
|
||||
|
||||
/** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */
|
||||
export interface PaginatedBookings {
|
||||
items: Booking[];
|
||||
@@ -1738,6 +1757,20 @@ export class BookingsService {
|
||||
(b as Booking & { handoverAwaitingSignature?: boolean }).handoverAwaitingSignature =
|
||||
pending.has(b.id);
|
||||
}
|
||||
this.attachPaymentDrainEnds(bookings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived, no query: end of the settlement drain tail after `paymentDeadline`.
|
||||
* The portal hides "Pay now" between the deadline and this instant — a payment
|
||||
* started just before the buzzer is still settling, so offering to pay again
|
||||
* would invite a double payment.
|
||||
*/
|
||||
private attachPaymentDrainEnds(bookings: Booking[]): void {
|
||||
for (const b of bookings) {
|
||||
(b as Booking & { paymentDrainEndsAt?: string | null }).paymentDrainEndsAt =
|
||||
paymentDrainEndsAtIso(b.paymentDeadline);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(
|
||||
@@ -2105,8 +2138,33 @@ export class BookingsService {
|
||||
.findOne({ where: { id: booking.trainScheduleId } });
|
||||
(booking as Booking & { trainScheduleStatus?: string | null }).trainScheduleStatus =
|
||||
schedule?.status ?? null;
|
||||
// Backoffice staff view: the allocated train's identity and clock, so the
|
||||
// detail page can state which train the booking rides and when it runs
|
||||
// without a second round-trip to the schedules API.
|
||||
(
|
||||
booking as Booking & { trainScheduleSummary?: TrainScheduleSummary | null }
|
||||
).trainScheduleSummary = schedule
|
||||
? {
|
||||
id: schedule.id,
|
||||
reference: schedule.reference ?? null,
|
||||
trainNumber: schedule.trainNumber ?? null,
|
||||
status: schedule.status ?? null,
|
||||
scheduledDepartureDate: schedule.scheduledDepartureDate?.toISOString() ?? null,
|
||||
scheduledArrivalDate: schedule.scheduledArrivalDate?.toISOString() ?? null,
|
||||
actualDepartureAt: schedule.actualDepartureAt?.toISOString() ?? null,
|
||||
actualArrivalAt: schedule.actualArrivalAt?.toISOString() ?? null,
|
||||
windowPhase: schedule.windowPhase ?? null,
|
||||
paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
// End of this booking's own pay window including the settlement drain tail —
|
||||
// the deadline staff should quote, since a payment landing inside the drain
|
||||
// still counts (see paymentDrainEndsAtIso).
|
||||
(booking as Booking & { paymentDrainEndsAt?: string | null }).paymentDrainEndsAt =
|
||||
paymentDrainEndsAtIso(booking.paymentDeadline);
|
||||
|
||||
// A generated-but-unsigned handover means the customer must approve delivery
|
||||
// from the portal. Self-haul: booking-based, one per booking. EDR last-mile:
|
||||
// per delivering truck (generated on truck exit), signed one by one.
|
||||
|
||||
@@ -3680,23 +3680,24 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// (provider query errored / payment still in flight) means we could not
|
||||
// confirm "not paid" — never expire on unknown; the next settle tick
|
||||
// asks again.
|
||||
if (reason === "payment" && (fresh?.paymentDeadline ?? booking.paymentDeadline)) {
|
||||
const reconcile = await this.billing.reconcilePayable(booking.id);
|
||||
if (reconcile.paid) {
|
||||
this.logger.log(
|
||||
`[BATCH] expire skipped for ${booking.reference} — gateway ` +
|
||||
`reconcile found a settled payment; payment.succeeded will allocate it`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (reconcile.unverifiable) {
|
||||
this.logger.warn(
|
||||
`[BATCH] expire deferred for ${booking.reference} — settlement ` +
|
||||
`unverifiable at the gateway; retrying next settle tick`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// TODO: CBE has no reconcile endpoint yet — re-enable once available.
|
||||
// if (reason === "payment" && (fresh?.paymentDeadline ?? booking.paymentDeadline)) {
|
||||
// const reconcile = await this.billing.reconcilePayable(booking.id);
|
||||
// if (reconcile.paid) {
|
||||
// this.logger.log(
|
||||
// `[BATCH] expire skipped for ${booking.reference} — gateway ` +
|
||||
// `reconcile found a settled payment; payment.succeeded will allocate it`,
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// if (reconcile.unverifiable) {
|
||||
// this.logger.warn(
|
||||
// `[BATCH] expire deferred for ${booking.reference} — settlement ` +
|
||||
// `unverifiable at the gateway; retrying next settle tick`,
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
const freedScheduleId = booking.trainScheduleId;
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
|
||||
Reference in New Issue
Block a user