Preskoči na sadržaj

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