Files

198 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* UI E2E seed — extends seed-core with a BOOKABLE trip + payment methods + promos so the portal
* search/booking flow and the backoffice config flow have real data to drive. Runnable standalone
* (`ts-node test/fixtures/seed-ui.ts`) or importable (`seedUi(prisma)`) from the Playwright
* global-setup. Reads DATABASE_URL from the environment (the UI stack points at the 5544 test DB).
*
* Searchability: a schedule shows in POST /search when it is SCHEDULED, not package-only, has a
* future departure on the searched date, operational coaches with AVAILABLE seats, and a resolvable
* fare (seat-class distance formula using the seeded route-stop distances + USD→ETB rate).
*/
import { PrismaClient } from "@prisma/client";
import { IDS, resetAndSeedCore } from "./seed-core";
export const UI_IDS = {
train: "00000000-0000-4000-8000-000000000100",
schedule: "00000000-0000-4000-8000-000000000101",
coach: "00000000-0000-4000-8000-000000000102",
// Passenger.id is set EQUAL to the IAM user id. The bookings controller overrides passengerId
// with the JWT user id (req.user.id), and the service only resolves iamUserId→passenger when it
// is NON-UUID — since IAM ids are UUIDs, it uses the id directly, so Passenger.id must equal it.
passenger: "11111111-0000-4000-8000-000000000001",
promoValid: "PROMO10",
promoExpired: "EXPIRED50",
// Return leg C→A on the same calendar date, for ROUND_TRIP scenarios (UA-6). The route stops are
// symmetric in distance (A=0, C=250) so the reverse leg prices identically to the outbound.
returnSchedule: "00000000-0000-4000-8000-000000000201",
returnCoach: "00000000-0000-4000-8000-000000000202",
} as const;
/** Days-from-now the sample trip departs (tests search on this calendar date, Addis TZ). */
export const DEPART_IN_DAYS = 2;
export function sampleDepartAt(): Date {
const d = new Date();
d.setUTCDate(d.getUTCDate() + DEPART_IN_DAYS);
d.setUTCHours(6, 0, 0, 0); // 06:00Z ~ 09:00 Addis — safely same calendar day either TZ
return d;
}
/** The date string a test passes to POST /search for the sample trip (YYYY-MM-DD). */
export function sampleDepartDate(): string {
return sampleDepartAt().toISOString().slice(0, 10);
}
export async function seedUi(prisma: PrismaClient): Promise<void> {
await resetAndSeedCore(prisma);
// Give the two seed-core seat classes the exact names the portal review flow maps by.
await prisma.seatClass.update({
where: { id: IDS.seatClassLocal },
data: { name: "Economy Regular" },
});
await prisma.seatClass.update({
where: { id: IDS.seatClassIntl },
data: { name: "Economy Regular Intl" },
});
// Enabled payment methods — the portal pay page renders ONLY enabled PaymentMethod rows.
await prisma.paymentMethod.createMany({
data: [
{ type: "WALLET", displayName: "Wallet", currency: "ETB", enabled: true, isDefault: true, sortOrder: 0 },
{ type: "TELEBIRR", displayName: "telebirr", currency: "ETB", enabled: true, sortOrder: 1 },
],
});
const departAt = sampleDepartAt();
const arriveAt = new Date(departAt.getTime() + 4 * 3600_000);
await prisma.train.create({
data: { id: UI_IDS.train, number: "UI-100", name: "UI Test Express" },
});
await prisma.trainSchedule.create({
data: {
id: UI_IDS.schedule,
trainId: UI_IDS.train,
routeId: IDS.route,
originStationId: IDS.stationA,
destinationStationId: IDS.stationC,
departureAt: departAt,
arrivalAt: arriveAt,
durationMinutes: 240,
status: "SCHEDULED",
stopsCount: 3,
isPackageOnly: false,
},
});
await prisma.tripStopTime.createMany({
data: [
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationA, sequence: 1, plannedDepartureAt: departAt, status: "OPEN" },
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationB, sequence: 2, plannedDepartureAt: new Date(departAt.getTime() + 2 * 3600_000), status: "OPEN" },
{ scheduleId: UI_IDS.schedule, stationId: IDS.stationC, sequence: 3, plannedArrivalAt: arriveAt, status: "OPEN" },
],
});
// Enough seats that a full suite run (many bookings share one seeded DB, seats are not released
// between specs) never exhausts availability: 12 rows × 4 cols = 48 seats.
const SEAT_ROWS = 12;
await prisma.coach.create({
data: { id: UI_IDS.coach, coachTypeId: IDS.coachType, number: "UI-C1", capacity: SEAT_ROWS * 4, sequence: 1, status: "ACTIVE" },
});
await prisma.coachAssignment.create({
data: { scheduleId: UI_IDS.schedule, coachId: UI_IDS.coach, positionNumber: 1, isOperational: true },
});
await prisma.seat.createMany({ data: buildSeats(UI_IDS.coach, SEAT_ROWS) });
// Logged-in passenger with a funded wallet + loyalty (used by the portal storageState + WALLET pay).
await prisma.passenger.create({ data: { id: UI_IDS.passenger } });
await prisma.walletAccount.create({
data: { passengerId: UI_IDS.passenger, balanceMinor: 100_000_000 },
});
await prisma.loyaltyAccount.create({
data: { passengerId: UI_IDS.passenger, pointsBalance: 500 },
});
// Promotions (schema field names, NOT the backoffice UI names). Unique exact codes.
await prisma.promotion.createMany({
data: [
{ title: "10% off", code: UI_IDS.promoValid, percentOff: 10, validUntil: new Date(Date.now() + 30 * 86400_000), active: true },
{ title: "Expired", code: UI_IDS.promoExpired, percentOff: 50, validUntil: new Date(Date.now() - 86400_000), active: true },
],
});
// One baggage allowance (for excess-baggage flows later).
await prisma.baggageAllowance.create({
data: { seatClassId: IDS.seatClassLocal, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 50 },
});
// ── Return leg (C→A) for ROUND_TRIP (UA-6) ──────────────────────────────────
// Same train, same day, departs after the outbound arrives. Distances are symmetric
// (A=0km … C=250km) so the reverse leg prices the same as the outbound.
const returnDepart = new Date(departAt.getTime() + 8 * 3600_000); // 8h after outbound departs
const returnArrive = new Date(returnDepart.getTime() + 4 * 3600_000);
await prisma.trainSchedule.create({
data: {
id: UI_IDS.returnSchedule,
trainId: UI_IDS.train,
routeId: IDS.route,
originStationId: IDS.stationC,
destinationStationId: IDS.stationA,
departureAt: returnDepart,
arrivalAt: returnArrive,
durationMinutes: 240,
status: "SCHEDULED",
stopsCount: 3,
isPackageOnly: false,
},
});
await prisma.tripStopTime.createMany({
data: [
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationC, sequence: 1, plannedDepartureAt: returnDepart, status: "OPEN" },
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationB, sequence: 2, plannedDepartureAt: new Date(returnDepart.getTime() + 2 * 3600_000), status: "OPEN" },
{ scheduleId: UI_IDS.returnSchedule, stationId: IDS.stationA, sequence: 3, plannedArrivalAt: returnArrive, status: "OPEN" },
],
});
await prisma.coach.create({
data: { id: UI_IDS.returnCoach, coachTypeId: IDS.coachType, number: "UI-C2", capacity: 48, sequence: 1, status: "ACTIVE" },
});
await prisma.coachAssignment.create({
data: { scheduleId: UI_IDS.returnSchedule, coachId: UI_IDS.returnCoach, positionNumber: 1, isOperational: true },
});
await prisma.seat.createMany({ data: buildSeats(UI_IDS.returnCoach, 12) });
}
/** Build `rows × 4` seats (cols AD) for a coach. */
function buildSeats(coachId: string, rows: number) {
const cols = ["A", "B", "C", "D"];
const seats: Array<{ coachId: string; seatNumber: string; row: number; col: string; isWindow: boolean; isAisle: boolean }> = [];
for (let row = 1; row <= rows; row++) {
for (const col of cols) {
seats.push({
coachId,
seatNumber: `${row}${col}`,
row,
col,
isWindow: col === "A" || col === "D",
isAisle: col === "B" || col === "C",
});
}
}
return seats;
}
// Standalone runner
if (require.main === module) {
(async () => {
const prisma = new PrismaClient();
try {
await seedUi(prisma);
// eslint-disable-next-line no-console
console.log(`[seed-ui] done. Sample trip ${IDS.stationA}${IDS.stationC} on ${sampleDepartDate()} (schedule ${UI_IDS.schedule}).`);
} finally {
await prisma.$disconnect();
}
})();
}