Case study
Payment & Booking
A service business books and checks out on the site. The owner posts availability. Customers pick a slot. The owner gets a calendar alert; the customer gets a receipt and a calendar file.
Sole engineer · 2024 – present · Live
Before
An email on the site, a conversation, a meeting, then a charge in Square.
Now
The owner posts availability. Customers pick a slot and finish on the page.
Context
Before this, the usual loop was: an email on the site, a conversation about needs, a meeting, then a charge in Square. I built booking and checkout into the site so that loop is gone. The owner puts availability up. People pick a time and finish on the page. The owner gets a calendar alert with what they need for the job. The customer lands on a confirmation they cannot guess their way into someone else's booking, plus a receipt and an .ics — phones that scan mail often drop it on the calendar without extra taps.
Constraints
- The browser cannot be trusted with the price. The server recomputes from the database.
- Two people cannot take the last slot. A hold and a confirmed booking both count as taken.
- A failed checkout needs a reason a person can act on, not a raw code.
- Receipts have to land in the inbox. Volume is low — receipts only — but junk mail still kills the process.
- The owner runs events and needs codes: dollars off, percent off, use limits, end dates, or all of those at once.
- Where the customer is changes the fee. Guessing from a typed address is not good enough.
What I built
- 1.Customer — Picks a slot, address, code if they have one
- 2.App — Next.js — the form. It does not own the total.
- 3.Server — Price, hold, promo, service-area fee, Square
- 4.Square — Card. We map whatever comes back.
- 5.Mail — Cloudflare. Receipt + .ics. Owner alert.
- 6.Database — Slots, holds, bookings, areas, codes, prices
The browser is a form. Totals, capacity, discounts, and “did this go through” are decided on the server. The live site is still on the host it launched on. A move onto the private mesh — edge in front, app and data off the public internet — is designed and partly built (hardened images, scan gate). It is not cut over yet.
Hard problems
Untrusted totals
If the client can set the amount, someone will. At pay time the server loads the booking, recomputes the total — services, add-ons, area fee, promo — and sends that number to Square. A client-supplied amount is rejected. The database is the price list, not the form.
The last slot
Two browsers can open the same time. A hold is a short reservation that counts against capacity the same way a confirmed booking does. Checkout only starts if something is left after holds and bookings are merged. Pay, and the hold becomes a booking. Walk away, and it expires.
Mail, after the obvious vendor said no
Amazon SES refused production for this business. I sent mail through Cloudflare instead and set the DNS (including DMARC) so a receipt is a receipt. I tested Gmail, Hotmail, and iCloud — they were landing in junk on the old path; they were not after the switch. I do not send campaigns. I send receipts. I have not stood up anything that reads DMARC reports yet. I know I want to.
Codes at events, fees by area
The owner gives dollars off, percent off, a use count, an end date, or a mix — including a free year of service from a raffle. That is a row in the database and a check at checkout, not a special build each time. Address is a Google suggestion so we are not matching typos. The fee comes from the service area that address falls in, not from calculated distance. I later used a geo lookup on another project and I would upgrade this the same way when I have time.
Outcome
- Status
- Live — book and check out on the site
- Owner
- Posts availability; gets a calendar alert
- Customer
- Receipt + calendar file
- Checkout
- 15+ payment errors in plain language
Close
- What this proves
- A live booking and checkout flow that replaced email, a meeting, and a charge in the Square app.
- What I would keep
- The server still owns the total. Mail stays on a path that actually arrives. Codes and area fees stay data, not one-off pages.
- What is still open
- The mesh cutover is not done. DMARC reports are not ingested. Area fees are still zones, not distance. I check in — the owner is not watching a dashboard.
