# Phase 2B — URL Serializer/Parser

> **Parent:** `2026-04-14-phase-2-online-board-master.md` (sub-plan 2B)
> **Status:** Ready to execute

## Goal

TDD port of the Angular `OnlineBoardUrlBuilderService` / `OnlineBoardUrlParserService` into pure TypeScript functions with zero Angular dependencies. Byte-exact URL parity with Angular.

## Deliverable

`src/features/online-board/url.ts` — pure functions, no side effects, no Date objects.

## URL Format Reference (from Angular analysis)

| Type | Pattern | Example |
|---|---|---|
| Start | `/onlineboard` or `/onlineboard/` | `/onlineboard` |
| Flight | `/onlineboard/flight/{carrier}{flightNumber}{suffix}-{yyyyMMdd}` | `/onlineboard/flight/SU0100D-20250115` |
| Departure | `/onlineboard/departure/{station}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/departure/SVO-20250115-08001800` |
| Arrival | `/onlineboard/arrival/{station}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/arrival/LED-20250115` |
| Route | `/onlineboard/route/{dep}-{arr}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/route/SVO-LED-20250115` |
| Details | `/onlineboard/{carrier}{flightNumber}{suffix}-{yyyyMMdd}` | `/onlineboard/SU0100-20250115` |

### Key rules

- **Date format:** `yyyyMMdd` (8 digits, no separators)
- **Time range:** `{HHmm}{HHmm}` (8 digits continuous, from-to)
- **Flight number:** zero-padded to 4 digits in the URL (e.g., `100` becomes `0100`), total flightNumber+suffix = last 5 chars
- **Carrier code:** 2 characters (IATA), or 3 characters if 3rd char is a letter (rare 3-char carriers)
- **Suffix:** optional single trailing letter on a flight identifier (e.g., `D` in `SU0100D`)
- **Station codes:** 3-letter IATA airport codes

## Exported API

```ts
type OnlineBoardParams =
  | { type: "start" }
  | { type: "flight"; carrier: string; flightNumber: string; suffix?: string; date: string }
  | { type: "departure"; station: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "arrival"; station: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "route"; departure: string; arrival: string; date: string; timeFrom?: string; timeTo?: string }
  | { type: "details"; carrier: string; flightNumber: string; suffix?: string; date: string };

function parseOnlineBoardUrl(path: string): OnlineBoardParams | null;
function buildOnlineBoardUrl(params: OnlineBoardParams): string;

// Lower-level helpers (also exported for 2C/2E consumption)
function parseFlightUrlParams(raw: string): IParsedFlightId | null;
function buildFlightUrlParams(id: IParsedFlightId): string;
function parseStationUrlParams(raw: string): { station: string; date: string; timeFrom?: string; timeTo?: string } | null;
function parseRouteUrlParams(raw: string): { departure: string; arrival: string; date: string; timeFrom?: string; timeTo?: string } | null;
```

## Tasks

### Task 1: Write failing tests for `parseFlightUrlParams`

**File:** `src/features/online-board/url.test.ts`

Test cases:
- `SU0100-20250115` -> `{ carrier: "SU", flightNumber: "0100", date: "20250115" }`
- `SU0100D-20250115` -> `{ carrier: "SU", flightNumber: "0100", suffix: "D", date: "20250115" }`
- `SU100-20250115` -> `{ carrier: "SU", flightNumber: "100", date: "20250115" }` (3-digit)
- `SU1234-20250115` -> `{ carrier: "SU", flightNumber: "1234", date: "20250115" }` (4-digit)
- Empty string -> `null`
- Missing date part -> `null`

### Task 2: Implement `parseFlightUrlParams` to pass tests

### Task 3: Write failing tests for `buildFlightUrlParams`

Test cases:
- `{ carrier: "SU", flightNumber: "100", date: "20250115" }` -> `SU0100-20250115` (padded)
- `{ carrier: "SU", flightNumber: "0100", suffix: "D", date: "20250115" }` -> `SU0100D-20250115`
- `{ carrier: "SU", flightNumber: "1234", date: "20250115" }` -> `SU1234-20250115`

### Task 4: Implement `buildFlightUrlParams` to pass tests

### Task 5: Write failing tests for `parseStationUrlParams`

Test cases:
- `SVO-20250115` -> `{ station: "SVO", date: "20250115" }`
- `SVO-20250115-08001800` -> `{ station: "SVO", date: "20250115", timeFrom: "0800", timeTo: "1800" }`
- Empty string -> `null`

### Task 6: Implement `parseStationUrlParams` to pass tests

### Task 7: Write failing tests for `parseRouteUrlParams`

Test cases:
- `SVO-LED-20250115` -> `{ departure: "SVO", arrival: "LED", date: "20250115" }`
- `SVO-LED-20250115-08001800` -> `{ departure: "SVO", arrival: "LED", date: "20250115", timeFrom: "0800", timeTo: "1800" }`
- Empty string -> `null`

### Task 8: Implement `parseRouteUrlParams` to pass tests

### Task 9: Write failing tests for `parseOnlineBoardUrl`

Test cases:
- `/onlineboard` -> `{ type: "start" }`
- `/onlineboard/` -> `{ type: "start" }`
- `/onlineboard/flight/SU0100-20250115` -> `{ type: "flight", carrier: "SU", flightNumber: "0100", date: "20250115" }`
- `/onlineboard/departure/SVO-20250115` -> `{ type: "departure", station: "SVO", date: "20250115" }`
- `/onlineboard/arrival/LED-20250115-08001800` -> `{ type: "arrival", station: "LED", date: "20250115", timeFrom: "0800", timeTo: "1800" }`
- `/onlineboard/route/SVO-LED-20250115` -> `{ type: "route", departure: "SVO", arrival: "LED", date: "20250115" }`
- `/onlineboard/SU0100-20250115` -> `{ type: "details", carrier: "SU", flightNumber: "0100", date: "20250115" }`
- `/some/other/path` -> `null`
- Empty string -> `null`

### Task 10: Implement `parseOnlineBoardUrl` to pass tests

### Task 11: Write failing tests for `buildOnlineBoardUrl`

Test cases:
- `{ type: "start" }` -> `onlineboard`
- `{ type: "flight", carrier: "SU", flightNumber: "100", date: "20250115" }` -> `onlineboard/flight/SU0100-20250115`
- `{ type: "departure", station: "SVO", date: "20250115" }` -> `onlineboard/departure/SVO-20250115`
- `{ type: "departure", station: "SVO", date: "20250115", timeFrom: "0800", timeTo: "1800" }` -> `onlineboard/departure/SVO-20250115-08001800`
- `{ type: "arrival", station: "LED", date: "20250115" }` -> `onlineboard/arrival/LED-20250115`
- `{ type: "route", departure: "SVO", arrival: "LED", date: "20250115" }` -> `onlineboard/route/SVO-LED-20250115`
- `{ type: "details", carrier: "SU", flightNumber: "0100", date: "20250115" }` -> `onlineboard/SU0100-20250115`

### Task 12: Implement `buildOnlineBoardUrl` to pass tests

### Task 13: Roundtrip tests

For every URL type: `buildOnlineBoardUrl(parseOnlineBoardUrl(url)) === url` (after normalization).

### Task 14: Edge case tests

- Suffixed flights roundtrip: `SU0100D-20250115`
- 3-digit flight numbers: build pads to 4, parse handles both
- Time range only partially specified (only timeFrom, no timeTo) -> no time range in URL
- Invalid date format -> null
- Unknown route prefix -> null

### Task 15: Export from barrel

Update `src/features/online-board/index.ts` to re-export the public API.

### Task 16: Verification

Run `pnpm typecheck && pnpm lint && pnpm test`.

## Exit criteria

- All tests pass
- `parseOnlineBoardUrl` returns null for invalid inputs, never throws
- Zero `any` types
- Pure functions only — no side effects, no imports from Angular
- Dates are strings (`yyyyMMdd`), not Date objects
