Case study · PHP · MySQL · Razorpay · Tourism
A booking engine for a tour operator, and the one thing the browser must never decide.
Wander Yak lets a traveler pick a trip, a stay and a date and pay a token to hold the seat. This is how it's built, why the server works out every amount, what the campaign module needed, and what a live speed test found.
The problem
A tour operator needs a booking engine that also happens to be a website.
Wander Yak was built for tour operators, and its live site belongs to one based in Tabo, Spiti Valley. In a small tour business the selling usually happens in chat: a traveler asks about a trip, the operator sends a price and an itinerary, they settle a date, and a payment follows. Every step is typed by a person, over and over, and often from a place with poor signal.
The brief that follows is plain. Put the price, the stay, the plan and the open dates on the page. Let a traveler pay to hold a seat without a person in the loop. Hand the operator one panel to run trips, bookings and customers from. And do it once as a product that can be licensed to other operators, not rebuilt for each.
How a trip is modelled
A trip isn't one price. It's a pickup city, a stay category, an option and a date.
A package page is really a small decision tree. The traveler picks a pickup city (each with its own trip length), then a stay category such as dual or triple sharing, then a package option, and for group trips a departure date by month and day. The price belongs to the stay category, per person. The stay details show the hotel for each night, with its star rating, meals and a photo gallery.
Packages come in kinds with different rules. A group package has fixed departures, so the booking must carry one of the dates the operator has opened. Any other package type doesn't, and the traveler is just asked for travel details. The server checks this itself: a group booking for a date that isn't offered is refused with "Selected date is not available for this group package."
Money is worked out on the server
The browser is never trusted with a price.
When a traveler books, the page sends the choices, not the amounts. The server looks up the price per person for the chosen stay category, multiplies by the number of people, then works out the token: 20% of the total, rounded to the nearest rupee, plus 5% GST on that rounded token, rounded again. For two travellers on a Rs 20,499 trip that comes to a total of Rs 40,998, a token of Rs 8,200 and Rs 8,610 payable now.
A Razorpay order is created for that amount and saved with the booking as "initiated". After the traveler pays in Razorpay's own window, the server checks Razorpay's signature before the booking is confirmed, then sends the confirmation email and, if the traveler is new, creates an account and sends a welcome email. Because of that signature check, a user can't mark their own booking as paid by editing the page.
Campaigns: a different kind of booking
A clean-up drive isn't a holiday.
In July 2026 I added a campaign module for volunteer-style trips, such as a lake clean-up in Spiti. Its flow differs from a trip in three ways. The person must accept a media consent and a declaration before they can register. The entry fee is optional, set per campaign, and Razorpay is used only when a fee exists. And a registered person can cancel their own registration.
The fee is read from the campaign's own record on the server, and an invalid fee is rejected. The payment is verified the same way as a booking, by checking the signature against that registration's order. I kept campaigns as a separate module rather than forcing them into the trip tables, because a registration has no price per person, no stay and no itinerary.
Currency: a display, not a charge
Showing prices in other currencies without changing what's charged.
Travelers come from other countries, so a currency switch lets them see prices in their own. When one is chosen, the server asks a free exchange-rate service for the day's rate and keeps it in the visitor's session, and every price on the page, and in the PDF brochure, is converted from rupees.
The booking itself is always charged in rupees. I kept it that way on purpose: converting at checkout would mean the amount a traveler is shown and the amount Razorpay charges could differ by the minute, and the whole point of working the token out on the server is that they never do.
A brochure on demand
A PDF, made when asked for.
Travelers ask for a brochure to forward to a family member, so every trip has a Download Brochure button. The server builds the PDF on the spot with TCPDF from the same data as the page: the plan, the stay, inclusions and exclusions, with prices in the chosen currency. Because it is generated and not uploaded, it can't go stale when the operator edits the trip.
How it got here
Fourteen commits and a code review.
The code lives in a Git repository, and its history shows the real order of work: a README and licence in July 2025, a final push in January 2026, Mailgun email in February, a trending order and date sorting, a rewritten payment module in May, then a pull request titled as a code review the same week, which hardened how the package type and slug travel from the page to the server, and the campaign module in July.
The review pass is worth mentioning because it's the kind of thing a client product needs. A booking page passes a package type and slug through JavaScript, and a value that reaches the server from the browser must be treated as untrusted whatever it looks like. That was tightened after the review.
Measuring speed
Great on stability, weak on photos.
On 3 October 2026 I ran Lighthouse against the live home page on its mobile preset. Accessibility scored 96 and best practices and SEO 100. Blocking time was 0 ms and layout shift 0.001. Performance scored 72. The page makes 57 requests and weighs 3.4 MB; its largest photo is about 650 KB and the largest image appears at 6.6 seconds on the test's slow connection. The server needed 670 ms to answer.
That is a photo problem, not a code problem, and the answer is already built elsewhere: the image pipeline on the Diksha site makes three sizes in WebP and AVIF and lets the browser pick. Wander Yak makes only one large and one thumbnail copy. Bringing the two together is the single change that would lift this score the most.
What it doesn't do
Three gaps worth knowing.
It doesn't count seats. A group departure lists its dates, but nothing stops a date selling past the vehicle's capacity, so the operator must close a date by hand. It has no payment webhook, so a booking is confirmed when the browser comes back from Razorpay, and a traveler who pays and closes the tab at that exact moment needs the operator to reconcile. Prices in the stay-category table are stored as text that can contain commas, which the booking code has to strip before it can calculate. It works, and it's a data-model wart I'd fix first in a rebuild.
The architecture
What's actually running under it.
- Backend
- Plain
PHPwith a small kernel for the database, queries and SEO, and models and includes for the public pages. About 8,400 lines in the top-level public files, and dozens of admin screens. - Database
MySQL, 41 tables created by a factory script: packages, stay categories and options, hotels, itineraries, departures, bookings, customers, campaigns and registrations, tour sheets, roles and more.- Payments
Razorpaythrough Composer, with server-side amounts and signature verification, for trip tokens and campaign fees.Mailgun,SendGridandPHPMailerbehind a small mailer layer, with templates the operator edits in the admin.- Documents
TCPDFfor brochures, built from the same data as the trip page.- Front end
- Server-rendered pages with Bootstrap and jQuery, and an installable web-app manifest.
- Admin
- A permission-checked panel with searchable lists loaded over AJAX, an analytics dashboard, tour sheets and editable email templates.
- Licence
- A custom no-resale licence: use it for your own business, modify it, never resell or publish it, and three months of free fixes on unmodified code.
- Verification
- No automated tests. Checked by using it, by a code-review pass, and by the Lighthouse run above.
The outcome
Honest about where it actually is.
Wander Yak is live at thewanderyak.com and does the thing it was built for: a traveler can choose a trip, a stay and a date, pay a token to hold the seat and get a confirmation, and the operator runs everything from one panel. It's a licensed product, so the next operator gets the same engine with their own trips and brand.
What it isn't yet: fast on mobile, seat-aware, or webhook-safe. Those are known, written down and fixable. The image pipeline from Diksha, a seat count per departure and a Razorpay webhook are the next three jobs, in that order.
Run a tour company and want a booking engine of your own, or want to license this one?
Get in touch