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, ticket_vat_percent, 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
✅ 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