mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Add contract booking windows feature
This commit is contained in:
@@ -7,6 +7,7 @@ import { TrainScheduleStatus as TrainScheduleStatusEnum } from '@edr/types';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { BookingBatchService } from './booking-batch.service';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
import { BATCH_TIMEZONE } from './booking-batch.constants';
|
||||
@@ -19,12 +20,13 @@ import { type BookingWindowConfig } from './booking-window.config';
|
||||
* schedule row, so every transition is derived purely from the clock — a restart
|
||||
* resumes mid-phase with no loss (onModuleInit runs one tick immediately).
|
||||
*
|
||||
* Import phases: PRE_WINDOW → OPEN (customers book) → DOC_REVIEW (staff accept
|
||||
* documents) → PAYMENT (batch reserves in priority order, customers pay) →
|
||||
* reopen same day | CLOSED_FOR_DAY | DONE (full → auto-finalized).
|
||||
* Import & domestic phases: PRE_WINDOW → OPEN (customers book) → DOC_REVIEW
|
||||
* (staff accept documents) → PAYMENT (batch reserves in priority order, customers
|
||||
* pay) → reopen same day | CLOSED_FOR_DAY | DONE (full → auto-finalized).
|
||||
* Export phases: PRE_WINDOW → OPEN → DONE (no batch, no priority).
|
||||
* Legacy/DOMESTIC schedules have windowPhase NULL and are served by the legacy
|
||||
* fill (runBatchFill), which this tick invokes every 5th minute.
|
||||
* Only PRE-MIGRATION rows have windowPhase NULL; those are served by the legacy
|
||||
* fill (runBatchFill), which this tick invokes every 5th minute. New schedules of
|
||||
* every direction get a window phase.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BookingWindowService implements OnModuleInit {
|
||||
@@ -37,6 +39,7 @@ export class BookingWindowService implements OnModuleInit {
|
||||
private readonly trainSchedulesRepository: TrainSchedulesRepository,
|
||||
private readonly bookingBatchService: BookingBatchService,
|
||||
private readonly trainSchedulingService: TrainSchedulingService,
|
||||
private readonly notifications: NotificationsService,
|
||||
) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
@@ -156,6 +159,7 @@ export class BookingWindowService implements OnModuleInit {
|
||||
await this.bookingBatchService.setWindow(schedule.id, 'OPEN');
|
||||
schedule.bookingWindowStatus = 'OPEN';
|
||||
}
|
||||
await this.notifyWindowOpened(schedule);
|
||||
this.logger.log(`Export booking window opened for schedule ${schedule.id}`);
|
||||
return true;
|
||||
}
|
||||
@@ -193,6 +197,8 @@ export class BookingWindowService implements OnModuleInit {
|
||||
await this.bookingBatchService.setWindow(schedule.id, 'OPEN');
|
||||
schedule.bookingWindowStatus = 'OPEN';
|
||||
}
|
||||
// Only announce the first opening of the day; reopen cycles don't re-notify.
|
||||
if (schedule.bookingCycleNo === 1) await this.notifyWindowOpened(schedule);
|
||||
this.logger.log(
|
||||
`Import booking window opened for schedule ${schedule.id} (cycle ${schedule.bookingCycleNo})`,
|
||||
);
|
||||
@@ -325,6 +331,67 @@ export class BookingWindowService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS + email every active-contract customer on this schedule's route when its
|
||||
* booking window opens, so they can book from the portal home before it closes.
|
||||
* Fire-and-forget; a failed notification never blocks the window transition.
|
||||
*/
|
||||
private async notifyWindowOpened(schedule: TrainSchedule): Promise<void> {
|
||||
try {
|
||||
const rows: Array<{ phone: string | null; email: string | null }> =
|
||||
await this.dataSource.query(
|
||||
`SELECT DISTINCT
|
||||
COALESCE(co.contact_person_phone, co.phone) AS phone,
|
||||
COALESCE(co.email, co.general_manager_email) AS email
|
||||
FROM freight.contract_routes cr
|
||||
JOIN freight.contracts c
|
||||
ON c.id = cr.contract_id
|
||||
AND c.status IN ('CONTRACT_ACTIVE', 'FULLY_EXECUTED')
|
||||
AND c.deleted_at IS NULL
|
||||
JOIN freight.companies co ON co.id = c.company_id
|
||||
WHERE cr.origin_yard_id = $1
|
||||
AND cr.destination_yard_id = $2
|
||||
AND cr.deleted_at IS NULL`,
|
||||
[schedule.originStationId, schedule.destinationStationId],
|
||||
);
|
||||
if (!rows.length) return;
|
||||
|
||||
const closes = schedule.windowClosesAt
|
||||
? schedule.windowClosesAt.toLocaleString('en-GB', { timeZone: BATCH_TIMEZONE })
|
||||
: 'later today';
|
||||
const depart = schedule.scheduledDepartureDate.toLocaleDateString('en-GB', {
|
||||
timeZone: BATCH_TIMEZONE,
|
||||
});
|
||||
const msg =
|
||||
`Booking is now open for the train departing ${depart}. ` +
|
||||
`Book your shipment from the portal home page before ${closes} EAT.`;
|
||||
|
||||
const seenPhone = new Set<string>();
|
||||
const seenEmail = new Set<string>();
|
||||
for (const r of rows) {
|
||||
if (r.phone && !seenPhone.has(r.phone)) {
|
||||
seenPhone.add(r.phone);
|
||||
await this.notifications
|
||||
.directSend('sms', r.phone, msg)
|
||||
.catch((e) => this.logger.warn(`Window-open SMS failed: ${(e as Error).message}`));
|
||||
}
|
||||
if (r.email && !seenEmail.has(r.email)) {
|
||||
seenEmail.add(r.email);
|
||||
await this.notifications
|
||||
.directSend('email', r.email, msg)
|
||||
.catch((e) => this.logger.warn(`Window-open email failed: ${(e as Error).message}`));
|
||||
}
|
||||
}
|
||||
this.logger.log(
|
||||
`Notified ${seenPhone.size} phone / ${seenEmail.size} email contacts of open window for schedule ${schedule.id}`,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`notifyWindowOpened failed for ${schedule.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async setPhase(
|
||||
schedule: TrainSchedule,
|
||||
patch: Partial<
|
||||
|
||||
Reference in New Issue
Block a user