
The problem
A tour-booking platform is mostly trust: prices that cannot be tampered with, reviews that come from people who actually went, and staff tools that show each role only what its own API will let it do.
Decisions
Bookings are priced from the tour record
createBooking takes a tour id, a participant count and a start date, then reads the unit price off the tour record and derives the total from the count. The request has no field for a price or a total, so a booking for ten travellers cannot be submitted carrying the total for two: there is nowhere in it to put one.
A review requires a booking
Reviewing is gated on a confirmed or completed booking for that tour, and the UI asks the API for that status before rendering a form the API would only reject.
The admin area splits into two route groups
Staff screens allow admin and lead-guide; the user list is a separate guard for admin alone, because that is what the API enforces. One combined guard would show a lead-guide a screen that can only ever 403. A signed-in user without the role goes to their account, not back to a sign-in form they have already filled in.
The hard part
Building a real catalogue out of an API that was never meant to produce one
The 59 tours are reshaped from OpenTripMap points across a fixed list of Kenyan regions, and almost everything fussy in that script is load-bearing.
Lodging, operators and unnamed points are filtered out, because OpenTripMap tags a beach resort as a beach and the catalogue otherwise fills with hotels. Prices and durations are seeded from a hash of each point’s id, so re-running the ingest does not reshuffle the whole catalogue. Names avoid two specific failures: repeating the region (“Mount Kenya Mount Kenya Trek”), and repeating the trip noun’s own word (“Galu Kinondo Beach… Beach Escape”). When they run long they drop the region, never truncating mid-word.
Cover images are backfilled by a separate idempotent pass against Wikimedia Commons, and both of its rules were learned by getting rate-limited. Search terms stay to one or two words, because the search engine ANDs every term: “Maasai Mara wildlife safari game drive Kenya” returns nothing where “Maasai Mara wildlife” returns thousands. And the per-file verification requests are never parallelised, because they start returning 429 within two or three calls, so the loop backs off and the script is written to be re-run, not to be fast.
The URL rewrite has the sharpest edge. Raw thumbnail hotlinks now answer 400, so URLs are rewritten to the canonical file path, and the pattern is not anchored to the end of the string, because Commons appends tracking parameters, and an end-anchored regex rejects the whole URL the moment it sees them, silently turning every match into null.