Skip to content

Ticketing Backend Implementation Tasks

This document provides a sequenced, comprehensive task list for AI-assisted implementation of the HNS Ticketing Backend (PHP 8.2+ / Symfony 7.x).

Overview

Attribute Value
Technology PHP 8.2+ / Symfony 7.x
Database PostgreSQL 15+
Cache/Queue Redis 7+
API Style REST (OpenAPI 3.0)
Reference API Specification
Reference Data Model
Reference Audit Logging Infrastructure
File Scope
Admin Portal Tasks Frontend implementation (Symfony Twig + Alpine.js)
Visual Stadium Editor Tasks Konva.js canvas seat map designer
E2 - Match & Stadium Tasks Epic-specific task breakdown
E3 - Seat Inventory Tasks Epic-specific task breakdown

Using with Ralph Wiggum (Autonomous AI Loop)

This task list is designed for iterative AI implementation using the Ralph Wiggum plugin - an autonomous loop that feeds Claude the same prompt repeatedly until completion.

Quick Start

/ralph-loop:ralph-loop "Implement HNS Ticketing Backend following ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.

Start from the first incomplete task (⬜). For each task:
1. Read the task and its verification criteria
2. Implement the task following TDD pattern
3. Run verification command
4. Update status in tasks-ticketing-backend.md: ⬜ → ✅ if passing, ⬜ → ❌ if blocked
5. Update PROGRESS.md with current state
7. Move to next ⬜ task

When all tasks in current phase are ✅, output: <promise>PHASE_COMPLETE</promise>
When ALL tasks are ✅, output: <promise>BACKEND_COMPLETE</promise>

If stuck for 3+ iterations on same task:
- Mark as ❌
- Document blocker in BLOCKERS.md
- Skip to next task" \
--completion-promise "BACKEND_COMPLETE" \
--max-iterations 100

Per-Phase Execution

For more controlled execution, run one phase at a time:

# Phase 1: Foundation (E1, E2, E3)
/ralph-loop:ralph-loop "Implement Phase 1 (Foundation) from ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.
Start from first ⬜ task. Follow TDD pattern. Update task status after each completion.
Output <promise>PHASE_COMPLETE</promise> when all Phase 1 tasks are ✅." \
--completion-promise "PHASE_COMPLETE" --max-iterations 50

# Phase 2: Core Purchase Flow (E5, E4, E8)
/ralph-loop:ralph-loop "Implement Phase 2 (Core Purchase Flow) from ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.
Start from first ⬜ task in Phase 2. Follow TDD pattern. Update task status after each completion.
Output <promise>PHASE_COMPLETE</promise> when all Phase 2 tasks are ✅." \
--completion-promise "PHASE_COMPLETE" --max-iterations 40

# Phase 3: Distribution & Management (E7, E9, E6)
/ralph-loop:ralph-loop "Implement Phase 3 (Distribution & Management) from ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.
Start from first ⬜ task in Phase 3. Follow TDD pattern. Update task status after each completion.
Output <promise>PHASE_COMPLETE</promise> when all Phase 3 tasks are ✅." \
--completion-promise "PHASE_COMPLETE" --max-iterations 35

# Phase 4: Operations & Security (E11, E10, E12)
/ralph-loop:ralph-loop "Implement Phase 4 (Operations & Security) from ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.
Start from first ⬜ task in Phase 4. Follow TDD pattern. Update task status after each completion.
Output <promise>PHASE_COMPLETE</promise> when all Phase 4 tasks are ✅." \
--completion-promise "PHASE_COMPLETE" --max-iterations 25

# Phase 5: Event Day & Analytics (E13, E15, E14)
/ralph-loop:ralph-loop "Implement Phase 5 (Event Day & Analytics) from ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md.
Start from first ⬜ task in Phase 5. Follow TDD pattern. Update task status after each completion.
Output <promise>PHASE_COMPLETE</promise> when all Phase 5 tasks are ✅." \
--completion-promise "PHASE_COMPLETE" --max-iterations 20

Progress Tracking

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

# Implementation Progress

## Current State
- **Phase:** 1
- **Feature:** E1-F1
- **Task:** Create `users` table
- **Iteration:** 5
- **Last Updated:** 2026-01-28T10:30:00Z

## Phase Summary
| Phase | Status | Tasks Done | Tasks Total | Progress |
|-------|--------|------------|-------------|----------|
| 1. Foundation | 🟡 In Progress | 15 | 112 | 13% |
| 2. Core Purchase | ⬜ Not Started | 0 | 88 | 0% |
| 3. Distribution | ⬜ Not Started | 0 | 76 | 0% |
| 4. Operations | ✅ Complete | 50 | 50 | 100% |
| 5. Event Day | ⬜ Not Started | 0 | 35 | 0% |

## Completed Features
- [x] E1-F1: User Registration API (6/6 tasks)
- [x] E1-F2: User Authentication (6/6 tasks)
- [ ] E1-F3: Default Ticketing Profile (0/11 tasks)

## Current Blockers
None currently.

Implementation Pattern (TDD)

For each task, follow this Test-Driven Development pattern:

1. READ    → Read task description and verification criteria
2. TEST    → Write failing test for the task
3. CODE    → Write minimal code to pass test
4. VERIFY  → Run test, ensure it passes
5. REFACTOR → Clean up if needed (keep tests green)
6. UPDATE  → Change ⬜ to ✅ in this file
7. NEXT    → Move to next ⬜ task

Example: Implement POST /users/register

# 1. Write test (RED)
cat > tests/Api/UserRegistrationTest.php << 'EOF'
<?php
namespace App\Tests\Api;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class UserRegistrationTest extends WebTestCase
{
    public function testRegisterReturns201WithUserId(): void
    {
        $client = static::createClient();
        $client->request('POST', '/users/register', [], [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode([
                'email' => 'test@example.com',
                'password' => 'SecurePass1',
                'oib' => '12345678903'
            ])
        );

        $this->assertEquals(201, $client->getResponse()->getStatusCode());
        $data = json_decode($client->getResponse()->getContent(), true);
        $this->assertArrayHasKey('user_id', $data);
    }
}
EOF

# 2. Run test (should fail)
./vendor/bin/phpunit tests/Api/UserRegistrationTest.php

# 3. Implement endpoint (write minimal code to pass)
# ... create Controller, Entity, etc. ...

# 4. Run test (should pass)
./vendor/bin/phpunit tests/Api/UserRegistrationTest.php

# 5. Update this file: ⬜ → ✅

# 6. Commit
git add -A && git commit -m "[TASK] E1-F1: Implement POST /users/register endpoint"

Escape Hatches & Stuck Protocol

If Stuck on a Task (3+ iterations, no progress)

  1. Mark task as blocked: Change ⬜ to ❌ in this file
  2. Document in BLOCKERS.md:
## E1-F1: POST /users/register
**Blocked since:** 2026-01-28
**Iterations attempted:** 5
**Issue:** Symfony security bundle configuration conflict
**Attempted solutions:**
- Modified config/packages/security.yaml
- Checked Symfony 7.x documentation
- Tried disabling firewall for registration endpoint
**Suggested resolution:** Manual security bundle setup needed
  1. Skip to next task: Continue with next ⬜ item
  2. Continue iteration

If All Remaining Tasks Are Blocked

Output: <promise>PHASE_BLOCKED</promise>

Include summary of blockers in output for human review.

Safety Limits

Limit Value Action When Reached
Max iterations per task 5 Mark ❌, document blocker, skip
Max iterations per phase 50 Output PHASE_BLOCKED
Max total iterations 100 Output BACKEND_BLOCKED
Consecutive failures 3 Pause and review BLOCKERS.md

Phase Completion Criteria

Phase 1: Foundation

Criterion Verification Command
All migrations applied php bin/console doctrine:migrations:status --no-interaction
E1 tests pass ./vendor/bin/phpunit tests/E1/ --testdox
E2 tests pass ./vendor/bin/phpunit tests/E2/ --testdox
E3 tests pass ./vendor/bin/phpunit tests/E3/ --testdox
No linting errors ./vendor/bin/phpcs src/ --standard=PSR12
All E1 tasks ✅ grep -c "| ✅ |" ../../hns-ticketing-docs/docstasks/tasks-ticketing-backend.md (count matches expected)

Completion Signal: <promise>PHASE_COMPLETE</promise>

Phase 2: Core Purchase Flow

Criterion Verification Command
E5 Queue tests pass ./vendor/bin/phpunit tests/E5/ --testdox
E4 Cart tests pass ./vendor/bin/phpunit tests/E4/ --testdox
E8 Payment tests pass ./vendor/bin/phpunit tests/E8/ --testdox
Redis connectivity php bin/console app:test-redis
Integration tests pass ./vendor/bin/phpunit tests/Integration/PurchaseFlow/ --testdox

Completion Signal: <promise>PHASE_COMPLETE</promise>

Phase 3: Distribution & Management

Criterion Verification Command
E7 Quota tests pass ./vendor/bin/phpunit tests/E7/ --testdox
E9 Ticket tests pass ./vendor/bin/phpunit tests/E9/ --testdox
E6 Loyalty tests pass ./vendor/bin/phpunit tests/E6/ --testdox
QR generation works ./vendor/bin/phpunit tests/E9/QrGeneration --testdox

Completion Signal: <promise>PHASE_COMPLETE</promise>

Phase 4: Operations & Security

Criterion Verification Command
E11 Blacklist tests pass ./vendor/bin/phpunit tests/E11/ --testdox
E10 Support tests pass ./vendor/bin/phpunit tests/E10/ --testdox
E12 Physical sales tests pass ./vendor/bin/phpunit tests/E12/ --testdox
Security audit passes ./vendor/bin/security-checker security:check

Completion Signal: <promise>PHASE_COMPLETE</promise>

Phase 5: Event Day & Analytics

Criterion Verification Command
E13 Access control tests pass ./vendor/bin/phpunit tests/E13/ --testdox
E15 Notification tests pass ./vendor/bin/phpunit tests/E15/ --testdox
E14 Analytics tests pass ./vendor/bin/phpunit tests/E14/ --testdox
Full test suite passes ./vendor/bin/phpunit --testdox

Completion Signal: <promise>BACKEND_COMPLETE</promise>


Task Legend

Status Meaning
Not started
🟡 In progress
Complete
Blocked

Implementation Sequence

Tasks are organized in dependency order:

  1. Phase 1: Foundation - Users, Matches, Stadiums, Seats (must be first)
  2. Phase 2: Core Purchase Flow - Queue, Cart, Payment
  3. Phase 3: Distribution & Management - Quotas, Ticket Management, Loyalty
  4. Phase 4: Operations & Security - Blacklist, Support, Physical Sales
  5. Phase 5: Event Day & Analytics - Access Control, Notifications, Reporting

Phase 1: Foundation

1.1 User & Profile Management (E1)

Epic: E1: User & Profile Management

Drupal Handles Registration & Authentication

User registration and password authentication are handled by HNS Drupal 10 Webshop. The ticketing backend only: - Validates Drupal SSO tokens - Maintains a local user reference for ticketing records - Manages ticketing-specific profiles (E1-F3, E1-F4)

E1-F1: Drupal User Sync

Feature: E1-F1: User Registration API

No Registration Endpoint

The ticketing backend does NOT expose a registration endpoint. Users register via Drupal. Local user records are created on first SSO token validation.

Database Migrations

Status Task Verification
Create users table (id, drupal_user_id, email, status, created_at, updated_at) psql -c "\d users" shows table
Create user_status enum (active, inactive, suspended) psql -c "SELECT enum_range(NULL::user_status)" returns
Add UNIQUE index on email column psql -c "\di users_email*" shows index
Add UNIQUE index on drupal_user_id column psql -c "\di users_drupal*" shows index

Business Logic

Status Task Verification
Create DrupalUserSyncService to create/update local user from SSO data Service callable
On first login, create local user record with drupal_user_id User record created
Sync email from Drupal if changed Email updated on login
Map Drupal user status to local status Status synced correctly

E1-F2: SSO Token Validation

Feature: E1-F2: User Authentication

SSO Integration

The ticketing backend validates Drupal SSO tokens, not username/password. The mobile app authenticates with Drupal first, then exchanges the Drupal token for a ticketing API token.

Database Migrations

Status Task Verification
Create sessions table (id, user_id, refresh_token_hash, device_id, expires_at, created_at) psql -c "\d sessions" shows table
Add index on user_id column psql -c "\di sessions_user_id*" shows index
Add index on expires_at column psql -c "\di sessions_expires*" shows index

API Endpoints

Status Task Verification
Implement POST /auth/login endpoint accepting drupal_token Returns ticketing API tokens
Validate drupal_token against Drupal SSO service Invalid token returns 401
Extract user identity from Drupal token (drupal_user_id, email) Identity extracted
Create/update local user record via DrupalUserSyncService User synced
Issue JWT access token (15 min expiry) Token issued
Issue refresh token (7 days expiry), store hashed in sessions table Refresh token stored
Store device_id for push notification targeting Device ID saved
Implement POST /auth/refresh endpoint New tokens issued
Implement POST /auth/logout endpoint Session invalidated

Business Logic

Status Task Verification
Create DrupalSsoService to validate tokens with Drupal Service callable
Handle Drupal SSO errors gracefully (timeout, invalid response) Errors handled
Cache Drupal token validation for 5 minutes (Redis) Cache reduces Drupal calls

E1-F3: Default Ticketing Profile

Feature: E1-F3: Default Ticketing Profile

Database Migrations

Status Task Reference
Create user_profiles table (user_id FK, full_name, date_of_birth, nationality, oib, passport_number, phone, updated_at) data-model.md#user_profiles
Add UNIQUE constraint on OIB column (globally unique) data-model.md#user_profiles
Encrypt passport_number field at rest (BYTEA) data-model.md#user_profiles

API Endpoints

Status Task Verification
Implement GET /users/me/profile endpoint returning 404 if profile doesn't exist Returns 404 for user without profile, 200 with data for user with profile
Implement POST /users/me/profile endpoint for initial profile creation Creates profile, returns 201 with profile data
Return 409 on POST if profile already exists Authenticated user with existing profile gets 409
Implement PUT /users/me/profile endpoint for profile updates Update saves correctly
Return 404 on PUT if profile doesn't exist User without profile gets 404 suggesting to use POST
Add global OIB uniqueness check before setting OIB OIB already used by another user returns 409
Add OIB checksum validation (ISO 7064 MOD 11-10) on POST/PUT Invalid OIB checksum returns 422
Make OIB field immutable after first set (reject updates to non-null OIB) Attempt to change OIB returns 400
Require OIB for Croatian nationality, passport_number for non-Croatian Croatian without OIB returns 422, non-Croatian without passport returns 422
Add age validation (18+ required for default profile) User under 18 returns 422 with age requirement message
Add profile completeness check (all required fields filled) API returns is_complete boolean in response

E1-F4: Saved Profiles

Feature: E1-F4: Saved Profiles

Database Migrations

Status Task Reference
Create saved_profiles table (id, user_id, full_name, date_of_birth, nationality, oib_encrypted, passport_encrypted, email, phone, relationship, created_at, updated_at) data-model.md#saved_profiles
Add index on user_id column data-model.md#saved_profiles
Add UNIQUE constraint on (user_id, oib_encrypted) data-model.md#saved_profiles

API Endpoints

Status Task Verification
Implement POST /users/me/saved-profiles endpoint Creates profile, returns id
Implement GET /users/me/saved-profiles endpoint returning list with masked OIB OIB shows only last 4 digits
Implement PUT /users/me/saved-profiles/{id} endpoint Update works
Implement DELETE /users/me/saved-profiles/{id} endpoint Soft or hard delete works
Enforce max 10 profiles per user 11th creation returns 400 with limit message
Check for duplicate OIB within user's profiles before create/update Duplicate OIB returns 409
Encrypt OIB and passport fields at rest DB inspection shows encrypted values

E1-F5: OIB Checksum Validation

Feature: E1-F5: OIB Checksum Validation

Business Logic

Status Task Verification
Implement OIB validation utility function (11 digits, checksum algorithm) ./vendor/bin/phpunit tests/E1/F5/OibValidatorTest.php --testdox passes with known valid/invalid OIBs
Document checksum algorithm (ISO 7064, MOD 11-10) test -f src/Service/Validation/README.md && echo "EXISTS" returns EXISTS
Export as internal service callable from other modules ./vendor/bin/phpunit tests/E1/F5/OibServiceIntegrationTest.php --testdox passes

1.2 Match & Stadium Management (E2)

Epic: E2: Match & Stadium Management

E2-F1: Match CRUD API

Feature: E2-F1: Match CRUD API

Database Migrations

Status Task Reference
Create match_type enum (HOME, AWAY) data-model.md#matches
Create match_status enum (DRAFT, PUBLISHED, ACTIVE, CLOSED, CANCELLED) data-model.md#matches
Create config_status enum (DRAFT, VALIDATED, LOCKED) data-model.md#matches
Create attendance_mode enum (WITH_DATA, WITHOUT_DATA) data-model.md#matches
Create matches table (id, home_team, away_team, competition, kick_off_time, venue_id, match_type, is_numbered, status, stadium_config_status, timestamps, closure fields, cancellation fields) data-model.md#matches
Add indexes on status, kick_off_time, venue_id, match_type data-model.md#matches

API Endpoints

Status Task Verification
Implement POST /admin/matches endpoint with required fields Returns 201 with match_id
Implement GET /admin/matches endpoint with pagination Returns list of matches
Implement GET /admin/matches/{id} endpoint Returns match details
Implement PUT /admin/matches/{id} endpoint Updates match details
Implement DELETE /admin/matches/{id} endpoint (soft delete) Match status becomes "deleted"
Add status validation (draft → published → active → closed) Invalid status transition returns 400
Add RBAC check for Match Manager role Non-admin returns 403

E2-F2: Sales Phase Configuration

Feature: E2-F2: Sales Phase Configuration

Database Migrations

Status Task Reference
Create phase_type enum (LOYALTY, GENERAL_PUBLIC, QUOTA_ONLY) data-model.md#sales_phases
Create sales_phases table (id, match_id, phase_type, name, start_at, end_at, eligibility_rules_json, ticket_limit, is_active, created_at) data-model.md#sales_phases
Add indexes on match_id and (start_at, end_at) data-model.md#sales_phases

API Endpoints

Status Task Verification
Implement POST /admin/matches/{id}/sales-phases endpoint Creates phase
Implement GET /admin/matches/{id}/sales-phases endpoint Returns phases for match
Implement PUT /admin/matches/{id}/sales-phases/{phase_id} endpoint Updates phase
Add overlap validation (phases cannot have overlapping dates) Overlapping dates return 400
Add eligibility_rules_json schema validation Invalid JSON structure returns 400

E2-F3: Stadium Template Management

Feature: E2-F3: Stadium Template Management

Database Migrations

Status Task Reference
Create stadium_type enum (HOME, AWAY) data-model.md#stadiums
Create stadiums table (id, name, city, country, address, type, base_capacity, is_numbered, created_at, updated_at) data-model.md#stadiums
Create sector_type enum (STANDARD, VIP, ACCESSIBILITY, STANDING) data-model.md#sectors
Create sectors table (id, stadium_id, name, code, capacity, sector_type, has_seat_map, seat_count, created_at) data-model.md#sectors
Add UNIQUE constraint on (stadium_id, code) data-model.md#sectors

Data Seeding

Status Task Verification
Seed database with Croatian stadium templates (Maksimir, Poljud, Opus Arena, Rujevica, Varazdin) with pre-configured sectors Query returns 5 templates with complete sector data

API Endpoints - Stadium Templates

Status Task Verification
Implement GET /admin/stadiums endpoint (list templates) Returns list filtered by type
Implement POST /admin/stadiums endpoint (create template) Creates stadium, returns 201
Implement GET /admin/stadiums/{stadiumId} endpoint Returns stadium with sectors

API Endpoints - Template Sectors

Status Task Verification
Implement GET /admin/stadiums/{stadiumId}/sectors endpoint Returns all sectors for stadium
Implement POST /admin/stadiums/{stadiumId}/sectors endpoint Creates sector in template
Implement GET /admin/stadiums/{stadiumId}/sectors/{sectorId} endpoint Returns sector with seat map info
Implement PUT /admin/stadiums/{stadiumId}/sectors/{sectorId} endpoint Updates sector details
Implement DELETE /admin/stadiums/{stadiumId}/sectors/{sectorId} endpoint Deletes sector (fails if has seat map or used by matches)
Validate sector code uniqueness within stadium Duplicate code returns 409

E2-F4: Match Stadium Configuration

Feature: E2-F4: Match Stadium Configuration

Database Migrations

Status Task Reference
Create price_categories table (id, match_id, name, price, currency, color, created_at) data-model.md#price_categories
Add UNIQUE constraint on (match_id, name) data-model.md#price_categories
Create match_sector_status enum (ACTIVE, CLOSED, MAINTENANCE) data-model.md#match_sectors
Create sector_purpose enum (GENERAL, AWAY_FANS, VIP, PRESS, FREE) data-model.md#match_sectors
Create config_status enum (DRAFT, VALIDATED, LOCKED) data-model.md#match_sectors
Create match_sectors table (id, match_id, sector_id, name, code, total_capacity, technical_seats, sellable_capacity, price_category_id, status, purpose, is_numbered, has_seat_map, created_at) data-model.md#match_sectors
Add indexes on match_id and price_category_id data-model.md#match_sectors

API Endpoints - Stadium Configuration

Status Task Verification
Implement GET /admin/matches/{matchId}/stadium endpoint Returns match stadium configuration
Implement POST /admin/matches/{matchId}/stadium endpoint (copy template) Creates match-specific config from template
Support stadium_template_id option (copy from template) Template copied correctly
Support import_from_match_id option (copy from previous match) Previous config copied
Support create_new option (inline away stadium creation) Away stadium created
Implement POST /admin/matches/{matchId}/stadium/validate endpoint Returns validation results
Validate all sectors have pricing Validation error if missing pricing
Validate capacity configuration Validation error if capacity issues

API Endpoints - Match Sectors

Status Task Verification
Implement GET /admin/matches/{matchId}/sectors endpoint Returns all match sectors
Implement POST /admin/matches/{matchId}/sectors endpoint Adds sector to match
Implement PUT /admin/matches/{matchId}/sectors/{sectorId} endpoint Updates sector status/purpose/pricing
Implement PUT /admin/matches/{matchId}/sectors/bulk endpoint Bulk updates multiple sectors
Prevent sector modification if tickets sold Returns 409 if tickets exist

API Endpoints - Price Categories

Status Task Verification
Implement GET /admin/matches/{matchId}/price-categories endpoint Returns price categories
Implement POST /admin/matches/{matchId}/price-categories endpoint Creates price category
Implement PUT /admin/matches/{matchId}/price-categories/{categoryId} endpoint Updates price category
Implement DELETE /admin/matches/{matchId}/price-categories/{categoryId} endpoint Deletes category (fails if in use)

Business Logic

Status Task Verification
Copy template sectors and seat maps when configuring match stadium Sectors and maps copied
Lock configuration after validation passes Status changes to LOCKED
Prevent unlock after sales start Returns 409 if sales active

E2-F5: Away Match Stadium Creation

Feature: E2-F5: Away Match Stadium Creation

API Endpoints

Status Task Verification
Implement POST /admin/stadiums endpoint for creating away stadium Creates stadium with type=AWAY
Add fields for name, city, country, address All fields saved
Add is_numbered flag for away stadiums Flag persists
Implement sector creation for away stadium Sectors created with capacity

E2-F6: Match Update Notifications

Feature: E2-F6: Match Update Notifications

Database Migrations

Status Task Reference
Create match_change_type enum (UPDATE, CANCEL, RESCHEDULE, PUBLISH, CLOSE) data-model.md#match_audit_logs
Create match_audit_logs table (id, match_id, changed_by, change_type, changes_json, notification_sent, created_at) data-model.md#match_audit_logs

Business Logic

Status Task Verification
Add notification_preference param to PUT /admin/matches/{id} (notify/silent) Param accepted
If notify: trigger email to all ticket holders Emails queued
If notify: trigger push notification to all ticket holders Pushes queued
Log all changes to match_audit_logs regardless of notification preference Audit event dispatched to Loki

E2-F7: Match Cancellation Workflow

Feature: E2-F7: Match Cancellation Workflow

API Endpoints

Status Task Verification
Implement POST /admin/matches/{id}/cancel endpoint Requires approval for published matches
Add Super Admin approval check for published match cancellation Non-super-admin returns 403

Business Logic

Status Task Verification
Trigger batch refund job for all paid tickets Refund records created
Send cancellation notification to all ticket holders Emails/push sent
Invalidate all QR codes (set ticket status to CANCELLED_MATCH_CANCELLED) Tickets marked cancelled
Generate cancellation report (tickets, refunds, notifications) Report accessible

E2-F8: Access Control Data Export

Feature: E2-F8: Access Control Data Export

Database Migrations

Status Task Reference
Create extra_barcodes table (id, match_id, barcode, is_used, used_at, assigned_ticket_id, created_at) data-model.md#extra_barcodes
Create barcode_export_logs table (id, match_id, file_name, total_barcodes, extra_barcodes, exported_by, exported_at) data-model.md#barcode_export_logs

API Endpoints

Status Task Verification
Implement GET /admin/matches/{id}/access-control-export endpoint Returns Excel file
Export only barcode column (no PII) Excel has barcode only
Include extra ~1000 codes for unsold seats and late changes Export count > sold tickets
File naming: MatchID_TicketExport_YYYYMMDD_HHMM.xlsx Filename matches pattern

E2-F9: Match Closure & Attendance

Feature: E2-F9: Match Closure & Attendance

Database Migrations

Status Task Reference
Create attendance_imports table (id, match_id, file_name, total_scanned, matched_count, unmatched_count, imported_by, imported_at) data-model.md#attendance_imports
Create unmatched_reason enum (NOT_FOUND, ALREADY_CANCELLED, INVALID_FORMAT) data-model.md#attendance_unmatched
Create attendance_unmatched table (id, import_id, barcode, scan_timestamp, reason) data-model.md#attendance_unmatched

API Endpoints

Status Task Verification
Implement POST /admin/matches/{id}/close endpoint Changes match status to CLOSED
Accept attendance file upload (Excel with barcodes) File parsed correctly
Match barcodes to tickets and update status to ATTENDED Matched tickets have ATTENDED status
Make match closure and attendance update atomic (transaction) Partial failure rolls back both
If no attendance data: require reason, mark all tickets ATTENDED All tickets updated
Trigger loyalty points calculation job after closure Job triggered

E2-F10: Sector Seat Map Configuration

Feature: E2-F10: Sector Seat Map Configuration

Visual Canvas Editor Approach

This feature uses a 2D JSON array storage model instead of the legacy row_configurations table. Admin draws seat maps visually using Konva.js canvas editor - see Visual Stadium Editor Tasks.

Database Migrations

Status Task Reference
Create sector_seat_maps table (id, sector_id, seat_map_json JSONB, total_seats INTEGER, configuration_status, created_at, updated_at) data-model.md#sector_seat_maps
Add UNIQUE constraint on sector_id (one seat map per sector) data-model.md#sector_seat_maps
Create seat_type enum (STANDARD, TECHNICAL, ACCESSIBILITY, COMPANION) data-model.md#seats
Create seats table (id, sector_id, row_identifier, seat_number, seat_type, created_at) data-model.md#seats
Add UNIQUE constraint on (sector_id, row_identifier, seat_number) data-model.md#seats
Create traversal_dir enum (TOP_TO_BOTTOM, BOTTOM_TO_TOP) data-model.md#sector_snake_configs
Create row_strategy enum (CENTER_OUTWARD, LEFT_TO_RIGHT, RIGHT_TO_LEFT) data-model.md#sector_snake_configs
Create sector_snake_configs table (id, sector_id, best_row_index, traversal_direction, within_row_strategy, subsector_grouping_json, created_at) data-model.md#sector_snake_configs

seat_map_json Structure

{
  "dimensions": { "rows": 50, "cols": 50 },
  "seats": [
    { "row": 0, "col": 0, "type": "standard" },
    { "row": 0, "col": 1, "type": "standard" },
    { "row": 0, "col": 4, "type": "technical" }
  ],
  "row_labels": ["A", "B", "C", "D"],
  "numbering_direction": "LTR"
}

Gaps (staircases, walls, aisles) are represented by missing entries in the seats array.

API Endpoints

Status Task Verification
Implement GET /admin/sectors/{sectorId}/seat-map endpoint Returns seat map JSON for canvas editor
Implement POST /admin/sectors/{sectorId}/seat-map endpoint Creates new seat map from canvas JSON
Implement PUT /admin/sectors/{sectorId}/seat-map endpoint Updates existing seat map
Implement DELETE /admin/sectors/{sectorId}/seat-map endpoint Deletes seat map (if no seats generated)
Validate seat_map_json schema on create/update Invalid JSON returns 422
Calculate total_seats from seat_map_json on save Count stored in total_seats column
Implement POST /admin/sectors/{sectorId}/seats/generate endpoint Creates Seat records from JSON
Implement seat generation preview option preview=true returns list without saving
Implement seat generation force_regenerate option Deletes existing seats first
Prevent regeneration if tickets sold Returns 409 if tickets exist
Support LTR numbering direction Seats numbered 1, 2, 3...
Support RTL numbering direction Seats numbered ...3, 2, 1
Auto-number seats skipping gaps Missing entries don't consume numbers
Implement POST /admin/sectors/{sectorId}/seats/classify endpoint Bulk classify seats by type
Implement PUT /admin/seats/{seatId}/type endpoint Updates single seat type
Add unique constraint validation (sector_id, row_identifier, seat_number) Duplicate row/seat returns error
Implement GET /admin/sectors/{sectorId}/snake-config endpoint Returns snake algorithm config
Implement PUT /admin/sectors/{sectorId}/snake-config endpoint Updates snake algorithm config
Implement GET /admin/matches/{matchId}/sectors/status-summary endpoint Returns aggregated seat status per sector

Business Logic

Status Task Verification
Create SeatMapJsonValidator service Validates JSON schema
Create SeatGenerationService Generates Seat records from JSON
Map row index to row_labels array during generation Row A = index 0, B = index 1, etc.
Calculate seat numbers based on numbering_direction LTR: left-to-right, RTL: right-to-left
Handle irregular row lengths (different col counts per row) Variable row lengths work
Create SnakeConfigService Manages snake algorithm settings

1.3 Seat Inventory Management (E3)

Epic: E3: Seat Inventory Management

E3-F1: Seat Status Model

Feature: E3-F1: Seat Status Model

Database Migrations

Status Task Reference
Create seat_status enum (AVAILABLE, RESERVED, SOLD, ALLOCATED, BLOCKED, TECHNICAL, OFFICIAL, MAINTENANCE, QUARANTINED, INACTIVE) data-model.md#match_seat_inventory
Create match_seat_inventory table (id, match_id, seat_id, status, last_modified_by, last_modified_at, version) data-model.md#match_seat_inventory
Add UNIQUE constraint on (match_id, seat_id) data-model.md#match_seat_inventory
Add index on (match_id, status) for availability queries data-model.md#match_seat_inventory
Create seat_audit_logs table (id, match_seat_inventory_id, from_status, to_status, reason, changed_by, ticket_id, order_id, created_at) data-model.md#seat_audit_logs

Business Logic

Status Task Verification
Define color codes for each status (config/constants) Config file updated
Define valid status transitions matrix Transition rules documented and coded

E3-F2: Individual Seat Status Change

Feature: E3-F2: Individual Seat Status Change

API Endpoints

Status Task Verification
Implement PUT /admin/seats/{id}/status endpoint Updates seat status
Validate status transition against allowed transitions Invalid transition returns 400
Require reason parameter for status change Missing reason returns 400
Log change to seat_audit_logs table Audit event dispatched to Loki
Warn if seat is locked (in cart) Response includes warning

E3-F3: Bulk Seat Operations

Feature: E3-F3: Bulk Seat Operations

API Endpoints

Status Task Verification
Implement POST /admin/seats/bulk-status endpoint Accepts seat IDs array
Add selection by section parameter Selects all seats in section
Add selection by row range (section + from_row + to_row) Selects correct rows
Add optional status filter (only change seats with specific current status) Filter applied
Process in batches with progress tracking Progress updates available
Skip locked seats and report in response Skipped seats listed

E3-F4: Seat Search & Lookup

Feature: E3-F4: Seat Search & Lookup

API Endpoints

Status Task Verification
Implement GET /admin/seats/search?seat_number=X endpoint Returns seat details
Implement GET /admin/seats/search?quota_id=X endpoint Returns all seats in quota
Implement GET /admin/seats/search?email=X endpoint Returns tickets for email
Implement advanced search with multiple filters (match, status, section, date range) Filters combine correctly
Add pagination to search results Page/limit params work

E3-F5: Stadium Visualization (API Support)

Feature: E3-F5: Stadium Visualization

Frontend Implementation

The Konva.js canvas visualization is implemented in the Admin Portal. See Visual Stadium Editor Tasks and Admin Portal Tasks.

API Endpoints

Status Task Verification
Implement GET /admin/matches/{matchId}/seats endpoint with filtering Returns seat inventory with status
Support sector filter parameter Filter by sector works
Support status filter parameter (multi-value) Filter by statuses works
Support row filter parameter Filter by row works
Include pagination for large sectors Pagination works
Return seat coordinates from seat_map_json Coordinates included
Implement GET /admin/matches/{matchId}/sectors/status-summary endpoint Returns aggregated status counts per sector
Calculate dominant_status per sector (most common status) Dominant status calculated
Include total_capacity and status_counts breakdown All counts present

Business Logic

Status Task Verification
Define seat status color codes as constants Colors defined
Calculate status summary in efficient single query Query optimized
Cache status summary for 30 seconds (Redis) Cache works

E3-F6: Sector-to-Sector Transfer

Feature: E3-F6: Sector-to-Sector Transfer

API Endpoints

Status Task Verification
Implement POST /admin/tickets/sector-transfer endpoint Accepts source seats and destination sector
Assign new seats in destination using snake algorithm Adjacent seats assigned
Update ticket seat_id to new seats Tickets point to new seats
Mark original seats as Blocked Original seats status changed
Send notification to affected ticket holders Email/push sent
Make transfer atomic (all or nothing) Partial failure rolls back

Phase 2: Core Purchase Flow

2.1 Waiting Queue System (E5)

Epic: E5: Waiting Queue System

E5-F1: Queue Join & Position Assignment

Feature: E5-F1: Queue Join & Position Assignment

Database Migrations

Status Task Reference
Create queue_status enum (WAITING, ACTIVE, EXPIRED, COMPLETED, LEFT) data-model.md#queue_entries
Create queue_entries table (id, user_id, match_id, position, status, joined_at, window_started_at, window_expires_at, last_heartbeat, grace_period_expires_at, is_connected, completed_at) data-model.md#queue_entries
Add UNIQUE constraint on (user_id, match_id) data-model.md#queue_entries
Add indexes on (match_id, status) and (match_id, position) data-model.md#queue_entries

Redis Data Structures

Status Task Verification
Create Redis sorted set for queue (queue:{match_id} -> {user_id: timestamp}) Can add users
Create Redis hash for queue metadata (queue_meta:{match_id}) Metadata stored
Create Redis hash for user session state (queue_session:{user_id}:{match_id}) with 30-min TTL Session state stored

API Endpoints

Status Task Verification
Implement POST /queue/{match_id}/join endpoint Returns position
Position = rank in sorted set + 1 Position accurate
Return estimated_wait_time and total_queue_size Values calculated
If user already in queue, return existing position No duplicate entries

E5-F2: Real-Time Position Updates

Feature: E5-F2: Real-Time Position Updates

Business Logic

Status Task Verification
Implement WebSocket or Firebase channel for queue updates Connection established
Send position update every 60 seconds Updates received
Include position, estimated_wait, total_size in update All fields present

E5-F3: Queue Progressive Notifications

Feature: E5-F3: Queue Progressive Notifications

Business Logic

Status Task Verification
Send push on significant position change Push sent
Send push at position ~50: "You're almost at the front!" Notification text correct
Send push at position 1: "It's your turn!" Notification text correct
Include deeplink to purchase flow Deeplink opens correct screen

E5-F4: Purchase Window Enforcement

Feature: E5-F4: Purchase Window Enforcement

Business Logic

Status Task Verification
Set purchase window start time when user reaches position 1 Timestamp recorded
Set 20-minute window expiration expires_at = start + 20min
Remove user from queue if window expires without purchase User removed
Send push: "Your purchase window has expired" Notification sent
User must rejoin at end of queue New join gets last position

E5-F5: Queue Connection Resilience

Feature: E5-F5: Queue Connection Resilience

Business Logic

Status Task Verification
Track last heartbeat timestamp per user Heartbeat updated on activity
Keep position for 30 minutes after last heartbeat Position retained
On reconnect within 30 min: restore position Same position returned
On reconnect after 30 min: position lost User not in queue

2.2 Ticket Purchase Flow (E4)

Epic: E4: Ticket Purchase Flow

E4-F1: Match Listing API

Feature: E4-F1: Match Listing API

API Endpoints

Status Task Verification
Implement GET /matches endpoint (public) Returns upcoming matches
Sort by kick_off_time ascending Matches ordered correctly
Include sale_status field (not_on_sale, on_sale, sold_out) Status accurate
Include pricing preview (pre-sale and regular) Prices shown

E4-F2: Zone Selection & Availability

Feature: E4-F2: Zone Selection & Availability

API Endpoints

Status Task Verification
Implement GET /matches/{id}/zones endpoint Returns zones with availability
Calculate availability from seat inventory (AVAILABLE status count) Count accurate
Apply pre-sale discount if before day of match Discounted price shown
Apply full price on match day Full price shown on match day
Limit quantity to min(4, available) Cannot select more than available

E4-F3: Snake Algorithm Seat Assignment

Feature: E4-F3: Snake Algorithm Seat Assignment

Business Logic

Status Task Verification
Implement snake algorithm function (zone_id, quantity) -> seat_ids[] Unit test returns adjacent seats
Support TOP_TO_BOTTOM traversal direction (configurable per sector) Seats assigned from top row
Support BOTTOM_TO_TOP traversal direction (configurable per sector) Seats assigned from bottom row
Fill rows alternating direction within row (L-to-R, then R-to-L snake pattern) Seat IDs follow snake pattern
Skip non-available seats Only AVAILABLE seats returned
Implement fallback to single-seat allocation when adjacency impossible Single seats allocated with warning
Handle irregular stadium geometries via subsector grouping Subsectors traverse correctly
Return error if not enough seats available Insufficient seats returns error

E4-F4: Cart Management & TTL

Feature: E4-F4: Cart Management & TTL

Redis Data Structures

Status Task Verification
Create cart data structure in Redis (cart:{user_id}:{match_id} -> {seats[], total_amount, created_at}) with 20-min TTL Can set and get cart
Create seat lock structure (seat_lock:{match_seat_inventory_id} -> cart_key) with 20-min TTL Prevents double-booking

API Endpoints

Status Task Verification
Implement POST /cart/add endpoint using snake algorithm Seats assigned and reserved
Set TTL from queue entry time (or current time if no queue) TTL correct
Implement GET /cart endpoint returning items and remaining TTL TTL countdown accurate
Implement DELETE /cart endpoint to clear cart Seats released
Implement Redis expiration callback to release seats Expired carts release seats

E4-F5: Checkout Ticket Holder Entry

Feature: E4-F5: Checkout Ticket Holder Entry

API Endpoints

Status Task Verification
Implement POST /checkout/ticket-holders endpoint Accepts ticket holder data
Auto-populate first ticket holder from user's default profile Data pre-filled
Lock first ticket holder to logged-in user (cannot change) Change attempt returns 400
Allow saved profile selection for additional tickets Profile IDs accepted
Allow manual entry for additional tickets Manual data accepted
Disable email field for minors (DOB < 18 years) Minor email forced to buyer email

E4-F6: Blacklist Validation at Checkout

Feature: E4-F6: Blacklist Validation at Checkout

Business Logic

Status Task Verification
Call blacklist check service for all ticket holder OIBs at checkout All OIBs checked
Block checkout if any OIB blacklisted Blacklisted OIB returns 403
Return MUP referral message on block Message includes MUP contact
Log blocked attempt with user/IP details violation_logs record created

E4-F7: Payment Processing (Stripe)

Feature: E4-F7: Payment Processing (Stripe)

Database Migrations

Status Task Reference
Create order_status enum (PENDING, PAYMENT_PENDING, PAYMENT_FAILED, COMPLETED, CANCELLED, REFUNDED) data-model.md#orders
Create orders table (id, user_id, match_id, order_number, status, ticket_amount, ticket_vat_amount, fee_amount, fee_vat_amount, total_amount, currency, sales_phase_id, queue_entry_id, timestamps, cancellation fields) data-model.md#orders
Add UNIQUE constraint on order_number data-model.md#orders
Add indexes on user_id, match_id, status, created_at data-model.md#orders
Create payment_status enum (PENDING, PROCESSING, SUCCEEDED, FAILED, CANCELLED) data-model.md#payments
Create payments table (id, order_id, stripe_payment_intent_id, stripe_charge_id, amount, currency, status, created_at, completed_at, failed_at, failure_reason) data-model.md#payments
Add UNIQUE constraint on stripe_payment_intent_id data-model.md#payments

Business Logic

Status Task Verification
Create payment_intent with Stripe API on checkout submit payment_intent_id returned
Store payment_intent_id in Order record Order has stripe reference
Implement Stripe webhook handler for payment_intent.succeeded Webhook processes successfully
Update order status to COMPLETED on payment success Order status changed
Handle payment_intent.payment_failed webhook Order remains pending

E4-F8: Order Confirmation & Ticket Generation

Feature: E4-F8: Order Confirmation & Ticket Generation

Database Migrations

Status Task Reference
Create ticket_status enum (SOLD, ATTENDED, CANCELLED, TRANSFERRED, CANCELLED_MATCH_CANCELLED, CANCELLED_BLACKLIST, REFUNDED) data-model.md#tickets
Create ticket_cancel_reason enum (SELF_SERVICE, SUPPORT_REQUEST, MATCH_CANCELLED, BLACKLIST_BUYER, BLACKLIST_HOLDER, SYSTEM) data-model.md#tickets
Create tickets table (id, order_id, match_id, match_seat_inventory_id, barcode, qr_code_data, status, price_amount, fee_amount, transfer_allowed, external_pdf_url, external_ticket_required, pdf_delivered_at, attended_at, cancelled_at, cancelled_by, cancellation_reason, created_at) data-model.md#tickets
Add UNIQUE constraint on barcode data-model.md#tickets
Add indexes on order_id, match_id, status data-model.md#tickets
Create holder_source enum (DEFAULT_PROFILE, SAVED_PROFILE, MANUAL_ENTRY) data-model.md#ticket_holders
Create ticket_holders table (id, ticket_id, full_name, date_of_birth, nationality, oib_encrypted, passport_encrypted, email, phone, is_minor, source, created_at, updated_at) data-model.md#ticket_holders
Add UNIQUE constraint on ticket_id (one-to-one) data-model.md#ticket_holders

Business Logic

Status Task Verification
Create tickets after payment success (one per seat) Ticket records created
Generate unique barcode for each ticket Barcodes are unique
Generate QR code with ticket ID encoded QR code scannable
Set ticket status to SOLD Tickets have SOLD status
Send order confirmation email with order details Email sent
Push notification: "Your tickets are ready" Push sent

2.3 Payment Processing (E8)

Epic: E8: Payment Processing

E8-F1: Stripe Integration

Feature: E8-F1: Stripe Integration

Database Migrations

Status Task Reference
Create stripe_webhook_logs table (id, event_id, event_type, payload_json, processed, processed_at, error, created_at) data-model.md#stripe_webhook_logs
Add UNIQUE constraint on event_id data-model.md#stripe_webhook_logs

Business Logic

Status Task Verification
Install Stripe SDK Package installed
Configure Stripe API key from environment (STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET) Keys loaded
Implement webhook endpoint /webhooks/stripe Endpoint reachable
Verify webhook signature Invalid signature rejected
Implement webhook handler for payment_intent.succeeded Payment success processed
Implement webhook handler for payment_intent.payment_failed Payment failure handled
Implement refund API call Refund succeeds

E8-F2: Service Fee Calculation

Feature: E8-F2: Service Fee Calculation

Database Migrations

Status Task Reference
Create fee_configurations table (id, match_id, fee_percent, home_ticket_vat_percent, away_ticket_vat_percent, home_fee_vat_percent, away_fee_vat_percent, effective_from, effective_to, created_at) data-model.md#fee_configurations

Business Logic

Status Task Verification
Add ticket_fee_percentage to Match config (default 6%) Config saved
Calculate ticket_amount = quantity * unit_price Calculation correct
Calculate fee_amount = ticket_amount * fee_percentage Calculation correct
Calculate VAT based on match type (HOME=5%, AWAY=0% for tickets; HOME=25%, AWAY=0% for fees) VAT correct

E8-F3: Refund Processing

Feature: E8-F3: Refund Processing

Database Migrations

Status Task Reference
Create refund_source enum (SELF_SERVICE, SUPPORT, SYSTEM) data-model.md#refunds
Create refund_status enum (PENDING, PENDING_APPROVAL, APPROVED, PROCESSING, COMPLETED, FAILED, REJECTED) data-model.md#refunds
Create refunds table (id, order_id, ticket_id, payment_id, stripe_refund_id, refund_amount, fee_retained, source, reason, status, requested_by, approved_by, created_at, completed_at) data-model.md#refunds
Add UNIQUE constraint on stripe_refund_id data-model.md#refunds

API Endpoints

Status Task Verification
Implement POST /admin/orders/{id}/refund endpoint Initiates refund
Refund ticket_amount only (not fee) Refund amount excludes fee
Call Stripe refund API Stripe refund created
Create credit note via e-racuni API Credit note created
Update ticket status to REFUNDED Status changed

E8-F4: Deferred Payment Offer Generation

Feature: E8-F4: Deferred Payment Offer Generation

Database Migrations

Status Task Reference
Create debtors table (id, name, oib, address, email, payment_terms_days, created_at, updated_at) data-model.md#debtors
Add UNIQUE constraint on oib data-model.md#debtors
Create offer_status enum (DRAFT, SENT, PAID, OVERDUE, CANCELLED) data-model.md#payment_offers
Create payment_offers table (id, debtor_id, e_racuni_id, e_racuni_reference, orders, total_amount, currency, due_date, status, created_by, created_at, sent_at, paid_at) data-model.md#payment_offers

API Endpoints

Status Task Verification
Implement POST /admin/payment-offers endpoint Creates offer
Group multiple order_ids for same debtor Orders grouped
Call e-racuni API to create payment offer e-racuni_id returned
Store e-racuni reference in PaymentOffer table Reference saved

E8-F5: E-Racuni Integration

Feature: E8-F5: E-Racuni Integration

Database Migrations

Status Task Reference
Create e_racuni_doc_type enum (PAYMENT_OFFER, INVOICE, CREDIT_NOTE) data-model.md#e_racuni_documents
Create e_racuni_status enum (DRAFT, SENT, FISCALIZED, CANCELLED) data-model.md#e_racuni_documents
Create e_racuni_documents table (id, document_type, local_reference_id, e_racuni_id, e_racuni_number, fiscalization_id, status, created_at, fiscalized_at) data-model.md#e_racuni_documents

Business Logic

Status Task Verification
Install/configure e-racuni API client Client initialized
Configure credentials from environment (ERACUNI_USERNAME, ERACUNI_PW, ERACUNI_TOKEN) Credentials loaded
Implement createPaymentOffer API call Offer created in e-racuni
Implement issueInvoice API call Invoice issued
Implement createCreditNote API call Credit note created
Implement status polling for invoice lifecycle Status updates received

Phase 3: Distribution & Management

3.1 Quota Management (E7)

Epic: E7: Quota Management

E7-F1: Quota Creation API

Feature: E7-F1: Quota Creation API

Database Migrations

Status Task Reference
Create allocation_algo enum (NM, REDOM) data-model.md#quotas
Create quota_status enum (PENDING, ACTIVE, PAST_DEADLINE, CANCELLED, FULLY_CLAIMED) data-model.md#quotas
Create cancellation_type enum (ALL_UNUSED, UNFULFILLED_ONLY) data-model.md#quotas
Create quotas table (id, match_id, recipient_email, recipient_name, internal_note, total_quantity, sector_ids, discount_percent, allocation_algorithm, expiration_date, can_create_subquotas, transfer_allowed, deferred_payment, auto_send_email, status, batch_id, created_by, created_at, cancelled_at, cancelled_by, cancellation_type) data-model.md#quotas
Add indexes on match_id, recipient_email, status, batch_id data-model.md#quotas
Create quota_seat_status enum (ALLOCATED, RESERVED, SOLD, DELEGATED, CANCELLED) data-model.md#quota_seats
Create quota_seats table (id, quota_id, match_seat_inventory_id, status, subquota_id, claim_cart_id, ticket_id, claimed_at, created_at) data-model.md#quota_seats
Add indexes on quota_id, match_seat_inventory_id, status, subquota_id data-model.md#quota_seats

API Endpoints

Status Task Verification
Implement POST /admin/quotas endpoint Creates quota
Allocate seats immediately using NM or REDOM algorithm quota_seats records created
Reserve allocated seats from public sale (seat status = ALLOCATED) Seat status changed
Send email invitation with deeplink Email sent

E7-F2: Quota CSV Bulk Import

Feature: E7-F2: Quota CSV Bulk Import

Database Migrations

Status Task Reference
Create quota_import_batches table (id, match_id, file_name, total_rows, successful_rows, failed_rows, error_report_json, imported_by, imported_at) data-model.md#quota_import_batches

API Endpoints

Status Task Verification
Implement POST /admin/quotas/import endpoint accepting CSV file File uploaded
Validate CSV format (email, sectors, quantity) Invalid format returns error
Validate each row (email format, sector exists, quantity available) Row errors reported
Create quotas in batch for valid rows Quotas created
Return error report with line numbers for invalid rows Line numbers in response

E7-F3: Multi-Match Quota Import

Feature: E7-F3: Multi-Match Quota Import

API Endpoints

Status Task Verification
Accept multiple match_ids in import request Param accepted
Create quota for each match per CSV row Quotas created for all matches
Link quotas by batch_id batch_id set

E7-F4: Quota Web Portal Dashboard

Feature: E7-F4: Quota Web Portal Dashboard

API Endpoints

Status Task Verification
Create quota holder login endpoint (same credentials as mobile) Login works
Implement GET /quota-portal/dashboard endpoint Returns quota summary
Display breakdown: ALLOCATED, RESERVED, SOLD, Delegated Counts accurate
Display seat numbers for numbered matches Seats listed

E7-F5: Quota Claiming Process

Feature: E7-F5: Quota Claiming Process

Database Migrations

Status Task Reference
Create claim_cart_status enum (ACTIVE, EXPIRED, COMPLETED, ABANDONED) data-model.md#quota_claim_carts
Create claim_payment_status enum (PENDING, DEFERRED, PAID, FREE) data-model.md#quota_claim_carts
Create quota_claim_carts table (id, quota_id, subquota_id, user_id, status, ticket_holders_json, subtotal, service_fee, total, stripe_payment_intent_id, payment_status, order_id, expires_at, created_at, completed_at) data-model.md#quota_claim_carts
Add indexes on quota_id, user_id, status data-model.md#quota_claim_carts

API Endpoints

Status Task Verification
Implement Step 1: POST /quota-portal/quotas/{id}/select-seats Seats selected
Implement Step 2: POST /quota-portal/quotas/{id}/ticket-holders Info saved, status=RESERVED
Implement Step 3: POST /quota-portal/quotas/{id}/payment Payment processed, status=SOLD
Skip Step 3 if discount_percent=100 (free) Free quota completes without payment

E7-F6: Subquota Creation

Feature: E7-F6: Subquota Creation

Database Migrations

Status Task Reference
Create subquota_status enum (ALLOCATED, RESERVED, SOLD, EXPIRED, CANCELLED, RETRACTED) data-model.md#subquotas
Create subquotas table (id, parent_quota_id, recipient_email, recipient_name, internal_note, quantity, status, expires_at, created_at, claimed_at, retracted_at) data-model.md#subquotas
Add indexes on parent_quota_id, recipient_email, status data-model.md#subquotas
Create subquota_seats table (id, subquota_id, quota_seat_id, created_at) data-model.md#subquota_seats
Add UNIQUE constraint on quota_seat_id data-model.md#subquota_seats

API Endpoints

Status Task Verification
Check can_create_subquotas flag before allowing False returns 403
Implement POST /quota-portal/quotas/{id}/subquotas endpoint Creates subquota
Select specific seats from parent quota Seats assigned to subquota
Send email to sub-recipient with deeplink Email sent
Send push notification to sub-recipient (if registered) Push received
Implement GET /quota-portal/quotas/{id}/subquotas endpoint Returns subquota list
Implement DELETE /quota-portal/subquotas/{id} endpoint (retract) ALLOCATED subquota cancelled
Validate subquota status = ALLOCATED before allowing retract Non-ALLOCATED returns 400
Return retracted seats to parent quota available balance Seats available again

E7-F7: Quota Cancellation

Feature: E7-F7: Quota Cancellation

API Endpoints

Status Task Verification
Implement DELETE /admin/quotas/{id}?option=all_unused ALLOCATED + RESERVED cancelled
Implement DELETE /admin/quotas/{id}?option=unfulfilled_only Only ALLOCATED cancelled
Return cancelled seats to inventory (seat status = AVAILABLE) Seat status changed
Send cancellation notification to quota holder Notification sent

3.2 Ticket Management & Delivery (E9)

Epic: E9: Ticket Management & Delivery

E9-F1: Ticket Generation & QR Code

Feature: E9-F1: Ticket Generation & QR Code

Business Logic

Status Task Verification
Generate unique ticket ID (UUID) IDs are unique
Generate unique barcode for entry scanning Barcodes are unique
Generate QR code with ticket ID encoded QR code scannable
Store QR code data in ticket record QR retrievable

E9-F2: Tiered Ticket Visibility

Feature: E9-F2: Tiered Ticket Visibility

API Endpoints

Status Task Verification
Implement GET /tickets/{id} with conditional fields based on time Fields vary by time
>4 days before match: return order info only (no seat, no QR) Seat/QR hidden
4 days to 5 hours: return seat details (no QR) QR hidden
<5 hours before match: return full ticket with QR QR visible

E9-F4: Digital Wallet Integration

Feature: E9-F4: Digital Wallet Integration

Database Migrations

Status Task Reference
Create wallet_type enum (APPLE, GOOGLE) data-model.md#wallet_passes
Create wallet_passes table (id, ticket_id, wallet_type, pass_id, pass_serial, last_updated_at, created_at) data-model.md#wallet_passes
Add UNIQUE constraint on (wallet_type, pass_id) data-model.md#wallet_passes

Business Logic

Status Task Verification
Install PassKit SDK for Apple Wallet SDK imported
Implement .pkpass file generation File generated
Implement Google Passes API integration Pass added
Include QR code, match details, seat info in pass Pass shows correct info

E9-F5: Ticket Transfer Self-Service

Feature: E9-F5: Ticket Transfer Self-Service

Database Migrations

Status Task Reference
Create transfer_type enum (SELF_SERVICE, SUPPORT) data-model.md#transfer_logs
Create transfer_logs table (id, ticket_id, from_holder_name, from_holder_oib_encrypted, to_holder_name, to_holder_oib_encrypted, to_holder_email, transfer_type, initiated_by, reason, transferred_at) data-model.md#transfer_logs
Add index on ticket_id data-model.md#transfer_logs

API Endpoints

Status Task Verification
Check ticket.transfer_allowed before allowing transfer Blocked if not allowed
Implement POST /tickets/{id}/transfer endpoint Accepts new holder data
Validate new holder against blacklist Blacklisted blocked
Update ticket holder fields Holder info changed
Log transfer to transfer_logs table Log record created
Send notification to original and new holder Both notified

E9-F6: Away Match PDF Ticket Handling

Feature: E9-F6: Away Match PDF Ticket Handling

Database Migrations

Status Task Reference
Create away_ticket_pdfs table (id, match_id, file_name, storage_url, ticket_id, uploaded_by, uploaded_at, distributed_at) data-model.md#away_ticket_pdfs
Add indexes on match_id and ticket_id data-model.md#away_ticket_pdfs

API Endpoints

Status Task Verification
Implement POST /admin/matches/{id}/upload-pdf-tickets endpoint Accepts files
Match PDFs to orders by attendee name or seat Matching works
Store PDF URL in ticket record (external_pdf_url) URL saved
Implement mass email distribution of PDFs Emails sent
Set external_ticket_required flag on tickets Flag set
Hide QR code in API response for external ticket matches QR not returned

E9-F7: View Order Details

Feature: E9-F7: View Order Details

API Endpoints

Status Task Verification
Implement GET /orders/{id} endpoint with order summary, tickets, payment info Returns full order data
Add refund_status and refunded_amount computed fields to response Fields included
Implement buyer-only access check (403 for non-buyers) Recipients cannot access
Include tickets list with status All tickets visible
Include invoice PDF URL PDF accessible

E9-F8: Self-Service Ticket Cancellation

Feature: E9-F8: Self-Service Ticket Cancellation

Database Migrations

Status Task Reference
Create cancellation_req_status enum (PENDING, PROCESSING, COMPLETED, FAILED) data-model.md#cancellation_requests
Create cancellation_requests table (id, order_id, ticket_ids, reason, refund_amount, initiated_by, status, created_at, completed_at) data-model.md#cancellation_requests

API Endpoints

Status Task Verification
Implement POST /orders/{id}/cancel-tickets endpoint Accepts ticket IDs
Add 48-hour rule validation (block if <48h before match) Error returned if too late
Add buyer-only authorization check Non-buyers get 403
Add validation for transferred/cancelled tickets Cannot cancel invalid tickets
Calculate refund amount (ticket price + service fee) Calculation correct
Update ticket status to CANCELLED on confirmation Status changes
Release cancelled seats back to inventory (AVAILABLE) Seats available again
Trigger Stripe refund via E8-F3 Refund initiated
Send confirmation email to buyer Email received
Send notification to affected ticket holders All parties notified
Update order refund_status (partial/full) Status updates correctly

3.3 Loyalty Program (E6)

Epic: E6: Loyalty Program

E6-F1: Loyalty Points Tracking

Feature: E6-F1: Loyalty Points Tracking

Database Migrations

Status Task Reference
Create loyalty_points table (id, user_id, match_id, ticket_id, points, earned_at) data-model.md#loyalty_points
Add UNIQUE constraint on (user_id, match_id) - one point per match data-model.md#loyalty_points
Add index on earned_at for rolling window queries data-model.md#loyalty_points
Create loyalty_balance view for aggregate queries data-model.md#view-loyalty_balance

API Endpoints

Status Task Verification
Implement points balance calculation (count where earned_at > 5 years ago) Rolling window works
Implement GET /users/me/loyalty endpoint Returns balance
Implement GET /users/me/loyalty/history endpoint for points history Returns match-by-match history
Implement GET /admin/users/{id}/loyalty endpoint for admin view Admin can view any user's points
Support both HOME and AWAY match types for points earning Both match types earn points

E6-F2: Loyalty Early Access Configuration

Feature: E6-F2: Loyalty Early Access Configuration

Database Migrations

Status Task Reference
Create loyalty_tier_configs table (id, match_id, min_points, max_tickets, created_at) data-model.md#loyalty_tier_configs

Business Logic

Status Task Verification
Add loyalty tier configuration (points_threshold -> ticket_limit) as configurable per match Config stored per match
Store loyalty tiers in SalesPhase.eligibility_rules_json JSON schema valid
Implement admin endpoint to view user loyalty points for allocation planning Admin can view distribution
Enforce hard limit of 4 tickets per match across all phases Exceeding limit blocked

E6-F3: Loyalty Points Award After Match

Feature: E6-F3: Loyalty Points Award After Match

Database Migrations

Status Task Reference
Create batch_status enum (PENDING, PROCESSING, COMPLETED, FAILED) data-model.md#loyalty_award_batches
Create loyalty_award_batches table (id, match_id, total_awarded, status, processed_at, created_at) data-model.md#loyalty_award_batches

Business Logic

Status Task Verification
Trigger job after match closure Job runs
Query tickets with status=ATTENDED and valid OIB (both HOME and AWAY matches) Correct tickets queried
Award 1 point per eligible ticket loyalty_points records created
Send push notification: "You earned 1 point" Notification sent

Phase 4: Operations & Security

4.1 Blacklist & Security (E11)

Epic: E11: Blacklist & Security

E11-F1: Blacklist Data Model

Feature: E11-F1: Blacklist Data Model

Database Migrations

Status Task Reference
Create blacklist_status enum (ACTIVE, REMOVED) data-model.md#blacklist
Create blacklist table (id, oib, first_name, last_name, date_of_birth, status, source, notes, created_by, created_at, removed_by, removed_at, removal_reason) data-model.md#blacklist
Add UNIQUE constraint on oib data-model.md#blacklist
Add index on status data-model.md#blacklist
Create blacklist_action enum (CREATE, UPDATE, REMOVE, RESTORE) data-model.md#blacklist_audit_logs
Create blacklist_audit_logs table (id, blacklist_id, action, changed_by, changes_json, created_at) data-model.md#blacklist_audit_logs

Business Logic

Status Task Verification
Implement data retention: copy removed entries to audit log before hard delete Loki audit log contains deleted entries

E11-F2: Blacklist CSV Import

Feature: E11-F2: Blacklist CSV Import

Database Migrations

Status Task Reference
Create blacklist_import_batches table (id, file_name, total_rows, successful_rows, failed_rows, duplicate_rows, error_report_json, imported_by, imported_at) data-model.md#blacklist_import_batches

API Endpoints

Status Task Verification
Implement POST /admin/blacklist/import endpoint accepting CSV File uploaded
Validate OIB checksum for each row Invalid OIBs flagged
Skip duplicate OIBs (in file or already in DB) Duplicates not inserted
Insert valid rows in batch Records created
Return import summary (imported, skipped, errors) Summary accurate

E11-F3: Blacklist Check Service

Feature: E11-F3: Blacklist Check Service

Database Migrations

Status Task Reference
Create violation_action enum (PURCHASE_ATTEMPT, TRANSFER_ATTEMPT) data-model.md#violation_logs
Create violation_logs table (id, oib, blacklist_id, action_type, match_id, user_id, session_id, ip_address, user_agent, blocked_at) data-model.md#violation_logs
Add indexes on oib, blacklist_id, match_id, blocked_at data-model.md#violation_logs

API Endpoints

Status Task Verification
Implement GET /internal/blacklist/check?oib=X endpoint Returns is_blacklisted boolean
Return standardized MUP message if blacklisted Message correct
Log blocked attempts to violation_logs Log record created

E11-F4: Auto-Cancel Tickets on Blacklist Entry

Feature: E11-F4: Auto-Cancel Tickets on Blacklist

Database Migrations

Status Task Reference
Create cancellation_scope enum (BUYER_ORDERS, HOLDER_TICKETS) data-model.md#blacklist_cancellations
Create blacklist_cancellations table (id, blacklist_id, cancellation_scope, orders_cancelled, tickets_cancelled, cancelled_by, cancelled_at) data-model.md#blacklist_cancellations

Business Logic

Status Task Verification
On blacklist entry creation, query for existing tickets Query runs
If found, warn admin with ticket details Warning displayed
If admin confirms, cancel tickets based on role (buyer=whole order, holder=single ticket) Correct scope cancelled
Do not issue refund for blacklist cancellations No refund created

E11-F5: Violation Monitoring Report

Feature: E11-F5: Violation Monitoring Report

API Endpoints

Status Task Verification
Implement GET /admin/blacklist/violations endpoint Returns violation records
Include filters (date range, OIB, match) Filters work
Export to CSV CSV download works

4.2 Customer Support Operations (E10)

Epic: E10: Customer Support Operations

E10-F1: Ticket Lookup Support

Feature: E10-F1: Ticket Lookup Support

API Endpoints

Status Task Verification
Implement GET /admin/tickets/search endpoint with multiple params Search works
Search by OIB OIB search returns tickets
Search by order_number Order search returns tickets
Search by email Email search returns tickets
Search by phone Phone search returns tickets

E10-F2: Support Ticket Transfer

Feature: E10-F2: Support Ticket Transfer

Database Migrations

Status Task Reference
Create support_req_status enum (PENDING, APPROVED, REJECTED, COMPLETED) data-model.md#support_transfer_requests
Create support_transfer_requests table (id, ticket_id, requested_by_name, requested_by_contact, new_holder_name, new_holder_oib_encrypted, new_holder_email, status, agent_id, created_at, completed_at) data-model.md#support_transfer_requests

API Endpoints

Status Task Verification
Implement POST /admin/tickets/{id}/transfer endpoint Accepts new holder data
Block if match < 48 hours away Returns 400 with policy message
Validate new holder against blacklist Blacklisted blocked with MUP message
Log transfer with agent_id and reason Audit event dispatched to Loki

E10-F3: Support Refund Processing

Feature: E10-F3: Support Refund Processing

API Endpoints

Status Task Verification
Implement POST /admin/orders/{id}/refund-request endpoint Creates refund request
If >48h: process without reason required Refund processed
If <48h: require reason field Missing reason returns 400
If <48h and high value: require supervisor approval Approval workflow triggered
Verify requester is ticket owner (purchaser) Non-owner request blocked

E10-F4: Emergency Ticket Printing

Feature: E10-F4: Emergency Ticket Printing

Database Migrations

Status Task Reference
Create print_logs table (id, ticket_id, agent_id, reason, identity_verified, verification_document_type, print_location, printed_at) data-model.md#print_logs
Add indexes on ticket_id and agent_id data-model.md#print_logs

API Endpoints

Status Task Verification
Implement GET /admin/tickets/{id}/print endpoint Returns printable format
Include QR code in print output QR code present
Add "EMERGENCY REPRINT" watermark Watermark visible
Log print event with agent_id and reason print_logs record created
Track duplicate prints with warning Subsequent prints show warning

4.3 Physical Sales - Petrol (E12)

Epic: E12: Physical Sales (Petrol)

E12-F1: Petrol Sales Section (Mobile App Backend)

Feature: E12-F1: Petrol Sales Section Mobile

Database Migrations

Status Task Reference
Create pin_status enum (ACTIVE, USED, EXPIRED, CANCELLED) data-model.md#petrol_pins
Create petrol_pins table (id, pin, user_id, match_id, quantity, ticket_holders_json, status, expires_at, created_at, used_at) data-model.md#petrol_pins
Add UNIQUE constraint on pin data-model.md#petrol_pins
Add indexes on (user_id, match_id) and status data-model.md#petrol_pins
Create petrol_deeplinks table (id, match_id, deeplink_url, qr_code_data, is_active, created_at) data-model.md#petrol_deeplinks
Add UNIQUE constraint on match_id data-model.md#petrol_deeplinks
Convert UNIQUE(match_id) to partial unique index (match_id) WHERE is_active = TRUE to allow regeneration history (E12-F4) Multiple inactive rows allowed, only one active per match

API Endpoints

Status Task Verification
Implement POST /petrol-sales/{match_id}/generate-pin endpoint PIN generated
Generate 5-digit numeric PIN on submit PIN is 5 digits
Store PIN in petrol_pins table Record created
Check availability before PIN generation Sold out shows error
Accept ticket holder entry data Data stored

E12-F2: Petrol Quota Portal

Feature: E12-F2: Petrol Quota Portal

Database Migrations

Status Task Reference
Create reservation_status enum (RESERVED, IDENTITY_VERIFIED, COMPLETED, CANCELLED, EXPIRED) data-model.md#petrol_reservations
Create petrol_reservations table (id, pin_id, staff_id, seats, status, reserved_at, expires_at, identity_verified_at, completed_at, cancelled_by, cancellation_reason) data-model.md#petrol_reservations
Add indexes on pin_id and status data-model.md#petrol_reservations
Create petrol_payment enum (CASH, CARD) data-model.md#petrol_sales
Create petrol_sales table (id, reservation_id, order_id, payment_method, payment_reference, total_amount, processed_by, processed_at) data-model.md#petrol_sales

Business Logic

Status Task Verification
Create Petrol Staff role in RBAC Role exists
Implement special quota type identifier for Petrol quotas Quota type stored

API Endpoints

Status Task Verification
Implement PIN lookup endpoint GET /petrol-portal/pins/{pin} Returns customer data
Reserve seats for 20 minutes on PIN lookup Reservation created
Implement POST /petrol-portal/pins/{pin}/confirm endpoint Sale completed
Deliver tickets to customer app on confirm Tickets appear in app

E12-F3: Petrol Reservation Management

Feature: E12-F3: Petrol Reservation Management

Business Logic

Status Task Verification
Set 20-minute TTL on reservation TTL set
Send warning at 5 minutes remaining (internal staff alert) Warning displayed
Send warning at 1 minute remaining Warning displayed
Auto-release seats on timeout Seats released

API Endpoints

Status Task Verification
Implement POST /petrol-portal/reservations/{id}/cancel Immediate release

E12-F4: Admin Petrol Section (Unified Quota + Auto-QR)

Feature: E12-F4: Admin Petrol Section

Database Migrations

Status Task Verification
Deprecate petrol_deeplinks table: drop migration OR stop writing (deeplink derived from Petrol quota ID) Table no longer referenced by new code
Add partial unique index quotas (match_id) WHERE quota_type='PETROL' AND status='ACTIVE' At most one active Petrol quota per match

API Endpoints

Status Task Verification
Implement POST /admin/petrol/quotas — forces quota_type=PETROL, deferred_payment=TRUE, can_create_subquotas=FALSE, transfer_permission=NO Quota created with forced flags
Implement GET /admin/petrol/quotas with filters (match, status, deferred-payment status, date range) Returns paginated list
Implement GET /admin/petrol/quotas/{id} — includes computed deeplink URL Returns detail
Implement GET /admin/petrol/quotas/{id}/qr.svg (derived from quota ID) SVG decodes to hns://petrol-sales/{quota-id}
Implement GET /admin/petrol/quotas/{id}/qr.png (≥600×600) PNG decodes to hns://petrol-sales/{quota-id}
Implement POST /admin/petrol/quotas/{id}/cancel (delegates to existing quota cancellation service) Cancel with standard options
Reject quota_type=PETROL in generic POST /admin/quotas with redirect hint 400/422 with clear error
Reject duplicate active Petrol quota creation with HTTP 409 Concurrent create returns 409

Business Logic

Status Task Verification
Enforce ROLE_MATCH_MANAGER on all /admin/petrol/quotas routes 403 for unauthorized roles
Emit audit events on Petrol quota create / cancel with admin_id, quota_id, match_id Audit records present in Loki
Validate match exists and has stadium configuration before Petrol quota creation Invalid match returns 422
Generic quota endpoints continue to return Petrol quotas (for cross-channel reporting) but reject Petrol-type mutations Generic endpoints read-only for Petrol

Phase 5: Event Day & Analytics

5.1 Access Control Integration (E13)

Epic: E13: Access Control Integration

E13-F1: Pre-Match Barcode Export

Feature: E13-F1: Pre-Match Barcode Export

API Endpoints

Status Task Verification
Query all valid tickets for match Query returns correct tickets
Generate Excel with barcode column only Excel has one column
Include extra codes for unsold seats (~1000) Extra codes included
Return file with correct naming convention (MatchID_TicketExport_YYYYMMDD_HHMM.xlsx) Filename correct
Log export to barcode_export_logs table Log record created

E13-F2: Post-Match Attendance Import

Feature: E13-F2: Post-Match Attendance Import

API Endpoints

Status Task Verification
Accept Excel file upload with scanned barcodes File parsed
Match barcodes to tickets Matches found
Update matched tickets to ATTENDED status Status changed
Report unmatched barcodes Unmatched listed
Store import results in attendance_imports table Import record created
Store unmatched barcodes in attendance_unmatched table Unmatched records created

5.2 Notifications & Communications (E15)

Epic: E15: Notifications & Communications

E15-F1: Transactional Email Service

Feature: E15-F1: Transactional Email Service

Database Migrations

Status Task Reference
Create email_templates table (id, template_key, name, subject, body_html, body_text, variables, is_active, created_at, updated_at) data-model.md#email_templates
Add UNIQUE constraint on template_key data-model.md#email_templates
Create email_status enum (QUEUED, SENT, DELIVERED, BOUNCED, FAILED) data-model.md#email_logs
Create email_logs table (id, template_key, recipient_email, subject, variables_json, provider, provider_message_id, status, sent_at, delivered_at, error_message, created_at) data-model.md#email_logs
Add indexes on recipient_email, status, created_at data-model.md#email_logs

Business Logic

Status Task Verification
Configure email provider (Symfony Mailer + Mailpit dev, Mailgun prod) MAILER_DSN loaded, Mailpit at http://localhost:8026
Create EmailLog entity and repository Entity maps to email_logs table
Create email template for order confirmation Template renders
Create email template for quota invitation Template renders
Create email template for ticket delivery Template renders
Implement sendEmail function (EmailService → Symfony Mailer) Email sent, EmailLog persisted with SENT/FAILED status
Log delivery status to email_logs table Log created with sentAt, errorMessage
Wire 10 cross-phase notification integrations All services call EmailService.send()

E15-F2: Push Notification Service (Firebase)

Feature: E15-F2: Push Notification Service

Database Migrations

Status Task Reference
Create push_platform enum (IOS, ANDROID) data-model.md#push_tokens
Create push_tokens table (id, user_id, device_id, fcm_token, platform, is_active, created_at, last_used_at) data-model.md#push_tokens
Add indexes on user_id and device_id (unique) data-model.md#push_tokens
Create push_priority enum (NORMAL, HIGH) data-model.md#push_logs
Create push_status enum (QUEUED, SENT, DELIVERED, FAILED) data-model.md#push_logs
Create push_logs table (id, user_id, notification_type, title, body, data_json, priority, fcm_message_id, status, sent_at, error_message, created_at) data-model.md#push_logs
Add indexes on user_id, notification_type, status, created_at data-model.md#push_logs

Business Logic

Status Task Verification
Configure Firebase project and credentials Credentials loaded (BLOCKED - requires Firebase credentials)
Implement FCM token storage endpoint Tokens stored
Implement sendPush function Push sent (DB-logged, actual Firebase sending BLOCKED)
Support priority levels (normal, high) Priority respected
Log delivery to push_logs table Log created

5.3 Reporting & Analytics (E14)

Epic: E14: Reporting & Analytics

E14-F1: Sales Reports

Feature: E14-F1: Sales Reports

Database Migrations

Status Task Reference
Create report_type enum (SALES, FINANCIAL, ATTENDANCE, QUOTA_USAGE) data-model.md#report_export_jobs
Create job_status enum (PENDING, PROCESSING, COMPLETED, FAILED) data-model.md#report_export_jobs
Create report_export_jobs table (id, report_type, parameters_json, status, file_url, requested_by, created_at, completed_at, error_message) data-model.md#report_export_jobs

API Endpoints

Status Task Verification
Implement GET /admin/reports/sales endpoint Returns data
Add filter params (match_id, date_from, date_to, ticket_type, sector) Filters work
Return preview (first 50 rows) Preview limited
Implement async full export to CSV Export job runs
Notify admin when export ready Notification sent (BLOCKED - requires push/email integration)

E14-F2: Real-Time Match Dashboard

Feature: E14-F2: Real-Time Match Dashboard

API Endpoints

Status Task Verification
Implement GET /admin/dashboard/match/{id} endpoint Returns KPIs
Return Total Sold, Revenue, Reserved, Available Values accurate
Return seat status aggregation by sector Aggregation correct
Enable WebSocket for real-time updates Updates received (BLOCKED - requires WebSocket infrastructure)

E14-F3: Financial Reconciliation Report

Feature: E14-F3: Financial Reconciliation Report

API Endpoints

Status Task Verification
Implement GET /admin/reports/financial endpoint Returns payment data
Group by payment method Grouping correct
Include delayed payment status (pending, overdue, paid) Status shown
Include e-racuni invoice status Status from e-racuni (BLOCKED - requires e-racuni API)

Cross-Cutting: Audit Logging Infrastructure

Reference: Audit Logging Infrastructure

Architecture

All 13 audit/log domains are stored in Grafana Loki as structured JSON logs. PostgreSQL remains the system of record for transactional data only. See Audit Logging Infrastructure.

Monolog Loki Handler

Status Task Verification
Install Loki handler package for Monolog composer show lists package
Configure dedicated Monolog channel audit in monolog.yaml Channel appears in config
Configure Loki HTTP Push endpoint (env-based: LOKI_URL) Env variable documented
Create AuditEventDispatcher service for structured audit logging Service registered in container
Configure Symfony Messenger async transport for audit events messenger.yaml has audit transport
Implement Redis-backed retry buffer for Loki unavailability Buffer fills when Loki down

PII Handling

Status Task Verification
Implement OIB masking in audit events (last 4 digits only) Loki logs show masked OIB
Ensure passport numbers are NEVER included in audit log lines No passport fields in Loki

Summary

Task Counts by Phase

Phase Epics Database Tasks API Tasks Business Logic Total Ralph Iterations (Est.)
Phase 1: Foundation E1, E2, E3 45 52 15 112 ~50
Phase 2: Core Purchase Flow E5, E4, E8 25 35 28 88 ~40
Phase 3: Distribution & Management E7, E9, E6 22 42 12 76 ~35
Phase 4: Operations & Security E11, E10, E12 18 22 10 50 ~25
Phase 5: Event Day & Analytics E13, E15, E14 12 15 8 35 ~20
Cross-Cutting: Audit Logging - 6 0 2 8 ~5
Total 15 epics 128 166 75 369 ~175

Critical Path Dependencies

E1 (Users) → E2 (Matches) → E3 (Seats) → E4 (Purchase) → E8 (Payment)
                                    ↓
                              E5 (Queue)

E3 (Seats) → E7 (Quotas) → E9 (Tickets) → E6 (Loyalty)

E1 (Users) → E11 (Blacklist) → E4 (Validation)

E9 (Tickets) → E13 (Access Control) → E14 (Analytics)

E15 (Notifications) - Cross-cutting, can run in parallel with Phase 2+

Ralph Wiggum Quick Reference

Command Purpose
/ralph-loop:ralph-loop Start autonomous implementation loop
PROGRESS.md Track current state and completed tasks
BLOCKERS.md Document stuck tasks for human review
<promise>PHASE_COMPLETE</promise> Signal phase completion
<promise>BACKEND_COMPLETE</promise> Signal full backend completion
<promise>PHASE_BLOCKED</promise> Signal all remaining tasks blocked

See Using with Ralph Wiggum section for full documentation.


Last Updated: February 2026