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
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)
- Mark task as blocked: Change ⬜ to ❌ in this file
- 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
- Skip to next task: Continue with next ⬜ item
- 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:
- Phase 1: Foundation - Users, Matches, Stadiums, Seats (must be first)
- Phase 2: Core Purchase Flow - Queue, Cart, Payment
- Phase 3: Distribution & Management - Quotas, Ticket Management, Loyalty
- Phase 4: Operations & Security - Blacklist, Support, Physical Sales
- 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
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
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
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
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
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
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
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
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
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