Engineering Money-Sensitive Multi-Role Workflows with QR Claims and Audit Trails
Lessons from structuring a real-time event and transaction platform around strict roles, fight states, teller balances, one-time claims, and public displays.
Complexity appeared between the roles
The difficult part of a multi-role transaction platform is rarely the number of pages. It is the number of ways one role can change what another role is allowed to do. In this system, administrators configure fights, users, commission, teller balances, and global controls. Declarators move fights through operational states and publish results. Tellers place bets, issue tickets, manage cash, scan claims, and print receipts. A public screen shows live information without exposing private controls.
I began with the end-to-end flow: an administrator creates a fight, betting opens, tellers record bets, the status moves through last call and closed, a declarator records an outcome, and valid tickets become claimable. Each arrow is a boundary where stale data, double actions, or an incorrect permission could affect money. That made state and authorization the foundation rather than a final security pass.
Making the state machine explicit
Labels such as Open, Last Call, Closed, Meron, Wala, Draw, and Cancel are not decoration. They decide whether a teller may accept a bet, whether a ticket can be voided, and whether a payout exists. Spreading these checks across buttons would make the interface appear correct while leaving direct requests inconsistent. The safer model defines allowed transitions on the server and treats the client as a clear explanation of those rules.
A result declaration also affects many records. The platform has to preserve the declared outcome, determine which tickets qualify, update the information shown on the public display, and keep the financial history auditable. Those changes belong inside a deliberate transaction boundary. Even when real-time updates make the interface feel immediate, database completion must happen before the system broadcasts the new truth.
- Open and close controls belong to authorized fight-management actions.
- Void eligibility ends at a defined lifecycle boundary, not when a button disappears.
- Result publication and payout eligibility derive from the same persisted outcome.
Authorization had to describe capabilities
Role-based access is often implemented as three dashboard redirects and a few menu conditions. That is not enough. A teller should not see another teller’s private financial data, but more importantly, the server must reject any request for it. A declarator can change result-related state without gaining access to user management or financial configuration. An administrator can manage the platform without impersonating every teller action.
Laravel middleware and policies provide the enforcement point, while Inertia page props help the React interface expose only relevant actions. I prefer capability-oriented rules such as “may declare this fight” or “may claim this ticket” over scattered role-name comparisons. Capability rules can include object state and ownership, which is essential when a role is allowed to act only before declaration, only on its own records, or only while a fight is open.
Treating teller balance as a ledger question
A teller balance changes through bets, cash in, cash out, payouts, adjustments, and transfers. Updating a single balance column without keeping the reason would make reconciliation nearly impossible. The system therefore needs transaction history around the displayed balance. Each movement should identify its type, amount, actor, related record, time, and approval state where applicable.
Transfers between tellers make the rule clearer. A transfer is not two unrelated edits; it is one business event with a debit side and a credit side. If approval is required, the funds should not appear final before that approval is recorded. The implementation described by the project includes transfer history and an approval workflow, which provides the operational trail needed to explain how balances changed instead of showing only the latest number.
Designing one-time QR claims
QR scanning improves teller speed, but the QR code itself is only an identifier. It is not proof that a payout is still valid. The server has to look up the ticket, confirm the fight result, check whether the ticket won, verify that it has not already been claimed or voided, and record the claim atomically. The camera may scan the same code twice, two devices may submit it close together, or a slow response may encourage the teller to retry.
That means the claim endpoint needs idempotent behavior and database protection, not only a disabled button. A second request should return the already-known result instead of paying twice. Clear status messages—invalid, not yet claimable, losing, already claimed, or paid—help the teller understand the outcome without exposing internal details. The same reasoning applies to voiding a ticket before declaration.
Real-time screens should share one truth
The system includes live odds, fight status, betting totals, and a public big-screen display. The tempting approach is to calculate separate versions for the teller dashboard and public display. That creates drift. Both should receive projections of the same fight and bet records, with private information removed for the public route. Real-time delivery changes when users see the update, not how the answer is calculated.
The public display also has a different reliability profile. It runs without authentication and may remain open for a long session, so it needs reconnection behavior and a useful initial server-rendered state. If a live connection drops, the screen should not silently keep presenting old information as current. A refreshed snapshot, visible connection state, or bounded polling fallback is more trustworthy than animation that hides staleness.
Building for a teller’s phone
Tellers work through a mobile-first interface with bottom navigation for dashboard, payout, history, cash, and settings. Camera scanning and Bluetooth printing are not secondary add-ons; they shape the flow. A bet amount needs a large, unambiguous input. Open and closed status must remain visible. Destructive or irreversible actions need stronger confirmation than ordinary navigation.
Capacitor provides an Android route while the PWA supports installation and offline assets. As with the parking application, native capabilities should remain adapters around server-owned transactions. Printer configuration can stay on the device. The issued ticket, claim status, and financial movement stay in Laravel and MySQL. This split makes the mobile experience fast without asking the device to become the accounting authority.
The payoff and the lasting lesson
The resulting platform connects fight management, status control, live odds, ticket generation, QR scanning, one-time payouts, cash movement, reporting, a public display, and printer settings across clearly separated roles. Its value is visible in that connected workflow. Each role can focus on its job while the system preserves the transitions that join them.
My strongest takeaway is that real-time financial interfaces must be built from durable invariants outward. Fast updates, mobile navigation, and QR scanning improve the operator experience, but they cannot compensate for ambiguous lifecycle rules. Model the states, enforce permissions on the server, record every balance movement, make repeated requests safe, and then broadcast the result. That order is what turns a collection of screens into an accountable system.