Ceremonies modelled as sessions: OOC01 (Opening) · OCC01 (Closing)
Event config
eventType = RACING_WEEKEND
code = F1-SGP-2026
1 weekend, 4 sub-sessions
Default currency USD
Downstream implications
One MatchDef, multiple SubGames: FP1 · Quali · Sprint · Race
Category scheme custom: Paddock Club · Gold Hospitality · Grandstand A/B/F · General Admission
Side effects
Notification event.created → role:ops_manager
Activates the event switcher dropdown — users can switch into it
Nothing allocated or purchased yet — event in DRAFT
super_adminevent_adminStatus: PLANNINGFreq: 1× per event
Why
Schedules are typically acquired 12+ months in advance. System accepts the whole season/tournament in one file, parses venues / categories / sessions, and creates MatchDef[] in bulk. Placeholder teams are preserved for later resolution.
6 categories possible: First · A · B · C · D · E (per-session subset)
204 medal sessions flagged
User-expected code ranges (future events)
ATH01–ATH104 (Athletics — up to 104 sessions)
FBL01–FBL52 (Football — up to 52)
BKB01–BKB30 (Basketball — up to 30)
Example stage mappings from user: FBL23 – Semi Final · BKB10 – Quarter Final · ATH15 – Gold Medal Final · ATH10 – 100m + Long Jump Finals
Medal-flag examples: ATH15 → Medal = YES · FBL10 → Medal = NO
Dynamic team example: FBL30 – Winner QF1 vs Winner QF2
super_adminevent_adminFreq: many per event
Form preview · Add match
New match · FIFA World Cup 2026
M64
Auto-format: M + 2-digit
Final (derived from M64)
19 Jul 2026
18:00
MetLife Stadium · New York ⌄
82500
Winner SF1 ⌄
Placeholder — resolves when M61 concludes
Winner SF2 ⌄
Placeholder — resolves when M62 concludes
17 Jul 2026
= matchDate − dispatchBufferHours(48)
☑ Yes
Placeholder team support
Placeholder values accepted at creation: TBD · Qualifier 1 · Winner QF1 · Winner Group A · Runner-up Group B
Stored verbatim in teamsOrDescription. Also parsed into a structured teams object with placeholderRef field
Tickets can be sold against placeholder matches immediately
See B7b · Placeholder resolution for how they resolve
Auto-sort (natural order)
Every match / session list in the app — the matches table, the Purchase (B12) line-item Match dropdown, the Sale (B14) line-item Match dropdown, placeholder resolver, categories picker, sub-games picker — renders matches in natural code order: M01 → M02 → M03 → … → M10 → … → M64 → … → M100 → M104. This works for any code format (FIFA M<n>, Olympics ATH01, ATH02, …, ATH15, racing FP1, Q1, R1). Kickoff time is the tiebreaker.
Seat-proximity penalty in allocation scoring (so scattered allocation is discouraged)
Client portal can filter by Block / Row / Seat range
event_adminFreq: throughout tournament
Form preview · Enter match result
Enter result · M57 (Quarter Final)
Brazil
France
Brazil ⌄
France (auto)
Cascade
All matches with teams.home.placeholderRef == "Winner QF1" update to "Brazil"
Each resolution triggers a notification match.teams_resolved → clients holding tickets for that match
Client portals refresh automatically (the Game column updates)
If resolving M57 makes M61 fully resolvable, cascade continues
Scenarios
M01–M48 Group → resolve winners after all 48 group matches
M49–M56 R16 → resolve Round of 16 winners
M57–M60 QF → QF winners → M61/M62 SF
M61–M62 SF → SF winners → M64 Final, losers → M63 3rd
Final resolution: M64 home/away both resolved ~4 days before match
Less bracket-heavy — Olympics S.Desc in the sample has relatively few placeholders (16 rows mention "Qualifier"). Most sports have heats → semis → finals within days; placeholders resolve within the same week.
ops_managerFreq: rare
Form preview
New vendor
TicketVault
TKV
MARKETPLACE ⌄
MARKETPLACE · DIRECT · AGENCY
France
https://ticketvault.example.com
Clara Dufresne
clara@ticketvault.example
+33 1 234 5678
Required — used for urgent contact on dispatch / payment issues
☑ Yes
Special: Matthias masking
Vendor emails in the _MATTHIAS_EMAILS allowlist (266 entries) trigger automatic anonymisation: Distribution writes Vendor = "Matthias" and keeps the real email in the Email column. Keep this rule when building the vendor model.
ops_managerFreq: rare
Form preview
New client
Meridian Travel
MRT
AGENCY ⌄
CORPORATE · AGENCY · INDIVIDUAL
500,000 USD
David Clarke
david@meridiantravel.ae
+971 50 123 4567
Required — used for urgent contact on dispatch / payment issues
Dubai · UAE
Net 30 ⌄
☐ No
VIP drives excludeVip policy
One client · many matches
A client buys tickets across many matches in one tournament. The Sales form's line-item section supports mixed matches — see B14.
Phone is mandatory on every client and vendor record. Ops needs a reachable number when a dispatch is stuck, a receipt is missing, or a payment needs chasing.
ops_manager
When to create a contract vs. just an invoice. Use a Contract (B10) for large, recurring, or negotiated-terms relationships. For one-off or small deals, skip B10 — the Purchase (B12) or Sale (B14) carries its own Invoice No. and stands alone.
Form preview
New contract
2025-100129
Text format — preserves dashes
PURCHASE ⌄
TicketVault (vendor) ⌄
FIFA World Cup 2026 ⌄
01 Jan 2026
31 Jul 2026
5,000,000 USD
USD ⌄
ACTIVE ⌄
contract-2025-100129.pdf
sr_operatorops_manager
Per-event or global
eventId = null → global credential (fallback across events)
eventId = evtX → event-specific (wins over global)
Encrypt at rest · PASSWORD_VIEWED audit fires on reveal
☑ Yes
Credential history
CREATEDUPDATEDDEACTIVATEDPASSWORD_VIEWED
operatorsr_operatorFreq: many per day
Form preview · revised layout
New purchase · FIFA WC 2026 Bulk across matches allowed
TicketVault ⌄
INV-2026-0042
Every purchase carries its own invoice — not auto-filled from any contract
— none — ⌄
Link only if this purchase is covered by an existing B10 contract; otherwise leave empty
18 Jun 2026
USD ⌄
e-ticket ⌄
☐ No
—
M28 — MEX vs KOR ⌄
Top Cat 1 · 43 · 1,893 USD
M28 — MEX vs KOR ⌄
Cat 2 · 12 · 850 USD
M32 — USA vs AUS ⌄
Cat 1 · 8 · 1,450 USD
103,199 USD · 63 units across 3 sets
Client rules
Invoice per purchase. Every new purchase carries its own Invoice No. — purchases are created as virgin entries with no pre-linked contract data. Contract is an optional afterthought, not an auto-fill.
Bulk across matches: same Vendor + Invoice (and, when relevant, Contract) can cover many matches in one purchase — Match is chosen per line, not per purchase.
Each line item = its own Set. A different match OR the same match + different category = a separate SetID. A set represents a contiguous seat bundle (Block/Row/Seats) for allocation.
Unit IDs are immutable. Once units are generated they keep their ID forever, even if the purchase is edited or partially cancelled.
Cancellation-safe. Cancelling a whole purchase, a single line, or an individual unit only deletes those rows — other units in the same purchase are untouched.
Match / session dropdown is natural-sorted. Tournament codes render M01 → M02 → M10 → … → M64 → M100 → M104 regardless of the order they were imported or created.
Data written
PurchasePurchaseID = P_<uuid>PurchaseLineItem × N (one Set per line)
Auto · system
What happens
For each line item with qty N → generate a new SetID and create N _PURCHASE_UNITS rows inside that set.
SetID = P<5-digit running counter> — e.g. P00012, P00013, P00014. Each line gets a fresh SetID, even within the same purchase.
UnitID = <SetID>-<SetPos> — e.g. P00012-1, P00012-2, … P00012-5. Sequential per set, 1-based.
SetSize = N · SetPos = 1..N · Status = AVAILABLE · AllocatedToSalesID = "".
Block/Row/Seat blank at first; allocator or supplier portal fills them later.
Example · 3-line purchase above
Line 1 · M28 Top Cat 1 · 43 tickets → SetID P00012 → Units P00012-1 … P00012-43
Line 2 · M28 Cat 2 · 12 tickets → SetID P00013 → Units P00013-1 … P00013-12
Line 3 · M32 Cat 1 · 8 tickets → SetID P00014 → Units P00014-1 … P00014-8
Purchase created. 63 units generated across 3 sets (P00012 · 43 · M28 Top Cat 1 · P00013 · 12 · M28 Cat 2 · P00014 · 8 · M32 Cat 1). Allocation can start now.
Immutability & cancellation
UnitIDs never change. Editing the parent Purchase (price correction, delivery channel change) preserves every unit row.
Partial cancel: cancelling one line or a few units only removes those specific rows — the rest of the set and other sets in the same purchase keep their IDs and statuses.
If a cancelled unit had already been allocated to a sale, the AllocatedToSalesID link is cleared and the linked DistRow flips back to UNALLOCATED.
Data written
_PURCHASE_UNITS × N (per line)_PURCHASE_META (audit)_PURCHASE_UNITS_AUDIT row
sr_operatorFreq: per purchase batch
Form preview
Assign credentials to _PURCHASE_UNITS
TicketVault ⌄
operations@mirra-dmcc.ae ⌄
All matches ⌄
BLANKS_ONLY ⌄
Or OVERWRITE
0 (all)
43 units identified
operatorsr_operatorFreq: many per day
Form preview · revised layout
New sale · FIFA WC 2026 USD / EUR only · no FX
Meridian Travel ⌄
Typable · best-match autocomplete
INV-S-2026-0247
Every sale carries its own invoice — not auto-filled from any contract
— none — ⌄
Link only for clients on a negotiated B10 contract; small deals leave empty
PO-2026-0042
EUR ⌄
USD or EUR only · no FX
15 Apr 2026
PENDING ⌄
Consecutive seats for Final · wheelchair access for M01
M06 — AUS vs POC ⌄
Cat 3 · 4 · 450 EUR
M32 — USA vs AUS ⌄
Cat 3 · 4 · 450 EUR
M64 — Final ⌄
Top Cat 1 · 2 · 2,950 EUR
Credit OK · 3 matches · 10 tickets · 9,500 EUR · No oversell
Form UX rules (revised)
Invoice No. is mandatory per sale — sales are virgin records, not auto-linked to any contract. Contract field is optional, manually chosen only when a negotiated B10 contract applies.
Client field moves up, typable with autocomplete. Client's Phone from the master (B9) is shown next to the client name for quick urgent-contact lookup.
Match field moves DOWN into each line item — one client can buy across many matches in one sale. Match dropdown is natural-sorted (M01 → M02 → M10 → … → M64 → M100 → M104).
Currency is USD or EUR only. FX field removed — user rationale: "only 2 currencies USD & EUR, no need for FX in here"
Real-time inventory feedback per line: green enough · amber close · red oversell
If the sale is re-imported (qty unchanged, same SourceSaleID), rows with matching SalesLineKey are preserved — operator edits to guest details / dispatch status are not wiped.
Sales ↔ Purchase traceability
When the allocator assigns units to this sale, each DistRow records the concrete UnitID(s) it received — e.g. S147-1 → P00012-7, S147-2 → P00012-8.
On the unit side, _PURCHASE_UNITS.AllocatedToSalesID is stamped with the SalesID that consumed it.
These two pointers are the permanent trail — for any ticket we can always answer "which client did this go to?" (Unit → SalesID) and "which tickets did this client get?" (SalesID → DistRow.AllocatedUnitIDs).
Edits or partial cancels on either side update the link but never change SalesID or UnitID themselves.
sr_operatorops_manager
4-strategy generator
EXACT — smallest single set with count ≥ qty (min waste)
GREEDY_LARGEST — fill from largest sets (fewer chunks)
GREEDY_SMALLEST — fill from smallest (tighter fit)
ROTATED_DESC — up to 5 rotations of largest-first ordering (vendor diversity)
Dedup canonical signatures. Sort: closest-to-target → fewest chunks → largest total. Return top 6.
Optional: operator picks consecutive seats on the rendered stadium layout. Seats flagged OCCUPIED. Same commit path, with Block/Row/Seat populated.
operatorsr_operatorops_managerFreq: many per day
Why this view exists
Before accepting a new sale, re-allocating, or quoting a small client, ops needs a single screen that answers "what do we still have on hand, and where are those seats?". The Remaining Stocks view reads _PURCHASE_UNITS where Status = AVAILABLE and groups by match → category → (block, row), so unassigned stock is visible at a glance.
Layout preview
Remaining stocks · FIFA WC 2026
All matches (or pick M04) ⌄
All categories ⌄
4 tickets · seats 7 – 10
4 tickets · seats 12 – 15
2 tickets · block/row/seat blank
3 tickets · seats 5 – 7
Group-by rules
Match — natural-sorted by code (M01 → M104).
Category — sorted by level (Top Cat 1 → Cat 1 → Cat 2 → … or First → A → B → C → …).
Seat location — grouped by (block, row). Every group shows count, seat-range (derived from consecutive seat numbers), and a link that opens those specific units in the allocator.
Unassigned tail — units where block/row/seat are blank render last per category as "unassigned / remaining".
Computation
rows = _PURCHASE_UNITS where status = 'AVAILABLE' & eventId = currentEventId
group by (matchId, categoryId, block, row) → count, min(seat)..max(seat)
section override: if venue.sectionLabel is set, display "Section N" instead of "Block N"
Client use case
Example ask from ops: "Do we still have any Cat 2 seats together for Match 04?" → filter Match = M04, Category = Cat 2 → see Section 325 · Row 5 · 4 seats (7–10) and Section 225 · Row 10 · 4 seats (12–15). Both groups are contiguous and can be offered as a 4-together.
sr_operatorops_manager
Operations
INV_unallocateSelectedSale — release all units for a sale
INV_unallocateBySetId — release units from one SetID only
Token URL generated, valid for event.portalTokenExpiryDays (default 14)
sr_operatorclientFreq: per-event
Why this exists
Clients (both logged-in on the portal and prospective small clients receiving a link) need to see what categories exist, what they cost, and what the seats actually look like — before they commit to a sale. This is one page with two modes of access.
Layout preview
Price list · FIFA World Cup 2026 matches natural-sorted M01 → M104
M04 — MEX vs KOR ⌄
MetLife Stadium · zones painted by category ↗
2,950 EUR · 12 seats left
1,450 EUR · 42 seats left
850 EUR · 10 seats left
450 EUR · 68 seats left
250 EUR · 120 seats left
Two access modes
Embedded in Client Portal (B21). Logged-in clients see a "Price List" tab alongside their allocation workbook. Seat-map hover / click highlights the category zone and the remaining count updates live from _PURCHASE_UNITS.
Standalone shareable link — ops generates an independent token URL (/price-list/<eventId>?token=…) for small clients who don't have a portal workbook yet. Read-only. Doesn't expose stock-per-block; shows category-level availability only ("Many", "Limited", "Sold out"). Expires on the same portalTokenExpiryDays default.
Seat-map rendering
Pulls the venue's _SEAT_MAP (B7) and re-colours zones using the active category palette.
Client ask: "Share the link Umed sent" — that was the inspiration. Mirra needs one canonical Price List URL per event that can be emailed to prospective buyers without creating them in the clients master first.
client
Advanced filter bar (new requirement)
Client portal · filters
M28, M32, M64 (3 selected) ⌄
Cat 3, Top Cat 1 ⌄
NOT_SENT + SENT ⌄
Block C, rows 12–15
"smith"
Searches First / Last / Email
Allocated ⌄
42 of 327 tickets match · 18 missing email
Yellow guest block (AB–AF)
One row per allocated ticket. Editable fields:
AB Clients notesAC First nameAD Last NameAE Client EmailAF Comments
P vs Q columns (screenshot #3)
P · Total SETS / Purchased qty — e.g. 4 if client bought 4 tickets
Q · Allc / Allocated qty — populated from Distribution. If 4 → 4 attendee rows editable; if 0 → all rows greyed (waiting allocation)
Copy-down
Common: all 4 tickets in a set go to one booker. One-click copies guest block down to selected rows.
OnEdit cascade
Client edits AB:AF cell
onEdit trigger writes to hidden _CLIENT_INPUTS vault (preserves data across refreshes)
Batch sync (or onEdit) writes back to Distribution: First name · Last Name · Client Email · Comment · Clients notes
Distribution onEdit cascades to Staff Queue (ClientFirstName / ClientLastName / ClientEmail) and Supplier Queue
Client: First name · Last Name · Email · Phone · Notes
staffExternal platform
Staff opens vendor website
Logs in using stored email + password
Searches for listing by INV_NO
Enters Client Email → send ticket
Captures proof (screenshot / confirmation URL) → returns to TicketOps
staff
Form
Mark dispatched
SENT ⌄
Or PENDING / ACCEPTED / ISSUE
21 Apr 2026 14:32
https://viagogo.com/confirmation/abc123
Sent to john.smith@meridiantravel.ae
Sequence (user-confirmed)
FEW supplier detected via SUPPLIER_manualGroupMap_() + email-pattern fallback
For every _PURCHASE_UNITS row from that supplier → row in supplier sheet (inventory listing)
Supplier can verify their own purchases against our records before any sale happens
Canonical data flow
① Client fills guest details → ② Distribution receives → ③ Allocation pushes needed detail to Supplier — client data only arrives at supplier via allocation, never directly.
Redaction (enforced at write-time)
Company = "MIRRA" hardcoded on every supplier row — on the supplier sheet this is letter-column N (14th column). User wording: "Column N hides or limits client details to protect privacy"