diff --git a/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.test.ts b/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.test.ts new file mode 100644 index 00000000..755326eb --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from "vitest"; +import { canBuyTicket } from "./buyTicketVisibility.js"; +import type { ISimpleFlight, FlightStatus } from "../../../types.js"; + +function makeFlight(status: FlightStatus, depUtc: string): ISimpleFlight { + return { + id: "SU0022-X", + routeType: "Direct", + flyingTime: "1h", + status, + flightId: { carrier: "SU", flightNumber: "0022", suffix: "", date: "20260417" }, + operatingBy: { carrier: "SU", flightNumber: "0022" }, + leg: { + arrival: { + scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + latest: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + dispatch: "", gate: "", terminal: "", + times: { scheduledArrival: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: "" } }, + }, + dayChange: 0, + departure: { + scheduled: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + latest: { airport: "", airportCode: "", city: "", cityCode: "", countryCode: "" }, + dispatch: "", gate: "", terminal: "", checkingStatus: "Scheduled", parkingStand: "", + times: { scheduledDeparture: { dayChange: { value: 0, title: "" }, local: "", localTime: "", tzOffset: 0, utc: depUtc } }, + }, + equipment: {}, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "1h", + index: 0, + operatingBy: {}, + status, + updated: "", + }, + } as ISimpleFlight; +} + +describe("canBuyTicket", () => { + it("returns false when status is Cancelled", () => { + const now = new Date("2026-04-17T10:00:00Z"); + expect(canBuyTicket(makeFlight("Cancelled", "2026-04-18T10:00:00Z"), now, 2, 72)).toBe(false); + }); + + it("returns false when status is InFlight", () => { + const now = new Date("2026-04-17T10:00:00Z"); + expect(canBuyTicket(makeFlight("InFlight", "2026-04-18T10:00:00Z"), now, 2, 72)).toBe(false); + }); + + it("returns true when now is in window (24h before)", () => { + const now = new Date("2026-04-17T10:00:00Z"); + expect(canBuyTicket(makeFlight("Scheduled", "2026-04-18T10:00:00Z"), now, 2, 72)).toBe(true); + }); + + it("returns false when now is too close ( { + const now = new Date("2026-04-17T09:00:00Z"); + expect(canBuyTicket(makeFlight("Scheduled", "2026-04-17T10:00:00Z"), now, 2, 72)).toBe(false); + }); + + it("returns false when now is too far (>maxHours before)", () => { + const now = new Date("2026-04-17T00:00:00Z"); + expect(canBuyTicket(makeFlight("Scheduled", "2026-04-25T00:00:00Z"), now, 2, 72)).toBe(false); + }); + + it("returns false when UTC departure is empty", () => { + const now = new Date(); + expect(canBuyTicket(makeFlight("Scheduled", ""), now, 2, 72)).toBe(false); + }); +}); diff --git a/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.ts b/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.ts new file mode 100644 index 00000000..c299dc2b --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/visibility/buyTicketVisibility.ts @@ -0,0 +1,30 @@ +import { parseISO, subHours, isAfter, isBefore } from "date-fns"; +import type { ISimpleFlight } from "../../../types.js"; + +/** + * Buy Ticket button is visible when: + * - flight is NOT Cancelled or InFlight + * - now falls within [departure - maxHours, departure - minHours] + */ +export function canBuyTicket( + flight: ISimpleFlight, + now: Date, + minHours: number, + maxHours: number, +): boolean { + if (flight.status === "Cancelled" || flight.status === "InFlight") return false; + + const leg = flight.routeType === "Direct" ? flight.leg : flight.legs[0]; + if (!leg) return false; + + const depUtc = leg.departure.times.scheduledDeparture.utc; + if (!depUtc) return false; + + const departure = parseISO(depUtc); + if (isNaN(departure.getTime())) return false; + + const showFrom = subHours(departure, maxHours); + const showUntil = subHours(departure, minHours); + + return isAfter(now, showFrom) && isBefore(now, showUntil); +}