Case study
Event4U
- Category
- Full-Stack
- Period
- Feb 2025 – May 2025
Online event ticketing with Midtrans payments and QR e-tickets
- Laravel 12
- Midtrans Snap API
- QR Code E-Ticket
- Docker
- GCP Cloud Run
Context
Event ticketing in Indonesia still runs largely on chat threads and manual bank transfers. A buyer sends a message, waits for a reply, transfers money, and sends a screenshot as proof. The organiser then confirms by hand, and the ticket arrives one at a time.
For a community meetup that might be tolerable. For anything with a few hundred attendees it collapses immediately, and the organiser is left with a spreadsheet of who paid and no reliable answer about who has not.
Problem
The manual flow breaks in four separate places:
- Orders are not trackable. A message thread is not a record. Nobody can answer "who bought a ticket and never paid?" a week later.
- Payment is not verified automatically. Proof of payment is a screenshot, which means confirmation depends on the organiser noticing it.
- Delivery is manual. Each ticket has to be sent to each buyer individually.
- Reporting does not exist. There is no way to know which event sold well, or how the month is tracking, without rebuilding the numbers by hand.
Solving only the payment step would leave the rest exactly as fragile as before.
Solution
One application covering the whole path: browse events, pick a ticket type, enter buyer details, pay through Midtrans, and download a QR-coded e-ticket as a PDF.
Two details matter more than they look. Guest checkout means a buyer never has to create an account to complete a purchase — the single biggest source of drop-off in event ticketing is the registration wall. And the e-ticket is generated as a real PDF with an embedded QR code, so entry at the door is a scan rather than a list check.
Administrators get the other half: event, category and ticket management, order and payment lists, payment status updates, per-event and overall sales analytics, and export of those reports to Excel or PDF.
Architecture
Blade templates with Alpine.js, and an admin panel under /admin protected by
role middleware.
Midtrans is wired through a single config/midtrans.php and a
MidtransServiceProvider, rather than being spread across controllers, so the
payment channel can be swapped or sandboxed in one place.
Orders carry a payment deadline. Overdue ones are closed by the
orders:clean-expired console command, which marks pending payments as expired
inside a database transaction and rolls back on failure — so a partial cleanup
cannot leave payments in a half state.
The container image is a two-stage build: a php:8.2-fpm builder installs
Composer dependencies and compiles extensions, and the runtime stage is
php:8.2-fpm-alpine with nginx and supervisor. Supervisor runs the web server,
the queue worker and the log viewer as separate processes, which is what makes
background work and live logs behave the same in a container as they do under
composer dev.
Challenges
Two checkout paths, one order model. A guest buys with an email address and no account; a registered buyer brings a user record. Both had to produce the same order, the same payment record and the same e-ticket, or every downstream report would need two code paths.
Webhooks arrive more than once. Midtrans retries a notification that was not acknowledged. Processing one twice must not create a second paid order or a second ticket, so status transitions are guarded rather than blindly applied.
Overdue orders leak inventory. A ticket held by an unpaid order is a ticket that cannot be sold to someone else. Expiring those on a schedule is what keeps capacity honest.
QR codes have to survive printing. A QR rendered into a PDF at screen resolution scans unreliably when the ticket is printed at A5. The code is generated at sufficient density and kept out of any lossy compression path.
Results
I led a five-person team across product, full-stack development and QA, and authored 97 of the 125 commits in the repository.
The application ships as a production container image, so the same nginx, supervisor, queue worker and log pipeline runs in a container as it does in local development. The admin side reports sales per event and overall, and those reports export to both Excel and PDF.
Automated test coverage here is deliberately modest — the effort went into the payment path and the delivery format instead. Recording that plainly seemed more useful than implying coverage that does not exist.


