The obvious seat map is a drawing: one bus, rows of four, an aisle down the middle. It looks right until the second route runs a double-decker, and a third carrier puts the driver's door on the other side. Every new coach would mean a new screen.
So the screen should not know what a bus looks like. The booking API describes the coach of each trip as data: decks, rows, columns, the gaps that are aisles, and for every cell whether it is a seat, a table, a door or nothing. The app lays out whatever the schema says, one cell at a time, and decides only how large a cell can be on the width it has.
Taken seats come from the same payload and stay visible but locked, so the traveler sees the real shape of the bus. Each passenger gets one seat, the choice can be edited or cleared, and every change feeds the live total at checkout. Seats are offered only with full or partial payment, a rule that sits beside the map rather than inside it, so the map stays a picture of a bus. The habit I would keep is the boring one: a fixture for every odd coach, rendered in a golden test, so a new layout shows up as a picture in a diff.