The confirmation dialog said 18 credits. The page it opened from said 0.
Same event, same moment, same database. The ticket tracking this had been open long enough to get lost once and found again, and it kept coming back because of this exact contradiction. A customer opens an event they’re about to delete, the dialog tells them they’ll get 18 credits back, they close it to double check, and the detail page they land on says the event is worth 0.
Two code paths, two questions
The Detail page was reading a raw inventory field, something close to event.credits_balance. That column gets written once when credits are allocated and mostly left alone.
The delete dialog never touched that column. It ran a calculation: walk the event’s units, sum what’s refundable per unit by category (free entries, purchased entries, last-year carryovers), and net it against anything already spent on setup.
So credits_balance answers “what got allocated.” The delete calculation answers “what would you actually get back right now.” Nobody had needed those two numbers to agree until a customer looked at both in the same five minutes.
The fast fix, and what it cost
We tried the quick one first: make the Detail page read what the dialog read. We found where the dialog pulled its number and pointed the Detail page’s credits display at the same helper. It worked on the one case we tested, an event with a straightforward purchase.
Then we ran it against a free-entry event and the page showed a negative number.
The helper was not a pure read. It did the refund math assuming a delete was actually happening, including deducting setup costs that only apply mid-delete-flow. We had wired a “what happens if you delete this” function into a “here’s your balance” display, and it told a customer they owed money on an event they hadn’t touched. That is worse than showing 0. We reverted it that hour rather than let it sit, even locally.
Splitting the calculation
The fix was to pull the one function apart into two clean questions:
- a refund total (free credits plus purchased credits), which is what the dialog needs
- a spendable balance (the same components, without the delete-flow deduction), which is what the Detail page needs
Once they were separate functions built from the same unit-level data, instead of one function with a delete flag threaded through it, each call site could ask its own question and get an honest answer. The Detail page now shows free-plus-purchased spendable credits. The delete dialog shows the refund total. They agree because they’re built from the same source rows, and where they differ, they say so.
The bug underneath
Once we’d traced it properly, the root cause turned out to be separate and uglier: event delete bypassed the credit refund chain entirely on one path. That means the raw credits_balance column could drift from reality any time an event was deleted through that path, since nothing recalculated it. The Detail page displaying that stale column surfaced an accounting gap. The delete dialog’s live calculation had been correct the whole time.
Testing and ship
Before shipping we went through every delete case by hand: free, locked, last-year, paid, competition-only, and setup delete. For each, we checked that the refund matched the spendable balance shown before the delete and the actual credit total after. All six passed. We shipped it as a hotfix and closed the ticket the same day.
Skipping the trace cost us an hour and nearly put a screen in front of a customer saying they were in debt. After tracing both paths back to their source rows, the second pass took twenty minutes with no revert. The first attempt assumed it was just relabeling a display. It wasn’t, because the same word on screen was hiding two different questions.