E12-F4: Admin Petrol Section (Unified Quota + Auto-QR)¶
Epic: E12: Physical Sales (Petrol)
Size: M (Medium)
Problem / Outcome¶
Provide a dedicated Petrol section in the admin portal that lets match managers create and manage Petrol quotas end-to-end — including the match-specific QR code that Petrol prints for retail distribution — without touching the generic Quota section and without an explicit "Generate QR" action.
Without this, petrol_deeplinks stays empty (or unused), no QR reaches Petrol, the mobile deeplink has nothing to open, and Petrol quotas get created inconsistently through the generic quota form (missing the Petrol type flag and/or deferred-payment flag).
Design Summary¶
- Petrol quotas are created inside the Petrol section, not the generic Quota section
- On create, the backend forces:
quota_type=PETROL,deferred_payment=TRUE,can_create_subquotas=FALSE,transfer_permission=NO - The QR code is auto-derived from the Petrol quota ID — deeplink is
hns://petrol-sales/{petrol-quota-id}. No explicit generate step, nopetrol_deeplinkstable needed - The Petrol section list shows all Petrol quotas with inline QR preview + SVG/PNG download
- Generic quota list shows Petrol quotas as read-only with a "Managed in Petrol section" link
Scope¶
In-Scope:
- New
/admin/petrol/quotassub-section in the Petrol area of the admin portal (list, create, detail, cancel) - Backend endpoints under
/admin/petrol/quotas/*that wrap the generic quota service with the Petrol-forced flags - On-demand QR rendering (SVG + PNG ≥ 600×600) at
/admin/petrol/quotas/{id}/qr.{svg,png}— deeplink derived from quota ID - Generic
POST /admin/quotasmust rejectquota_type=PETROLwith a redirect hint - Generic quota list displays Petrol quotas as read-only with link to Petrol section
- Deprecate the
petrol_deeplinkstable (see Data Model Impact)
Out-of-Scope:
- Scan analytics / telemetry (separate observability feature)
- Physical printing / poster design — Petrol handles downstream
- Mobile app deeplink handling for
/petrol-sales/{quota-id}— that's in E12-F1; this feature only ensures URL format alignment - Two-legged financial model implementation (customer-leg payment externality, Petrol deferred settlement integration) — tracked separately under E12 — see Admin Petrol Setup Flow → Financial Model
- QR rotation / token signing — intentionally not a first-class action; to "rotate" admin cancels and recreates the Petrol quota
Acceptance Criteria¶
- AC1: Admin creates a Petrol quota from
/admin/petrol/quotas— resulting quota hasquota_type=PETROL,deferred_payment=TRUE,can_create_subquotas=FALSE,transfer_permission=NO(forced by backend, not user-selectable) - AC2: At most one active Petrol quota exists per match (409 on duplicate create)
- AC3: Petrol quota list in Petrol section shows inline QR thumbnail, SVG/PNG download, copy-deeplink-URL, match context, counters (allocated/reserved/sold), deferred-payment status
- AC4: SVG and PNG (≥ 600×600) downloads decode back to
hns://petrol-sales/{petrol-quota-id} - AC5: Generic quota list shows Petrol quotas read-only with "Managed in Petrol section" link; edit/cancel disabled from generic section
- AC6: Generic
POST /admin/quotasreturns 400/422 with a clear error ifquota_type=PETROLis sent - AC7: Cancelling a Petrol quota follows standard cancellation rules; after cancel the deeplink resolves to "Petrol sales unavailable for this match" at the mobile side
- AC8: All create / cancel actions audit-logged with admin id, quota id, match id
Data Model Impact¶
petrol_deeplinks table → deprecated. The deeplink is derived from the Petrol quota ID (no storage needed). Options during implementation:
- Drop the table (cleanest): add a drop-table migration. Removes the partial-index concern from the earlier design.
- Keep the table but stop writing (safest): mark deprecated in schema docs, backend services stop inserting, readers stop being used. Leaves the physical table for a later cleanup sprint.
No new tables required. The quotas table needs a partial unique index to enforce AC2:
CREATE UNIQUE INDEX idx_quotas_petrol_active_per_match
ON quotas (match_id)
WHERE quota_type = 'PETROL' AND status = 'ACTIVE';
Permissions/Roles¶
ROLE_MATCH_MANAGER(or equivalent admin role that already manages match setup)
How to Verify¶
# Backend
./vendor/bin/phpunit tests/E12/PetrolQuotaAdminTest.php
# Admin portal
cd ../hns-admin-portal && npm run test:e2e -- --grep "petrol section"
Expected:
- Petrol quota created via
/admin/petrol/quotashas forced flags - Generic
POST /admin/quotasrejectsquota_type=PETROL - QR SVG/PNG decodes to
hns://petrol-sales/{quota-id} - At most one active Petrol quota per match
Dependencies¶
- E2: Match & Stadium Management — match must exist first
- E7-F1: Quota Creation API — Petrol endpoints wrap this
- QR code generation library (same dep as E9-F1 ticket QR generation)
Implementation Tasks¶
See E12: Physical Sales (Petrol) Tasks → F4.
Doc References¶
- Admin Petrol Setup Flow — unified flow (Petrol section owns quota + auto-QR)
- Physical Sales Petrol Station Flow — customer + POS end-to-end
- Deferred Payment Collection Flow — post-match settlement with Petrol
Last Updated: 2026-04-13