A modern, web-based operations platform engineered for snooker centres, billiard halls, and cue-sport academies — covering tables, members, battles, food & beverage, billing, and analytics, all from one console.
Running a snooker centre is an act of choreography — tables turning, members arriving, kitchens firing, scoreboards flickering, ledgers closing. This guide is the script.
The Snooker King platform was designed not to replace the rhythm of a busy room, but to amplify it. Every screen, every button, every report exists because a real operator, on a real night shift, asked for it. This guide walks you through the system as it actually behaves — from a member tapping a QR code at the door, to the moment the manager closes the till and reviews the day.
Read it linearly the first time. Keep it on a shelf for the second. The chapters move from the broadest concepts (architecture, roles, daily flow) toward the deepest mechanics (settings, audit logs, troubleshooting). Cross-references and a glossary close the volume.
— The Snooker King Team
June 2026
"A well-run room is the sum of small, well-kept records."
Snooker King is a multi-tenant cloud platform for cue-sport venues. One installation can serve a single hall, a regional chain, or a national franchise — without code changes, from the same browser.
The platform is composed of four cooperating surfaces. Understanding them in this order will help every later chapter make sense.
Every record in the system belongs to a four-level hierarchy. Permissions, reports, and even the colour of the navigation bar depend on which level you are signed into.
| Level | Owns | Typical user |
|---|---|---|
| Client (tenant) | Brand, billing, organizations | The franchise / company owner |
| Organization | Centres, regional rules, memberships | Regional director |
| Centre | Tables, employees, F&B menu, prices, devices | Branch manager |
| Member / Guest | Bookings, wallet, history, battles | Customer |
Snooker King is a browser-first application. There is no installer, no desktop client, and no required plugins.
| Surface | Recommended | Minimum |
|---|---|---|
| Operations Console | Chrome / Edge 120+ | Safari 14+ |
| Live TV Monitor | Chrome / Edge fullscreen, 1920×1080 | 1366×768 |
| Member Portal (web) | Latest mobile Safari / Chrome | iOS 14, Android 9 |
| Mobile App | iOS 15+, Android 10+ | iOS 13, Android 8 |
Here is the daily rhythm of a typical centre — every event in this list is a feature you will meet later in the guide.
Before you can do anything, the system must know who you are and which centre you speak for. Every screen, button, and report obeys your role.
Snooker King ships with six functional roles, mapped onto five physical user types in the database. manager and operator share the employee table and are differentiated by the employee's role field; the other four are stored in their own tables (admins, clients, organizations, members). Permissions follow the role at every request — pages, modals, and even individual rows.
| Role | Scope | Typical capability |
|---|---|---|
| ADMIN | Whole platform | All clients, organizations, centres, audit |
| CLIENT | One client | Own organizations, branding, billing, all reports |
| ORGANIZATION | One organization | Centres in scope, pricing, cross-centre reports |
| MANAGER | One centre | Employees, shifts, voids, comps, devices |
| OPERATOR | Live shift | Tables, F&B orders, take payments |
| MEMBER | Own data | Profile, bookings, battles, wallet, social |
The console exposes pages by role. Unlisted modules are hidden from the navigation entirely.
| Page | Adm | Cli | Org | Mgr | Op | Mem |
|---|---|---|---|---|---|---|
| Dashboard | ✓ | ✓ | — | ✓ | ✓ | — |
| Operations | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Revenues / Reports | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Members | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Memberships | ✓ | ✓ | ✓ | ✓ | — | — |
| Centres | ✓ | ✓ | ✓ | ✓ | — | — |
| Organizations | ✓ | ✓ | — | — | — | — |
| Employees · Campaigns | ✓ | ✓ | ✓ | ✓ | — | — |
| Clients · Admins | ✓ | — | — | — | — | — |
Multi-centre managers can change scope from the header context switcher. Switching is audited (a row in System Logs ties subsequent actions to the new scope). Manual sign-out clears local storage immediately and revokes the active session.
The Operations Console is the cockpit. Every other feature in the platform is launched from here, and every report ends here. We treat it as the single most-used surface in the entire product.
A persistent header bar, a sidebar of pages, and a centre stage that swaps between Operations, Revenues, Members, Centres, and other modules. Heavy actions open in modals so context never gets lost.
Three sidebar links are always visible to anyone signed in (Operations, Deposits, Revenues); the rest light up by role. Order is fixed so muscle memory carries between centres.
Members and POS aren't separate sidebar tabs — the M shortcut (or the table action panel) opens member search; POS opens from the table tab itself, since every order is bound to a table or member.
The Dashboard is a focused communication surface — its job is to surface the announcements that matter to the signed-in user, not to act as a number-board. Live operational numbers live on the Operations grid (the next page), and financial numbers live in Revenues.
If a manager wants live shift numbers, the Operations grid shows occupancy and pending-payment counts in real time, and the Revenues module shows tender breakdown and totals.
Each module has its own search field and filter chips. The Operations grid lets you filter by table type, status, zone, and assignment. The Members directory lets you search by name, phone, RFID card ID, or member number. Revenues lets you filter by date range, payment method, employee, and centre.
Most actions open in a modal so the underlying live grid keeps updating. The most common modals you will meet:
The header bell shows live alerts: low stock, new bug report, membership renewal due, large discount applied, end-of-shift cash variance. Click to acknowledge; alerts persist until handled.
Centres are the heartbeat of the system. Almost every other entity — tables, members, invoices, employees, F&B items — belongs to exactly one centre. Get the centre right and the rest follows.
Each centre carries its own opening hours, table inventory, F&B menu, price lists, and employee roster. The Centre wizard does the heavy lifting in a guided flow:
Every centre exposes a deep tree of settings. The full path from least-changed to most-changed:
Centre ├── Identity ........... name, address, contact, time zone ├── Branding ........... logo, colours, social links, monitor footer ├── Hours .............. weekly schedule, holidays, last call ├── Tariffs ............ table tiers, peak/off-peak, member discounts ├── F&B Menu ........... categories, items, modifiers, combos ├── Inventory .......... stock items, low-stock alert, movements log ├── Employees .......... roles, permissions, status ├── Devices ............ printers, monitors, RFID readers, lighting relays ├── Integrations ....... payment, e-invoice (LHDN), notifications └── Compliance ......... tax, e-invoicing, age policy
Tariffs are the most-edited setting after F&B. The model is intentionally simple — three layers, applied in order:
Rates can be overridden per table for special tables (championship, VIP). The effective rate column on the operations grid always shows the final number a customer will pay.
For a multi-centre rollout the most useful shortcut is the clone action: configure the flagship completely, then clone-create new centres so almost everything carries over. Address and table count typically need to differ; everything else can be inherited at clone time and adjusted in place per centre afterward.
Other multi-centre operations — bulk menu sync, organization-wide tariff broadcast — are handled centre-by-centre today; do them with care during quiet hours and verify with the proforma screen on a representative invoice before publishing.
Tables are why the customer is here. This chapter covers the live operations grid, the session lifecycle, and the lighting integration that ties software to the physical room.
Five canonical states live on center_tables.current_status: available, occupied, checkedout, inactive, and maintenance. The Operations grid renders more than five badges by layering two derived signals on top — pending (session finished, invoice not yet settled) and rental-active / rental-idle for tables in a rental window — plus a static fnb label for F&B-only tables. The stored column is the audit truth; the rendered badge is the operator's at-a-glance.
| State | Stored? | Meaning | Triggered by |
|---|---|---|---|
available | yes | Idle, ready for play | Default; restored after checkout or abandonment cleanup |
occupied | yes | Active session, meter running, light on | Operator starts a session, F&B order opens, or split-bill recovers |
pending | derived | Game ended, awaiting settlement (rendered, not stored) | Session session_status is checked_out or pending_payment while the invoice is unpaid |
checkedout | yes | Brief post-payment state, customer leaving | Invoice fully paid; cleared by next assignment |
inactive | yes | Hidden / decommissioned (table not in use this shift) | Manager hides the table |
maintenance | yes | Out of service for repair | Operator marks unavailable, reason logged |
fnb | derived (flag) | F&B-only table; static badge, no game timer | Table flagged is_fnb in centre setup |
rental-active / rental-idle | derived | Table is in a rental window (active rental session) or idle inside one | Rental booking opens / closes; remaining-time clock drives the badge |
occupied, and energises the table light via the lighting relay.pending_payment; the final amount appears; choose Pay now or hand to a cashier.checkedout briefly, then resets to available.Snooker King controls table lighting through Vyrox's MQTT relay gateway. The platform sends commands over an HTTPS bridge (vyrox.vip/mqtt_light/*) which fans them out to the ESP-based relays mounted above each table. The architecture is command-and-query, not push-heartbeat — operators always see real-time state because the console asks the gateway every few seconds, not because the relay phones home.
on or off. There is no stored "error" state; if the gateway is unreachable the operator sees a neutral placeholder badge while the platform retries.light_control_failure row is written to System Logs with the table, channel, and HTTP error so it can be triaged after shift.To take a table out of service, open the table action panel and switch its status to Maintenance (or Inactive if the table is being decommissioned for a longer period). The cell is hidden from the live operations grid and the public Live TV Monitor stops advertising it. Maintenance is a soft signal, not a hard lock — bookings already in the future on that table are not auto-cancelled, so a manager should review the day's bookings after marking a table down for repair.
A session can have one host member plus additional players. Each player is recorded against the session for leaderboard, history, and split-bill purposes. Players can be added or removed mid-session; the duration is shared but the bill can be split per person at checkout (Chapter XVI).
Some venues sell time as packages — for example "60 minutes for a flat fee". The countdown module attaches a remaining-time clock to a session; when it reaches zero, an upsell prompt appears at the operator station. Packages are configured per centre with their own pricing.
Every centre has at least one wall-mounted display showing all tables, their state, prices, and rotating promotions. The Monitor is browser-based, fullscreen, and updates without refresh.
The Live TV Monitor Editor in the Settings modal exposes a structured config covering theme preset, layout, header, summary stats, footer, and rotation. A Reset to default button restores the canonical seed for that centre.
The monitor ships with four ready-made looks. Pick one per centre; pixel-level overrides are available where needed.
| Preset | Best for |
|---|---|
| Broadcast Pro | Sports-bar feel; bold cards, large numerals, deep accents. |
| Apex Lounge | Premium club; muted palette, tall header, refined typography. |
| Glass Modern | Light, airy, glass-morphism; suits boutique venues. |
| Departure Board | Airport-board energy; high-contrast monospaced rows for very busy halls. |
Density is selectable from compact, comfortable, or spacious.
When you create a new centre, the wizard seeds the Monitor config in one shot from a canonical template — never start from a blank screen. Reset to default restores the same template at any time. The footer brand strip is loaded from a global setting, so re-branding the company updates every venue's monitor at once.
| Hall size | Display | Player |
|---|---|---|
| ≤ 12 tables | Single 55″ 1080p | Mini-PC or Chromecast in kiosk mode |
| 12–30 tables | 65–75″ 4K | Mini-PC, Ethernet, fullscreen browser |
| 30–60 tables | 2× 65″ tiled | Two browser instances on the same config |
Big matches deserve an audience. Live Cast turns any battle on any table into a public, token-gated URL that anyone with the link can watch — frame scores, fouls, breaks, and a customisable overlay, all in real time.
/live/?token=X, /live.php?token=X, and /debug/cast.php?token=X all resolve to the same Cast page; the token alone is the credential.battles.broadcast_token with the additional gate broadcast_enabled = 1. A revoked or never-enabled match returns a friendly empty-state page rather than data.Three presentation styles ship with the platform:
| Layout | Vibe |
|---|---|
| Crucible Classic | Tournament TV — dark chrome, big break number, scoreboard chyron. |
| Premier League Theatre | High-energy event — accent stripes, animated transitions, sponsor strip. |
| Pressroom Minimal | Clean reporter-style — no decoration, only the facts; suits embedded streams. |
A member is more than a phone number. The member record is the spine of loyalty, billing, social play, and analytics. Every interaction in the room echoes back to it.
Tiers are not a hard-coded ladder — every organization defines its own set in the Memberships module, and tier names ("Silver", "Gold", "Champion", "Premier", whatever fits the brand) are free-text. Each memberships row carries a consistent shape that pricing and rewards bind against:
| Field | Purpose |
|---|---|
name | Display label shown to staff and members. |
fee + validity_days | One-off price + how long membership lasts before renewal. validity_days = 0 is a common setup for a perpetual / free tier. |
base_credits / bonus_credits / reward_points / tokens | Four issuable wallet currencies. Each has its own *_expiry_days column so a top-up bonus can expire faster than the deposit it came with. |
service_charge_rate / sst_rate | Optional tier-level overrides on the centre tax stack — usually used to comp service charge for top tiers. |
is_default | Marks the tier auto-applied to a new member when no explicit tier is chosen. |
status | active / inactive — retiring a tier never deletes existing members on it; they grandfather until renewal. |
Discounts are configured on a separate per-tier table (the membership-discount module): a tier can carry a list of discount rules — by table type, by F&B category, by day-band — that apply automatically when the member is identified at session start or invoice creation.
Every member has a unique QR code in the portal and (optionally) a physical RFID card linked to the same account. Scanning either at the kiosk or on the operator app:
Where the venue has the appropriate consent and hardware, members can be identified at the door by face. The recognition pipeline is browser-side via WebRTC: a video element + canvas overlay, three sequential gates — detect (a face is present and large enough), liveness (anti-spoof signal: blink / micro-movement), and match (cosine similarity against the centre's enrolled face embeddings). Voice prompts are localised (English / 中文 / Bahasa Malaysia); on a confirmed match the operator station auto-fills the member into the next-table flow.
Sensitivity is tunable per centre on the Face Settings panel: face_match_threshold (0.05–0.75), face_duplicate_threshold, face_detection_confidence, face_min_face_size_pct, face_liveness_check, face_antispoof_check, and face_scan_timeout_sec. Embeddings live in the member_faces table and are scoped per organization. Recognition is opt-in and fully disable-able — flip face_mode to off and the door modal disappears entirely.
The Member Portal is a full mobile-first experience. It is the surface most customers touch most often, and it is engineered for one-handed, mid-frame use.
The portal is intentionally swipe-driven. Across the bottom navigation, modal sheets, and inner pages, members reach nine main surfaces:
A guest is elevated to a free member in three taps. The flow is intentionally idempotent — running it twice is safe.
The wallet is the spine of the loyalty programme. Balances tracked on every account include:
Showcards are short animated full-screen recognitions — a member's first century break, a tier upgrade, a tournament win. They appear on the portal and on the Live TV Monitor for everyone in the room to see. Operators can trigger them manually from the member's profile.
The portal has a sound system that plays subtle confirmation, success, and error tones, plus open/close cues for full-screen sheets. Members can mute the entire sound bus from settings.
A battle is a managed match between two members on a real table — best-of-frames, automatic scoring assist, and a rating that follows the player across centres. Tournaments are scheduled multi-battle ladders that the platform runs end-to-end.
Snooker King's battle engine ships with a broad catalogue of cue-sport rule files, grouped into seven format families. Each family carries its own scoring rules and frame structure; many include short-form variants. The full per-format catalogue lives in Appendix B.
Every battle moves through one of six recorded states on the battles.status column:
TIMESTAMPDIFF on UTC timestamps to avoid PHP/MySQL TZ skew. Reports always show the correct elapsed time.
Battles update each player's Glicko-2 rating — a successor to Elo that also tracks rating volatility and uncertainty. New players start with a high uncertainty that narrows as they play more matches; the result is fairer match-making and a more stable leaderboard.
Tournaments move through four states:
Operators record frames using a simple panel: enter break / clearance / fouls per player. The system handles tie-break rules and updates the leaderboard in real time. Frame results stream to any active Cast URL (Chapter VII) instantly.
Every centre has a leaderboard panel that filters by game format and by time window. The score blends Glicko-2 rating with frames won and opponent strength.
Each leaderboard row exposes a Coach Notes field, visible only to the member and their authorised coach. Use it to track drills, weakness areas, and plan for the next tournament.
Where house rules permit, battles can carry a stake. The organization-level setting controls whether stakes are off, credit-only, or cash. In jurisdictions where wagering is restricted, leave it on off or credit-only; the system will refuse cash stakes and show a notice.
When the operator pushes a battle to the public Cast URL or to the Live TV Monitor, the same three layouts apply (Chapter VII). Choose Crucible Classic for finals, Premier League Theatre for sponsor-heavy events, and Pressroom Minimal for embedded streams or coaching review.
Reservations live alongside the live grid. Customers book through the member portal or by calling staff who use the Bookings panel in the operations console.
status='confirmed'. The platform does not currently capture a monetary deposit at booking time — front-desk staff handle deposits as cash receipts against the member wallet if required.checked_in and is linked to the live game session.completed by the same handler that closes the table.A booking can be cancelled by the member from the portal or by an operator from the operations grid. Bookings that are still confirmed after their end-time pass are auto-flipped to no_show by a maintenance pass — the platform doesn't wait for someone to remember to do it manually. Cancellation policies — free-cancellation windows, deposit-forfeit rules, repeat-no-show consequences — are venue policy decisions today; enforce them as house rules at the front desk and capture the reason in the booking note. The platform tallies a member's lifetime no-show count from the booking history, which managers use to gate large-party reservations from chronic no-shows.
When a table is booked but the booked party is late, the operator decides whether to hold the table or assign it to a walk-in. The platform's job is to record the decision and the resulting session against the right booking and member, so the audit trail is intact regardless of which way the call went.
F&B is often the difference between a profitable centre and a struggling one. Snooker King treats the kitchen as a first-class citizen — orders bind to tables, kitchen tickets print automatically, stock decreases live, and the menu is in your hands at all times.
selected_options, selected_preferences, and selected_addons so reports stay aggregable.Modifiers are stored as structured data on each order line, so the kitchen ticket reads cleanly and reports stay aggregable. Examples:
A combo is a curated bundle that appears as a single item on the menu but expands into its component lines on the kitchen ticket. The ticket shows the components so the cook line is never guessing; the receipt shows only the bundle line so the customer sees the value.
Every item carries a photo, short description, and an optional long description. These power the member-portal Marketplace and the operator POS picker. A clean photograph routinely lifts attach-rate by double-digit percentages.
Each menu item carries an active / inactive flag in the menu editor. Toggling an item to inactive removes it from the operator POS and from any customer-facing menu surface (such as the QR-token public order page). When inventory is configured (Chapter XIV), low-stock items are surfaced for review so a manager can deactivate them before the cook line runs out — that step is operator-driven, not automatic.
Two patterns work for most chains:
When the customer is hungry, every second matters. The POS is built so a moderately busy bar can take, modify, and send an order in well under a minute.
draft to submitted; kitchen items print on the kitchen printer; bar items print at the bar.Every submitted order produces a KOT formatted for thermal printers. Each ticket shows table, member name (if any), items, modifiers, quantity, special notes, and a sequence number. Re-prints are one tap away from the operator station.
State is split across three rows for clean accounting — the order itself, each line item on the order, and the F&B session that groups orders for one table:
| Row | Column | Values | Meaning |
|---|---|---|---|
fnb_orders | status | draft | Operator is still building; nothing printed. |
fnb_orders | status | submitted | Sent to kitchen; KOT printed; utc_submitted_at stamped. |
fnb_order_items | payment_status | unpaid | Default after submission; reduces table grand total. |
fnb_order_items | payment_status | paid | Settled by an invoice; invoice_id stamped. |
fnb_order_items | payment_status | cancelled | Voided line; excluded from totals and unpaid counts. |
fnb_sessions | status | active / completed | End-of-meal — set when the table is checked out. |
There is no platform-wide "completed" state on fnb_orders itself; once submitted, the order is read-only and "delivery" is the cook line's job — the platform does not track plate-on-table state.
Snooker King does not run a screen-based KDS where cooks tap "in progress" and "ready". The kitchen workflow is print-first — every submission emits a fresh KOT to the kitchen printer (or bar printer for bar-routed categories) and the cook line works the printed tickets. Re-prints are one tap from the operator station; modifications after submit print a delta KOT so the cook can spot what changed without re-reading the whole ticket.
Mistakes happen. Snooker King draws a strict distinction between three actions:
| Action | Use when | Audit |
|---|---|---|
| Void | Item never delivered (cancelled order) | Reason required; manager can review |
| Comp | Item delivered but given free (service recovery) | Reason required; counts as marketing cost |
| Refund | Money returned to customer post-payment | Manager approval required |
An order without a table binds to a member instead. The KOT prints with the member's name and phone for pick-up; the customer can also track readiness in the portal Marketplace tab.
The POS can take payment immediately or push the line into the table's running tab. If immediate, the same multi-tender splitter used for table sessions applies (Chapter XV).
Inventory in Snooker King is deliberately simple — a flat stock-level tracker bolted onto the existing item catalog, not a full recipe-driven cost-of-goods engine. Most centres need a clean low-stock signal, not theoretical-vs-actual variance accounting; the platform delivers the former by default.
min_stock_alert) — when current stock falls to or below this number, the item shows as low-stock in the inventory list and on the operator console.The Inventory tab uses a three-panel layout: Categories on the left, Items in the middle, Movements / Combo Detail on the right. Items can be drag-reordered and grouped by category. The view toggles between list and grid; sort presets include "lowest stock first" so the morning shift can scan the top of the list and place orders.
Every economic event in the centre — table time, F&B order, membership upgrade, comp, refund — flows into invoices. The invoice engine is the audit backbone of the platform.
An invoice progresses through four payment_status values on its revenues row: Pending (lines can still be added or edited), Outstanding (issued but not fully paid; outstanding_amount > 0; settle only with a monetary tender, Appendix C), Paid (locked from edits without explicit re-open by an authorised employee), and Cancelled / Voided (retired before settlement with a reason in System Logs). The platform does not auto-flag "overdue" — the outstanding state is the only unpaid signal, read live. Historical invoices stay in revenues with their immutable number; nothing is archived elsewhere.
Settlement splits into monetary tenders (real money in/out) and internal-ledger tenders (member balances applied at creation). The two pools are kept separate so an outstanding-balance receipt can never be paid down with non-monetary credit:
| Method | Pool | Settlement | Notes |
|---|---|---|---|
| Cash | Monetary | Immediate | Counted in shift close, change calculated |
| Card | Monetary | T+1 to T+3 | Provider-dependent; reference captured |
| E-wallet | Monetary | Real-time | QR or NFC; transaction reference captured |
| Bank transfer | Monetary | Manual confirmation | UTR / statement reference captured |
| Member wallet (deposit) | Internal | Immediate | Applied at invoice creation; never on outstanding-balance settlement |
| Member credits / comp credit | Internal | Immediate | Applied at invoice creation only |
| Loyalty points | Internal | Immediate | Burn-rate per centre; creation-time only |
| Stamps / token redemption | Internal | Immediate | Discount line at creation; not a settlement tender |
Multi-tender across the four monetary pools is supported on a single invoice (part cash, part card, balance e-wallet, all in one settle). Internal-ledger tenders (wallet, points, stamps) apply while the invoice is still being built. Appendix C lists the formal allowed-tender set for outstanding-balance receipts.
Discounts apply at three levels, in order:
The printed and PDF invoice is structured for both customers and auditors:
Refunds are gated by the operator's role and refund permission flag. The flow:
The payment layer is idempotent: if the network drops mid-payment and the operator retries, the system de-duplicates the second request and never double-charges. The operator can retry safely without calling support.
pending_payment blocks the table cell from returning to available. Settling every child invoice in a split-bill is what releases the table — the cell flips automatically once the last child is paid.
Splits are common in social play. Snooker King supports split modes that match how groups actually settle their bills, and keeps the parent invoice and revenue counters perfectly in sync.
A refund applies to a single child invoice. The parent's totals recompute to keep the audit chain coherent.
Member-initiated splits in the portal create QR codes that other members scan to claim their share. The operator station shows progress in real time.
For Malaysian operators, Snooker King produces compliant e-invoices and submits them to LHDN's MyInvois system. For other jurisdictions, the same engine applies a two-stage tax stack — service charge first, then tax — and stamps both rates on every line.
revenues.status column carries this value while the receipt is still editable.Valid, not "Validated" — the platform stores it verbatim so a status check against LHDN never drifts from the source of truth.)The check-status handler treats ['Valid', 'Invalid', 'Cancelled'] as terminal — once LHDN reports one of those, the platform stops polling MyInvois for that document.
Each submission persists the document UUID, submission timestamp, original MyInvois status string, item codes/quantities/unit prices/taxes per line, buyer details (name, NRIC validated to 12 continuous digits, address) where required, and any error messages from LHDN's response. For most operators e-invoicing is invisible — submission runs in the background and only two screens matter: the compliance dashboard (daily count of submitted, valid, failed) and the failed queue (Invalid invoices with the underlying error and a one-tap resubmit).
Deposits and member credit are the platform's prepay layer. Members fund their wallet ahead of time and spend it across tables, F&B, and tournament entries.
Each member's wallet, scoped per-organization on the member_organization_accounts row, holds four numbers:
base_credit_balance) — the member's own money, loaded by top-up. This is what a refund returns.bonus_credit_balance) — credit issued by the venue (top-up promotion, comp, recovery). Usually non-refundable; spend-rule configuration decides which discounts it can stack with.Base credit is refundable at any time, subject to the centre's policy and the operator's refund permission. Bonus credit is non-refundable by default. Every cash-out creates a permanent ledger entry; the original deposit row is preserved so the audit chain stays intact.
The stamp card is a dual-currency loyalty system: stamps for visits and milestones, points for spend. Members earn passively as they play; redemption is a single tap.
Stamps are a per-centre digital loyalty currency written to a single ledger (stamp_card_transactions); each row is auditable with source plus originating revenue_id / session_id / table_id / invoice_id. They are awarded when a qualifying event fires (session close, invoice settled, manual add). A per-period cap (centre columns stamp_period_mode, stamp_max_per_period, stamp_period_hours) limits earn within a rolling window — mode is day (centre TZ), hours (rolling N), or unset. Spend is on any catalog item with items.stamps_cost set; points work identically via items.points_cost. Practical difference: stamps are small integers (one per visit), points scale to spend amount so they fit higher-value redemptions; reports show stamp-cost and point-cost separately.
Redemption is staff-driven and identity-bound — there is no portal-generated redemption code that an operator types in:
stamp_resolve_member_card to bind the order to that member.stamp_list_redeemable_items) filtered by mode = stamps / points / all.revenues.stamp_redemption_count / points_redemption_count on the receipt.stamp_* mutating action, and the API short-circuits with a 403 if the redeeming member is not in scope of the operator's centre.Campaigns are the lever a manager pulls when a Tuesday afternoon needs a lift. A campaign is a discount rule with a window, a scope, and a redemption record — and that is the unit of work the platform manages.
Every campaign is a discount applied to qualifying invoices. The shape is consistent regardless of how the customer encounters it:
Common patterns built from this shape: a publishable coupon code (one or many uses), an automatic time-banded discount (no code needed), a member-tier discount, a single-use voucher issued by management to a specific member or list.
Two campaigns plus a member-tier discount can compound in unintended ways. Always walk the resulting price through the proforma screen on a sample bill before publishing — every step of the pricing pipeline (Appendix D) is visible there, so the diverging point is obvious if the final number isn't what you expect.
Three smaller modules round out a typical centre's revenue streams: rental of equipment, ticketing for events, and countdown packages that bundle time into flat-fee promotions.
Cues, gloves, chalks, and accessories can be rented per session or per visit. The Rental module is built around four handler families:
Every rental_* POST is CSRF-validated and session-write-closed before processing, so rapid-fire scans at a busy counter never fight for the session lock.
"Ticketing" in this platform is not event ticketing — it's a centre-level toggle (centers.ticketing_mode) that switches the operator station into a kiosk flow optimised for fast table starts. With ticketing mode on, a member scans their QR or RFID card, the system resolves the card via resolve_table_by_qr, and a table is activated for them in one tap — bypassing the full operations grid for venues whose front-of-house works more like a turnstile than a host stand.
A countdown package is a flat fee for a fixed block of time. Each package row in countdown_packages carries name, duration_hours + duration_minutes, price, its own sst_rate and service_charge_rate, a status, and a table_selection_mode that decides whether the package is restricted to specific tables or available across the centre. When the package is sold and started, a session-level ledger row in countdown_session_packages tracks the remaining time and links to the parent game_sessions.id; once the time runs out the operator is prompted to sell another package, never auto-charged.
QR codes are universal but a physical card is faster. RFID Card Scan Mode lets a member tap a card on a reader at the door or counter and be identified instantly.
Cards are linked to members from the member profile screen. A single member can carry multiple cards (e.g. a primary and a backup). Cards can be deactivated instantly if lost.
Reports are not a byproduct of a Snooker King install — they are one of the reasons to install it. Three families of reports cover almost every question a manager will ask.
| Report | Question it answers |
|---|---|
| Daily Sales Summary | What did we sell today, by category and tender? |
| Shift Reconciliation (80mm) | Did the cash drawer match the system at end of shift? |
| Tax Report | How much SST/VAT do I owe, and which invoices contributed? |
| Comp & Void Report | Who is comping/voiding, how often, and why? |
| Refund Register | All refunds in a window, with reason and approver. |
| Outstanding Balances | Open invoices, by age and centre. |
duration_seconds on closed sessions; filterable by table type and date range.fnb_order_items with modifier breakdown.The platform does not currently ship a built-in occupancy heat-map, a maintenance-hours roll-up, or a recipe-driven stock-variance report. If those numbers matter to your business, derive them from the underlying data via export.
stamp_card_transactions, member-credit ledger, and revenues.Acquisition-funnel and retention-cohort dashboards (e.g. "% of new members still active at 30 / 60 / 90 days") are a frequent ask but are not built into the platform today. The data needed to compute them is present in members and revenues — it just hasn't been packaged as a one-click report.
The Revenue Report module produces a compact 80mm thermal-printer summary perfect for the safe envelope at end of shift: tender breakdown, voids, comps, refunds, and the cash variance against the float. Reprints are one tap.
Reports can be exported as CSV directly from the operator console, or printed as 80 mm thermal slips through the Revenue Report module. Excel and PDF export, and recurring scheduled-email reports, are not currently part of the platform — pipe the CSV exports into your accounting system or BI tool when those formats are required.
The platform's reports are designed to be read every day, not subscribed to passively. The most useful rituals:
min_stock_alert jump to the top of the inventory tab — a 30-second scan after lunch service avoids weekend-evening run-outs.If reports tell you what happened, system logs tell you who, when, from where, and why. The audit trail is the platform's memory, designed for forensic clarity rather than developer convenience.
Every consequential action — and many merely interesting ones — produces a log row. The viewer is role-scoped, so each user sees only what they are entitled to see.
The log_type column uses an open vocabulary, so new event categories can be added without schema migration. Common values seen in the running system today:
| Category | Examples (real log_type values) |
|---|---|
| Authentication | login_success, login_failure, logout |
| Sessions | heartbeat_session (operator session-presence ping) |
| Payments | payment_success, payment_failure |
| Invoices | invoice_deleted, transaction_slip_deleted |
| Devices | light_control_failure (MQTT command timeout / failure) |
| System / Domain | Custom strings emitted by individual modules — schema migrations, settings changes, F&B voids, member-tier transitions all add their own log_type as needed. |
log_type — open vocabulary; categorises the event.user_id, user_type, user_name — actor identity.center_id — scope of the event.action, entity_type, entity_id — what was done to what.outcome — success / failure / partial.details — JSON payload (sensitive keys stripped before persistence).ip_address, created_at — request context.The Log Viewer supports filters on every column. Common starting points:
payment_failure in the past week.Before any payload is persisted, the writer strips a deny-list of sensitive keys (passwords, card numbers, certain identifiers). This means you can log freely in development and the production audit will never leak credentials.
When something looks wrong (missing cash, complaint, unauthorised refund), the standard playbook is:
Real software lives or dies by how quickly its operators can flag problems and how transparently those problems are resolved. The Bug Reports module is built around that loop.
The status ENUM has exactly four values:
| Status | Meaning |
|---|---|
| New | Awaiting triage; default filter for the support team. Filing a report immediately emails the admin. |
| In Progress | Engineer is actively investigating or fixing. (Renamed from the historical Fixing in 2026.) |
| Pending Human Decision | Cannot be auto-resolved; needs a person (auth call, taste call, vague repro). Triggers an admin notification. |
| Resolved | Fix deployed and verified; closed. Triggers a dual notification — admin and reporter, deduped if the reporter is the admin. |
There is no "Won't Fix" status in the platform — out-of-scope tickets are kept on Pending Human Decision with the rationale in a thread comment, so the audit trail and the reasoning live together.
A snooker centre is also a small fleet of devices — printers, displays, RFID readers, and lighting relays. Snooker King treats each as a first-class entity with a friendly name, a centre binding, and an explicit health story so problems are noticed in minutes rather than at end-of-shift.
Printers are configured per centre with two technical types: escpos (thermal, 58 / 80 mm, USB / Bluetooth / network) and windows (A4 driver-printer for invoices). Each carries a friendly name, a connectivity mode, an optional centre-default flag, and a status (active / inactive). Routing to specific printers is decided by menu category (Chapter XII), not by individual item.
Every job — a kitchen ticket, a thermal receipt, a cash-up slip — passes through a self-healing per-centre queue rather than printing inline. A small browser-side poller running on each operator station fetches pending jobs scoped to its centre, marks them printing while the driver is engaged, and writes back printed or failed with the timestamp. Jobs hold their full payload (HTML for ESC/POS rendering, raw bytes for Windows drivers) so a stuck printer can be requeued from another station without losing the original document.
queued ──poller picks it up──▶ printing printing ──driver returns OK──▶ printed printing ──driver fails / times out──▶ failed failed ──operator requeues──▶ queued (new attempt row)
Failed jobs are not auto-retried; an operator must resubmit from the print queue panel. This is a deliberate guard against double-printing. The queue stores its rows in the print_queue table; the table is auto-created by the API on first use, so a fresh deployment never blocks on a missing schema.
Wall displays open the public Monitor URL with a centre-specific token; the URL itself is the credential, so revoke and reissue if a screen leaves the venue. The Monitor refreshes its data on a polling interval rather than a push channel, which keeps it dependable on hotel-style Wi-Fi.
Most RFID readers behave as USB keyboard wedges — they "type" the scanned card number into a focused input field. The platform binds a quick-scan input on the operator console; whatever the reader emits gets resolved through stamp_resolve_member_card against the member_cards table (Chapter XXII).
Lighting relays drive the table lamps via Vyrox's MQTT gateway (Chapter V). The platform issues turn on / turn off commands on session start and end, and queries channel state on demand for the operator's relay badge — there is no relay-side heartbeat. Command failures (timeout, unreachable gateway) raise a light_control_failure log row with the table and channel for after-shift triage; a test-mode toggle lets a manager pulse a relay during commissioning without disturbing live tables.
Theming is a deeper feature than most platforms expose. Snooker King ships with multiple curated themes for the operations console and the Live TV Monitor; each can be tuned to match a venue's house style.
The console ships with a family of themes inspired by cue-sport rooms. Examples:
Additional presets ship from time to time. Each user picks their own; managers can also pin a per-shift theme to shared terminals.
The Live TV Monitor has its own theme family separate from the console: Broadcast Pro, Apex Lounge, Glass Modern, and Departure Board (Chapter VI). They are intentionally bolder and louder than console themes, because they read across a room.
Branding is configured at the organization level and pushed down to centres:
Two lightweight communication tools sit alongside the operations console: a centre chat for in-venue conversations, and an announcements board for broadcasts.
Centre chat is a real-time channel scoped to a single centre. Use it to coordinate operators, confirm a kitchen call, or pass a member request to a manager without leaving the console. Messages are stored in the platform database and are subject to the same role-based visibility as everything else.
Announcements are broadcast messages — created at organization or client level — that appear in the operator console and (where configured) on the member portal Home tab. Use them for shift handover notes, scheduled maintenance, or campaign launches.
The member-side equivalent is the Social tab in the portal. Members chat with friends inside social_chat_rooms (rooms can be 1:1, group, or attached to a social game), post photos, react to showcards, and see localised leaderboards. Operator-role accounts do not have read access to those threads — the message bodies are stored to power the conversation but the access-control layer keeps them out of staff dashboards. Honour that boundary in your house policy too: chat content belongs to the members, not to the venue.
The mobile app is the member portal hardened with native capabilities. It is the surface most customers touch most often, engineered for one-handed, mid-frame use.
| Capability | Why native |
|---|---|
| FCM push notifications | Reliable booking reminders, battle invites, payment alerts |
| Local notifications | In-app reminder rendering when the app is foregrounded |
| Location & geocoding | Suggests the nearest centre; never sent to third parties |
| Secure storage | Bearer token kept in the OS keychain |
| Receipt cache | Past invoices remain readable without a connection |
| WebSocket chat | Long-lived connection with auto-restart for member chat |
The native app talks to the platform through a dedicated, versioned mobile API (MOBILE_API_VERSION = "1.0.0"). Authentication is via Bearer token in the Authorization header; tokens are valid for 30 days from issue, after which the app re-authenticates silently. The mobile API endpoint is purpose-built for the Snooker King Flutter app — it is not a general-purpose public partner API today, so third-party integrations should be brokered through the platform team rather than coded against this surface directly. Login attempts are rate-limited and return HTTP 429 once a per-IP cap is hit.
Settings are organised by scope, not by feature. The closer the scope to the user, the more often the setting changes.
| Scope | Examples | Edited by |
|---|---|---|
| Platform | Schema, deploy keys, SMTP | System administrator |
| Client | Logo, billing, branding | Client owner |
| Organization | Member tiers, points ratio, tax | Organization director |
| Centre | Tariffs, hours, devices, F&B | Manager |
| User | Theme, notifications | Each user |
center_id (comma-separated for multi-centre staff).can_edit_invoice_details, can_edit_invoice_payment_method, can_delete_invoice, can_create_member, can_edit_paid_invoice); revenue_date_limit caps how far back the employee can edit revenue rows.The platform takes nightly database backups, retained for a configurable window. Manual point-in-time restores are available on request from support. Granular restore (e.g. one invoice or one member) is not exposed in the UI, by design — that surface area would be a fraud vector.
Most issues resolve in under five minutes. The decision tree below covers most cases reported by new operators.
Real-time updates are pushed live. If the connection drops, the grid stops updating. Hard-refresh restores it; if it recurs, your network may be unstable.
Settle is disabled if the table has open kitchen tickets. Look for the small bell or chip on the cell — it indicates pending kitchen items. Mark those completed first.
Check, in order: was the member scanned? Is their tier active? Is the discount in the active time band? Is there an organization-level override that excludes that item?
The monitor browser sometimes loses focus during OS updates. Press F5 on the wall keyboard, or use Devices → Monitors → Refresh from the console to push a remote refresh. If the issue recurs, disable display sleep on the monitor PC.
Check the relay heartbeat in Devices → Lighting. A device showing error is offline; the table can still be played with a manual lamp switch while the relay is investigated.
The platform is cloud-native. Short outages are tolerated by the operator app (orders queue locally), but settle and payment require connectivity. We recommend a backup mobile router for centres in unstable-internet areas.
The Live TV Monitor renders cleanly up to about 60 tables on a 1080p display. The console grid scrolls indefinitely; some customers run well over a hundred tables on one centre.
Yes. The platform accepts CSV imports for members, F&B menus, and historical sales. Custom migrations from common POS products are available via the support team.
Yes — TLS in transit, AES-256 at rest. Sensitive identifiers (member phone, email) are encrypted at the column level with path-derived keys; QR cards remain valid across migrations between domains.
Yes — CSV, Excel, JSON, or PDF. Full database exports are available on request. We believe in your right to leave with your data.
Kitchen printers connect over the local network. If the cloud is down but the LAN is up, kitchen tickets continue to print using a local relay so the cooks are never stranded.
Snooker King's authentication layer is engineered for shared terminals, busy shifts, and the inevitable bad actor.
Each role logs in through its own form, and the system tracks user type in the session for the visit's lifetime: Admin login (platform administrators), Client login (tenant owners), Organization login (regional directors), Employee login (managers and operators), and Member login (customers, web portal).
The platform enforces rate limits on two independent axes — by source IP and by the targeted email — so neither a single noisy IP nor a botnet rotating IPs against one account can grind through:
| Action | Per IP | Per email / account |
|---|---|---|
| Sign-in attempt | 5 per 5 min | Per-account lockout counter (auto-extends lockout window) |
| OTP request | 5 per 5 min | 3 per 10 min |
| OTP verification | 10 per 5 min | 5 wrong tries per 10 min — invalidates all outstanding OTPs for that email |
| Password-reset email | 3 per 15 min | — |
| Member registration | 3 per 10 min | 1 per 24 h per email |
When a cap is hit the API responds with a wait-minutes message without leaking which axis tripped; successful sign-in resets the IP counter immediately. The form refuses double-clicks while a request is in flight (single-flight submit), every successful sign-in calls session_regenerate_id(true) to discard any planted session ID (session-fixation defence), and when an email exists in a different login surface the response steers the user to the right page without confirming the password (helpful but not leaky).
Every form carries a CSRF token tied to the session. If the page sat open for hours and the token went stale, the platform refetches the login HTML, parses a fresh token from the meta tag, and retries the submission once before surfacing an error. Most operators never notice.
SHA-256 hash, never the raw token, so a database read alone can't replay the link.password_hash() using PASSWORD_DEFAULT.Most roles log in by email; employees can additionally log in by username. The lookup is case-insensitive (LOWER(username) = LOWER(?)), the row must be active and not soft-deleted. Choose usernames you would be happy to see on an audit row — they appear unmodified in System Logs against every action.
An employee belongs to a centre via center_id; managers reaching multiple centres carry a separate allowed-centre list checked on each request. Each role keeps its own session variables (no identity bleed across roles). The session is closed and rewritten before rapid sub-calls (consecutive RFID scans) so file-lock contention can't slow operator taps. For shared operator stations, expect sign-out at the end of every shift — treat the operator account like a key, not a public terminal.
Every attempt — success or failure — writes to System Logs (Chapter XXIV) with actor, timestamp, IP, user-agent, and outcome. A spike in login_failure entries from one IP is the first signal worth looking at.
Customers don't need to log in to order. A QR sticker on the table opens a public, token-gated ordering page that runs in any phone's browser.
validateQRToken() runs on the server side for each request the page makes; a stale or revoked link can't be reused after revocation.Referrer-Policy: no-referrer as both an HTTP header and a meta tag, so the token never leaks to external CDNs through outbound image / link requests.The page deliberately does not implement a CSRF token (it has no logged-in session to bind one to) and does not rate-limit image fetches — both safeguards rely on the upstream token check and the no-referrer policy.
Mid-flow, the customer can link the order to a member account by entering their phone number; an OTP is sent and verified, after which any tier discount or wallet credit applies. Linking is optional — a guest can still complete the order without it.
Beyond the centre's own F&B menu, the Member Portal hosts a peer-to-peer marketplace where members buy and sell cue-sport gear directly from each other.
| State | Meaning |
|---|---|
| Active | Visible in Browse, accepting enquiries. |
| Sold | Marked sold by the seller; archived but visible in their history. |
| Expired | Past its visibility window; the seller can renew. |
| Removed | Taken down by the seller, or by moderation. |
Tapping a listing opens its detail page:
Buyer-seller conversations live in the same chat infrastructure as the rest of the portal (Chapter XXVIII), so push notifications and unread badges work consistently. Operators do not see the contents of marketplace chats.
Every fresh deployment runs through a guided wizard. By the time you exit it, you have a working organization, a centre, and a populated table grid — all without touching a settings page.
The progress strip at the top of the wizard reads Organization → Center → Table → Rate. Each step has its own form and persists its result before advancing.
If an existing organization, centre, or table set is detected, the corresponding step is skipped automatically. This means the wizard can re-run without any risk of duplicate data — useful if a setup is interrupted.
Each step records its created entity IDs, allowing forward and backward navigation without losing partial progress. If you exit and return tomorrow, the wizard resumes where you left it.
You land on the Operations page with an empty grid of correctly-configured tables. From there:
A busy centre is full of audible cues. Snooker King's notification and sound system is opinionated about which alerts demand attention and which fade into the background.
Each operator can independently mute two streams:
Mute state persists in the browser. If an operator working a quiet shift mutes everything, their preferences come back the next day on the same device.
Console sounds are synthesised in the browser — no audio file downloads, no licensing complexity:
The member portal exposes a global memberSound object whose methods correspond to specific UX moments. Each method emits a synthesised tone matched to the moment's emotional weight:
| Method | When it plays |
|---|---|
memberSound.tabSwitch() | Bottom-nav tab change |
memberSound.pageOpen() | Opening a full-screen sub-page |
memberSound.pageClose() | Closing or backing out of a full-screen sub-page |
memberSound.tap() | Generic button / card tap (capture-phase, throttled to 60 ms) |
memberSound.success() | Generic success — fires automatically via the notification helper |
memberSound.error() | Generic error — fires automatically via the notification helper |
memberSound.qrSuccess() | QR code scanned successfully |
memberSound.packageSelect() | Selecting a package or option from a list |
memberSound.paymentSuccess() | Payment / purchase completed (full fanfare) |
memberSound.gameStarted() | Game / session started |
memberSound.battlePaired() | Battle opponent paired (match-found fanfare) |
memberSound.messageSent() | Chat message sent |
memberSound.messageReceived() | Chat message received from another member |
Members can mute the entire sound bus from their settings drawer. The preference is stored locally so the choice survives reloads.
Battles run through a dedicated audio system organised around Match Vibe presets. Each preset bundles sound effects, voice (text-to-speech commentary), and visual animation into a named look, so the operator picks one option rather than balancing three sliders. Presets shipped with the platform include broadcast-style, minimal, and silent options.
Even with rigorous live calculations, rounding and concurrency can produce tiny ledger drifts over thousands of invoices. Snooker King ships four admin-only reconciliation utilities to put the books straight without touching customer-facing data.
| Tool | Fixes | Scope |
|---|---|---|
| Discount invariant | Recomputes the displayed sum of discount lines so it equals discount + coupon − invoice-additional discount. | One invoice, display column only. |
| Revenue items — IDCC scope | One-shot historical pass that enforces the line-total invariant on a single named scope (the IDCC tenant), used as the proven blueprint before extending to other scopes. | Single tenant, historical sweep. |
| Revenue items — cascade | Multi-row distribution of accumulated rounding deltas, largest-row-first, bounded by a per-row tolerance. | One invoice, gap below threshold. |
| Revenue items — org-wide | Identifies all invoices in the organization with a violation; produces a baseline report before any apply. | Whole organization. |
Three legal pages ship with the platform — Terms of Service, Privacy Policy, and Cookie Disclaimer. They are versioned, consented to, and easy to amend without a release.
Every document carries a version. When a new version is published, members and operators are presented with the change on next sign-in and asked to consent. The consent (with timestamp and version) is recorded against the user. Old versions remain readable in the legal archive for as long as any consent of that version exists.
Document content lives in configuration files separate from the application code. Updating a clause does not require a code release — only a re-publish of the relevant document with a new version number.
Behind every member QR card, every public Cast URL, every public order link, and every Live TV Monitor token sits the same small set of cryptographic primitives. They are the difference between a feature and a leak.
| Surface | What is encrypted | Why |
|---|---|---|
| Member QR card | Member identifier + organization + tier | Survives migrations between domains; cannot be forged client-side |
| Live TV Monitor URL | Centre identifier + monitor profile | Public link must reveal nothing useful to an attacker copying it |
| Public Cast URL | Battle identifier + valid window | Same — and the URL becomes inert outside the configured window |
| FNB QR ordering token | Centre + table + ephemeral secret | Reused tokens, post-revocation requests, and brute-force token guessing must all fail |
| Sensitive member fields | Phone, email (column-level) | Encrypted at rest; the database dump alone leaks nothing without the path-derived key |
Encryption keys are derived from a small set of immutable values held server-side, including the deployment path itself. The benefits are practical:
Every encrypted token follows the same five-step validation pipeline on the server:
no-referrer so tokens never bleed to upstream CDNs.Not everything happens because a button was pressed. A small fleet of background tasks keeps the audit trail tidy, releases stuck sessions, expires bookings, and pushes off-peak compliance work to quiet hours.
The platform's confirmed scheduled task is the battle auto-timeout in debug/cron/. Other background-style behaviours are handled in-request (lazily, on first eligible request after the time window passes) rather than by a separate scheduler.
| Task | What it does | How it runs |
|---|---|---|
| Battle auto-timeout | Closes a battle whose single confirmation has been waiting in pending_confirm for more than 24 hours. | Cron entry on the host (debug/cron/battle-auto-timeout.php) |
| Abandoned F&B cart cleanup | Releases tables stranded on a guest cart that never converted to a session. | In-request, via order/php/cleanup-abandoned-fnb.php on relevant routes |
| Schema migration | Idempotent schema-migrations.php runs on first request after a release. | On deploy / first request |
| Opcache reset | Bearer-token guarded endpoint flushes opcache + APC after every deploy. | Triggered by deploy scripts |
Reconciliation tools (Chapter XXXVII) and one-off maintenance scripts are not on a cron — they are invoked by an administrator with a bearer token. They are designed to run on a quiet hour, in dry-run first, and apply only after review.
All timestamps used by background workers are UTC. Display layers convert to local time at render. Two practical implications:
Each worker writes a structured row to System Logs (Chapter XXIV) with log_type describing the worker and outcome reflecting the result. Filter the log by worker name to see every run; filter by outcome=failure to see incidents.
log_type when something looks stuck.
A new centre opens at 18:00 on a Friday. By 22:00 the room is full. The difference between a smooth launch and a stressful one is two days of methodical setup. Use this chapter as a printable run-sheet.
A single page you can pin behind the till. Every recurring action a centre takes in a normal day, in the order it usually happens.
available.pending_payment; the operator panel offers Pay now.checkedout briefly, then available.expired; deposit handling follows policy.available or maintenance; nothing on pending_payment should be left.Chapter IX introduced the Member Portal at a high level. This chapter goes lower — the surfaces a member actually swipes through, the small UX guarantees that make those surfaces feel like an app rather than a website, and the pieces that matter when something goes wrong.
The portal runs as a single web surface but is internally two layered generations cooperating. v2 (legacy PWA core) owns bottom navigation, identity, wallet, the classic battle invite flow, sticky CTAs, and push/badge primitives; v3 (modern shell) owns the Me-page swipe state machine, centre-detail full-screen modals, distance/favourites filter chips, debounced search, and the showcard recognition layer. Both share auth, session, and database — users never see the seam, but knowing it exists explains why some screens feel newer.
The portal exposes five primary surfaces as a fixed bottom-nav tab bar driven by switchMemberTab(). Tabs are persistent; switching tabs preserves scroll, search, and selected filters per tab. Coupons, tournaments, announcements, and other vertical surfaces are reached through these five — not via additional bottom-nav tabs.
| Tab key | Label | What lives there |
|---|---|---|
nearby | Centres | Geo-aware list of nearby centres; distance chips (sentinels: 0 = All, -1 = Favourites); Become-Member CTA on each card. |
apps | Apps | App-drawer overlay — coupons, tournaments, announcements, packages, bookings, battle replays, and other deep surfaces. |
activities | Activity | Recent activity timeline — visits, payments, joins, redemptions, battles played. |
social | Social | Friends, chat (social_chat_rooms), photo posts, recognitions, feed. |
me | Me | Avatar, tier, wallet balances, QR card, achievements; horizontally swipable across stat cards. |
The Me page is horizontally swipable across multiple stat cards (visits, points, stamps, achievements). The state machine guarantees three things:
Tapping a centre card opens a full-screen detail page. The page is built around four Quick Action chips just below the photo carousel — each is a one-tap operator-free shortcut:
| Chip | Effect |
|---|---|
| Phone | Opens the system dialer pre-loaded with the centre's number. |
| Opens WhatsApp to the centre's published number with a default greeting. | |
| Direction | Hands off to the device's native maps app with the centre's coordinates. |
| Share | Native share sheet with the centre's portal link. |
Below the chips: real-time table availability, today's prices, hours, photos, member-only offers, and any battles currently in progress on the floor.
A guest can become a member from the Centre Detail page in three taps. The flow is engineered to be idempotent — running it twice produces the same result, never two memberships:
linkMemberToOrganization(). If not signed in, an OTP capture runs first.mpV3CenterDetailLoaded) propagates the new is_member state to v2's sticky CTA, so the join survives a reload.When a full-screen modal (Book Table, Host Game, Become Member) opens on top of the centre detail page, the portal captures the previous header / nav / body-overflow state on open and restores it exactly on close. This is the difference between a clean modal close and the bug where the page underneath leaks visible navigation.
Sounds (Chapter XXXVI) are bound globally with capture-phase tap delegation, throttled to 60 ms. Showcards — celebratory full-screen recognitions for first-century-break, tier upgrade, championship win — render on top of everything, hold for 4 seconds, and broadcast a low-energy event so the Live TV Monitor can mirror them in the venue.
A reference table of the most-used mobile endpoints, the bearer-token model behind them, and the set of native services the Flutter app ships with today — FCM push, location, secure storage, receipt cache, and a WebSocket chat layer.
Issued on mobile login as 32-byte random hex stored on members.api_token; lifetime 30 days from issuance, expiry on api_token_expires_at (constant MOBILE_API_TOKEN_EXPIRY_DAYS); sent as Authorization: Bearer <token> on every request; logout sets api_token = NULL; expired tokens are rejected at validation; every login (success/failure) and logout is audited in System Logs.
The mobile API is a single PHP entry point — /debug/mobile-api/mobile-api.php — that dispatches by an action query parameter. Every call therefore looks like POST /mobile-api.php?action=<name> with the bearer token in the header. The headline actions, grouped by surface:
| Surface | Actions |
|---|---|
| Auth | login, logout |
| Profile | profile, update_profile, dashboard, social_stats |
| Centres | centers, centers_nearby, switch_organization |
| Tables | tables |
| Sessions | session_active, session_history, session_start, session_start_qr, session_end, session_add_player, session_remove_player |
| Open / social games | games_list, game_create, game_join, game_leave, game_cancel, game_history, my_hosted_games |
| Memberships & coupons | memberships, coupons |
| Wallet history | transactions |
| Chat | chat_rooms, chat_messages, chat_send, chat_mark_read, chat_unread_count |
Additional actions handle bookings, battles, marketplace, and loyalty. The full list lives in the dispatch switch at the top of mobile-api.php and grows release by release.
The platform's most important write paths — invoice payment, split-bill checkout, table session start, and the client-portal API router — accept an optional client_request_uuid and deduplicate on it (typically against the row being mutated). When the mobile app drops a connection mid-payment and retries, the server returns the prior response instead of double-writing.
The mobile API returns the standard CORS headers (Access-Control-Allow-Origin: *, allowed methods GET, POST, OPTIONS, allowed headers Authorization, Content-Type). The bearer token, not the origin, is the security boundary.
The Flutter mobile app ships with the following capabilities, each backed by a dedicated service in lib/services/:
A single centre operates differently from a chain of fifteen. This chapter is the playbook for the chain — what to centralise, what to delegate, and which features only earn their cost above a certain footprint.
| Concern | Where it should live |
|---|---|
| Branding (logo, colours) | Organization — push to centres |
| Membership tiers & perks | Organization |
| Tax registration & e-invoice profile | Organization |
| Tariffs (base rates) | Organization with per-centre overrides |
| F&B menu | Organization with per-centre 86 / price overrides |
| Opening hours, address, phone | Centre |
| Devices (printers, lighting) | Centre |
| Employees & shifts | Centre |
| Live Monitor footer | Organization (global brand strip) + centre overlay |
For a multi-centre rollout, set up the flagship completely, then use the wizard's clone pattern:
A free-tier membership at one centre is portable across the same organization without re-enrolling. Paid tiers are organization-wide unless explicitly scoped. Wallet balances are per-organization (not per-centre) so the member can spend their credit at any sister centre.
Campaigns can target an entire organization, a group of centres, or a single centre. The most common patterns:
| Role | What this role typically does in a chain |
|---|---|
| Client owner | Sets brand strategy, billing, multi-org rules. |
| Org director | Owns chain P&L; sets tariffs, menus, tiers. |
| Area manager (manager role across centres) | Reviews per-centre KPIs, mentors centre managers, resolves cross-centre member escalations. |
| Centre manager | Owns shift schedules, voids, comps, devices for one centre. |
| Operator / cashier | Lives in Operations during a shift; rarely needs anything else. |
Beyond Chapter XXXVIII (Legal & Consent), this chapter covers what members can ask for, what operators must respond with, and how the platform makes both practical.
The platform's policy commits to honouring the standard set of data rights. Where the member can self-serve, the path is in the portal; where it requires support involvement, the operator (or system administrator) handles the request and records the action in System Logs.
| Right | How it is honoured |
|---|---|
| Access | The member sees their profile, visit history, invoices, wallet, and marketplace listings throughout the portal. A consolidated export is fulfilled by support on request. |
| Portability | Where machine-readable export is requested, support produces a structured archive (CSV / JSON for the listed surfaces). |
| Rectification | The member can edit most profile fields directly; immutable fields (e.g. an issued invoice) are corrected by support with an audit trail. |
| Erasure | A delete request anonymises the member profile, replaces identifiers on retained invoices with a placeholder for tax compliance, and revokes any active mobile tokens. |
| Restriction | Marketing categories can be muted from the portal notification settings; full processing pauses are handled by support. |
| Objection | Auto-generated marketing or loyalty signals can be opted out at the request of the member. |
Every consequential action — a void, a comp, a refund, a tier change, an exported data archive, a data-erasure request — produces a System Logs row (Chapter XXIV) tied to the actor. Members investigating "who edited my profile" get an answer in seconds.
| Data class | Default retention | Legal driver |
|---|---|---|
| Authentication logs | 180 days | Forensic / abuse |
| Invoices & tax documents | 7 years | Tax law |
| Member profile | Until erasure request | Member control |
| Marketing analytics | 2 years rolling | Reasonable use |
| Bug-report screenshots | 1 year | Engineering follow-up |
The platform discloses, in a public sub-processor list, every third-party service (email delivery, payment gateway, push notification, mapping). The list is versioned; material additions trigger a notice to clients before activation.
An erasure request anonymises the member's profile but preserves invoice history with the personal name replaced by an "Erased Member" placeholder. Tax law requires the invoice itself; privacy law requires the member's identity to disappear. Both are honoured.
Snooker King is the heart of a centre, not the entire universe. This chapter inventories the external systems the platform speaks to today — payment, tax, mail, push, chat, and native maps.
The settlement engine accepts five tender families on every invoice (Chapter XV): cash, card, e-wallet, bank transfer, and member credit / wallet. Each family is implemented as a routing path inside the payment module rather than a third-party SDK; gateway-specific references (auth code, UTR, e-wallet transaction ID) are captured with the payment row so reconciliation can match them later. For tax, LHDN MyInvois (Malaysia) moves invoices through unvalidated → Submitted → Valid (or Invalid), with terminal set {Valid, Invalid, Cancelled} (Chapter XVII; Appendix C). Other jurisdictions use the two-stage stack (service charge → tax) and periodic export. Multiple unvalidated invoices can be merged as a consolidated e-invoice for month-end runs.
Transactional mail — password reset, OTP, booking confirmation, bug-report notifications, status changes — is sent via a server-side helper authenticated against an SMTP relay; the helper is the single funnel, so new features add a caller, never a new sender. Push runs through Firebase Cloud Messaging — the Flutter app registers its FCM token on login and unregisters on logout, with background and foreground handlers wired consistently across iOS and Android. Member chat traffic runs over a long-lived WebSocket connection that the mobile app pairs with an auto-restart service so transient disconnects recover without the user noticing.
From the Centre Detail page, Direction hands off to the device's native maps app (Apple Maps on iOS, Google Maps on Android, or the platform default) using the centre's coordinates. The platform never embeds a third-party map iframe, which keeps location queries off the network from any viewer.
The design choices that let one Snooker King install run a sleepy 8-table hall on a Tuesday afternoon and a packed 60-table championship night on a Saturday — without anyone changing a setting.
| Hall size | Concurrent operators | Comfortably handled |
|---|---|---|
| Up to 12 tables | 1–3 | Single web instance, no special config |
| 12–30 tables | 3–6 | Single instance, larger DB plan, dedicated print queue |
| 30–60 tables | 6–12 | Higher-tier plan, two Live Monitor instances tiled, dedicated kitchen-print server |
| Over 60 tables | 12+ | Plan with support before launch; multi-monitor and zoned operator stations |
The four hot paths each have their own optimisation: the Operations grid refreshes incrementally with polling so only changed cells re-render; the Live TV Monitor uses an independent polling cadence so it never blocks the console; battle scoring writes are tiny and the Cast URL polls a single condensed JSON; the POS print queue absorbs bursts so a stuck printer never blocks the next order. Reliability is built on five patterns: idempotency keys on every state-changing endpoint (duplicate retries never double-write), single-flight on form submits (no double-posts), exponential backoff on outbound deliveries and e-invoice submissions, append-only audit (investigations always have a record), and graceful degradation (kitchen print, RFID scan, lighting stay local-first so cloud outages don't strand the floor).
Backups should be invisible until they are the only thing that matters. This chapter is what you do before, during, and after a serious incident.
| Layer | Cadence | Retention |
|---|---|---|
| Database | Nightly + intra-day deltas | 30 days rolling, monthlies kept 12 months |
| Uploaded media (member photos, F&B) | On write + nightly | 30 days rolling |
| Config (per-centre, per-org) | On change | Indefinite (versioned) |
| Audit log | Append-only; replicated | Indefinite |
members) from a known-good snapshot. Used for accidental mass deletes.Snooker King runs in any country, every centre with its own currency, language, and tax model. Configuration is per-organization for things that follow the legal entity, per-centre for things that follow the venue.
Currency code is set per organization (MYR, SGD, IDR, AUD, USD, etc.); the display symbol, precision, grouping, and decimal mark follow that code. Rounding is per-organization — round-to-nearest 0.05 is the common cash rule, some operators use 0.10 for low-friction change. Internal storage is canonical 2-decimal DECIMAL; per-minute billing uses 4 decimals internally and rounds at display.
Three languages ship in the portal sound bus and announcement TTS: English, Bahasa Malaysia, Mandarin; UI translation is config-driven, additional languages add without a release. Per-user preference falls back to centre default, then organization default; receipts and emails honour the recipient's language wherever known. Every centre carries its own IANA time zone, all timestamps stored UTC and converted at display. Date format is per-locale (DD/MM/YYYY for most of the world, YYYY-MM-DD where ISO is preferred); first day of the week is per-locale; reports honour the user's setting.
The tax engine applies a two-stage stack per line — service charge first (on subtotal), then tax (on subtotal + service charge). Both rates are per-centre and stamped on every line for audit. Where a single tax label covers the obligation (SST in Malaysia, VAT in the EU, GST in Australia / Singapore), set the tax stage; jurisdictions needing a third layer (US local sales + city tax, excise / sin tax) apply it as an explicit line item rather than expecting a third tier. Real-time submission integrations run where required (Malaysia LHDN); elsewhere periodic export covers the obligation.
Software that runs a business has to evolve carefully. Snooker King ships small, frequent, transparent changes — and gives operators the tools to know what changed and what to expect next.
Bug fixes deploy continuously as they pass review; feature increments roll out weekly to a staging audience first; operator-visible release notes ship monthly with a summary email to managers; the quarterly roadmap names target windows for the next quarter. A change reaches you in four steps: engineering review & staging tests, soft-launch to a small cohort with telemetry watched, general release with an in-product release-note toast on next sign-in, and a feedback loop where any release-tagged bug short-circuits back to engineering.
Schema migrations run on the first request after a release and are idempotent so they never double-apply. Backwards compatibility is default — operator screens never break mid-shift because of a release. Major data migrations go through the reconciliation tools (Chapter XXXVII), in dry-run first.
Each note is a Headline (one line, plain language), a Why (the operator-visible reason), an Action needed (almost always "none"; when not, listed clearly), and a Where to find more link into the relevant chapter of this guide.
The vocabulary of Snooker King, defined for quick reference.
Battle — managed match between two members, frame by frame, Glicko-2 rated.
Cast URL — token-gated public broadcast page for a battle (Ch VII).
Centre — a single physical venue with tables, employees, menu, prices.
Client — the top-level tenant, usually a brand or franchise owner.
Combo — a bundle of menu items sold as one line.
Comp — an item delivered free; recorded as a marketing cost.
Console — the web app used by employees and management.
Consolidated invoice — multiple invoices merged into one e-invoice submission.
Countdown package — flat-fee fixed-time table package.
Countup session — table session billed by the minute on an open meter.
F&B — food and beverage; kitchen and bar side of the business.
Frame — one game within a battle, ends on concede or all balls potted.
Glicko-2 — the rating system used to score battle players.
Grace period — short window at session start during which time isn't billed.
KOT — Kitchen Order Ticket; printed instruction to the cook line.
LHDN — Malaysia's tax authority; e-invoice compliance target.
Live TV Monitor — wall display showing tables, prices, promotions.
Marketplace — peer-to-peer listings tab in the Member Portal (Ch XXXIV).
Member Portal — the mobile-first customer-facing app.
MyInvois — the LHDN portal that stores validated e-invoices.
NFC card — physical card identifying a member by tap.
Operator — front-of-house employee with shift-level access.
Organization — a group of centres under one client.
Official Receipt (OR) — formally numbered receipt; pattern OR{YY}{org}{seq}.
QR card — the default member identification method.
RFID — radio-frequency card scanned by a reader for instant ID.
Showcard — animated full-screen recognition for member milestones.
Snapshot — frozen HTML of an invoice; prevents retroactive drift on reprint.
Stamp card — the dual-currency loyalty system: stamps and points.
Tariff — price-per-hour rule for a table type and time band.
Tender — method of payment (cash, card, e-wallet, transfer, credits).
Transaction slip — short-duration sale recorded without e-invoice submission.
Vibe preset — bundled sound, voice, and animation choices for battle audio.
Void — cancellation of an item that was never delivered.
Every shortcut the operations console responds to. Mastering even five of these will measurably speed up a busy shift.
| Key | Action |
|---|---|
| ← → ↑ ↓ | Move highlight across the table grid |
| Enter | Open the highlighted table or POS |
| 1 – 9 | Open table by position |
| Esc | Clear highlight |
| Key | Action |
|---|---|
| Esc | Blur focused input → close topmost modal → return to operations |
| Enter | Process / confirm / open POS (context-aware) |
| M | Focus member search; pressing again removes member |
| U | Undo remove member |
| ↑ ↓ | Navigate member-search results |
| A | Toggle select-all checkboxes (where applicable) |
| Key | Action |
|---|---|
| S | Focus search field in POS |
| 0 – 9 | Pick category or item by position |
| D | Remove last cart item |
| C | Toggle coupon (apply / remove) |
| P | Print proforma (or normal blue print) |
| B | Print thermal (black button) |
| E | End game / session |
| Key | Action |
|---|---|
| R | Add a new round |
| I | Add an item to the active round |
| N | Focus the newest round's note field |
| Key | Action |
|---|---|
| 0 – 9 | Pick countdown package by position |
| M | Focus member search |
| Enter | Confirm and start meter |
| Key | Action |
|---|---|
| ← / K | Previous page |
| → / J | Next page |
| Home / End | Jump to first / last page |
| T | Toggle table of contents |
| P | Save as PDF |
| Esc | Close TOC |
Snooker King's battle engine supports a deep catalogue of cue-sport formats — each with its own rules, frame mechanics, and scoring panel.
| Format | Notes |
|---|---|
| Standard Snooker | Fifteen reds, six colours, full standard rules. |
| Six-Red Snooker | Six reds, faster matches, popular for league play. |
| Ten-Red Snooker | Ten reds, intermediate length. |
| Power Snooker | Variant ruleset with reduced reds and bonus mechanics. |
| Format | Notes |
|---|---|
| Eight-Ball (English) | UK rules; smaller table; "two-shot carry" foul handling. |
| Eight-Ball (American) | Bigger table; BCA-style rules. |
| Bank Pool (8-ball variant) | Banks-only; advanced format. |
| Chinese Eight-Ball | Chinese ruleset; spotted balls; popular in Asia. |
| Nine-Ball | Standard nine-ball; rotation play. |
| Ten-Ball | Like nine-ball but with ten balls; called-pocket. |
| Chinese Nine-Ball | Regional variant of nine-ball. |
| Format | Notes |
|---|---|
| Three-Cushion Billiards | Pocketless table; cushion contacts before scoring. |
| English Billiards | Three balls, points for cannons, hazards, pots. |
| Russian Pyramid | Heavy white balls, single red, specific pocket rules. |
| Straight Pool | Continuous play to a target score. |
Across formats the frame screen exposes consistent keyed buttons (data-verb): Foul (reason picker), Safety, Undo, Concede, Settings (Match Vibe). Format-specific actions — pots, cannons, breaks, frame completion — surface inline on the frame screen rather than as universal verbs.
A complete reference of every invoice and revenue status, what triggers it, and what is or isn't possible from there.
| Status | Meaning | Editable? |
|---|---|---|
| Unvalidated | Created locally; not yet submitted to the tax authority. | Yes |
| Submitted | Sent to LHDN MyInvois; awaiting acceptance. | No |
| Valid | Accepted by MyInvois; portal link is live. (LHDN's literal status string is Valid; the platform stores it verbatim.) | Adjusted only |
| Invalid | Rejected by MyInvois post-submission for a fixable reason. | Yes (resubmit after fix) |
| Cancelled | User-revoked within the cancellation window. | No |
| Voided | Reversed for accounting; no refund. | No |
| Reversed | Reversal entry posted; refund issued via original method. | No |
| Consolidated | Merged into a single consolidated e-invoice. | No |
| Adjusted | Post-validation edit; flagged with a pink marker. | Limited |
| Rejected | Rejected by the tax authority. | No |
| Outstanding | Partial or zero payment; settle with a monetary tender to clear. | Pay only |
| Refunded | One or more refund lines posted against a paid invoice; recorded in System Logs and visible on the invoice detail. | No (the refund itself is a new ledger event) |
| Partial-paid (F&B) | An fnb_orders row whose lines are part-paid, part-unpaid; orchestrated through the parent invoice settle flow. | Lines only |
Two columns hold these values on a revenues row: payment_status tracks whether the customer has paid (pending / outstanding / paid / cancelled), and myinvois_status tracks the LHDN compliance lane (unvalidated → Submitted → Valid / Invalid / Cancelled). Reading them in isolation is fine; conflating them is the most common analyst mistake.
Outstanding-balance settlement accepts only the four monetary tenders: Cash (overpay → change), Card (reference / authorisation code), E-wallet (QR or transaction reference), and Bank transfer (UTR / statement reference). Member credit, points, stamps, and tokens apply at original invoice creation, never on outstanding-balance settlement.
When you change one number, knowing the order of operations protects you from surprises.
When a member is identified, the engine resolves which discount rule applies in a strict priority order, lowest-fallback first:
Overrides are not stacked. A package-level rule fully replaces the table-level one, which fully replaces the centre-level one. Each rule may be percentage or fixed-amount, may carry a min/max-spend gate, and may be restricted to specific weekdays or time bands (overnight ranges supported — the platform handles the wrap-around).
Rounding is configured at the organization level via rounding_precision (default 0.05 = nearest five cents) and currency_decimals (default 2). The implementation uses PHP's round() with mode half-to-even (banker's rounding) — meaning a midpoint amount snaps to the nearest even increment. For five-cent precision: 1.025 → 1.02, 1.075 → 1.08; for two-decimal cents: 0.125 → 0.12, 0.135 → 0.14. Reward points have their own helper that allows up to three decimals but respects the org's currency_decimals when smaller.
client_request_uuid; replays of the same UUID return the original outcome rather than double-charging.Putting a member discount before service charge produces a different total than putting it after. Snooker King fixes the order so a manager comparing two centres can rely on the numbers being computed the same way. If your venue's accounting requires a different order, raise it with support; it is a centre-level configuration, not a code change.
Twenty short, named procedures for the situations that actually come up on a busy floor. Each one is a single paragraph; the full path is in the chapters above.
1 · Start a session for a walk-in. Tap the table → Start. No member needed. The cell turns occupied; the lighting relay fires.
2 · Attach a member after the session has started. Open the table panel → Member → scan QR or type the phone number. Tier discount and stamp accrual apply from the next billing tick onward, not retroactively.
3 · Move a session to a different table. Open the source table panel → Transfer → pick the destination → confirm. The meter does not pause; lighting follows the move automatically.
4 · End a session early without billing. Manager-only. Open the table panel → Comp Session → enter reason. The session closes with a comp invoice; nothing is charged but everything is logged.
5 · Take a table out of service mid-shift. Open the table panel → Maintenance → pick reason and ETA. The Live TV Monitor hides its price; bookings to that table are blocked until you mark it available again.
6 · Add an F&B order to a running table. Tap the table → POS → search items → modifiers → quantity → Send to kitchen. The KOT prints; lines join the table's open tab.
7 · 86 an item for the rest of the shift. Open Menu Editor → tap the item → Mark unavailable. It disappears from operator POS and the public order page simultaneously; reverse the toggle the next day.
8 · Reprint a kitchen ticket. Open the table tab → find the order line → Reprint KOT. The ticket reprints with a "REPRINT" header so the cook line knows it's not new.
9 · Void an order line. Tap the order line → Void → enter reason. The line stays visible (audit), totals recalculate, and the kitchen gets a "VOID" notice if the order was already submitted.
10 · Comp an item for service recovery. Tap the order line → Comp → enter reason. The line stays on the receipt at full price with a comp adjustment line — customer sees the value, books see the marketing cost.
11 · Pay an invoice with two tenders. Open the invoice → Settle → enter cash amount → tap Add tender → enter card amount. The system tracks each part separately on receipt and audit log.
12 · Split a bill four ways equally. Open the invoice → Split bill → mode Even → set 4 payers. Each child invoice shows its share; settle independently. Parent flips to paid only when all four clear.
13 · Split a bill itemised. Same path; pick Itemised. Hand the station to each member in turn (or open the portal split flow), each ticking the lines they consumed.
14 · Refund a paid invoice. Find the invoice → Refund → pick lines and amount → enter reason → confirm. Recorded in System Logs against your account. Card-tender reversal is initiated where the gateway supports it; cash refunds open the drawer.
15 · Apply a coupon at checkout. Open the invoice or split-bill → Coupon → type the code or scan the QR. Eligibility is verified server-side; if the member or time band doesn't qualify, the dialog explains why.
16 · Issue an RFID card to a member. Open the member profile → Cards → tap a card on the configured reader → save. The card resolves to the same account as the QR; either works at the door.
17 · Top up a member wallet. Open the member profile → Top up → enter amount → pick tender → confirm. The deposit posts to the ledger as a positive entry; spend draws against it automatically.
18 · Issue comp credit (service recovery). Manager-only. Open the member profile → Comp credit → enter amount, expiry, and reason. The reason is recorded; the member sees the credit in their portal wallet immediately.
19 · Check in a booking. Open Operations → Bookings → find the row → Check In. If the booking carried a countdown package, the session auto-starts on the assigned table.
20 · Push a battle to a Cast URL. Open the battle from the Operations grid → Cast → pick layout. The token-gated URL is generated; share with anyone who should watch. Frame updates flow live without any extra operator action.
A flat reference of every state machine the platform exposes, with valid transitions. Use this when you suspect a record has skipped a state.
Five canonical states are stored; the Operations grid layers extra rendered badges (pending, fnb, rental-active, rental-idle) on top — see Chapter V.
available ──start session──▶ occupied occupied ──session ends──▶ (rendered as pending while invoice unpaid) * ──invoice fully paid──▶ checkedout checkedout ──reset / next assignment──▶ available available ──manager marks down──▶ maintenance available ──manager hides──▶ inactive maintenance ──restore──▶ available
active ──end button / closeout──▶ checked_out checked_out ──invoice fully paid──▶ completed active ──admin force-end──▶ ended * ──manager comp / void──▶ completed (with comp marker)
Game sessions do not carry a paused state — pause semantics live on frame-by-frame turn tracking and on the table's own status, not on the session row. The session row stays active from start to closeout, then transitions through checked_out while waiting for payment, and finally to completed (or ended for force-closes by an administrator).
pending ──finalise──▶ outstanding outstanding ──pay rest──▶ paid pending ──pay full at once──▶ paid * ──cancel/void with reason──▶ cancelled / voided
There is no "overdue" or "refunded" status on the invoice itself; partial-payment is captured by outstanding_amount while payment_status stays outstanding until fully paid. Refunds are line-level adjustments that reduce outstanding_amount rather than a separate state.
pending_accept ──opponent accepts──▶ pending_confirm pending_confirm ──host confirms + table allocated──▶ in_progress in_progress ──final frame entered──▶ completed in_progress ──cron 24h timeout──▶ completed * ──party cancels──▶ cancelled * ──integrity flag──▶ disputed
confirmed ──party arrives──▶ checked_in checked_in ──table closes──▶ completed confirmed ──cancel──▶ cancelled confirmed ──end-time elapsed without check-in──▶ no_show
Bookings are written directly as confirmed — no "pending confirmation" step. no_show is automatic once utc_end_time passes without check-in.
fnb_orders.status: draft ──Send to kitchen──▶ submitted (KOT printed)
fnb_order_items.payment_st: unpaid ──invoice settles──▶ paid
unpaid ──void w/ reason──▶ cancelled
fnb_sessions.status: active ──end-of-meal closeout──▶ completed
unvalidated ──submit to MyInvois──▶ Submitted Submitted ──LHDN validates──▶ Valid Submitted ──LHDN rejects──▶ Invalid Valid ──cancel within 72h window──▶ Cancelled Submitted ──submission failure──▶ error
LHDN literal status strings stored verbatim. Terminal set {Valid, Invalid, Cancelled} — platform stops polling on terminal.
bug_reports.status: New ──triage──▶ In Progress
In Progress ──cannot auto-resolve──▶ Pending Human Decision
In Progress ──fix deployed + verified──▶ Resolved
member tier: upgrade/downgrade ──▶ tier (logged)
member wallet: top-up ──▶ +ledger entry
spend ──▶ -ledger entry
refund of specific deposit ──▶ reverse entry
Concise references for fifteen features and modules that earn a closer look once the basics are running. The italic note at the end of each entry points to the chapter where the feature is set up.
Official Receipts (OR). A formally numbered receipt is issued for paid invoices once required by tax law; the numbering pattern is OR{YY}{org}{seq} (year, organization, sequence). ORs reprint from the invoice detail panel; the sequence never changes, so the same OR number always refers to the same invoice. See Ch XV.
QR Validity & Rotation. Centre QR tokens (member cards, table F&B stickers, monitor URLs, cast URLs) carry a centre-level validity policy. Rotating a token invalidates old printed materials immediately; revoke from the device panel when a screen leaves the venue or stickers are reprinted. See Ch XXXIX.
Invoice Snapshots. Every issued invoice freezes its rendered HTML at issue time so a reprint months later still matches the original — discount rules can change without rewriting history. Snapshot bytes live in invoice_snapshots, produced lazily on first reprint where a snapshot is missing. See Ch XV, glossary.
E-Invoice (non-Malaysia). The two-stage tax stack (service charge → tax) and per-line stamping is portable; jurisdictions without a real-time API rely on periodic CSV export rather than the LHDN MyInvois integration. Add a third tax label as an explicit line item. See Ch XVII, L.
Unified Payment Layer. Multi-tender splits, idempotency on retry (client_request_uuid), and gateway-reference capture all flow through a single payment module rather than per-tender SDKs — that's why a card+cash+e-wallet split appears on one receipt with one audit row. See Ch XV, App D.
Print Queue Status. Submitted KOTs and invoice prints land in a per-printer queue; jobs retry with exponential backoff and surface in operator notifications when a printer stays offline beyond two retries. The queue absorbs bursts so a stuck printer never blocks the next order. See Ch XIII, XXVI.
Ticketing Kiosk Mode. Per-centre toggle (centers.ticketing_mode) flips the operator station into a one-tap-per-scan flow optimised for kiosk-style entry. Members tap RFID or scan QR; the system resolves and activates the next available table without operator confirmation. See Ch XXI, XXII.
RFID Scan Mode actions. Each reader can be configured per-centre with a default action (auto-assign-to-next-active-session, open-member-profile, or check-in-pending-booking). Cards link to members from the profile screen; a member may carry multiple cards (primary + backup) and any card is deactivable instantly if lost. See Ch XXII.
Table History module. Beyond the headline report, the module exposes per-session drill-down: opening time, duration, members present, F&B lines, settlements, voids, comps. Filterable by table, employee, member, date range; CSV-exportable. See Ch XXIII.
Live Monitor JSON config. The config object covers theme preset, layout (grid / list / spotlight), header height, summary-stat selection, footer marquee, and rotation panel. Field-level overrides are accepted; reset-to-default reseeds from the canonical template (Ch VI). See Ch VI.
Custom Notifications & Sounds. Each user mutes information and alerts independently from the avatar menu (state persists per browser). Custom sound effects per centre are configured in the Notifications panel; per-role routing decides whether an alert pushes to email, in-app, or both. See Ch XXXVI.
Keyboard Shortcuts (custom). Appendix A covers the shipped shortcuts; per-role rebinding lives in the user-settings drawer (managers can pin a shift theme + shortcut layout to a shared terminal). Disabled shortcuts don't fire on focused inputs even if the layout would otherwise capture them. See App A, Ch XXX.
System Logs custom types. The log_type column is open vocabulary — new modules add their own values without schema migration. The viewer is role-scoped (admin / client / org / manager / operator / member) and the writer strips a deny-list of sensitive keys before persistence. See Ch XXIV.
Social Moderation. The operator-side social console exposes flagged listings, reported chat threads, and a one-tap remove + reason for marketplace listings. Members never see operator-side comments; status-change rows are immutable and join the System Log audit trail. See Ch XXVIII, XXXIV.
Admin Utilities. Beyond the four reconciliation tools (Ch XXXVII), bearer-token-guarded admin endpoints handle bulk re-tokenisation, organisation-wide tariff broadcasts, schema validation, and one-shot data corrections. All run dry-run by default and write a System Log row on apply. See Ch XXXVII.
For trusting your room, your members, and your livelihood to Snooker King. We will keep working to deserve it.