Skip to content

Flow: Admin Petrol Setup (Dedicated Petrol Section)

Actor

HNS Admin (Match Manager) preparing a match for Petrol-channel sales.

Preconditions

  • Admin is authenticated in the Admin Portal with ROLE_MATCH_MANAGER
  • Target match exists and has stadium/sector configuration
  • Inventory is sufficient for the Petrol allocation
  • QR code generation library available backend-side

Context

Petrol sales live in a dedicated Petrol section of the admin portal (not inside the generic Quota section). Creating a Petrol quota in that section automatically:

  • Flags the quota with the Petrol type identifier (enables PIN functionality at POS — per OQ-E12-2 resolution)
  • Forces deferred_payment = TRUE (Petrol settles with HNS post-match — see Financial Model below)
  • Provisions an auto-generated deeplink + QR code derived from the Petrol quota ID — no separate "Generate QR" action, no separate deeplinks table to manage

Petrol staff accounts are provisioned separately (out of scope for this flow; see account provisioning docs).

Financial Model — Two-Legged Settlement

Petrol sales have two distinct payment legs that must not be conflated:

Leg A — Customer ↔ Petrol (at the station):

  • Customer pays Petrol at the POS register using Petrol's own payment system (cash, card, voucher — however Petrol handles it)
  • This leg is entirely external to HNS — HNS does not collect, route, or reconcile this money
  • In the quota portal, Petrol staff simply mark the reservation as sold after collecting payment at their register (no payment_method or total_amount captured by HNS)

Leg B — HNS ↔ Petrol (deferred settlement):

  • HNS sells the entire Petrol allocation on deferred payment terms to Petrol as a B2B transaction
  • Petrol is effectively the quota recipient / counterparty — HNS invoices Petrol for the allocated ticket total (or adjusted to actually-sold total, per contract)
  • Settlement happens post-match via Deferred Payment Collection — e-racuni payment offer → bank transfer → reconciliation
  • Makes Petrol responsible for the full allocation (sold or unsold, per contract terms), which is why the allocation is pre-committed

Implication for the quota record: deferred_payment = TRUE, recipient email is the Petrol ops inbox (not a customer), and the quota appears in the Finance team's Deferred Payment Collection queue after match close.

Flow Steps

Step 1 — Navigate to Petrol Section

  • Admin opens Admin Portal sidebar → Petrol
  • Lands on the Petrol section overview, which shows:
    • List of all Petrol quotas across matches (see Step 4 for columns)
    • "Create Petrol Quota" CTA
    • Existing observability tabs (overview, pins) per current /admin/petrol scaffolding

Step 2 — Create Petrol Quota

  • Admin clicks "Create Petrol Quota"
  • Form fields (all in the Petrol section — admin does not touch the generic Quota section):
    • Match — picker, only shows matches with stadium config complete
    • Recipient email — Petrol operations inbox (e.g. petrol-ops@petrol.hr); pre-fillable from a configured default
    • Sectors — pre-allocated sectors agreed with Petrol (e.g. D1)
    • Quantity — total seats for this match's Petrol channel (e.g. 220)
    • Pricing — standard (no discount); Petrol settles at full price
    • Allocation algorithm — default REDOM (Sequential) — spreads seats across sector so the snake algorithm assigns them fairly across PIN lookups
    • Expiration — default: match date − 2 hours (configurable)
    • Hidden / forced flags (admin cannot change):
      • quota_type = PETROL
      • deferred_payment = TRUE
      • can_create_subquotas = FALSE
      • transfer_permission = NO
  • Admin saves. Backend:
    • Creates the quota with the forced flags
    • Allocates seats per the algorithm
    • The deeplink hns://petrol-sales/{petrol-quota-id} is immediately resolvable (no DB insert required — the quota ID is the stable reference)
    • Audit log records quota creation

Step 3 — View Auto-Generated QR in List

  • After save, admin returns to the Petrol quotas list (or the new quota's detail view)
  • The QR code is already available — it's rendered on demand from the quota ID
  • From the list row, admin can:
    • Preview QR inline (small thumbnail)
    • Click through to a detail panel/page showing the full QR, deeplink URL, and match context
    • Download SVG (print-ready scaling) or PNG (≥ 600×600 px) directly
    • Copy the deeplink URL to clipboard
  • Admin sends the SVG/PNG to Petrol via the agreed distribution channel (email / partner portal)

Step 4 — List View Columns

  • Match (teams, date, venue)
  • Recipient email
  • Quantity / Allocated / Reserved / Sold counters
  • Quota status (Active / Past Deadline / Cancelled)
  • Deferred payment status (Pending / Invoiced / Paid) — links to Deferred Payment Collection
  • QR preview thumbnail + download actions
  • Created / expires timestamps
  • Filters: match, quota status, deferred-payment status, date range

Step 5 — Ongoing Management

  • Edit — admin can adjust quantity, sectors, expiration within the same Petrol section (same restrictions as other quotas)
  • Cancel — cancels the quota; printed QR stops resolving (backend returns "Petrol sales unavailable for this match" to scans) because the quota is no longer in an active state. Follows standard quota cancellation rules (Cancel All Unused / Cancel Unfulfilled Only)
  • Rotate QR (rare) — not supported as a first-class action because the QR = quota ID. To "rotate", admin cancels and recreates — this is intentional: the operational cost (notifying Petrol, reprinting) should be deliberate, not a casual click

Step 6 — Visibility in Generic Quota Section

  • Petrol quotas are visible but read-only in the generic Quota section (for cross-channel reporting)
  • Clicking a Petrol quota row in that section shows a banner: "Managed in Petrol section →" with a deeplink
  • Admins cannot create / edit / cancel Petrol quotas from the generic Quota section — all operations happen in the Petrol section

Alternative Flows

A1: Match Not Yet Configured

  • Admin tries to create a Petrol quota for a match without stadium/sector config
  • System blocks: "Configure stadium for this match before creating a Petrol quota."

A2: Petrol Quota Already Exists For Match

  • Backend enforces at most one active Petrol quota per match (partial unique index on quotas (match_id) WHERE quota_type = 'PETROL' AND status = 'ACTIVE')
  • Form pre-check shows existing Petrol quota with a link; second create attempt returns 409

A3: Backend QR Library Failure

  • QR rendering is on demand — if it fails server-side at download time, admin sees a generic error and can retry. The quota itself is unaffected.

A4: Cancel With Active Reservations in Flight

  • Cancelling a Petrol quota follows standard quota cancellation rules — ALLOCATED seats released immediately; RESERVED seats (PINs in flight at a POS) behave per selected option (Cancel All Unused vs Cancel Unfulfilled Only)
  • Petrol staff attempting to complete a PIN against a cancelled quota see "Quota cancelled by admin" at the POS

Technical Requirements

Deeplink derivation:

  • Deeplink URL format: hns://petrol-sales/{petrol-quota-id} (or configured universal-link equivalent)
  • Mobile app resolves the quota ID → loads match details + sector availability for that Petrol quota
  • No petrol_deeplinks table needed in the new design. The backend-migrated petrol_deeplinks table becomes deprecated / optional (see Data Model Impact in E12-F4)

Backend endpoints (admin):

  • POST /admin/petrol/quotas — create Petrol quota (forces quota_type=PETROL, deferred_payment=TRUE). Body same shape as generic POST /admin/quotas minus the forced fields.
  • GET /admin/petrol/quotas — list Petrol quotas with filters (match, status, deferred-payment status, date range)
  • GET /admin/petrol/quotas/{id} — detail including computed deeplink URL + QR as SVG/PNG (or separate /qr.svg / /qr.png endpoints)
  • GET /admin/petrol/quotas/{id}/qr.svg — returns QR SVG for download
  • GET /admin/petrol/quotas/{id}/qr.png — returns QR PNG at ≥ 600×600
  • POST /admin/petrol/quotas/{id}/cancel — cancel with standard cancellation options (delegates to existing quota cancellation service)

Generic quota endpoints:

  • POST /admin/quotas must reject quota_type=PETROL — direct the caller to /admin/petrol/quotas instead
  • GET /admin/quotas continues to return Petrol quotas (for cross-channel reporting) but admin UI disables mutations on them

QR Payload:

  • Default: QR encodes the deeplink URL directly (= hns://petrol-sales/{quota-id})
  • Optional hardening: encode a signed token whose resolution lives server-side — defer to security review if adopted (a signed token would require a stored record, reintroducing something like petrol_deeplinks)

Universal Links:

  • iOS apple-app-site-association and Android assetlinks.json must include the petrol-sales/* path — coordinate with mobile team (E12-F1)

Mobile deeplink handler (E12-F1):

  • Needs to resolve /petrol-sales/{id} where {id} is a Petrol quota ID
  • Calls backend GET /petrol-quotas/{id}/context → returns match info + sector availability + Petrol quota status
  • If quota is CANCELLED or EXPIRED → shows "Petrol sales unavailable for this match" (no PIN generation)

Last Updated: 2026-04-13