Building it
MGM used an outside vendor to collect room deposits from travel agents, group organizers and other people paying before a guest’s stay. The process involved manual work and separate handling for different hotel reservation systems. I was asked to build the payment page for its replacement.
I started in May 2025 and was the only engineer on the page for the first ten weeks. A lobby host would send a payment link, and the person paying could open it, review the reservation and pay from their browser. I built support for individual and group bookings, including bookings with several reservations.
The designs and backend API were still changing while I worked. I built against a temporary version of the API that followed the agreed spec, which let me keep moving before the real service was ready. For card entry, I used the payments team’s embedded form so our app never handled raw card details.
We soft-launched in August, 88 days after my first commit, with no critical or high-priority bugs left open.
Making it reliable
The hardest part was handling what happened between entering a card and recording the deposit. A card could be charged successfully while the next step failed. Showing a generic error and inviting the guest to try again would give them the wrong next step.
I worked with product to handle that case explicitly: stop further submissions and tell the guest to call for help. I also gave expired links, too many attempts and card validation failures their own messages, so guests knew whether they could fix something themselves.
Some payments were also getting stuck on a spinner. I traced one cause to two messages our page sent to the payment widget at the same time. The widget sometimes missed the second one. I worked with the payments team to add a reply our page could wait for before continuing.
After launch, guests with non-US billing addresses hit another version of the problem. I reworked the payment sequence with the payments team and checked it end to end using a UK billing address before we released the fix.
There was still a gap. In a later sample of 50 payment sessions, 40 reached confirmation, nine reached a defined error and one stayed on the spinner. The known failures had clear outcomes, but a widget that never replied could still leave someone waiting.
Handing it over
The app went on to handle more than 20,000 payment attempts in the period covered by our usage records. Another team added a call-center payment flow without changing the core widget integration or error handling.
When ownership moved to the payments team, they copied the app into their own repository. Five weeks later, 97% of the lines were unchanged, including the entire main payment component.
Their maintainer guide also preserved decisions that were easy to miss just by reading the screen: the order of payment events, how errors were handled, and why the form stayed mounted while a payment was processing. Those details came from problems I’d worked through during the build, and they became part of how the next team maintained it.