mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Adding ticket generation and booking for staff employees logic
This commit is contained in:
206
apps/edr-passenger-api/test/schedule-lifecycle.e2e-spec.ts
Normal file
206
apps/edr-passenger-api/test/schedule-lifecycle.e2e-spec.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Schedule lifecycle coverage — bulkGenerateSchedules, createSchedule's "must have at least
|
||||
* one coach assigned" guard (added recently — the exact validation whose ordering broke
|
||||
* checkin-cutoff.e2e-spec.ts's fixture helper earlier this session), and
|
||||
* TasksService.syncScheduleStatuses' full schedule-level state machine
|
||||
* (SCHEDULED -> BOARDING -> EN_ROUTE -> ARRIVED). checkin-cutoff.e2e-spec.ts already covers
|
||||
* the per-stop CHECKIN_CLOSED/OPEN half of syncScheduleStatuses — not repeated here.
|
||||
*/
|
||||
import { SchedulesService } from "../src/modules/schedules/schedules.service";
|
||||
import { TasksService } from "../src/modules/tasks/tasks.service";
|
||||
import { createServiceHarness, ServiceHarness } from "./setup/slim-app";
|
||||
import { IDS, resetAndSeedCore } from "./fixtures/seed-core";
|
||||
|
||||
function asyncStub(): any {
|
||||
return new Proxy({}, { get: () => async () => undefined });
|
||||
}
|
||||
|
||||
describe("Schedule lifecycle — bulk generate, coach guard, status transitions", () => {
|
||||
let harness: ServiceHarness;
|
||||
let schedulesService: SchedulesService;
|
||||
let tasksService: TasksService;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createServiceHarness();
|
||||
schedulesService = await harness.moduleRef.resolve(SchedulesService);
|
||||
tasksService = new TasksService(harness.prisma as any, asyncStub(), asyncStub());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetAndSeedCore(harness.prisma);
|
||||
});
|
||||
|
||||
const future = (mins: number) => new Date(Date.now() + mins * 60_000);
|
||||
|
||||
async function createCoach(trainNumber: string) {
|
||||
return harness.prisma.coach.create({
|
||||
data: { coachTypeId: IDS.coachType, number: `${trainNumber}-C1`, capacity: 4, sequence: 1, status: "ACTIVE" },
|
||||
});
|
||||
}
|
||||
|
||||
describe("createSchedule coach-assignment guard", () => {
|
||||
it("rejects a schedule with no coachIds and no route coach template", async () => {
|
||||
const train = await harness.prisma.train.create({ data: { number: `SCH-NOCOACH-${Date.now()}`, name: "Test" } });
|
||||
|
||||
await expect(
|
||||
schedulesService.createSchedule({
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
departureAt: future(180).toISOString(),
|
||||
arrivalAt: future(280).toISOString(),
|
||||
} as any),
|
||||
).rejects.toThrow(/must have at least one coach assigned/i);
|
||||
|
||||
const orphan = await harness.prisma.trainSchedule.findFirst({ where: { trainId: train.id } });
|
||||
expect(orphan).toBeNull(); // no dead schedule left behind
|
||||
});
|
||||
|
||||
it("auto-applies the route's coach template when coachIds are omitted", async () => {
|
||||
const train = await harness.prisma.train.create({ data: { number: `SCH-TEMPLATE-${Date.now()}`, name: "Test" } });
|
||||
const coach = await createCoach(train.number);
|
||||
await harness.prisma.routeCoachTemplate.create({
|
||||
data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 },
|
||||
});
|
||||
|
||||
const schedule = await schedulesService.createSchedule({
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
departureAt: future(180).toISOString(),
|
||||
arrivalAt: future(280).toISOString(),
|
||||
} as any);
|
||||
|
||||
const assignments = await harness.prisma.coachAssignment.findMany({ where: { scheduleId: schedule.id } });
|
||||
expect(assignments).toHaveLength(1);
|
||||
expect(assignments[0].coachId).toBe(coach.id);
|
||||
});
|
||||
|
||||
it("explicit coachIds override the route's coach template", async () => {
|
||||
const train = await harness.prisma.train.create({ data: { number: `SCH-OVERRIDE-${Date.now()}`, name: "Test" } });
|
||||
const templateCoach = await createCoach(`${train.number}-tmpl`);
|
||||
const explicitCoach = await createCoach(`${train.number}-explicit`);
|
||||
await harness.prisma.routeCoachTemplate.create({
|
||||
data: { routeId: IDS.route, coachId: templateCoach.id, positionNumber: 1 },
|
||||
});
|
||||
|
||||
const schedule = await schedulesService.createSchedule({
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
departureAt: future(180).toISOString(),
|
||||
arrivalAt: future(280).toISOString(),
|
||||
coachIds: [explicitCoach.id],
|
||||
} as any);
|
||||
|
||||
const assignments = await harness.prisma.coachAssignment.findMany({ where: { scheduleId: schedule.id } });
|
||||
expect(assignments).toHaveLength(1);
|
||||
expect(assignments[0].coachId).toBe(explicitCoach.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bulkGenerateSchedules", () => {
|
||||
it("generates one schedule per repeat interval across the date range", async () => {
|
||||
const train = await harness.prisma.train.create({ data: { number: `BULK-${Date.now()}`, name: "Test" } });
|
||||
const coach = await createCoach(train.number);
|
||||
await harness.prisma.routeCoachTemplate.create({ data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 } });
|
||||
|
||||
const start = future(2 * 24 * 60); // 2 days out, clear of "today" edge cases
|
||||
const result = await schedulesService.bulkGenerateSchedules({
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
startDateTime: start.toISOString(),
|
||||
forNextDays: 5,
|
||||
repeatEveryDays: 2,
|
||||
durationHours: 3,
|
||||
} as any);
|
||||
|
||||
// while(currentDate < endDate) stepping by 2 days over a 5-day window: day 0, 2, 4.
|
||||
expect(result.schedulesCreated).toBe(3);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.scheduleIds).toHaveLength(3);
|
||||
|
||||
const schedules = await harness.prisma.trainSchedule.findMany({ where: { trainId: train.id }, orderBy: { departureAt: "asc" } });
|
||||
expect(schedules).toHaveLength(3);
|
||||
const daysBetween = (a: Date, b: Date) => Math.round((b.getTime() - a.getTime()) / (24 * 60 * 60 * 1000));
|
||||
expect(daysBetween(schedules[0].departureAt, schedules[1].departureAt)).toBe(2);
|
||||
expect(daysBetween(schedules[1].departureAt, schedules[2].departureAt)).toBe(2);
|
||||
});
|
||||
|
||||
it("collects per-day errors without aborting the rest of the run", async () => {
|
||||
const train = await harness.prisma.train.create({ data: { number: `BULK-ERR-${Date.now()}`, name: "Test" } });
|
||||
const coach = await createCoach(train.number);
|
||||
await harness.prisma.routeCoachTemplate.create({ data: { routeId: IDS.route, coachId: coach.id, positionNumber: 1 } });
|
||||
|
||||
const start = future(2 * 24 * 60);
|
||||
const dto = {
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
startDateTime: start.toISOString(),
|
||||
forNextDays: 4,
|
||||
repeatEveryDays: 1,
|
||||
durationHours: 3,
|
||||
} as any;
|
||||
|
||||
const first = await schedulesService.bulkGenerateSchedules(dto);
|
||||
expect(first.schedulesCreated).toBe(4);
|
||||
expect(first.errors).toHaveLength(0);
|
||||
|
||||
// Re-running the exact same range collides with EVERY day just created (same
|
||||
// train+route+date already exists) — proves errors are collected per-day, not thrown,
|
||||
// and the count/errors array accurately reflect zero successes this time.
|
||||
const second = await schedulesService.bulkGenerateSchedules(dto);
|
||||
expect(second.schedulesCreated).toBe(0);
|
||||
expect(second.errors).toHaveLength(4);
|
||||
expect(second.errors[0]).toMatch(/already exists/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncScheduleStatuses — schedule-level state machine", () => {
|
||||
async function scheduleWithCoach(trainNumber: string, departureAt: Date, arrivalAt: Date) {
|
||||
const train = await harness.prisma.train.create({ data: { number: trainNumber, name: "Test" } });
|
||||
const coach = await createCoach(trainNumber);
|
||||
return schedulesService.createSchedule({
|
||||
trainId: train.id,
|
||||
routeId: IDS.route,
|
||||
departureAt: departureAt.toISOString(),
|
||||
arrivalAt: arrivalAt.toISOString(),
|
||||
coachIds: [coach.id],
|
||||
} as any);
|
||||
}
|
||||
|
||||
it("SCHEDULED -> BOARDING once within the 30-min cutoff window", async () => {
|
||||
const schedule = await scheduleWithCoach(`SYNC-BOARD-${Date.now()}`, future(20), future(80));
|
||||
expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("SCHEDULED");
|
||||
|
||||
await tasksService.syncScheduleStatuses();
|
||||
|
||||
expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("BOARDING");
|
||||
});
|
||||
|
||||
it("BOARDING -> EN_ROUTE once departure has passed", async () => {
|
||||
const schedule = await scheduleWithCoach(`SYNC-ENROUTE-${Date.now()}`, future(20), future(80));
|
||||
await tasksService.syncScheduleStatuses();
|
||||
await harness.prisma.trainSchedule.update({ where: { id: schedule.id }, data: { departureAt: new Date(Date.now() - 60_000) } });
|
||||
|
||||
await tasksService.syncScheduleStatuses();
|
||||
|
||||
expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("EN_ROUTE");
|
||||
});
|
||||
|
||||
it("EN_ROUTE -> ARRIVED once arrival has passed", async () => {
|
||||
const schedule = await scheduleWithCoach(`SYNC-ARRIVED-${Date.now()}`, future(20), future(80));
|
||||
await tasksService.syncScheduleStatuses();
|
||||
await harness.prisma.trainSchedule.update({
|
||||
where: { id: schedule.id },
|
||||
data: { departureAt: new Date(Date.now() - 120_000), arrivalAt: new Date(Date.now() - 60_000) },
|
||||
});
|
||||
|
||||
await tasksService.syncScheduleStatuses();
|
||||
await tasksService.syncScheduleStatuses(); // BOARDING->EN_ROUTE and EN_ROUTE->ARRIVED are separate updateMany calls in one pass — one call suffices, second call is a no-op idempotency check
|
||||
|
||||
expect((await harness.prisma.trainSchedule.findUnique({ where: { id: schedule.id } }))?.status).toBe("ARRIVED");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user