All work

For a US company that sells tickets and concessions online for independent cinema chains, under NDA. 2022

Selling real cinema seats through four box-office systems

Independent cinema chains each run their own box-office system, and this platform sells their seats online, on a branded site for every cinema. I worked on it from July to October 2022, on billing, checkout and refunds: a processing fee on every transaction, Stripe’s real decline reasons, a checkout timer that gives back the time spent on a payment, and refunds that also cancel the concession order.

POS adapters in the platform
4
Steps in its purchase pipeline
~25
Seat maps merged with live seats
426
Storefront theme folders
31

How the ticketing platform works

The problem

Every chain runs its own box office

Independent cinema chains sell tickets at the door through their own box-office point-of-sale (POS) system, and those systems do not agree: one takes XML requests, the others JSON, and they do not all support the same operations. An online sale has to hold real seats in that chain’s POS and charge the customer exactly once, and every chain wants a website in its own colours.

The hub

One backend in front of four box-office systems

The client’s team built the platform around one Laravel and Nova backend with a central database, and it is the only part that talks to the POS systems and to Stripe. Behind one interface, adapter classes for four POS systems cover about 18 operations: showtimes, seat maps, reservations, purchases, refunds, gift cards and loyalty. The right class is found by name from the theatre’s POS, so supporting a new POS means adding classes, not changing controllers.

Every 15 minutes the scheduler queues one job per active theatre to warm its showtime cache, with posters, cast and trailers matched from TMDB.

The purchase

The seats are sold before the card is charged

Every purchase runs through about 25 chained steps. It fetches ticket prices again from the live POS, not the cache, and rejects any cart whose price or fee does not match. It builds the order, works out fees and taxes, submits the order to the POS and only then charges the card through Stripe.

If any step fails, the POS order is refunded and the partial records are deleted. Each order keeps the platform’s, the cinema’s and any reselling site’s fees apart, along with its taxes.

The storefronts

A site for every cinema, from one codebase

Each cinema or chain gets its own deployment of one Blade, Tailwind and Alpine storefront: a single-location site, or a multi-location site with a location picker. A cinema’s theme overrides the views it needs and falls back to the default for the rest, across 31 theme folders.

Checkout keeps the cart in the session under a countdown. 426 seat maps are merged with live availability, and a rule stops a customer leaving one seat stranded, with wheelchair and companion seats exempt. An abandoned checkout releases its seats through a browser beacon and a queued cancel job.

My part

Fees, declines, the timer and refunds

I worked on the platform from July to October 2022. I built its processing fee, worked out on every transaction from the theatre’s setting, then the chain’s, then the platform default, with Stripe’s own fee kept separately and a command that backfilled past payments.

In checkout, I made a declined card show Stripe’s actual reason instead of a generic message, gave the seconds spent waiting on the payment back to the timer, and made “start over” reset it. I made a ticket refund also cancel the linked concession order, and made the refund window configurable per theatre or chain. I also forked the default theme into a new theme for one chain.

How it fits together

One order, from the cinema’s site to a refund. Each stage lists what the platform already did, and what I changed.

  1. 1Storefront

    The platform

    • One codebase, deployed once per cinema or chain
    • Theme views fall back to the default theme

    What I changed

    • A new theme for one chain, forked from the default
  2. 2Checkout

    The platform

    • 426 seat maps merged with live availability
    • A rule against stranding a single seat
    • The cart kept in the session under a countdown

    What I changed

    • After a declined card, the seconds spent waiting on the payment go back on the timer
    • “Start over” resets the timer
  3. 3Purchase

    The platform

    • Prices re-checked against the live POS
    • The order sent to the POS before the card is charged
    • A rollback if any step fails

    What I changed

    • A platform processing fee on every transaction: the theatre’s setting, then the chain’s, then the default
  4. 4Payment

    The platform

    • Stripe charges and saved cards
    • POS gift cards

    What I changed

    • Stripe’s actual decline reason shown, not a generic message
    • Stripe’s own fee kept separately
    • A command that backfilled the fee on past payments
  5. 5Refunds

    The platform

    • Refunds from the admin, and self-service refunds for customers

    What I changed

    • A ticket refund also cancels the linked concession order
    • A refund window set per theatre or chain

The work

Client
A US company selling tickets and concessions online for independent cinema chains, under NDA
Role
Full-stack developer on an established codebase, July to October 2022
My part
The processing fee, the decline messages, the checkout timer and refund fixes, and a new theme forked from the default. The platform, its design and its tests are other developers’ work.
  • Laravel
  • PHP
  • Laravel Nova
  • MySQL
  • Stripe
  • Tailwind CSS
  • Alpine.js
  • TMDB API