Skip to content

Quota Portal Implementation Tasks

This document provides a sequenced, comprehensive task list for AI-assisted implementation of the HNS Quota Portal (Symfony Twig + Alpine.js frontend) — the self-service web interface where quota holders manage their ticket allocations, claim tickets, and delegate to sub-recipients.

Overview

Attribute Value
Technology PHP 8.2+ / Symfony 7.x / Twig
CSS SCSS (imported from HNS Drupal theme hns_theme)
Interactivity Alpine.js (reactive UI state, multi-step forms)
Build Gulp (SCSS compilation, JS minification)
Fonts Hustle (headings), Maven Pro (body) — same as Drupal webshop
Authentication Drupal SSO (same credentials as HNS mobile app)
API Backend Ticketing Backend/api/v1/quota-portal/* endpoints
Portal Spec Quota Portal
Epic E7 - Quota Management
API Spec API Specification
Design Source HNS Drupal Theme (hns_theme) — header, footer, forms, colors

Technology Decisions

Why SCSS instead of Tailwind? The HNS Drupal webshop (shop.hns.family) uses SCSS with CSS custom properties and the Gorko utility framework. To ensure visual consistency, the Quota Portal imports the same design tokens (colors, spacing, fonts, form styles) from the Drupal theme rather than introducing a separate design system.

Why Alpine.js instead of jQuery? The Drupal site uses jQuery + Drupal.behaviors, but the Quota Portal is a standalone Symfony app with complex interactive requirements (multi-step claiming wizard, dynamic card selection, real-time polling, Stripe integration). Alpine.js provides declarative reactivity in Twig templates without a full SPA framework, and is a better fit for these state-heavy interactions than jQuery.

Portal Responsibilities

The Quota Portal serves quota holders (sponsors, partners, officials, fan groups) for:

  • Viewing allocated quotas and seat-level detail per match
  • Claiming tickets through a guided 3-step process (select → holder info → payment)
  • Delegating tickets to sub-recipients via subquota creation (if permitted)
  • Managing and retracting unclaimed subquotas
  • Viewing claimed ticket history
File Scope
Ticketing Backend Tasks Backend API implementation
E7 Quota Management Tasks Backend quota API tasks
Admin Portal Tasks Admin-side quota management UI

User Flows

Flow Description
Quota Holder Web Delegation Primary flow for web portal usage
Mobile Quota Claiming Mobile app claiming (shared API)
Admin Quota Creation How quotas are created by admin

Task Legend

Symbol Status
Not started
🟡 In progress
Complete
Blocked

Implementation Sequence

Tasks are organized in dependency order:

  1. Phase 0: Docker Infrastructure — Docker setup, environment variables, networking, auth flow
  2. Phase 1: Foundation — Symfony project, design system import, layout (header/footer from Drupal)
  3. Phase 2: Dashboard & Quota Detail — My Quotas overview, quota detail view
  4. Phase 3: Ticket Claiming — 3-step claiming flow (select, holder info, payment)
  5. Phase 4: Subquota Management — Create, list, retract subquotas
  6. Phase 5: My Tickets & Polish — Claimed ticket history, error states, real-time sync
  7. Phase 6: Testing — Infrastructure, functional, access control, browser, integration

Using with Ralph Wiggum (Autonomous AI Loop)

This task list is designed for iterative AI implementation using the Ralph Wiggum plugin with RED/GREEN TDD.

TDD Workflow (Red/Green/Refactor)

For every task, follow this cycle:

  1. RED: Write a failing test that asserts the verification criteria
  2. GREEN: Write the minimum code to make the test pass
  3. REFACTOR: Clean up while keeping tests green
  4. VERIFY: Run full suite: docker-compose exec -T quota-php ./vendor/bin/phpunit
  5. UPDATE: Mark task ⬜ → ✅ in this file

Test Directory Structure

tests/
├── Phase0/                    # Docker connectivity smoke tests
├── Phase1/                    # Auth, layout, component rendering
│   ├── AuthTest.php
│   ├── LayoutTest.php
│   └── ComponentTest.php
├── Phase2/                    # Dashboard, quota detail
│   ├── DashboardTest.php
│   └── QuotaDetailTest.php
├── Phase3/                    # 3-step claiming wizard
│   ├── SeatSelectionTest.php
│   ├── HolderInfoTest.php
│   ├── PaymentTest.php
│   └── Validation/
│       ├── OibChecksumTest.php
│       ├── BlacklistTest.php
│       └── MinorDetectionTest.php
├── Phase4/                    # Subquota CRUD
│   ├── CreateSubquotaTest.php
│   └── RetractSubquotaTest.php
├── Phase5/                    # Edge cases, error states
│   ├── ErrorStatesTest.php
│   ├── CartTimeoutTest.php
│   └── RefundDisplayTest.php
└── Browser/                   # Playwright end-to-end tests
    ├── LoginFlowTest.php
    ├── ClaimingFlowTest.php
    └── SubquotaFlowTest.php

Quick Start

# Full implementation (all phases)
# See COMMANDS.md for the complete ralph-loop command

# Per-phase execution
# See COMMANDS.md for per-phase ralph-loop commands with TDD instructions

Progress Tracking

The AI should maintain PROGRESS.md in the project root:

# Implementation Progress

## Current State
- **Component:** Quota Portal
- **Phase:** 0
- **Section:** 0.1 Docker Setup
- **Task:** Create Dockerfile
- **Iteration:** 1
- **Tests:** 0 passing, 0 failing
- **Last Updated:** 2026-03-19T10:00:00Z

Phase 0: Docker Infrastructure & Environment

0.1 Docker Setup

Architecture Pattern

The Quota Portal follows the same Docker pattern as the Admin Portal (hns-admin-portal): PHP 8.4-FPM + Nginx reverse proxy, connecting to the Ticketing Backend API over Docker networking. The portal runs on port 8002 (backend: 8000, admin: 8001).

Docker Compose Configuration

Status Task Verification
Create Dockerfile based on php:8.4-fpm with extensions: intl, zip, opcache, redis docker build succeeds
Create docker/nginx/default.conf routing static files and FastCGI to PHP-FPM Nginx proxies to php:9000
Create docker-compose.yml with services: quota-php, nginx docker-compose up -d starts both
Expose Nginx on port 8003 (8003:80) Portal accessible at http://localhost:8003
Mount project directory to /var/www/html in both php and nginx containers Code changes reflected without rebuild
Create hns-quota-portal internal bridge network Internal network created
Connect to ticketing backend network as external: hns-ticketing-backend_hns-ticketing Portal can reach backend API
Create docker/docker-entrypoint.sh that runs composer install and cache:clear on startup Dependencies installed on first run
Create docker-compose-nebion.yml for deployment (no ports, Traefik-compatible) Deployment config ready
Create .nebion.yml with post-rollout tasks (composer install, cache clear) Nebion deployment configured

Makefile

Status Task Verification
Create Makefile with make start / make stop targets Start/stop with single command
Add make start-all to start backend + quota portal together All services start
Add make stop-all to stop entire stack All services stop
Add make setup for first-time setup (stop, start, install deps, build assets) Fresh setup works
Add make shell to open bash in PHP container Shell accessible
Add make build to compile SCSS assets via Gulp Assets compile
Add make dev to start Gulp watch mode for SCSS development SCSS recompiles on save
Add make cache-clear to clear Symfony cache Cache cleared
Add make info to show container status and URLs Status displayed

0.2 Environment Variables

.env (Base Defaults — committed)

APP_ENV=dev
APP_SECRET=<generate-unique-secret>
DEFAULT_URI=http://localhost:8003

# ─── Ticketing Backend API ───
BACKEND_API_URL=http://hns-ticketing-nginx:80/api/v1

# ─── Drupal SSO Authentication ───
DRUPAL_SSO_URL=http://host.docker.internal:8060
DRUPAL_API_PREFIX=/167ef06c8e2d572663138d287700485f

# ─── Stripe (for paid quota claiming — same test keys as Drupal webshop) ───
STRIPE_PUBLISHABLE_KEY=pk_test_51NIXAJIkFtloCtI1u6vqpR0XhnXQHt9L6bmza9QqoNPUG1umNKeE5MsMJYweJqpEm3m9vMm2Oeidjq7D5ZoYkdTa008atiaAF8
STRIPE_SECRET_KEY=sk_test_51NIXAJIkFtloCtI1o1guvyf2tGHCMYBHURT9yzRc08WSZq0PZJwLug6o4XkXoeTjfWuw6m2tMrj5EMYeNXBfFo6R00lA7nJb4I
STRIPE_WEBHOOK_SECRET=whsec_placeholder

# ─── Logging ───
LOKI_PUSH_URL=
LOKI_PUSH_USER=
LOKI_PUSH_PASSWORD=

Docker Networking Notes

  • BACKEND_API_URL uses hns-ticketing-nginx — the backend's Nginx container name reachable via the shared hns-ticketing-backend_hns-ticketing external network.
  • DRUPAL_SSO_URL uses host.docker.internal on macOS/Windows to reach Drupal running on port 8060 from inside the Docker container. On Linux, use the host IP or create a shared Docker network with Drupal.
  • DRUPAL_API_PREFIX is the hard-coded JSON:API base path from Drupal's web/sites/default/all.services.yml.

.nebion.env (Staging/Production — committed)

APP_ENV=prod
BACKEND_API_URL=https://back.hnst.dev.wsagency.io/api/v1/
DRUPAL_SSO_URL=https://shop.hns.family
DRUPAL_API_PREFIX=/167ef06c8e2d572663138d287700485f
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
DEFAULT_URI=https://quota.hnst.dev.wsagency.io

Environment Variable Reference

Variable Purpose Dev Value Prod Value
APP_ENV Symfony environment dev prod
APP_SECRET CSRF/cookie signing Random hex Unique per env
BACKEND_API_URL Ticketing Backend API base URL http://hns-ticketing-nginx:80/api/v1 https://back.hnst.dev.wsagency.io/api/v1/
DRUPAL_SSO_URL Drupal webshop for SSO validation http://host.docker.internal:8060 https://shop.hns.family
DRUPAL_API_PREFIX Drupal JSON:API path prefix /167ef06c8e2d572663138d287700485f Same
STRIPE_PUBLISHABLE_KEY Stripe frontend key (for Payment Element) pk_test_51NIXA... (same as Drupal) Production key
STRIPE_SECRET_KEY Stripe backend key (for PaymentIntent creation) sk_test_51NIXA... (same as Drupal) Production key
STRIPE_WEBHOOK_SECRET Stripe webhook signing secret whsec_placeholder Production secret
DEFAULT_URI Portal's own URL (for redirects) http://localhost:8002 https://quota.hnst.dev.wsagency.io
LOKI_PUSH_URL Grafana Loki endpoint (optional) Empty (disabled) Loki URL

Environment Tasks

Status Task Verification
Create .env with all variables listed above File exists with defaults
Create .nebion.env with production values File exists
Add .env.local to .gitignore (for local secret overrides) Not committed
Configure config/services.yaml to inject BACKEND_API_URL, DRUPAL_SSO_URL, DRUPAL_API_PREFIX, STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY Services receive env vars
Create BackendApiClient service that calls ticketing backend with JWT forwarding API calls succeed
Create DrupalSsoClient service that validates user credentials against Drupal SSO validation works
Test portal can reach backend: curl http://hns-ticketing-nginx:80/api/v1/health from php container Backend reachable
Test portal can reach Drupal: curl {DRUPAL_SSO_URL}/user/check?_format=json from php container Drupal reachable

0.3 Service Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                        Docker Host (macOS)                              │
│                                                                         │
│  ┌─────────────────────────┐  ┌──────────────────────────────────────┐ │
│  │  hns-quota-portal       │  │  hns-ticketing-backend               │ │
│  │  (network: hns-quota)   │  │  (network: hns-ticketing)            │ │
│  │                         │  │                                      │ │
│  │  nginx  ──→ php-fpm     │  │  nginx:8000 ──→ php ──→ postgres    │ │
│  │  :8002       :9000      │  │                    └──→ redis        │ │
│  │                         │  │                    └──→ mailpit:8026 │ │
│  └────────┬────────────────┘  └──────────────────────────────────────┘ │
│           │                            ▲                                │
│           │  external network:         │                                │
│           │  hns-ticketing-backend_    │                                │
│           │  hns-ticketing             │                                │
│           └────────────────────────────┘                                │
│                                                                         │
│  ┌─────────────────────────┐                                           │
│  │  hns (Drupal)           │                                           │
│  │  :8060 (via Traefik)    │ ◄── host.docker.internal:8060            │
│  │  nginx → php → mariadb  │     (from quota portal container)        │
│  └─────────────────────────┘                                           │
│                                                                         │
│  ┌─────────────────────────┐                                           │
│  │  hns-admin-portal       │                                           │
│  │  :8001                  │                                           │
│  └─────────────────────────┘                                           │
│                                                                         │
│  ┌─────────────────────────┐                                           │
│  │  hns-ticketing-docs     │                                           │
│  │  :9090 (MkDocs)         │                                           │
│  └─────────────────────────┘                                           │
└─────────────────────────────────────────────────────────────────────────┘

Port Allocation:

Service Port URL
Ticketing Backend API 8000 http://localhost:8000/api/v1
Admin Portal 8001 http://localhost:8001/admin
Quota Portal 8002 http://localhost:8002/quota-portal
Drupal Webshop 8060 http://localhost:8060
MkDocs (this docs) 9090 http://localhost:9090
Mailpit (email viewer) 8026 http://localhost:8026
PgAdmin (DB viewer) 5050 http://localhost:5050
ntfy (push viewer) 9091 http://localhost:9091/hns-push

0.4 Authentication Flow (Drupal SSO → JWT)

How Quota Portal Authenticates

The portal does NOT call Drupal directly for each page load. Instead:

  1. User enters credentials on portal login page
  2. Portal sends credentials to Ticketing Backend POST /api/v1/auth/login
  3. Backend validates against Drupal (Firebase SSO or Basic Auth proxy)
  4. Backend returns JWT (15-min access token) + refresh token (7-day)
  5. Portal stores tokens in server-side Symfony session (HTTP-only cookies)
  6. All subsequent API calls use JWT in Authorization: Bearer {token} header

The portal talks to one backend — the Ticketing Backend handles all Drupal SSO complexity internally via DrupalSsoService.

Status Task Verification
Implement BackendAuthService that calls POST /api/v1/auth/login with user credentials Login returns JWT
Support Firebase SSO: forward firebase-token header from portal to backend auth endpoint Firebase login works
Support Basic Auth: forward email + password as JSON to backend auth endpoint Password login works
Store access_token, refresh_token, token_expires_at in Symfony session Tokens persisted server-side
Create BackendApiClient HTTP service that auto-attaches Authorization: Bearer header API calls authenticated
Implement token refresh: call POST /api/v1/auth/refresh before access token expires Token refreshes transparently
Handle 401 from backend: clear session and redirect to login Re-auth flow works

Phase 1: Foundation

1.1 Project Setup

Symfony Bundle Configuration

Status Task Verification
Create Symfony 8.0 project with Twig templates directory (templates/quota_portal/) ls templates/quota_portal/ shows structure
Set up SCSS build pipeline with Gulp (matching Drupal theme's gulpfile.js pattern) npm run start watches SCSS, npm run compile builds
Install and configure Alpine.js via CDN for reactive components Alpine x-data directives work in Twig templates
Create base layout template (base.html.twig) with header, content, footer regions Layout renders matching Drupal site structure
Set up route prefix /quota-portal/* with firewall Routes resolve correctly
Configure CORS for API calls to ticketing backend (/api/v1/quota-portal/*) API calls succeed from portal

Design System Import (from HNS Drupal Theme)

Source Theme

All design tokens are imported from the Drupal theme at hns/web/themes/custom/hns_theme/scss/base/. The Quota Portal must look like a section of the HNS webshop, not a separate application.

Status Task Verification
Import CSS custom properties from Drupal theme _colors.scss into portal :root All --deep-red, --darkblue, --scarlet-red, etc. available
Import spacing scale from _spacings.scss (--spacing-tiny through --spacing-huge) Spacing variables match Drupal site
Import border radius tokens from _radius.scss (--radius-xs through --radius-large) Radius variables available
Import font weight scale from _font-weight.scss Weight variables match
Import font files: Hustle (headings) from assets/fonts/Hustle/ (woff2, woff, ttf) Hustle font renders for h1-h6
Import font files: Maven Pro (body) via Google Fonts CDN Maven Pro renders for body text
Import typography scale from _typography.scss (clamp-based fluid sizing for h1-h6) Heading sizes match Drupal site
Import form element styles from components/form.scss (inputs, labels, checkboxes, radios) Form elements match Drupal site exactly
Import button styles from components/button/button.scss (data-variant pattern) Buttons match Drupal site (darkblue, red, plain, cancel variants)
Import modal dialog styles from components/modal/modal.scss Modals match Drupal site
Add quota-specific status colors not in Drupal theme (ALLOCATED gray, DELEGATED purple) Status colors extend palette
Import SVG icon system — using inline SVGs in Twig templates Icons render inline
Set up responsive breakpoints matching Drupal: sm (36em/576px), md (48em/768px), lg (62em/992px) Breakpoints match
Create equivalent utility classes (flex, margin, padding, gap utilities) Utility classes available

1.2 Authentication (Drupal SSO)

Login Flow

Authentication Architecture

The Quota Portal uses the same credentials as the HNS mobile app. Authentication flows through Drupal SSO → Ticketing Backend JWT. The backend auth services are set up in Phase 0.4 above. See Drupal Auth Proposal for architecture details.

Status Task Verification
Create login page (/quota-portal/login) matching Drupal webshop login page styling Page renders with HNS logo, --darkblue accents
Implement email + password login form using Drupal form styling (48px inputs, Maven Pro) Form matches webshop login
Implement Firebase SSO buttons (Google, Facebook, Apple) — placeholder UI SSO buttons visible and clickable
Call POST /api/v1/auth/login with Drupal credentials (Firebase token or Basic Auth) Backend returns JWT + refresh token
Store JWT access token and refresh token in secure HTTP-only session Tokens stored securely
Implement token refresh logic (access token TTL: 15 min, refresh: 7 days) Token refreshes before expiry
Implement logout with session termination Logout clears all tokens
Redirect unauthenticated users to login page Redirect works for all protected routes

Email-Based Access Control

Access Control

Quota access is email-based, not role-based. The portal only displays quotas where recipient_email matches the authenticated user's email.

Status Task Verification
Implement BackendAuthenticator that validates JWT and extracts user email Email extracted from token
Create QuotaPortalUser security model with email as identifier User model works
Configure Symfony security firewall for /quota-portal/* routes Firewall protects routes
Implement access check: verify user email has at least one active quota Users without quotas see "No quotas" message
Handle deeplink entry (/quota-portal/login?email={email}&quota_id={id}) Deeplink pre-fills email and redirects after login
If user already logged in with different email than deeplink, show mismatch notice: "This quota is assigned to {quota_email}. Please login with the correct account." Mismatch notice shown
Provide "Login with Different Account" option that logs out and pre-fills correct email Switch account works

Session Management

Status Task Verification
Implement token refresh interceptor middleware (auto-refresh before expiry) Token refreshes transparently
Add session timeout warning (5 min before JWT expiry) Warning modal appears
Implement automatic token refresh on user activity Session extends on interaction
Handle expired session gracefully (redirect to login with return URL) User can resume after re-login
Implement auto-logout after JWT expiry if refresh also expired User redirected to login

1.3 Layout & Navigation

Header (from Drupal Theme)

Shared Header

The header must visually match the HNS webshop (shop.hns.family) header. Replicate the structure from hns_theme/templates/components/header/region--header.html.twig with different navigation links appropriate for the Quota Portal.

Status Task Verification
Replicate Drupal header structure: logo (left), navigation (center), user controls (right) Header matches webshop visually
Use HNS logo.svg from Drupal theme assets/ Same logo as webshop
Style header with --darkblue (#172641) background, Hustle font for nav items, uppercase Header colors/fonts match
Add portal-specific nav links: My Quotas, My Tickets Portal links visible
Add user email display and logout button (right side) User controls visible
Implement hamburger menu for mobile (below 991px breakpoint) matching Drupal pattern Mobile menu works
Add active state: white text on --vivid-red (#C10000) background for current page Active state matches
Add hover state: --vivid-red text color on nav items Hover matches

Shared Footer

The footer must visually match the HNS webshop footer. Replicate the structure from hns_theme/templates/components/footer/region--footer.html.twig.

Status Task Verification
Replicate Drupal footer structure with --blue (#111C30) background Footer matches webshop visually
Include footer sections: Support links, Terms, Payment Methods Sections present
Implement mobile accordion pattern for footer sections (below 991px) Accordion works on mobile
Style footer links: Maven Pro 0.875rem, hover color --red (#D90429) Link styling matches

Page Layout

Status Task Verification
Create .layout-container wrapper matching Drupal theme (max-width: 1800px, centered) Layout matches
Create breadcrumb component (matching Drupal breadcrumb styling) Breadcrumbs show path
Create main content area with .stack layout pattern (vertical flex with gap) Content area uses stack pattern
Make layout responsive (desktop + tablet; mobile users directed to app) Layout adapts to screen size
Status Task Verification
Create navigation menu items with icons Menu renders with icons
Implement active state highlighting Current page highlighted
Show/hide "Create Subquota" based on can_create_subquotas flag (template-ready, data from API) Conditional rendering works

Menu Structure:

Quota Portal
├── My Quotas (Dashboard)
├── My Tickets
└── Logout

Per-quota actions (shown in quota detail):

Quota Detail
├── Allocation Summary
├── Seat List
├── Claim Tickets (3-Step)
├── Subquota List (if can_create_subquotas)
│   ├── Create Subquota
│   └── Retract Subquota
└── Back to Dashboard


1.4 Shared Components

Form Components (matching Drupal theme styling)

Form Styling Reference

All form elements must match the HNS webshop forms exactly. Source: hns_theme/scss/components/form.scss

Input specs: 2px solid --greyish-blue (#D7DCE0) border, 3px radius, 48px height, Maven Pro 500 weight, --darkblue text, --grey placeholder. Focus: --darkblue border. Error: --red border. Hover: subtle box-shadow.

Status Task Verification
Create text input component matching Drupal form styles (48px height, 2px border, 3px radius) Input matches webshop
Create email input component with format validation Email validation works
Create date picker component SCSS (Flatpickr styles ready, library to be added in Phase 3) Date selection works
Create dropdown/select component SCSS (Choices.js styles ready, library to be added in Phase 3) Dropdown matches webshop
Create OIB input with ISO 7064 Mod 11,10 checksum validation (Alpine.js formValidator) OIB validates correctly
Create phone input component SCSS (intl-tel-input styles ready, library to be added in Phase 3) Phone input matches webshop
Create custom checkbox component matching Drupal: 20x20px, 2px --darkblue border, --red checkmark Checkboxes match webshop
Create custom radio button component matching Drupal: 20px circle, --red center dot Radios match webshop
Create form labels: Maven Pro 500 weight, 0.9rem, --darkblue, red asterisk for required Labels match webshop
Create form validation service (client-side Alpine.js formValidator() with OIB, email, required) Validation runs on blur/submit
Create form error summary component Errors summarized at top
Add ARIA labels and accessible attributes to all form controls Screen reader accessible
Implement keyboard navigation for all interactive components Tab/Enter/Escape work

Data Display Components

Status Task Verification
Create data table component with sorting and pagination (styled with Drupal table patterns) Table renders with controls
Create responsive data table (horizontal scroll on small screens) Table usable on tablet
Create quota card component using Drupal card pattern (white bg, --radius-normal, hover: 2px black outline) Cards match webshop product card feel
Create badge/tag component for statuses (ALLOCATED, RESERVED, SOLD, DELEGATED, CANCELLED) Badges show correct colors
Create progress bar component (for status breakdown) Progress displays
Create step indicator component (Step 1/2/3 for claiming) Steps show progress
Create countdown timer component (for 20-min cart TTL during claiming) Timer counts down accurately
Create button variants using data-variant attribute pattern matching Drupal theme Buttons use data-variant="darkblue", "red", "plain", "cancel"

Feedback Components

Status Task Verification
Create toast notification system matching Drupal message.scss pattern Toasts appear with correct styling
Create modal dialog component matching Drupal modal (white bg, --athensgray title bar, 860px max-width) Modals match webshop
Create confirmation dialog (retract subquota, dangerous actions) Confirmation required
Create loading spinner matching Drupal AJAX spinner (--darkblue + --red rotating gradient) Loading states match webshop
Create empty state component ("No quotas assigned") with --back-grey background Empty states render
Create error state component (API failures, network errors) Errors displayed gracefully

Status Color Definitions

Status Color Badge
ALLOCATED Gray Available for claiming
RESERVED Blue Info filled, pending payment
SOLD Green Completed
DELEGATED Purple Assigned to subquota
CANCELLED Red Cancelled by admin
Active Quota Green badge Quota is active
Past Deadline Yellow badge Past expiration (still claimable)
Fully Claimed Blue badge All seats sold/delegated
Cancelled Quota Red badge Admin cancelled
Pending Quota Gray badge Created but not yet accessed

Phase 2: Dashboard & Quota Detail (E7-F4)

2.1 My Quotas Dashboard

Quota List

Reference: E7-F4: Quota Web Portal Dashboard

Status Task Verification
Create dashboard page (/quota-portal/dashboard) Page renders
Call GET /api/v1/quota-portal/dashboard to fetch user's quotas API called, data returned
Display quota cards grouped by match Cards grouped correctly
Show match info on each card (teams, date, venue) Match details visible
Show allocated sectors on each card (D1, C1, B2, etc.) Sectors listed
Show total seat count per quota Count displayed
Show status breakdown counters (Allocated, Reserved, Sold, Delegated) with percentages Breakdown accurate
Show expiration date with countdown ("5 days remaining") Countdown visible
Show quota status badge (Active = green, Past Deadline = yellow, Fully Claimed = blue, Cancelled = red) Badges colored correctly
Show discount info if applicable (e.g., "100% Sponsor Discount" or "Free" for GR code) Discount visible
Show quota creation date Date displayed
Distinguish received subquotas from own quotas (show "Delegated from: {parent name}" if parent_quota_id set) Subquota origin visible
After partial claiming, show updated remaining count ("15 available, 5 claimed") Remaining count accurate

Quota Card Actions

Status Task Verification
Add "Claim Tickets" button (primary CTA, visible when ALLOCATED seats exist) Button visible
Add "View Details" button (always visible) Button works
Add "Send tickets to someone else" button (only if can_create_subquotas = TRUE) Conditional rendering works
Hide action buttons for cancelled quotas Buttons hidden
Show "Past Deadline" warning banner for expired quotas (still claimable) Warning visible

Quota Card Layout

Reference Layout

See Quota Portal - Dashboard for the detailed card wireframe layout.

Status Task Verification
Implement responsive card grid (1 column mobile, 2 columns tablet, 3 columns desktop) Grid adapts
Implement card hover state with subtle shadow Hover effect visible
Sort cards by match kickoff date (upcoming first) Sort order correct
Show "No quotas assigned" empty state when user has no quotas Empty state renders

2.2 Quota Detail View

Allocation Summary

Status Task Verification
Create quota detail page (/quota-portal/quotas/{id}) Page renders
Validate quota belongs to logged-in user (email match) Access denied for wrong user returns 403
Show match details header (teams, date, venue, competition) Info displayed
Show allocated sectors list Sectors shown
Show discount information (percentage or "Free" for GR code) Discount visible
Show expiration date with visual reminder Date shown
Show transfer permission indicator (YES/NO) with explanation text Indicator visible
Show transfer rules help text: "YES = recipients can self-transfer, NO = contact support within 48h before match" Help text visible
Show quota creation date Date displayed
Show deferred payment indicator if deferred_payment = TRUE Indicator visible
Show "Past Deadline" warning if past expiration date (still claimable until admin cancels) Warning displayed

Status Breakdown

Reference Layout

See Quota Portal - Status Breakdown for the progress bar wireframe.

Status Task Verification
Create horizontal stacked progress bar showing seat status distribution Bar renders
Color segments: gray (Allocated), blue (Reserved), green (Sold), purple (Delegated) Colors correct
Show legend with counts below bar Legend accurate
Update breakdown on page load from API response seat_status_counts Counts match API

Seat List Table

API Limitation

The current GET /quota-portal/dashboard endpoint returns quotas with seat_status_counts but may not return individual seat details. A separate GET /quota-portal/quotas/{id}/seats endpoint may be needed — coordinate with backend team.

Status Task Verification
Create seat list data table (Sector, Row, Seat, Status, Assigned To, Claimed Date) Table renders
Show seat numbers for numbered matches Numbers visible
Color-code status column with badges Badge colors correct
Show ticket holder name for SOLD seats Name displayed
Show "→ Subquota #{recipient_name}" reference for DELEGATED seats Reference shown
Show "Pending..." for RESERVED seats Pending text visible
Add sorting by sector, row, seat number Sorting works
Add status filter (show only Available, Sold, etc.) Filter works
Show CANCELLED seats with strikethrough styling Cancelled seats visually distinct

Detail Page Actions

Status Task Verification
Add "Claim Tickets" button (navigates to claiming flow) Button navigates
Add "Send tickets to someone else" button (if can_create_subquotas = TRUE) Conditional button works
Add "Back to Dashboard" navigation Back link works
Create tabbed layout: Details / Seats / Subquotas (if permitted) Tabs navigate

2.3 Subquota List (on Quota Detail)

Status Task Verification
Show subquota section only if can_create_subquotas = TRUE Conditional rendering
Call GET /api/v1/quota-portal/quotas/{id}/subquotas to fetch subquotas API called
Display subquota table (Recipient, Email, Seats, Status, Created, Actions) Table renders
Show subquota status badges (Yellow=ALLOCATED, Blue=RESERVED, Green=SOLD, Red=Cancelled) Badges colored
Show seat count per subquota Count displayed
Show seat-level status breakdown per subquota (which seats are ALLOCATED/RESERVED/SOLD) Seat detail visible
Add "View" action button on each subquota row Button works
Add "Retract" action button (only for ALLOCATED subquotas) Button visible only for ALLOCATED
Add "Create Subquota" button above table Button navigates
Show empty state when no subquotas exist Empty state renders
Implement polling refresh for subquota list (detect when recipients claim tickets) Status updates without page reload

Phase 3: Ticket Claiming (E7-F5)

3.1 Claiming Flow: Step 1 — Select Seats

Reference: E7-F5: Quota Claiming Process

Numbered Matches (Seat Selection)

Card Grid, Not Seat Map

Tickets are displayed as cards in a grid layout, NOT as a stadium seat map. Each card represents one allocated seat.

Status Task Verification
Create seat selection page (/quota-portal/quotas/{id}/claim/step-1) Page renders
Display step indicator (Step 1 of 3: Select Seats) Indicator shows
Fetch available (ALLOCATED) seats from quota detail API Seats loaded
Display seats as clickable card grid (sector, row, seat number per card) Cards displayed
Implement click-to-select/deselect on each card Selection toggles
Highlight selected cards (border color change, checkmark icon) Selected cards visually distinct
Gray out unavailable seats (SOLD, DELEGATED, RESERVED) Unavailable seats grayed
Group cards by sector with section headers Grouped by sector
Show selection summary at bottom ("Selected: 3 seats — Row 5: Seats 1-3") Summary updates
Add "Select All Available" button Selects all ALLOCATED
Add "Clear Selection" button Clears all
Validate at least 1 seat selected before continue Validation blocks empty
Add "Back to Quota" link Navigation works
Add "Continue to Step 2" button (calls POST /api/v1/quota-portal/quotas/{id}/select-seats) API called, seats transition to RESERVED
Start 20-minute cart countdown timer after seat selection submitted Timer starts on API success
Display countdown timer in sticky header/footer across all claiming steps Timer visible on Steps 1-3

Non-Numbered Matches (Quantity Selection)

Status Task Verification
Detect non-numbered match type Detection works
Show quantity selector instead of card grid Quantity input shown
Show available quantity (total ALLOCATED count) Available count displayed
Validate quantity between 1 and available Validation works
Add increment/decrement buttons Buttons work

3.2 Claiming Flow: Step 2 — Ticket Holder Information

Per-Ticket Identity Form

Status Task Verification
Create holder info page (/quota-portal/quotas/{id}/claim/step-2) Page renders
Display step indicator (Step 2 of 3: Ticket Holder Information) Indicator shows
Show important banner: "Email Required: Each ticket will be delivered to the HNS mobile app linked to the email address you provide. All tickets with the same email must enter the stadium together on the same device." Banner visible
Show additional note below banner: "If someone doesn't have a mobile phone or email address, use the email of the person who will accompany them and have both tickets on the same device." Note visible
Create per-ticket form sections (one form per selected seat) Forms rendered per seat
Show seat reference on each form (e.g., "Ticket 1 of 3 — Row 5, Seat 1") Reference visible
Implement form fields: Full Name (required), Date of Birth (required), Nationality (required dropdown), OIB (required for Croatians), Passport (required for foreigners), Email (required), Phone (optional) All fields present
Pre-fill email with logged-in user's email (editable) Pre-fill works
Show OIB field when nationality = Croatia, Passport field otherwise Conditional fields toggle
Implement OIB checksum validation (ISO 7064 Mod 11,10) OIB validates
Detect duplicate emails across ticket forms and show warning: "Tickets with the same email will be on the same device and must enter the stadium together" Warning displayed
Handle blank email field: if left empty, show note "Ticket will remain on your app. You must enter the stadium together with this person." Blank email handled
Show email finality warning near email field: "Email assignment is final and cannot be changed after claiming" Warning visible
Implement minor detection: if DOB indicates under 18, disable email field Email disabled for minors
Show minor restriction message: "Tickets for minors cannot be assigned to a separate email. Minor must enter with accompanying adult." Message visible
Implement progress indicator ("2 of 5 tickets completed") Progress updates

Saved Profiles

Status Task Verification
Add "Select from saved profiles" dropdown at top of each form Dropdown available
Populate dropdown from localStorage (previously saved profiles) Profiles loaded
Auto-fill form fields when profile selected Fields populated
Add "Save as new profile" checkbox Checkbox available
Save profile to localStorage on form submit if checked Profile persisted
Add "Enter new details" option in dropdown Option available
Add "Manage saved profiles" option (view, edit, delete saved profiles) Profile management works

Validation & Blacklist

Status Task Verification
Implement client-side form validation (required fields, email format, OIB checksum) Validation runs
Show inline validation errors per field Errors shown inline
Implement silent blacklist check on OIB/passport submission Check runs in background
Show exact blacklist rejection message: "HNS is prevented from issuing a ticket to this person pursuant to applicable law. For all information regarding this restriction, please contact MUP (Ministry of Interior)." Error displayed with exact text
Block only the specific blacklisted ticket, allow remaining tickets to proceed Partial claiming works around blacklisted entry
Return rejected ticket(s) to quota holder's available balance Rejected seats return to ALLOCATED
Add "Back to Seat Selection" button Navigation works
Add "Continue to Payment" button (submits all holder data) Submits POST /api/v1/quota-portal/quotas/{id}/ticket-holders
Handle API error responses (seat no longer available, validation errors) Errors shown

3.3 Claiming Flow: Step 3 — Payment

Order Summary

Status Task Verification
Create payment page (/quota-portal/quotas/{id}/claim/step-3) Page renders
Display step indicator (Step 3 of 3: Payment) Indicator shows
Show order summary: ticket count, sector, row numbers Summary accurate
Show original price per ticket Price shown
Show discount calculation (percentage and amount) Discount displayed
Show final total after discount Total correct
Add "Back to Holder Info" button Navigation works

Free Quotas (discount_code = GR)

Status Task Verification
Detect free quota (discount = 100% or discount_code = GR) Detection works
Skip payment form for free quotas Form hidden
Show "Total: €0.00" with "Complete Claim" button Zero total shown
Call POST /api/v1/quota-portal/quotas/{id}/payment without payment intent API called

Deferred Payment Quotas

Deferred Payment

When quota.deferred_payment = TRUE, payment is collected later (post-match invoicing). The claiming flow completes without Stripe, similar to free quotas but with a different message.

Status Task Verification
Detect deferred payment quota (deferred_payment = TRUE) Detection works
Skip Stripe payment form for deferred quotas Form hidden
Show order summary with note: "Payment will be invoiced separately" Deferred note visible
Call POST /api/v1/quota-portal/quotas/{id}/payment without stripe_payment_intent_id API auto-completes
Show confirmation with deferred payment acknowledgment Confirmation shows deferred status

Stripe Implementation Reference

Uses the same Stripe account as the HNS Drupal webshop (shop.hns.family). The Drupal site uses Stripe Payment Element with stripe_payment_element plugin (Commerce Stripe v2.1.0). The Quota Portal should use the same Stripe Payment Element approach for visual consistency and feature parity (card, Apple Pay, Google Pay, PayPal).

Test cards: 4242 4242 4242 4242 (Visa), 5555 5555 5555 4444 (Mastercard), any future date, any CVC.

Status Task Verification
Load Stripe.js from https://js.stripe.com/v3/ with publishable key from STRIPE_PUBLISHABLE_KEY env var Stripe loaded
Create Stripe Payment Element (stripe.elements()elements.create('payment')) matching Drupal's stripe_payment_element approach Payment form renders with card, Apple Pay, Google Pay
Create PaymentIntent on server via STRIPE_SECRET_KEY before rendering payment form (amount, currency EUR, metadata: quota_id, match_id) PaymentIntent created with client_secret
Pass clientSecret to Stripe Elements for frontend rendering Payment Element initializes
Confirm payment via stripe.confirmPayment({ elements, confirmParams: { return_url } }) Payment processes
On Stripe success, call POST /api/v1/quota-portal/quotas/{id}/payment with stripe_payment_intent_id Backend confirms and marks seats SOLD
Handle payment success → redirect to confirmation page Redirect works
Handle payment failure → show Stripe error message, allow retry Error shown, retry available
Handle 3D Secure / SCA authentication redirect and return 3DS works
Display accepted payment methods: Visa, Mastercard, Apple Pay, Google Pay, PayPal (matching Drupal's gateway display label) Logos displayed
Style Stripe Payment Element appearance to match Drupal theme (Maven Pro font, --darkblue colors, 3px radius) Stripe element visually matches site

Confirmation Screen

Status Task Verification
Create confirmation page (/quota-portal/quotas/{id}/claim/confirmation) Page renders
Show success message with green checkmark Success visible
Show claimed ticket summary (match, seats, holders) Summary accurate
Show message: "Tickets have been delivered to recipients' HNS mobile apps" Message visible
Show order number/reference Reference displayed
Add "Back to Dashboard" button Navigation works
Add "Claim More Tickets" button (if more ALLOCATED seats exist) Button conditionally shown
Show confirmation email notification: "A confirmation email has been sent to {email}" Email notice visible
Show invoice/receipt link: "PDF invoice has been generated" (e-racuni.com integration) Invoice link visible

Phase 4: Subquota Management (E7-F6)

4.1 Create Subquota

Reference: E7-F6: Subquota Creation

One-Level Delegation Only

Sub-recipients cannot create further sub-subquotas. The backend enforces can_create_subquotas = FALSE on all subquotas regardless of parent setting.

Seat Selection for Delegation (Numbered Matches)

Status Task Verification
Create subquota page (/quota-portal/quotas/{id}/subquota/create) Page renders
Verify can_create_subquotas = TRUE before rendering page 403 if not permitted
Display available (ALLOCATED) seats as selectable card grid Cards displayed
Implement click-to-select/deselect on each card Selection toggles
Gray out unavailable seats (SOLD, RESERVED, already DELEGATED) Unavailable grayed
Show selection summary ("Selected for subquota: 3 seats") Summary updates
Validate at least 1 seat selected Validation blocks empty

Seat Selection for Delegation (Non-Numbered Matches)

Status Task Verification
Detect non-numbered match and show quantity selector instead of card grid Quantity input shown
Show available quantity for delegation Available count displayed
Validate quantity between 1 and available ALLOCATED count Validation works

Recipient Details Form

Status Task Verification
Create recipient details form below seat selection Form renders
Add Recipient Email field (required, format validated) Email validates
Add Recipient Name field (required) Name required
Add Internal Note field (optional) Note saved
Show note: "Multiple subquotas to the same email are allowed" Note visible
Add "Claim for Myself" alternative button (navigates to claiming flow instead of delegation) Alternative action works
Add "Cancel" button (returns to quota detail) Cancel navigates
Add "Create Subquota" button (submits form) Button submits

Subquota Submission

Status Task Verification
Call POST /api/v1/quota-portal/quotas/{id}/subquotas with seat_ids, email, name API called
Show subquota inheritance info: "Subquota will inherit: {discount}%, transfer={YES/NO}, expires={date}" Inheritance preview shown
Handle success: show toast "Subquota created. Email and push notification sent to {email}" Toast appears
Redirect to quota detail page after creation Redirect works
Handle API errors (seats no longer available, permission denied) Errors displayed
Show loading state during submission Spinner visible
Verify parent seats transition to DELEGATED status after API success Parent seat list updates

4.2 Retract Subquota

Backend Endpoint Note

The SubquotaService.retractSubquota() method exists in the backend but the controller endpoint may not yet be exposed in the OpenAPI spec. Coordinate with backend team to ensure DELETE /quota-portal/quotas/{id}/subquotas/{subquotaId}/retract is available.

Status Task Verification
Add "Retract" button on subquota rows with status = ALLOCATED Button visible only for ALLOCATED
Show confirmation dialog: "Are you sure? {Recipient Name} will be notified that their allocation has been revoked." Confirmation shown
Call DELETE /api/v1/quota-portal/quotas/{id}/subquotas/{subquotaId}/retract on confirm API called
Handle success: show toast "Subquota retracted. Seats returned to your allocation." Toast appears
Refresh subquota list and status breakdown after retraction UI updates
Handle error with exact message: "Cannot retract subquota. Sub-recipient has already started or completed claiming process." Error message shown with exact text
Disable "Retract" button for RESERVED, SOLD, or CANCELLED subquotas Button disabled
Show tooltip on disabled "Retract" button: "Subquotas can only be retracted if sub-recipient hasn't started claiming" Tooltip visible on hover

4.3 Subquota Detail View

Status Task Verification
Create subquota detail modal or inline expansion Detail view opens
Show recipient info (name, email) Info displayed
Show delegated seat list with per-seat status (sector, row, seat, status) Seats listed with individual statuses
Show overall subquota status (ALLOCATED, RESERVED, SOLD) Status shown
Show seat status breakdown bar (same component as quota detail) Breakdown visible
Show creation date Date displayed
Show claiming progress (if recipient has started) Progress shown
Show inherited properties: discount, transfer permission, expiration Properties visible
Show "Allocated by: {parent quota holder name}" attribution Attribution visible

Phase 5: My Tickets & Polish

5.1 My Tickets Page

Status Task Verification
Create My Tickets page (/quota-portal/my-tickets) Page renders
Fetch tickets where logged-in user is the ticket holder Data loaded
Display tickets in data table (Match, Seat, Ticket Holder, Status, Actions) Table renders
Show match info (teams, date, venue) per ticket Match info visible
Show seat info (sector, row, seat number) Seat info visible
Show status badge (SOLD, Transferred, Refunded) Badges colored
Add "View Ticket" action to open ticket detail Detail opens
Show empty state when no claimed tickets Empty state renders
Sort by match date (upcoming first) Sort works

5.2 Error States & Edge Cases

Quota Status Edge Cases

Status Task Verification
Handle PENDING quota: show "Quota is being set up" if status = PENDING Message displayed
Handle cancelled quota: show exact message "This quota has been cancelled by the administrator." Message displayed with exact text
For cancelled quotas, show previously SOLD tickets as still valid SOLD tickets remain visible
Handle past deadline: show yellow "Past Deadline" warning but allow all actions (no auto-expiry) Warning visible, actions work
Handle FULLY_CLAIMED quota: hide "Claim Tickets" button, show "All tickets claimed" Button hidden
Handle empty quota (all seats cancelled): show appropriate message Message displayed
Handle partially claimed quota: show remaining available count, keep "Claim Tickets" button Button visible with updated count

Refund Impact Display

Refund Behavior

When a ticket claimed from a subquota is refunded, the seat does NOT return to the subquota owner. The seat is treated as used. Admin must manually create a new allocation if needed.

Status Task Verification
Show "Cancelled (Refunded)" status for refunded seats in seat list Refund status visible
Do not show refunded seats as available for re-claiming Refunded seats excluded from claiming

Admin Cancellation Impact

Cancellation Types

Admin can cancel with two options: "Cancel All Unused" (ALLOCATED + RESERVED) or "Cancel Unfulfilled Only" (only ALLOCATED, preserving RESERVED tickets for completion).

Status Task Verification
Handle "Cancel All Unused": both ALLOCATED and RESERVED seats show as cancelled All unused cancelled
Handle "Cancel Unfulfilled Only": RESERVED seats remain claimable (can complete payment) RESERVED seats preserved
Show cancellation notification banner on affected quota Banner visible
Show cancelled subquotas in subquota list with red badge Cancelled subquotas visible

Network & API Error Handling

Status Task Verification
Handle network timeout during claiming (show retry option) Retry available
Handle 401 Unauthorized (redirect to login) Redirect works
Handle 403 Forbidden (email mismatch: "You don't have access to this quota") Error shown
Handle 404 Not Found (quota doesn't exist) 404 page shown
Handle 409 Conflict (seat no longer available during claiming) Conflict error handled
Handle 500 Server Error (generic error message with retry) Error page shown
Handle payment gateway timeout (show order status check) Status check available

Cart Timeout Handling

20-Minute Cart TTL

Quota claim carts expire after 20 minutes. The portal must handle timeout gracefully during the 3-step claiming process.

Status Task Verification
Show countdown timer during claiming flow (20 min from seat selection) Timer visible
Show warning at 5 minutes remaining Warning appears
Handle cart expiration: show message "Your session has expired. Selected seats have been released." Expiration handled
Show "Restart Claiming" button that returns to Step 1 to re-select seats Restart CTA visible
Redirect to quota detail if user dismisses expiration modal Redirect works

5.3 Real-Time Sync

Dual-Channel Sync

Changes made in the mobile app must reflect in the web portal and vice versa. The portal should refresh data when navigating between pages.

Status Task Verification
Refresh quota data on each page navigation (not cached stale data) Fresh data loaded
Add manual "Refresh" button on dashboard Refresh works
Handle optimistic UI updates during claiming (show loading then confirm) UX smooth
Handle concurrent claiming conflict (seat claimed in app while selecting in portal) Conflict detected
Auto-refresh dashboard every 60 seconds (optional toggle) Auto-refresh works

5.4 Notifications Display

Status Task Verification
Show inline notification when subquota recipient claims tickets Notification visible
Show notification when admin cancels quota Notification visible
Show notification when quota is approaching deadline Warning visible

5.5 Audit Logging

Audit Trail

All portal actions should be logged for audit purposes. The backend handles persistence; the portal ensures actions are attributed to the correct user.

Status Task Verification
Log claiming actions (seat selection, holder info, payment) via API calls Actions recorded in backend
Log subquota creation (who delegated, to whom, which seats) Delegation logged
Log subquota retraction (who retracted, timestamp) Retraction logged
Include user identifier (email) in all API calls for attribution Attribution correct

Phase 6: Testing

Infrastructure Testing

Status Task Verification
Test docker-compose up -d starts all services cleanly Containers running
Test portal accessible at http://localhost:8003 Page loads
Test PHP container can reach backend: curl http://hns-ticketing-nginx:80/api/v1 Backend responds
Test PHP container can reach Drupal: curl {DRUPAL_SSO_URL}/user/check?_format=json Drupal responds
Test SCSS build: make build compiles without errors CSS files generated
Test make dev starts Gulp watch mode SCSS recompiles on file change
Test make stop / make start cycle Clean restart works

Functional Testing

Status Task Verification
Test login with valid Drupal credentials Login succeeds
Test login with invalid credentials Error shown
Test login via deeplink with email pre-fill Deeplink pre-fills email
Test dashboard displays all user's quotas (own + received subquotas) All quotas visible
Test quota detail shows correct seat breakdown Breakdown accurate
Test full claiming flow: select → holder info → payment → confirmation End-to-end works
Test free quota claiming (discount_code = GR, skips payment) Free claim completes
Test deferred payment quota claiming (skips Stripe, auto-completes) Deferred claim completes
Test paid quota claiming (Stripe integration) Payment processes
Test partial claiming (claim some seats, return later to claim more) Remaining seats claimable
Test subquota creation (numbered match with seat selection) Subquota created, email sent
Test subquota creation (non-numbered match with quantity) Subquota created
Test subquota retraction (ALLOCATED only) Retraction works, seats returned
Test cannot retract RESERVED/SOLD subquota Error shown correctly
Test blacklist rejection during claiming Rejection works
Test OIB checksum validation Invalid OIB rejected
Test minor detection (email disabled for under-18) Minor handling works
Test cart timeout after 20 minutes (seats released, warning shown) Timeout handled gracefully
Test quota status transitions: ACTIVE → FULLY_CLAIMED after all seats claimed Status updates
Test expired quota still allows claiming (until admin cancels) Claiming works past deadline
Test quota seat status transitions: ALLOCATED → RESERVED → SOLD Transitions correct
Test blacklist partial rejection (block one ticket, allow others) Partial claiming works
Test email mismatch on deeplink entry (logged in as different user) Mismatch notice shown
Test cancelled quota still shows previously SOLD tickets as valid SOLD tickets visible
Test blank email field handling (ticket stays on quota holder's device) Blank email handled

Access Control Testing

Status Task Verification
Test user can only see quotas assigned to their email Other quotas not visible
Test user cannot access another user's quota by URL 403 returned
Test can_create_subquotas = FALSE hides delegation buttons Buttons hidden
Test unauthenticated access redirects to login Redirect works
Test expired JWT triggers re-authentication Re-auth works

Browser Testing

Status Task Verification
Test in Chrome (latest) All features work
Test in Firefox (latest) All features work
Test in Safari (latest) All features work
Test responsive layout (tablet: 768px+) Layout adapts

Integration Testing

Status Task Verification
Test claiming in web portal reflects in mobile app Sync works
Test claiming in mobile app reflects in web portal Sync works
Test concurrent claiming conflict (same seat selected in app and portal simultaneously) Conflict detected, one succeeds
Test subquota creation sends email invitation with deeplink Email received with correct link
Test subquota creation sends push notification (if recipient registered) Push received
Test deeplink from email opens portal with correct quota Deeplink works
Test subquota inheritance: discount, transfer, expiration match parent Properties inherited correctly
Test subquota recipient can claim via both mobile app and web portal Both channels work

Last Updated: March 2026