# E_events-nexgate-service(9)

# Events Guides & Philosophy

# Nexgate-Event-mng-Requrements

### 1. PLATFORM PHILOSOPHY (the 7 unbreakable laws)
1. One person = One lifelong QR forever (physical / online / hybrid / exclusive)
2. Host gets paid 48 hours after each session ends – fastest in the industry
3. Money held in escrow until the session actually happens
4. 100 % offline + cryptographically signed QR tokens – zero fraud possible
5. Zero queues – unlimited scanners + self-service kiosks
6. True recurring series that feel exactly like Google Calendar
7. One toggle controls everything: Public ↔ Private ↔ Application-only

### 2. ALL EVENT TYPES & COMBINATIONS SUPPORTED
```
Visibility →   Public | Private (link) | Application-only
Location  → In-person | Online | Hybrid       → every combination allowed
Time      → One-off | Multi-day | Recurring | Free → every combination allowed
```

### 3. MONEY & PAYOUT RULES (applies to EVERYTHING above)
| Situation                        | Attendee pays                  | Host receives money (bank)        |
|----------------------------------|--------------------------------|-----------------------------------|
| Public / Private                 | Instantly                      | 48 h after session ends           |
| Application-only                 | Only after host approves       | 48 h after session ends           |
| Online / Hybrid                  | Same rules                     | 48 h after scheduled end time     |
| Recurring pay-per-session        | Per session                    | 48 h after each class             |
| Monthly unlimited                | 1st of month                   | 3–5th of next month               |
| Class packs (10-class etc.)      | Upfront or per use             | Per use (or full after 1st class if trusted host) |

### 4. FULL PROFESSIONAL FORMS – DOTTED LINES (2025 standard)

```
FORM 1 ─ CREATE NEW EVENT – STEP 1
┌────────────────────────────────────────────────────────────────────┐
│ ← All events                                              New event │
├────────────────────────────────────────────────────────────────────┤
│ Event title                                                        │
│ Weekend Yoga & Sound Healing Retreat                        │     │
│                                                                            │
│ Tagline (shown under title)                                        │
│ 2-day oceanfront immersion · March 15–16                    │     │
│                                                                            │
│ Cover photo                                                        │
│ [ Drag & drop or click to upload ]                                 │
│                                                                            │
│ Category                                                               │
│ Retreat & Wellness ▼                                                       │
│ Tags                                                                       │
│ #yoga #soundhealing #miami #oceanview #retreat                             │
│                                                                            │
│ Visibility & Access                                                        │
│ ○ Public – anyone can find & book                                          │
│ ○ Private – only people with the direct link                               │
│ ● Application required – you approve every attendee                       │
│                                                                            │
│ Location type                                                              │
│ ○ In-person only       ○ Online only       ● Hybrid (both)                │
│                                                                            │
│                                                                [Continue →]│
└────────────────────────────────────────────────────────────────────┘


FORM 2 ─ RECURRING SERIES SETUP (the legendary screen)
┌────────────────────────────────────────────────────────────────────┐
│ Event type                                                                 │
│ ○ One-time event        ● Recurring series                                 │
│                                                                            │
│ Repeats                                                                        │
│ ● Every week        ○ Every 2 weeks        ○ Every month                 │
│ ○ Custom (advanced)                                                        │
│                                                                            │
│ Repeats on                                                                 │
│ [●] Mon   [●] Tue   [ ] Wed   [●] Thu   [ ] Fri   [ ] Sat   [●] Sun        │
│                                                                            │
│ + Add another class time (different title/time/price)                     │
│                                                                            │
│ Live preview – next 6 weeks                                                │
│ Mon Mar 10  10:00  Gentle Flow                  $25                    │
│ Tue Mar 11  18:30  Vinyasa Power                $28                    │
│ Thu Mar 13  18:30  Vinyasa Power                $28                    │
│ Sun Mar 16  17:00  Sunday Restore               $25                    │
│ … continues forever                                                        │
│                                                                            │
│ End condition                                                              │
│ ● No end date (we show next 18 months automatically)                     │
│ ○ Ends on ___/___/____                                                     │
│ ○ After ___ occurrences                                                    │
│                                                                            │
│                                                                [Continue →]│
└────────────────────────────────────────────────────────────────────┘


FORM 3 ─ APPLICATION-ONLY CUSTOM QUESTIONS
┌────────────────────────────────────────────────────────────────────┐
│ Application questions – attendees must answer before payment              │
│                                                                            │
│ 1. Why do you want to attend this retreat?                                 │
│    Long text – required                                                    │
│                                                                            │
│ 2. Dietary restrictions or allergies?                                      │
│    Long text – optional                                                    │
│                                                                            │
│ 3. How experienced are you with yoga?                                      │
│    Beginner / Intermediate / Advanced                                      │
│                                                                            │
│ 4. Instagram handle (we love to repost!)                                   │
│    Short text – optional                                                   │
│                                                                            │
│ 5. Bring a +1 partner?                                                     │
│    Yes / No                                                                │
│                                                                            │
│ + Add another question                                                     │
│                                                                [Continue →]│
└────────────────────────────────────────────────────────────────────┘


FORM 4 ─ ATTENDEE APPLYING TO EXCLUSIVE EVENT
┌────────────────────────────────────────────────────────────────────┐
│ Application – Weekend Yoga & Sound Healing Retreat                         │
│                                                                            │
│ Why do you want to attend?                                                 │
│ [ I’ve been following Sun Studio for 2 years and this…            ]       │
│                                                                            │
│ Dietary restrictions?                                                      │
│ [ Vegan, allergic to nuts                                          ]       │
│ Instagram handle                                                           │
│ @emmayoga                                                                  │
│                                                                            │
│ You will only be charged if approved                                       │
│                                                                            │
│                                                       [Submit Application]│
└────────────────────────────────────────────────────────────────────┘


FORM 5 ─ HOST APPROVAL DASHBOARD
┌────────────────────────────────────────────────────────────────────┐
│ Applications (52 pending · 28 approved · 123 total)                        │
│                                                                            │
│ ○ Emma Wilson         @emmayoga         Vegan, no nuts        [+ Approve] │
│   “Long-time follower, perfect for my birthday”                            │
│                                                                            │
│ ○ Michael & Ana       Couple – 10th anniversary            [+ Approve ×2]   │
│                                                                            │
│ ○ John Smith          “Will bring camera crew”               [− Reject]      │
│                                                                            │
│ [ ] Select all       [Approve selected]    [Message selected]               │
│ Waitlist: 31 people (auto-approve if spot opens)                           │
└────────────────────────────────────────────────────────────────────┘


FORM 6 ─ ETERNAL PASS / TICKET (what every attendee has forever)
┌────────────────────────────────────────────────────────────────────┐
│ Emma Wilson – Eternal Pass                                                 │
│ Valid forever across all Sun Studio events                                 │
│                                                                            │
│ ████████████████████████████████████████████████████████████████         │
│                                                                            │
│ Next: Vinyasa Power – Tue Mar 11, 18:30                                    │
│       Gentle Flow   – Mon Mar 17, 10:00                                    │
│                                                                            │
│ ● Hybrid event                                                             │
│   [ SCAN AT DOOR ]                 [ JOIN ZOOM LIVESTREAM ]                │
│                                                                            │
│ Location revealed 48 h before (approved attendees only)                    │
│                                                                            │
│ + Add to Apple Wallet         + Add to Google Wallet                       │
└────────────────────────────────────────────────────────────────────┘


FORM 7 ─ SELF-SERVICE KIOSK / SCANNER
┌────────────────────────────────────────────────────────────────────┐
│ Welcome to Sun Studio                                                      │
│                                                                            │
│ Hold your QR code to the camera                                            │
│                                                                            │
│                    ████████████████████████████                            │
│                                                                            │
│ → ✓ Emma Wilson – Approved & paid                                          │
│   Welcome back! Enjoy your class                                           │
└────────────────────────────────────────────────────────────────────┘
```

### 5. FINAL SUMMARY – You now have everything
- Complete philosophy & rules
- Every event type & combination
- Exact money & payout logic
- Every single professional form with dotted lines
- Ready to print and give to designers, developers, investors

# Nexgate Platform - Check-in System Architecture

## Table of Contents
1. [System Overview](#system-overview)
2. [Architecture Principles](#architecture-principles)
3. [System Components](#system-components)
4. [Ticket Generation Flow](#ticket-generation-flow)
5. [Scanner Registration Flow](#scanner-registration-flow)
6. [Ticket Validation Flow](#ticket-validation-flow)
7. [Offline Mode Architecture](#offline-mode-architecture)
8. [Security Model](#security-model)
9. [Data Models](#data-models)
10. [API Specifications](#api-specifications)
11. [Database Schema](#database-schema)
12. [Deployment Architecture](#deployment-architecture)

---

## System Overview

### What is the Check-in System?

The Check-in System is a critical component of the Nexgate platform that handles secure, scalable ticket validation for events. It enables event organizers to verify attendee tickets at entry gates using mobile scanner devices, with the capability to work both online and offline.

### Key Features

- **Cryptographic Security**: Uses RSA-signed JWT tokens to prevent ticket forgery
- **Offline Capability**: Scanners can validate tickets without internet connectivity
- **Multi-Gate Support**: Coordinate validation across multiple entry points
- **Real-time Validation**: Immediate duplicate detection when online
- **Scanner Management**: Secure device registration and revocation
- **Audit Trail**: Complete tracking of all scan activities

### Inspiration

The system is inspired by electrical meter voucher systems commonly used in Tanzania and other African countries, where vouchers must be validated offline after purchase, with reconciliation happening later when connectivity is restored.

---

## Architecture Principles

### 1. Security First
- All tickets are cryptographically signed
- Cannot be forged without the server's private key
- Scanner devices only receive public keys for verification

### 2. Offline-First Design
- Scanners must function without network connectivity
- Local validation using JWT signature verification
- Queue-based synchronization when connectivity returns

### 3. Zero-Trust Scanner Model
- Each scanner is individually registered and can be revoked
- Scanners receive time-limited registration tokens
- All scanner actions are logged and auditable

### 4. Scalability
- Stateless ticket validation (JWT-based)
- No database queries required for offline validation
- Server handles only registration and synchronization

---

## System Components

### Component Diagram

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           NEXGATE PLATFORM                                  │
│                                                                             │
│  ┌─────────────────┐         ┌──────────────────┐                          │
│  │ Admin Dashboard │────────>│ Event Management │                          │
│  └────────┬────────┘         └────────┬─────────┘                          │
│           │                           │                                     │
│           │                           v                                     │
│           │                  ┌─────────────────┐                            │
│           │                  │ Ticketing       │                            │
│           │                  │ Service         │                            │
│           │                  └────────┬────────┘                            │
│           │                           │                                     │
│           v                           v                                     │
│  ┌─────────────────┐         ┌──────────────────┐                          │
│  │ Check-in        │<────────│                  │                          │
│  │ Service         │         │                  │                          │
│  │ ⚡ CORE SYSTEM  │         │                  │                          │
│  └────────┬────────┘         │                  │                          │
│           │                  │                  │                          │
│           v                  v                  v                          │
│      ┌─────────────────────────────────┐                                   │
│      │   PostgreSQL Database           │                                   │
│      └─────────────────────────────────┘                                   │
└─────────────────────────────────────────────────────────────────────────────┘
           ▲                           ▲                    ▲
           │                           │                    │
           │                           │                    │
┌──────────┴────────┐       ┌──────────┴────────┐  ┌───────┴────────┐
│  Scanner App      │       │  Customer App     │  │  Email Service │
│  ⚡ MOBILE CLIENT │       │  Mobile/Web       │  │  (External)    │
└───────────────────┘       └──────────┬────────┘  └────────────────┘
                                       │
                                       v
                            ┌────────────────────┐
                            │  Payment Gateway   │
                            │  (External)        │
                            └────────────────────┘

Legend:
  ──────>  Data Flow
  ⚡       Critical Component
```

### Core Components

#### 1. Check-in Service (Spring Boot Backend)
**Responsibilities:**
- Generate and manage scanner registration tokens
- Issue scanner credentials
- Generate RSA key pairs for ticket signing
- Create JWT-based tickets with QR codes
- Handle online ticket validation requests
- Receive and process scan logs from scanners
- Manage scanner settings and configurations
- Maintain scan history and analytics

**Technology Stack:**
- Spring Boot 3.x
- Spring Security
- PostgreSQL
- Redis (for caching and rate limiting)
- JWT (io.jsonwebtoken library)
- ZXing (QR code generation)

#### 2. Scanner Mobile App (Android)
**Responsibilities:**
- Register scanner device using QR code
- Store scanner credentials and server public key
- Scan ticket QR codes
- Validate tickets offline using JWT verification
- Validate tickets online when connected
- Queue scan logs for synchronization
- Sync with server periodically
- Display scan history and statistics

**Technology Stack:**
- Android (Kotlin)
- Room Database (local storage)
- WorkManager (background sync)
- ZXing (QR code scanning)
- JWT library for validation
- Retrofit (API communication)

#### 3. Admin Dashboard (Web)
**Responsibilities:**
- Generate scanner registration QR codes
- View and manage registered scanners
- Revoke scanner access
- Configure scanner settings
- View scan analytics and reports
- Monitor real-time scanning activity
- Export scan data

**Technology Stack:**
- React.js / Vue.js
- Chart.js (analytics)
- WebSocket (real-time updates)

---

## Ticket Generation Flow

### Overview
When a customer purchases a ticket, the system generates a cryptographically signed JWT token that is embedded in a QR code. This QR code serves as the ticket that the customer presents at the event gate.

### Detailed Flow

```
Customer          Ticketing         Check-in          Database        Email
   │               Service           Service              │          Service
   │                  │                 │                 │              │
   │  Purchase        │                 │                 │              │
   │  Ticket          │                 │                 │              │
   ├─────────────────>│                 │                 │              │
   │                  │                 │                 │              │
   │                  │  Create Booking │                 │              │
   │                  ├────────────────────────────────────>│              │
   │                  │                 │                 │              │
   │                  │  Request Ticket │                 │              │
   │                  │  Generation     │                 │              │
   │                  ├────────────────>│                 │              │
   │                  │                 │                 │              │
   │                  │                 │ Load Private    │              │
   │                  │                 │ Key             │              │
   │                  │                 │─┐               │              │
   │                  │                 │ │               │              │
   │                  │                 │<┘               │              │
   │                  │                 │                 │              │
   │                  │                 │ Create JWT      │              │
   │                  │                 │ Payload         │              │
   │                  │                 │─┐               │              │
   │                  │                 │ │               │              │
   │                  │                 │<┘               │              │
   │                  │                 │                 │              │
   │                  │                 │ Sign JWT with   │              │
   │                  │                 │ Private Key     │              │
   │                  │                 │─┐               │              │
   │                  │                 │ │               │              │
   │                  │                 │<┘               │              │
   │                  │                 │                 │              │
   │                  │                 │ Generate QR     │              │
   │                  │                 │ Code            │              │
   │                  │                 │─┐               │              │
   │                  │                 │ │               │              │
   │                  │                 │<┘               │              │
   │                  │                 │                 │              │
   │                  │                 │ Store Ticket    │              │
   │                  │                 ├────────────────>│              │
   │                  │                 │                 │              │
   │                  │  Return JWT +   │                 │              │
   │                  │  QR Code        │                 │              │
   │                  │<────────────────┤                 │              │
   │                  │                 │                 │              │
   │                  │  Send Email     │                 │              │
   │                  │  with QR        │                 │              │
   │                  ├──────────────────────────────────────────────────>│
   │                  │                 │                 │              │
   │                  │                 │                 │  Email with  │
   │<────────────────────────────────────────────────────────── QR Code  │
   │                  │                 │                 │              │
   │  Return Ticket   │                 │                 │              │
   │  Details         │                 │                 │              │
   │<─────────────────┤                 │                 │              │
   │                  │                 │                 │              │

Legend:
  ───>   Synchronous request
  ──>    Response
  ─┐ │   Internal processing
```

### Step-by-Step Process

#### Step 1: Customer Completes Purchase
```
Customer → Ticketing Service
- Selects event and ticket type
- Completes payment
- Receives booking confirmation
```

#### Step 2: Ticketing Service Requests Ticket Generation
```
Ticketing Service → Check-in Service
POST /api/tickets/generate
{
  "bookingId": "booking-uuid",
  "eventId": "event-uuid",
  "attendeeName": "John Doe",
  "attendeeEmail": "john@example.com",
  "ticketType": "VIP",
  "validFrom": "2025-12-01T18:00:00Z",
  "validUntil": "2025-12-01T23:59:59Z"
}
```

#### Step 3: Check-in Service Creates JWT

**JWT Header:**
```json
{
  "alg": "RS256",
  "typ": "JWT"
}
```

**JWT Payload:**
```json
{
  "ticketId": "ticket-uuid-123",
  "bookingId": "booking-uuid",
  "eventId": "event-uuid",
  "eventName": "Tech Conference 2025",
  "attendeeName": "John Doe",
  "attendeeEmail": "john@example.com",
  "ticketType": "VIP",
  "seatNumber": "A-12",
  "iat": 1701234567,
  "exp": 1703826567,
  "nbf": 1701234567
}
```

**Signing Process:**
```
1. Encode Header as Base64URL
2. Encode Payload as Base64URL
3. Create signature: 
   SHA256withRSA(base64(header) + "." + base64(payload), PRIVATE_KEY)
4. Final JWT = header.payload.signature
```

#### Step 4: Generate QR Code
```
1. Take complete JWT string
2. Generate QR code image (300x300 pixels)
3. Encode as Base64 string
4. Store in ticket record
```

#### Step 5: Store and Distribute
```
Database Record:
- ticket_id
- booking_id
- event_id
- jwt_token (full JWT string)
- qr_code_base64
- status (ACTIVE, SCANNED, CANCELLED)
- created_at

Distribution:
- Email to customer with QR code image
- Available in mobile app
- Printable PDF option
```

### Ticket JWT Example

```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aWNrZXRJZCI6ImFiYy0xMjMiLCJldmVudElkIjoiZXZlbnQtMDAxIiwiYXR0ZW5kZWVOYW1lIjoiSm9obiBEb2UiLCJhdHRlbmRlZUVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsImlhdCI6MTcwMTIzNDU2NywiZXhwIjoxNzAzODI2NTY3fQ.signature_here
```

---

## Scanner Registration Flow

### Overview
Before a scanner device can validate tickets, it must be registered with the system. This process is similar to WhatsApp's "Link Device" feature, using a time-limited QR code for secure pairing.

### Detailed Flow

```
Admin    Dashboard    Check-in       Database      Scanner
                      Service                       App
  │          │           │              │             │
  │ Click    │           │              │             │
  │ "Add     │           │              │             │
  │ Scanner" │           │              │             │
  ├─────────>│           │              │             │
  │          │           │              │             │
  │          │ Generate  │              │             │
  │          │ Token     │              │             │
  │          ├──────────>│              │             │
  │          │           │              │             │
  │          │           │ Create Token │             │
  │          │           │ (UUID)       │             │
  │          │           │ Set Expiry   │             │
  │          │           │ (5 min)      │             │
  │          │           │─┐            │             │
  │          │           │ │            │             │
  │          │           │<┘            │             │
  │          │           │              │             │
  │          │           │ Store Token  │             │
  │          │           ├─────────────>│             │
  │          │           │              │             │
  │          │  Return   │              │             │
  │          │  Token    │              │             │
  │          │<──────────┤              │             │
  │          │           │              │             │
  │          │ Generate  │              │             │
  │          │ QR Code   │              │             │
  │          │─┐         │              │             │
  │          │ │         │              │             │
  │          │<┘         │              │             │
  │          │           │              │             │
  │  Display │           │              │             │
  │  QR Code │           │              │             │
  │<─────────┤           │              │             │
  │          │           │              │             │
  │                                                   │
  │                    Scanner scans QR code          │
  │                                                   │
  │  Scan QR │           │              │             │
  ├──────────────────────────────────────────────────>│
  │          │           │              │             │
  │          │           │              │  Extract    │
  │          │           │              │  Token      │
  │          │           │              │─┐           │
  │          │           │              │ │           │
  │          │           │              │<┘           │
  │          │           │              │             │
  │          │           │  Register    │             │
  │          │           │  Scanner     │             │
  │          │           │<─────────────┼─────────────┤
  │          │           │  {token,     │             │
  │          │           │   name}      │             │
  │          │           │              │             │
  │          │           │ Validate     │             │
  │          │           │ Token        │             │
  │          │           ├─────────────>│             │
  │          │           │              │             │
  │          │           │ Token Valid? │             │
  │          │           │<─────────────┤             │
  │          │           │              │             │
  │          │           │ Generate     │             │
  │          │           │ Credentials  │             │
  │          │           │ (JWT)        │             │
  │          │           │─┐            │             │
  │          │           │ │            │             │
  │          │           │<┘            │             │
  │          │           │              │             │
  │          │           │ Get Public   │             │
  │          │           │ Key          │             │
  │          │           │─┐            │             │
  │          │           │ │            │             │
  │          │           │<┘            │             │
  │          │           │              │             │
  │          │           │ Create       │             │
  │          │           │ Scanner      │             │
  │          │           │ Record       │             │
  │          │           ├─────────────>│             │
  │          │           │              │             │
  │          │           │ Mark Token   │             │
  │          │           │ as Used      │             │
  │          │           ├─────────────>│             │
  │          │           │              │             │
  │          │           │  Return      │             │
  │          │           │  {id,        │             │
  │          │           │   credentials│             │
  │          │           │   publicKey, │             │
  │          │           │   settings}  │             │
  │          │           ├──────────────┼────────────>│
  │          │           │              │             │
  │          │           │              │  Store      │
  │          │           │              │  Config     │
  │          │           │              │─┐           │
  │          │           │              │ │           │
  │          │           │              │<┘           │
  │          │           │              │             │
  │  Show    │           │              │  Show       │
  │  Success │           │              │  Success    │
  │<─────────────────────────────────────────────────┤
  │          │           │              │             │

Legend:
  ───>   Request/Response
  ─┐ │   Internal processing
  ....   Scanner scans QR
```

### Step-by-Step Process

#### Step 1: Admin Initiates Registration
```
Admin Dashboard:
1. Navigate to "Scanners" section
2. Click "Add New Scanner" button
3. Specify scanner details:
   - Scanner Name: "Gate A - Main Entrance"
   - Validity: 5 minutes (default)
   - Notes: Optional description
```

#### Step 2: Server Generates Registration Token
```
POST /api/registration-tokens/generate
Request:
{
  "validityMinutes": 5,
  "notes": "Gate A scanner for Main Entrance"
}

Server Process:
1. Generate UUID token
2. Calculate expiry time (now + 5 minutes)
3. Store in database:
   - token: "abc-123-xyz-789"
   - expires_at: "2025-11-29T10:15:00Z"
   - used: false
   - created_by: "admin@nexgate.com"

Response:
{
  "token": "abc-123-xyz-789",
  "qrCodeBase64": "data:image/png;base64,iVBORw0KG...",
  "expiresAt": "2025-11-29T10:15:00Z",
  "validityMinutes": 5
}
```

#### Step 3: Display QR Code
```
Admin Dashboard displays:
- Large QR code containing the token
- Expiry countdown timer
- Token details
- "Waiting for scanner to connect..." message
```

#### Step 4: Scanner Scans QR Code
```
Scanner App:
1. Open camera for QR scanning
2. Scan QR code displayed on admin dashboard
3. Extract token string: "abc-123-xyz-789"
4. Prompt user to confirm device name
```

#### Step 5: Scanner Sends Registration Request
```
POST /api/scanners/register
Request:
{
  "token": "abc-123-xyz-789",
  "deviceName": "Gate A Scanner",
  "deviceInfo": {
    "model": "Samsung Galaxy S21",
    "osVersion": "Android 14",
    "appVersion": "1.0.0"
  }
}
```

#### Step 6: Server Validates and Issues Credentials
```
Server Validation:
1. Find token in database
2. Check if token exists
3. Check if token.used == false
4. Check if token.expiresAt > now()

If valid:
1. Generate scanner credentials (JWT):
   {
     "scannerId": "scanner-uuid",
     "scannerName": "Gate A Scanner",
     "type": "scanner_credential",
     "iat": now,
     "exp": now + 1 year
   }

2. Sign with server private key

3. Create scanner record in database:
   - scanner_id: UUID
   - name: "Gate A Scanner"
   - credentials: JWT
   - status: ACTIVE
   - settings: default settings JSON
   - created_at: now

4. Mark token as used:
   - used: true
   - used_at: now
   - scanner_name: "Gate A Scanner"

Response:
{
  "scannerId": "scanner-uuid",
  "credentials": "eyJhbGc...scanner_jwt",
  "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
  "settings": {
    "offlineModeEnabled": false,
    "syncIntervalMinutes": 15,
    "maxOfflineHours": 24
  }
}
```

#### Step 7: Scanner Stores Configuration
```
Scanner App (Local Storage):
1. Save scanner ID
2. Save credentials JWT
3. Convert publicKey string to PublicKey object
4. Save public key
5. Save settings
6. Mark device as "registered"

Scanner is now ready to validate tickets!
```

### Registration Token Lifecycle

```mermaid
stateDiagram-v2
    [*] --> Generated: Admin creates token
    Generated --> Active: Token created, timer starts
    Active --> Used: Scanner registers successfully
    Active --> Expired: Time runs out
    Used --> [*]: Token consumed
    Expired --> Deleted: Cleanup job runs
    Deleted --> [*]
    
    note right of Active
        Valid for 5 minutes
        Can only be used once
    end note
```

---

## Ticket Validation Flow

### Online Mode (Default)

```mermaid
sequenceDiagram
    participant Attendee
    participant ScannerApp
    participant CheckInService
    participant Database
    participant Cache
    
    Attendee->>ScannerApp: Present QR Code
    ScannerApp->>ScannerApp: Scan QR Code<br/>Extract JWT
    
    ScannerApp->>CheckInService: Validate Ticket<br/>POST /api/tickets/validate<br/>{jwt, scannerId}
    
    CheckInService->>CheckInService: Verify JWT Signature<br/>using Public Key
    
    alt Signature Invalid
        CheckInService-->>ScannerApp: Error: Invalid Ticket (Forged)
        ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Invalid Ticket
    else Signature Valid
        CheckInService->>CheckInService: Check JWT Expiration
        
        alt Ticket Expired
            CheckInService-->>ScannerApp: Error: Ticket Expired
            ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Ticket Expired
        else Ticket Valid
            CheckInService->>Cache: Check if already scanned<br/>(Redis: ticketId)
            
            alt Already Scanned
                Cache-->>CheckInService: Ticket found in scanned set
                CheckInService-->>ScannerApp: Error: Already Scanned
                ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Ticket Already Used
            else First Scan
                CheckInService->>Cache: Add to scanned set<br/>SET scanned:ticketId
                CheckInService->>Database: Record Scan<br/>{ticketId, scannerId, timestamp}
                CheckInService-->>ScannerApp: Success: Entry Granted
                ScannerApp-->>Attendee: ✅ ENTRY GRANTED<br/>Welcome!
            end
        end
    end
```

### Offline Mode (Emergency)

```mermaid
sequenceDiagram
    participant Attendee
    participant ScannerApp
    participant LocalDB
    participant PublicKey
    
    Note over ScannerApp: Scanner is OFFLINE<br/>No internet connection
    
    Attendee->>ScannerApp: Present QR Code
    ScannerApp->>ScannerApp: Scan QR Code<br/>Extract JWT
    
    ScannerApp->>ScannerApp: Parse JWT<br/>(header.payload.signature)
    
    ScannerApp->>PublicKey: Verify Signature<br/>using stored Public Key
    
    alt Signature Invalid
        ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Invalid Ticket
    else Signature Valid
        ScannerApp->>ScannerApp: Check JWT Expiration<br/>(from exp claim)
        
        alt Ticket Expired
            ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Ticket Expired
        else Ticket Valid
            ScannerApp->>LocalDB: Check local scanned list<br/>SELECT WHERE ticketId = ?
            
            alt Already Scanned Locally
                LocalDB-->>ScannerApp: Ticket found
                ScannerApp-->>Attendee: ❌ ENTRY DENIED<br/>Already Scanned (Local)
            else First Scan Locally
                ScannerApp->>LocalDB: Add to scanned list<br/>INSERT scan record
                ScannerApp->>LocalDB: Add to sync queue<br/>{ticketId, timestamp, offline:true}
                ScannerApp-->>Attendee: ✅ ENTRY GRANTED<br/>(OFFLINE MODE)
                
                Note over ScannerApp: Scan queued for sync<br/>when connection returns
            end
        end
    end
```

### Validation Details

#### JWT Signature Verification (Cryptographic Process)

```
Server Side (Ticket Generation):
1. Create payload: {ticketId, eventId, ...}
2. Sign: SIGNATURE = SHA256withRSA(header.payload, PRIVATE_KEY)
3. Result: JWT = header.payload.signature

Scanner Side (Validation):
1. Split JWT: parts = jwt.split(".")
2. Extract: header = parts[0], payload = parts[1], signature = parts[2]
3. Verify: SHA256withRSA.verify(header.payload, signature, PUBLIC_KEY)
4. If verification succeeds → Ticket is authentic
5. If verification fails → Ticket is forged/tampered
```

#### Online Validation Steps

```
1. Authentication Check
   - Verify scanner credentials (JWT)
   - Check if scanner is ACTIVE (not revoked)

2. Ticket Signature Verification
   - Parse JWT
   - Verify RSA signature
   - If invalid → REJECT (forged ticket)

3. Expiration Check
   - Extract exp claim from JWT
   - Compare with current time
   - If expired → REJECT

4. Duplicate Check (Redis)
   - Key: "scanned:{ticketId}"
   - Check if key exists
   - If exists → REJECT (already scanned)
   - If not exists → SET key with TTL (24 hours)

5. Database Logging
   - Insert into ticket_scans table:
     {
       ticket_id: from JWT,
       scanner_id: from request,
       scanned_at: current timestamp,
       validation_mode: "ONLINE",
       scan_result: "SUCCESS"
     }

6. Response
   - Return success with attendee details
   - Scanner displays: "Welcome, {attendee_name}!"
```

#### Offline Validation Steps

```
1. JWT Parsing
   - Split JWT into parts
   - Decode Base64URL payload

2. Signature Verification (Local)
   - Use stored PUBLIC_KEY
   - Verify signature cryptographically
   - If invalid → REJECT

3. Expiration Check (Local)
   - Extract exp from payload
   - Compare with device time
   - If expired → REJECT

4. Local Duplicate Check
   - Query local SQLite: 
     SELECT * FROM scanned_tickets WHERE ticket_id = ?
   - If found → REJECT
   - If not found → Continue

5. Local Recording
   - INSERT INTO scanned_tickets (ticket_id, scanned_at)
   - INSERT INTO sync_queue (ticket_id, scanned_at, synced: false)

6. Response
   - Display: "Entry Granted (Offline Mode)"
   - Show sync pending indicator
```

---

## Offline Mode Architecture

### Why Offline Mode?

Events often happen in locations with poor or no internet connectivity:
- Rural areas
- Basements/underground venues
- High-attendance events (network congestion)
- Outdoor festivals
- Emergency situations

The system must continue functioning even without internet.

### Offline Capabilities

```mermaid
graph TB
    subgraph "Online Operations"
        O1[Real-time duplicate detection across all gates]
        O2[Immediate sync to central database]
        O3[Live analytics dashboard]
        O4[Scanner settings updates]
    end
    
    subgraph "Offline Operations"
        F1[JWT signature verification]
        F2[Local duplicate detection at same gate]
        F3[Scan logging to local database]
        F4[Queue for later sync]
    end
    
    subgraph "Limitations in Offline Mode"
        L1[Cannot detect duplicates at other gates]
        L2[Cannot receive scanner revocations immediately]
        L3[Cannot update settings in real-time]
        L4[Relies on device clock for expiry check]
    end
    
    style F1 fill:#90EE90
    style F2 fill:#90EE90
    style L1 fill:#FFB6C1
    style L2 fill:#FFB6C1
```

### Offline Data Flow

```mermaid
sequenceDiagram
    participant Gate1 as Scanner (Gate 1)
    participant Gate2 as Scanner (Gate 2)
    participant Server as Check-in Service
    
    Note over Gate1,Gate2: Both scanners go OFFLINE
    
    rect rgb(255, 200, 200)
        Note over Gate1,Gate2: OFFLINE PERIOD
        
        Gate1->>Gate1: Scan Ticket ABC<br/>✅ Valid (first scan at Gate 1)
        Gate2->>Gate2: Scan Ticket ABC<br/>✅ Valid (Gate 2 doesn't know)
        
        Note over Gate1,Gate2: PROBLEM: Same ticket scanned twice!<br/>Offline mode cannot prevent this.
    end
    
    Note over Gate1,Gate2: Connection restored
    
    rect rgb(200, 255, 200)
        Note over Gate1,Gate2: SYNC PERIOD
        
        Gate1->>Server: Sync scans<br/>[{ticketABC, 10:00am}]
        Server->>Server: Record: Gate 1 scanned ABC at 10:00
        
        Gate2->>Server: Sync scans<br/>[{ticketABC, 10:05am}]
        Server->>Server: Detect: ABC already scanned!<br/>Flag as duplicate
        Server->>Server: Create alert for investigation
    end
```

### Sync Strategy

#### When to Sync

```
1. Automatic Sync Triggers:
   - Every N minutes (configurable, default: 15 minutes)
   - When connection restored after being offline
   - When scanner app comes to foreground
   - Before device goes to sleep

2. Manual Sync:
   - Admin can trigger sync from scanner UI
   - Force sync button available

3. Smart Sync:
   - Only sync if there are pending scans
   - Batch multiple scans in single request
   - Retry failed syncs with exponential backoff
```

#### Sync Process

```mermaid
sequenceDiagram
    participant Scanner
    participant LocalDB
    participant CheckInService
    participant Database
    
    Scanner->>LocalDB: Get pending scans<br/>SELECT * FROM sync_queue<br/>WHERE synced = false
    LocalDB-->>Scanner: Return scan records
    
    Scanner->>CheckInService: POST /api/scanners/sync<br/>{scannerId, scans: [...]}
    
    CheckInService->>CheckInService: Authenticate Scanner
    
    loop For each scan
        CheckInService->>Database: Check if ticket already scanned
        
        alt First scan of this ticket
            CheckInService->>Database: Record scan
            CheckInService->>CheckInService: Mark as SUCCESS
        else Duplicate scan
            CheckInService->>Database: Record as DUPLICATE_SCAN
            CheckInService->>CheckInService: Mark as DUPLICATE<br/>Create alert
        end
    end
    
    CheckInService->>CheckInService: Get latest scanner settings
    CheckInService-->>Scanner: Return {syncResults, settings}
    
    Scanner->>LocalDB: Update sync_queue<br/>SET synced = true
    Scanner->>Scanner: Apply new settings if changed
```

#### Sync Payload Example

```json
POST /api/scanners/sync
Request:
{
  "scannerId": "scanner-uuid",
  "scans": [
    {
      "ticketId": "ticket-123",
      "scannedAt": "2025-11-29T10:05:00Z",
      "validationMode": "OFFLINE",
      "deviceTime": "2025-11-29T10:05:00Z"
    },
    {
      "ticketId": "ticket-456",
      "scannedAt": "2025-11-29T10:10:00Z",
      "validationMode": "OFFLINE",
      "deviceTime": "2025-11-29T10:10:00Z"
    }
  ],
  "lastSyncAt": "2025-11-29T09:00:00Z"
}

Response:
{
  "syncResults": [
    {
      "ticketId": "ticket-123",
      "status": "SUCCESS",
      "message": "Scan recorded"
    },
    {
      "ticketId": "ticket-456",
      "status": "DUPLICATE",
      "message": "Ticket already scanned at Gate B",
      "originalScanTime": "2025-11-29T10:08:00Z",
      "originalScanner": "Gate B Scanner"
    }
  ],
  "settings": {
    "offlineModeEnabled": true,
    "syncIntervalMinutes": 15,
    "maxOfflineHours": 24
  },
  "serverTime": "2025-11-29T10:30:00Z"
}
```

### Conflict Resolution

```
Scenario: Same ticket scanned at multiple gates while offline

Gate A (10:05am): Scans ticket ABC - Valid ✅
Gate B (10:08am): Scans ticket ABC - Valid ✅ (doesn't know about Gate A)

Both sync at 10:30am:

Server Resolution:
1. Receive scan from Gate A (ABC at 10:05)
   - First scan seen by server
   - Record as VALID
   
2. Receive scan from Gate B (ABC at 10:08)
   - Server already has ABC scanned at 10:05
   - Record as DUPLICATE
   - Create alert for investigation
   
3. Admin Investigation:
   - Review: Was this intentional fraud?
   - Or: Genuine user scanned at wrong gate first?
   - Action: Take appropriate measures

4. Prevention for Future:
   - Reduce offline periods
   - More frequent syncs
   - Better gate coordination
```

---

## Security Model

### Cryptographic Architecture

```mermaid
graph TB
    subgraph "Server (Trust Anchor)"
        PrivateKey[RSA Private Key<br/>4096-bit<br/>NEVER leaves server]
        PublicKey[RSA Public Key<br/>Distributed to scanners]
    end
    
    subgraph "Ticket Generation"
        Ticket[Ticket Data<br/>JSON Payload]
        JWT[Signed JWT Token]
        QR[QR Code<br/>Contains JWT]
    end
    
    subgraph "Scanner Device"
        StoredPubKey[Stored Public Key]
        Verification[Signature Verification]
    end
    
    PrivateKey -->|Signs| JWT
    PublicKey -->|Copied to| StoredPubKey
    Ticket -->|Payload| JWT
    JWT -->|Encoded in| QR
    
    QR -->|Scanned| Verification
    StoredPubKey -->|Verifies| Verification
    
    style PrivateKey fill:#ff6666
    style PublicKey fill:#66ff66
    style Verification fill:#6666ff
```

### Security Layers

#### Layer 1: Scanner Authentication
```
Every scanner request must include:
- Scanner credentials (JWT)
- Signed with server's private key during registration
- Contains: scannerId, scannerName, expiry (1 year)

Server validates:
1. JWT signature is valid
2. JWT not expired
3. Scanner status is ACTIVE (not revoked)
4. Scanner ID exists in database
```

#### Layer 2: Ticket Cryptography
```
Ticket Security Guarantees:

1. Cannot Forge Tickets
   - Requires server's private key to sign
   - Private key never leaves server
   - Scanners only have public key (can verify, not sign)

2. Cannot Tamper with Tickets
   - Any modification invalidates signature
   - Changing even 1 character breaks verification
   - Scanner detects tampering immediately

3. Cannot Reuse Expired Tickets
   - Expiry timestamp in JWT payload
   - Verified during each scan
   - Cannot be modified (signature protection)

4. Cannot Clone Tickets (Online Mode)
   - Each ticketId tracked in Redis
   - Duplicate detection across all gates
   - Scan recorded in database
```

#### Layer 3: Network Security
```
All API Communication:
- HTTPS/TLS 1.3 only
- Certificate pinning in mobile app
- API rate limiting
- Request signing for sensitive operations
```

#### Layer 4: Scanner Revocation
```
Immediate Revocation:
1. Admin marks scanner as REVOKED
2. Scanner added to revocation list (Redis)
3. Next API request from scanner → DENIED
4. Settings push: {status: "REVOKED"}
5. Scanner clears local data

Scanner receives revocation on:
- Next sync attempt
- Real-time push (if WebSocket connected)
- Settings update request
```

### Attack Scenarios and Mitigations

#### Attack 1: Ticket Forgery
```
Attack: Attacker creates fake ticket JWT
Mitigation: 
- Cannot sign without private key
- Signature verification fails
- Scanner rejects ticket
Result: ❌ Attack prevented
```

#### Attack 2: Ticket Cloning
```
Attack: User shares same ticket QR with friend

Online Mode:
- First scan: ✅ Valid
- Second scan: ❌ Duplicate detected
Result: ✅ Attack prevented

Offline Mode:
- Different gates: Both scans succeed locally
- Server detects on sync
- Alert generated for investigation
Result: ⚠️ Detected post-facto
```

#### Attack 3: Scanner Credential Theft
```
Attack: Attacker steals scanner credentials from device

Mitigation:
1. Admin revokes stolen scanner
2. Scanner credential becomes invalid
3. Server denies all requests
4. New scanner issued with new credentials

Best Practice:
- Secure credential storage (Android Keystore)
- Device encryption
- Remote wipe capability
Result: ✅ Contained quickly
```

#### Attack 4: Replay Attack
```
Attack: Attacker captures network traffic, replays requests

Mitigation:
- HTTPS prevents traffic capture
- Request timestamps checked
- Nonce validation for sensitive operations
- Short-lived sessions
Result: ✅ Attack prevented
```

---

## Data Models

### Database Entities

#### 1. registration_tokens
```sql
CREATE TABLE registration_tokens (
    id UUID PRIMARY KEY,
    token VARCHAR(100) UNIQUE NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    used BOOLEAN DEFAULT FALSE,
    used_at TIMESTAMP,
    created_by VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    validity_minutes INTEGER NOT NULL,
    scanner_name VARCHAR(200),
    notes TEXT
);

-- Indexes
CREATE INDEX idx_token ON registration_tokens(token);
CREATE INDEX idx_valid_tokens ON registration_tokens(used, expires_at) 
    WHERE used = FALSE;
```

**Purpose:** Store time-limited tokens for scanner registration

**Lifecycle:**
1. Created when admin generates QR code
2. Marked as used when scanner registers
3. Cleaned up after 7 days (scheduled job)

#### 2. scanners
```sql
CREATE TABLE scanners (
    id UUID PRIMARY KEY,
    scanner_id VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(200) NOT NULL,
    credentials TEXT NOT NULL,
    status VARCHAR(20) NOT NULL, -- ACTIVE, REVOKED
    settings JSONB NOT NULL,
    device_info JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_synced_at TIMESTAMP,
    created_by VARCHAR(100)
);

-- Indexes
CREATE INDEX idx_scanner_id ON scanners(scanner_id);
CREATE INDEX idx_scanner_status ON scanners(status);
CREATE INDEX idx_last_synced ON scanners(last_synced_at);
```

**Settings JSONB Structure:**
```json
{
  "offlineModeEnabled": false,
  "syncIntervalMinutes": 15,
  "offlineDataSource": "AUTO_SYNC",
  "maxOfflineHours": 24,
  "allowedEventIds": ["event-1", "event-2"]
}
```

#### 3. tickets
```sql
CREATE TABLE tickets (
    id UUID PRIMARY KEY,
    ticket_id VARCHAR(100) UNIQUE NOT NULL,
    booking_id UUID NOT NULL,
    event_id UUID NOT NULL,
    attendee_name VARCHAR(200) NOT NULL,
    attendee_email VARCHAR(200),
    ticket_type VARCHAR(50),
    jwt_token TEXT NOT NULL,
    qr_code_base64 TEXT,
    status VARCHAR(20) NOT NULL, -- ACTIVE, SCANNED, CANCELLED
    valid_from TIMESTAMP NOT NULL,
    valid_until TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (booking_id) REFERENCES bookings(id),
    FOREIGN KEY (event_id) REFERENCES events(id)
);

-- Indexes
CREATE INDEX idx_ticket_id ON tickets(ticket_id);
CREATE INDEX idx_booking_id ON tickets(booking_id);
CREATE INDEX idx_event_id ON tickets(event_id);
CREATE INDEX idx_ticket_status ON tickets(status);
```

#### 4. ticket_scans
```sql
CREATE TABLE ticket_scans (
    id UUID PRIMARY KEY,
    ticket_id VARCHAR(100) NOT NULL,
    scanner_id UUID NOT NULL,
    scanned_at TIMESTAMP NOT NULL,
    validation_mode VARCHAR(20) NOT NULL, -- ONLINE, OFFLINE
    scan_result VARCHAR(20) NOT NULL, -- SUCCESS, DUPLICATE, EXPIRED, INVALID
    device_time TIMESTAMP,
    synced_at TIMESTAMP,
    metadata JSONB,
    
    FOREIGN KEY (scanner_id) REFERENCES scanners(id)
);

-- Indexes
CREATE INDEX idx_ticket_scans_ticket ON ticket_scans(ticket_id);
CREATE INDEX idx_ticket_scans_scanner ON ticket_scans(scanner_id);
CREATE INDEX idx_ticket_scans_time ON ticket_scans(scanned_at);
CREATE INDEX idx_ticket_scans_result ON ticket_scans(scan_result);
```

**Metadata JSONB Example:**
```json
{
  "attendeeName": "John Doe",
  "eventName": "Tech Conference 2025",
  "gateLocation": "Main Entrance",
  "duplicateOf": "scan-uuid-123",
  "alertGenerated": true
}
```

#### 5. rsa_keys
```sql
CREATE TABLE rsa_keys (
    id UUID PRIMARY KEY,
    key_version INTEGER UNIQUE NOT NULL,
    private_key TEXT NOT NULL, -- Encrypted
    public_key TEXT NOT NULL,
    algorithm VARCHAR(20) DEFAULT 'RS256',
    key_size INTEGER DEFAULT 4096,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    rotated_at TIMESTAMP,
    status VARCHAR(20) NOT NULL -- ACTIVE, ROTATED, REVOKED
);

-- Only one active key at a time
CREATE UNIQUE INDEX idx_active_key ON rsa_keys(status) 
    WHERE status = 'ACTIVE';
```

### Scanner Local Database (SQLite)

```sql
-- Scanner credentials and config
CREATE TABLE scanner_config (
    key VARCHAR(50) PRIMARY KEY,
    value TEXT NOT NULL
);

-- Locally scanned tickets
CREATE TABLE scanned_tickets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ticket_id VARCHAR(100) UNIQUE NOT NULL,
    scanned_at TIMESTAMP NOT NULL,
    attendee_name VARCHAR(200),
    event_name VARCHAR(200),
    validation_result VARCHAR(20) NOT NULL
);

-- Pending sync queue
CREATE TABLE sync_queue (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ticket_id VARCHAR(100) NOT NULL,
    scanned_at TIMESTAMP NOT NULL,
    device_time TIMESTAMP NOT NULL,
    validation_mode VARCHAR(20) NOT NULL,
    synced BOOLEAN DEFAULT FALSE,
    sync_attempts INTEGER DEFAULT 0,
    last_sync_attempt TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX idx_sync_pending ON sync_queue(synced) WHERE synced = FALSE;
CREATE INDEX idx_scanned_ticket_id ON scanned_tickets(ticket_id);
```

---

## API Specifications

### Base URL
```
Production: https://api.nexgate.com/v1
Staging: https://staging-api.nexgate.com/v1
```

### Authentication
```
All requests require authentication via JWT in header:
Authorization: Bearer {scanner_credentials_jwt}
```

### API Endpoints

#### 1. Generate Registration Token
```http
POST /api/registration-tokens/generate
Authorization: Bearer {admin_jwt}

Request Body:
{
  "validityMinutes": 5,
  "notes": "Gate A scanner for Main Entrance"
}

Response: 201 Created
{
  "token": "abc-123-xyz-789",
  "qrCodeBase64": "data:image/png;base64,iVBORw0KG...",
  "expiresAt": "2025-11-29T10:15:00Z",
  "validityMinutes": 5
}

Errors:
400 Bad Request - Invalid validity minutes
401 Unauthorized - Invalid admin credentials
```

#### 2. Register Scanner
```http
POST /api/scanners/register

Request Body:
{
  "token": "abc-123-xyz-789",
  "deviceName": "Gate A Scanner",
  "deviceInfo": {
    "model": "Samsung Galaxy S21",
    "osVersion": "Android 14",
    "appVersion": "1.0.0"
  }
}

Response: 201 Created
{
  "scannerId": "scanner-uuid",
  "credentials": "eyJhbGc...scanner_jwt",
  "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg...",
  "settings": {
    "offlineModeEnabled": false,
    "syncIntervalMinutes": 15,
    "maxOfflineHours": 24
  }
}

Errors:
400 Bad Request - Invalid or expired token
409 Conflict - Token already used
```

#### 3. Validate Ticket (Online)
```http
POST /api/tickets/validate
Authorization: Bearer {scanner_credentials}

Request Body:
{
  "jwt": "eyJhbGc...ticket_jwt",
  "scannerId": "scanner-uuid",
  "deviceTime": "2025-11-29T10:30:00Z"
}

Response: 200 OK
{
  "valid": true,
  "ticketId": "ticket-123",
  "attendeeName": "John Doe",
  "eventName": "Tech Conference 2025",
  "ticketType": "VIP",
  "message": "Entry granted"
}

Response: 400 Bad Request (Duplicate)
{
  "valid": false,
  "ticketId": "ticket-123",
  "reason": "DUPLICATE",
  "message": "Ticket already scanned",
  "originalScanTime": "2025-11-29T10:25:00Z",
  "originalScanner": "Gate B Scanner"
}

Response: 400 Bad Request (Expired)
{
  "valid": false,
  "ticketId": "ticket-123",
  "reason": "EXPIRED",
  "message": "Ticket has expired",
  "expiredAt": "2025-11-29T10:00:00Z"
}

Response: 400 Bad Request (Invalid)
{
  "valid": false,
  "reason": "INVALID_SIGNATURE",
  "message": "Ticket signature is invalid (possible forgery)"
}

Errors:
401 Unauthorized - Invalid scanner credentials
403 Forbidden - Scanner revoked
```

#### 4. Sync Scanner
```http
POST /api/scanners/sync
Authorization: Bearer {scanner_credentials}

Request Body:
{
  "scannerId": "scanner-uuid",
  "scans": [
    {
      "ticketId": "ticket-123",
      "scannedAt": "2025-11-29T10:05:00Z",
      "validationMode": "OFFLINE",
      "deviceTime": "2025-11-29T10:05:00Z"
    }
  ],
  "lastSyncAt": "2025-11-29T09:00:00Z"
}

Response: 200 OK
{
  "syncResults": [
    {
      "ticketId": "ticket-123",
      "status": "SUCCESS",
      "message": "Scan recorded"
    }
  ],
  "settings": {
    "offlineModeEnabled": true,
    "syncIntervalMinutes": 15
  },
  "serverTime": "2025-11-29T10:30:00Z",
  "pendingUpdates": []
}

Errors:
401 Unauthorized - Invalid scanner credentials
403 Forbidden - Scanner revoked
```

#### 5. Get Scanner Settings
```http
GET /api/scanners/{scannerId}/settings
Authorization: Bearer {scanner_credentials}

Response: 200 OK
{
  "offlineModeEnabled": false,
  "syncIntervalMinutes": 15,
  "offlineDataSource": "AUTO_SYNC",
  "maxOfflineHours": 24,
  "allowedEventIds": ["event-1", "event-2"]
}

Errors:
401 Unauthorized - Invalid scanner credentials
404 Not Found - Scanner not found
```

#### 6. Revoke Scanner
```http
POST /api/scanners/{scannerId}/revoke
Authorization: Bearer {admin_jwt}

Response: 200 OK
{
  "scannerId": "scanner-uuid",
  "status": "REVOKED",
  "revokedAt": "2025-11-29T10:30:00Z"
}

Errors:
401 Unauthorized - Invalid admin credentials
404 Not Found - Scanner not found
```

---

## Database Schema

### Full Schema Diagram

```mermaid
erDiagram
    REGISTRATION_TOKENS ||--o| SCANNERS : "used_by"
    SCANNERS ||--o{ TICKET_SCANS : "performs"
    TICKETS ||--o{ TICKET_SCANS : "scanned"
    EVENTS ||--o{ TICKETS : "contains"
    BOOKINGS ||--|| TICKETS : "generates"
    RSA_KEYS ||--o{ TICKETS : "signs"
    
    REGISTRATION_TOKENS {
        uuid id PK
        varchar token UK
        timestamp expires_at
        boolean used
        timestamp used_at
        varchar created_by
        int validity_minutes
        varchar scanner_name
    }
    
    SCANNERS {
        uuid id PK
        varchar scanner_id UK
        varchar name
        text credentials
        varchar status
        jsonb settings
        jsonb device_info
        timestamp last_synced_at
    }
    
    TICKETS {
        uuid id PK
        varchar ticket_id UK
        uuid booking_id FK
        uuid event_id FK
        varchar attendee_name
        text jwt_token
        text qr_code_base64
        varchar status
        timestamp valid_from
        timestamp valid_until
    }
    
    TICKET_SCANS {
        uuid id PK
        varchar ticket_id FK
        uuid scanner_id FK
        timestamp scanned_at
        varchar validation_mode
        varchar scan_result
        jsonb metadata
    }
    
    EVENTS {
        uuid id PK
        varchar name
        timestamp event_date
        varchar venue
    }
    
    BOOKINGS {
        uuid id PK
        uuid event_id FK
        varchar customer_email
        timestamp booking_date
    }
    
    RSA_KEYS {
        uuid id PK
        int key_version UK
        text private_key
        text public_key
        varchar status
    }
```

### Key Relationships

```
1. registration_tokens → scanners
   - One token can register one scanner
   - Token is marked as used when scanner created
   
2. scanners → ticket_scans
   - One scanner performs many scans
   - Track which scanner scanned which ticket
   
3. tickets → ticket_scans
   - One ticket can be scanned multiple times (duplicates logged)
   - Each scan recorded separately
   
4. events → tickets
   - One event has many tickets
   - Tickets belong to specific event
   
5. bookings → tickets
   - One booking generates one or more tickets
   - Ticket inherits customer info from booking
   
6. rsa_keys → tickets
   - Active RSA key used to sign all new tickets
   - Key rotation supported for security
```

---

## Deployment Architecture

### Infrastructure Overview

```mermaid
graph TB
    subgraph "Client Layer"
        ScannerApp[Scanner Android App]
        AdminWeb[Admin Web Dashboard]
        CustomerApp[Customer Mobile App]
    end
    
    subgraph "Load Balancer"
        LB[AWS Application Load Balancer]
    end
    
    subgraph "Application Layer"
        API1[Check-in Service Instance 1]
        API2[Check-in Service Instance 2]
        API3[Check-in Service Instance 3]
    end
    
    subgraph "Cache Layer"
        Redis[(Redis Cluster<br/>Scan Deduplication)]
    end
    
    subgraph "Database Layer"
        PG_Primary[(PostgreSQL Primary)]
        PG_Replica[(PostgreSQL Replica)]
    end
    
    subgraph "Storage Layer"
        S3[S3 Bucket<br/>QR Code Images]
    end
    
    subgraph "Monitoring"
        CloudWatch[CloudWatch Logs]
        Grafana[Grafana Dashboard]
    end
    
    ScannerApp --> LB
    AdminWeb --> LB
    CustomerApp --> LB
    
    LB --> API1
    LB --> API2
    LB --> API3
    
    API1 --> Redis
    API2 --> Redis
    API3 --> Redis
    
    API1 --> PG_Primary
    API2 --> PG_Primary
    API3 --> PG_Primary
    
    PG_Primary --> PG_Replica
    
    API1 --> S3
    API2 --> S3
    API3 --> S3
    
    API1 --> CloudWatch
    API2 --> CloudWatch
    API3 --> CloudWatch
    
    CloudWatch --> Grafana
```

### Deployment Configuration

#### Production Environment

```yaml
Check-in Service:
  Instances: 3 (Auto-scaling: 2-10)
  Instance Type: t3.large (2 vCPU, 8 GB RAM)
  Deployment: Blue-Green with ECS
  Health Check: /api/health every 30s
  
Database:
  Type: Amazon RDS PostgreSQL 15
  Instance: db.r6g.xlarge (4 vCPU, 32 GB RAM)
  Storage: 500 GB SSD (auto-scaling enabled)
  Replication: 1 read replica in different AZ
  Backup: Daily automated backups, 30-day retention
  
Redis:
  Type: Amazon ElastiCache
  Node Type: cache.r6g.large (2 vCPU, 13 GB RAM)
  Cluster: 3 nodes (1 primary, 2 replicas)
  Persistence: AOF enabled
  
Load Balancer:
  Type: Application Load Balancer
  SSL: AWS Certificate Manager
  Zones: Multi-AZ deployment
  
Monitoring:
  CloudWatch: All application and infrastructure metrics
  Grafana: Custom dashboards for scan analytics
  PagerDuty: Alert escalation
  
Backups:
  Database: Daily automated + on-demand
  Redis: Daily snapshots
  S3: Versioning enabled
```

### Scaling Strategy

```
Horizontal Scaling (API Instances):
- Metric: CPU > 70% or Request Count > 1000/min
- Scale up: Add 1 instance
- Scale down: Remove 1 instance if CPU < 30%
- Min instances: 2
- Max instances: 10

Database Scaling:
- Vertical: Upgrade instance type during low-traffic window
- Horizontal: Add read replicas for reporting/analytics
- Connection pooling: HikariCP with max 100 connections

Redis Scaling:
- Vertical: Upgrade node type
- Horizontal: Add replica nodes
- Cluster mode: Enable for > 10M keys

Regional Expansion:
- Deploy Check-in Service in multiple AWS regions
- Use Route 53 for geo-routing
- Replicate database across regions (read replicas)
```

### Disaster Recovery

```
RTO (Recovery Time Objective): 1 hour
RPO (Recovery Point Objective): 5 minutes

Recovery Procedures:

1. Database Failure:
   - Automatic failover to replica (< 2 minutes)
   - Promote replica to primary
   - Update application config
   - Restore read replica from backup

2. API Service Failure:
   - Auto Scaling Group spawns new instances
   - Load Balancer routes to healthy instances
   - Failed instances terminated and replaced

3. Redis Failure:
   - Automatic failover to replica
   - Application continues with slight latency
   - Rebuild cache from database if needed

4. Complete Region Failure:
   - Route 53 failover to backup region
   - Promote backup region database to primary
   - Update scanner apps via backend config
```

---

## Monitoring and Analytics

### Key Metrics

```
Scanner Metrics:
- Total active scanners
- Scanners online vs offline
- Scan rate per scanner
- Sync frequency and success rate
- Average offline duration

Ticket Metrics:
- Total scans per event
- Valid scans vs duplicates vs invalid
- Scan success rate
- Average scan time (online vs offline)
- Peak scan throughput

Performance Metrics:
- API response time (p50, p95, p99)
- Database query performance
- Redis hit/miss ratio
- Error rate by endpoint

Security Metrics:
- Invalid ticket attempts
- Scanner authentication failures
- Duplicate scan alerts
- Suspicious patterns (multiple duplicates)
```

### Alerting

```
Critical Alerts (PagerDuty):
- Database connection pool exhausted
- API error rate > 5%
- Redis cluster down
- Duplicate scan rate > 10%

Warning Alerts (Slack):
- Scanner offline > 30 minutes
- Sync failure rate > 20%
- API response time > 2s (p95)
- Database replication lag > 10s

Info Alerts (Email):
- Daily scan summary
- Weekly duplicate report
- Monthly scanner registration report
```

---

## Appendix

### Glossary

**RSA (Rivest-Shamir-Adleman)**: Asymmetric encryption algorithm used for digital signatures

**JWT (JSON Web Token)**: Compact, URL-safe means of representing claims between two parties

**QR Code**: Two-dimensional barcode that can be scanned by cameras

**Scanner Registration**: Process of linking a scanner device to the system

**Offline Mode**: Scanner operation without internet connectivity

**Duplicate Scan**: Attempt to scan the same ticket multiple times

**Sync Queue**: Local storage of scans waiting to be sent to server

**Public Key**: Cryptographic key used to verify signatures (safe to distribute)

**Private Key**: Cryptographic key used to create signatures (must remain secret)

### References

- JWT RFC: https://datatracker.ietf.org/doc/html/rfc7519
- RSA Cryptography: PKCS #1 v2.2
- QR Code Standard: ISO/IEC 18004:2015
- Android Keystore: https://developer.android.com/training/articles/keystore

### Change Log

**Version 1.0** (2025-11-29)
- Initial architecture document
- Complete system design
- All core flows documented

---

**Document Maintained By:** Nexgate Platform Team  
**Last Updated:** November 29, 2025  
**Next Review:** December 29, 2025

# NEXGATE EVENT MANAGEMENT PLATFORM

**Version:** 3.0  
**Last Updated:** November 2024  
**Document Owner:** Product Team

---

## 1. PLATFORM OVERVIEW

### What is Nexgate Events?

A **social commerce-integrated event management platform** where events and products live together in one ecosystem.

### The Problem We Solve

**For Hosts:**
- Slow payouts (5-7 days)
- High fees (8-15%)
- Events disconnected from products
- Complex ticket management
- Limited check-in devices

**For Attendees:**
- Events discovered through spam
- Tickets scattered everywhere
- Can't see which friends are going
- No product connection

### The Nexgate Solution

**For Hosts:**
- ✅ 48-hour payout (fastest in industry)
- ✅ 10% platform fee (transparent)
- ✅ Link products to events (unlimited)
- ✅ Simple per-tier pricing
- ✅ Unlimited scanner devices
- ✅ Coupon code system
- ✅ Social feed discovery

**For Attendees:**
- ✅ Discover through friends
- ✅ One QR per ticket
- ✅ Shop event products
- ✅ Hybrid options (physical/online)
- ✅ Mix free & paid tickets
- ✅ Use coupon codes

---

## 2. CORE PHILOSOPHY

### The 7 Unbreakable Laws

#### 1. One Ticket = One Unique QR Code
Every ticket has its own cryptographically signed QR code. Prevents fraud, perfect tracking, works offline.

#### 2. 48-Hour Payout Rule
Host receives money 48 hours after event ends. Period.

#### 3. Social Commerce First
Events ARE products. They live in the same feed. Events drive product sales, products drive event attendance.

#### 4. Hybrid Events, Hybrid Pricing
Physical ≠ Online. Different experiences = different prices.

#### 5. Per-Tier Pricing Freedom
Each tier decides: FREE or PAID. No global toggle. Mix freely.

#### 6. Device Linking for Scanners
No accounts needed. Generate link → Scan with any device → Revoke anytime.

#### 7. Gallery is Required
Minimum 3 photos. Events with photos get 3x more bookings.

---

## 3. USER ROLES

### Host (Event Creator)

**What they can do:**
- Create/edit/delete events
- Set ticket pricing & tiers
- Link unlimited products
- Generate scanner links
- Create coupon codes
- View analytics
- Receive payouts
- Respond to reviews

### Attendee

**What they can do:**
- Browse/search events
- Purchase tickets (with coupons)
- Transfer tickets
- Check-in with QR
- Leave reviews
- Shop event products
- Share events

### Scanner Device (No Account)

**What it is:**
- Any device with scanner link
- No Nexgate account needed

**What it can do:**
- Scan QR codes (specific event only)
- Show check-in confirmation
- Display real-time count
- Work offline

---

## 4. EVENT TYPES

### One-Time Event

Single date, fixed capacity.

**Use cases:** Product launches, pop-ups, workshops, concerts

**Example:**
```
Fashion Launch Party
March 15, 2025, 7:00 PM
Wynwood Gallery, Miami
$25 General, $50 VIP
100 capacity
```

### Recurring Series

Repeats weekly/monthly.

**Use cases:** Yoga classes, networking meetups, bootcamps

**Configuration:**
- Repeat: Weekly/Bi-weekly/Monthly
- Days: Mon, Wed, Fri
- End: Never/After X times/End date

**Example:**
```
Sunday Morning Yoga
Every Sunday, 8:00 AM
$25 per class
30 capacity per class
```

### How Recurring Payments Work

**Pay-Per-Class Model:**

Each class is a separate purchase with its own ticket.

```
USER BUYS TICKETS:

March 16 class → $25 → QR Code #1
March 23 class → $25 → QR Code #2  
March 30 class → $25 → QR Code #3

Each ticket = unique QR code
Each purchase = separate transaction
Each class = separate payout 48h after
```

**Why different QR codes?**
- Clear separation (each class is distinct)
- Works offline (no database lookup needed)
- Prevents fraud (can't reuse old ticket)
- Easy refunds (refund specific class)
- Simple transfers (transfer one class)
- Better tracking (attendance per class)

**User Experience:**

Tickets grouped in wallet by series:
```
┌─────────────────────────────────────┐
│ SUNDAY MORNING YOGA SERIES          │
├─────────────────────────────────────┤
│ Upcoming classes you purchased:     │
│                                     │
│ ✓ March 16 · 8:00 AM                │
│   [View QR Code]                    │
│                                     │
│ ○ March 23 · 8:00 AM                │
│   [View QR Code]                    │
│                                     │
│ ○ March 30 · 8:00 AM                │
│   [View QR Code]                    │
└─────────────────────────────────────┘

Smart features:
- Grouped by series name
- Highlights today's class
- Reminders show correct QR
- "Class 1 of 3 you purchased"
```

**Host Dashboard:**
```
SUNDAY MORNING YOGA (Recurring)

Next 6 classes:
━━━━━━━━━━━━━━━━━━
March 16 · $25 · 12/30 sold
March 23 · $25 · 18/30 sold
March 30 · $25 · 20/30 sold
April 6 · $25 · 25/30 sold
April 13 · $25 · 30/30 sold
April 20 · $25 · 30/30 sold

Revenue per class
Payout 48h after each class
```

**Future Enhancement (Phase 2):**
- Class packs: 10-class pack for $200
- Monthly unlimited: $99/month subscription
- Same QR for packs (use anytime)

### Multi-Day Event

Multiple consecutive days, same ticket.

**Use cases:** Festivals, retreats, conferences

**Features:**
- Same QR works all days
- Track attendance per day
- Allow re-entry each day

**Example:**
```
Wellness Retreat
March 15-17, 2025 (3 days)
Bali Resort
$450 (all 3 days)
50 capacity
```

### Location Types

#### In-Person Only
- Google Maps address (always visible)
- Parking info (optional)
- Venue capacity

#### Online Only
- Zoom/Google Meet/Custom
- Link revealed after purchase
- Unlimited or limited capacity

#### Hybrid (Physical + Online)
- Separate pricing for each
- Different capacities
- Different benefits
- Independent tracking

---

## 5. TICKET SYSTEM

### Per-Tier Pricing (Core Concept)

**Each tier independently decides: FREE or PAID**

No global toggle. Maximum flexibility.

### Creating Ticket Tiers

**Every tier has:**
1. **Name** - "VIP", "General", "Economy"
2. **Pricing** - ○ FREE or ○ PAID $[__]
3. **Capacity** - How many available
4. **Description** - What's included

### Example: In-Person Event

```
HOST CREATES:

TIER 1: Economy
- Pricing: ● FREE
- Capacity: 30
- Description: Standing room, general entry

TIER 2: Regular
- Pricing: ● PAID $35
- Capacity: 20
- Description: Reserved seat, drink, gift bag

TIER 3: VIP
- Pricing: ● PAID $75
- Capacity: 10
- Description: Front row, meet & greet, merch

SYSTEM DISPLAYS:
"FREE & PAID OPTIONS"
"From FREE to $75"
Total: 60 tickets
```

### Example: Hybrid Event

```
PHYSICAL TICKETS:

TIER 1: Standard In-Person
- ● PAID $40 · 25 capacity
- Entry, seating, refreshments

TIER 2: VIP In-Person
- ● PAID $85 · 10 capacity
- Front row, meet & greet

ONLINE TICKETS:

TIER 1: Basic Virtual
- ● FREE · Unlimited
- Livestream only

TIER 2: Premium Virtual
- ● PAID $20 · 100 capacity
- HD stream, recording, Q&A
```

### Ticket Purchase Flow

#### Single Tier Purchase
```
1. Select tier: "VIP ($75)"
2. Quantity: 2
3. Enter names for each
4. Apply coupon (optional)
5. Payment: $150
6. 2 unique QR codes sent
```

#### Mixed Tier Purchase
```
1. Select "Economy (FREE)" × 2
2. Add "VIP ($75)" × 1
3. Enter 3 names
4. Apply coupon: "FRIEND20" (-$15)
5. Payment: $60
6. 3 unique QR codes sent
```

### QR Code Structure

**Each QR contains:**
- Unique ticket ID
- Event ID
- User ID
- Tier ID
- Cryptographic signature
- Cannot be forged
- Works offline

---

## 6. HYBRID EVENTS & PRICING

### Why Hybrid Matters

Physical ≠ Online = Different pricing

### Host Setup

```
STEP 1: Choose ● Hybrid

STEP 2: Physical Tiers
Economy: FREE, 20 capacity
Premium: $50, 15 capacity

STEP 3: Online Tiers
Basic: FREE, Unlimited
Premium: $20, 50 capacity
```

### Attendee Selection

```
STEP 1: Choose Type
● In-Person  ○ Virtual

STEP 2: Choose Tier
[Based on selection]

Physical:
💚 Economy - FREE
⭐ Premium - $50

Virtual:
💻 Basic - FREE
💻 Premium - $20
```

---

## 7. SCANNER SYSTEM (DEVICE LINKING)

### How It Works

Generate link → Open on ANY device → Scan

No app. No account. Any device.

### Creating Scanner Links

```
EVENT DASHBOARD → [+ Add Scanner]

Device name: "Front Desk iPad"
Expires: ● After event  ○ Custom

[Generate Link]

→ Link: nexgate.app/scan/abc123xyz
→ QR code to share
→ Valid for this event only
```

### Using Scanner

**On ANY device:**
1. Open link in browser
2. Camera permission
3. Point at ticket QR
4. Instant validation

**Scanner Interface:**
```
┌─────────────────────┐
│ NEXGATE SCANNER     │
│ Beach Yoga - Mar 16 │
├─────────────────────┤
│ [CAMERA VIEW]       │
│ Point at QR code    │
├─────────────────────┤
│ 28/60 checked in    │
└─────────────────────┘
```

**Success:**
```
✓ Emma Wilson
Ticket: #2847
Tier: VIP ($75)
First-time! 🎉
```

### Features

**Works Online:**
- Real-time validation
- Live updates
- Full details

**Works Offline:**
- Cryptographic validation
- Local storage
- Syncs when online

**Security:**
- Auto-expires
- Revoke anytime
- Event-specific
- Activity tracking

---

## 8. PRODUCT INTEGRATION (SIMPLIFIED)

### Link Products to Events

Attach products from your shop or any shop on Nexgate. Unlimited products.

### Adding Products

```
EVENT DASHBOARD → Products Tab

[+ Add Product from My Shop]
[+ Add by Product Link]

OPTION 1: From Your Shop
→ Browse your products
→ Select any/all
→ [Add Selected]

OPTION 2: By Link
→ Paste: nexgate.com/products/yoga-mat
→ System fetches details
→ [Add Product]
```

### Managing Products

```
ATTACHED PRODUCTS (12):

📸 Yoga Mat - $65
   From: @yogagear
   [Remove]

📸 Water Bottle - $15
   From: Your Shop
   [Remove]

[+ Add More Products]

Can add/remove anytime:
✓ Before event
✓ During event
✓ After event
```

### Display to Attendees

**On Event Page:**
```
EVENT MARKETPLACE

📸 Yoga Mat - $65
   by @yogagear
   [View Product]

📸 Water Bottle - $15
   by @sunstudio
   [View Product]

📸 Workshop Recording - $10
   by @sunstudio
   [View Product]

+ View All Products (9 more)

[Browse Event Marketplace →]
```

Shows first 3-5 products, then "View All"

**Event Marketplace Page:**
Full grid of all attached products (unlimited)

### That's It!

**Simple rules:**
- ✅ Attach unlimited products
- ✅ Yours or others' shops
- ✅ Add/remove anytime
- ✅ No timing restrictions
- ✅ Display on event page
- ✅ Users shop when they want

---

## 9. COUPON SYSTEM (NEW!)

### Generate Discount Codes

Create coupon codes for ticket discounts.

### Creating Coupons

```
EVENT DASHBOARD → Coupons Tab

[+ Create New Coupon]

┌─────────────────────────────────────┐
│ CREATE COUPON                       │
├─────────────────────────────────────┤
│ Coupon code:                        │
│ [EARLYBIRD2025]                     │
│                                     │
│ Discount type:                      │
│ ● Percentage: [20]%                 │
│ ○ Fixed amount: $[__]               │
│                                     │
│ Applies to:                         │
│ ☑️ All ticket tiers                 │
│ ☐ Specific tiers only               │
│                                     │
│ Usage limit:                        │
│ ● Unlimited uses                    │
│ ○ Limited: [100] total uses         │
│ ○ One use per customer              │
│                                     │
│ Valid period:                       │
│ Start: [March 1, 2025]              │
│ End: [March 10, 2025]               │
│ ○ No expiration                     │
│                                     │
│ [Create Coupon]                     │
└─────────────────────────────────────┘
```

### Managing Coupons

```
ACTIVE COUPONS (3):

┌─────────────────────────────────────┐
│ EARLYBIRD2025                       │
│ 20% off all tickets                 │
│ Used: 23 times                      │
│ Valid until: Mar 10                 │
│ [Copy Code] [Edit] [Disable]       │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ FRIEND50                            │
│ $5 off all tickets                  │
│ Used: 7/50 times                    │
│ No expiration                       │
│ [Copy Code] [Edit] [Disable]       │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ VIP100                              │
│ $10 off VIP tier only               │
│ Used: 2/10 times                    │
│ Valid until: Mar 15                 │
│ [Copy Code] [Edit] [Disable]       │
└─────────────────────────────────────┘
```

### Attendee Using Coupon

```
CHECKOUT

Order Summary:
VIP Ticket × 1          $75.00

Have a coupon code?
[EARLYBIRD2025        ] [Apply]

✓ Coupon applied: EARLYBIRD2025
Discount (20%):        -$15.00
────────────────────────────────
Total:                 $60.00

[Complete Purchase - $60]
```

### Sharing Coupons

```
EARLYBIRD2025

[Copy Code] → "Copied!"

Share via:
[📧 Email] [📱 SMS] [📋 Copy Link]

Direct link with coupon:
nexgate.com/events/beach-yoga?coupon=EARLYBIRD2025
```

### Coupon Analytics

```
COUPON PERFORMANCE

EARLYBIRD2025:
- Uses: 23
- Discount given: $345
- Revenue generated: $1,380
- Conversion boost: +15%

Top performers:
1. EARLYBIRD2025 (23 uses)
2. FRIEND50 (7 uses)
3. VIP100 (2 uses)
```

---

## 10. REVIEW SYSTEM

### Collection Timeline

**2 hours after event:**
"Quick rating ⭐" → Tap 5 stars

**24 hours later:**
Full review request + incentives

**1 week later:**
Final reminder

### Review Form

```
RATE BEACH YOGA

Overall: ⭐⭐⭐⭐⭐ (required)

Quick ratings (optional):
Venue: ⭐⭐⭐⭐⭐
Instructor: ⭐⭐⭐⭐⭐
Organization: ⭐⭐⭐⭐☆

Tell us more: (optional)
[Amazing sunrise session...]

Add photos: [+] [+] [+]

Privacy:
● Public  ○ Anonymous  ○ Private

EARN:
✓ 50 points ($5 credit)
✓ 10% off next event
+ Photos: 100 bonus points

[Submit Review]
```

### Display

```
⭐ 4.8 out of 5 (127 reviews)

BREAKDOWN:
⭐⭐⭐⭐⭐ ████████ 89%
⭐⭐⭐⭐☆ ███      8%

MOST MENTIONED:
🌅 Great vibes (42)
🧘 Amazing instructor (38)

REVIEWS:

Emma Wilson ⭐⭐⭐⭐⭐
"Magical sunrise session..."
📸📸📸
👍 12 helpful

↳ Host: "Thank you Emma! 🙏"
```

---

## 11. PAYMENT & PAYOUT

### When Attendee Buys

```
1. Select ticket
2. Apply coupon (optional)
3. Enter payment
4. Charge immediately
5. Funds → Escrow
6. Confirmation sent
```

### Escrow System

```
Purchase → Escrow → Event Completes → Wait 48h → Payout

Timeline:
Mar 1: Ticket bought ($75) → Escrow
Mar 16: Event ends (7pm)
Mar 18: Payout (7pm, 48h later)
```

### The 48-Hour Rule

**Event End Time + 48 Hours = Payout**

### Payout Calculation

```
SIMPLE:
Gross: $1,000
Platform fee (10%): -$100
Net: $900

MIXED FREE + PAID:
Free: 40 tickets (no revenue)
Paid: 30 × $25 = $750
Fee (on paid only): -$75
Net: $675

WITH COUPONS:
Gross: $1,500
Coupons used: -$200
Platform fee (10% of $1,300): -$130
Net: $1,170
```

### Payout Dashboard

```
UPCOMING:
Beach Yoga - Mar 16
Payout: Mar 18, 7pm
Amount: $945
Status: ⏰ 32h left

COMPLETED:
Sunset Yoga - Mar 9
Paid: Mar 11, 6:30pm
Amount: $612.50
Status: ✓ Deposited
```

### Refunds

**Host-initiated:**
- Full refund to attendee
- Platform fee non-refundable
- Host absorbs fee cost

**Automatic (event cancelled):**
- All attendees refunded
- Full amount + platform fee
- Nexgate absorbs cost
- Within 24 hours

---

## 12. NOTIFICATION SYSTEM

### Philosophy

**Simple, smart, not annoying.**

Only send notifications that matter. Automatic reminders based on event timing.

### Attendance Confirmation (Optional)

**Host can enable:**

```
EVENT SETTINGS → Attendee Management

☑️ Request attendance confirmation
Send [3] days before event

Attendees confirm they're coming
Host sees who confirmed vs pending
```

**Attendee receives:**
```
┌─────────────────────────────────────┐
│ CONFIRM YOUR ATTENDANCE             │
├─────────────────────────────────────┤
│ Beach Yoga - March 16, 5pm          │
│                                     │
│ Are you still coming?               │
│                                     │
│ [✓ Yes, I'm Coming]                 │
│ [✗ Can't Make It]                   │
└─────────────────────────────────────┘
```

**Host Dashboard:**
```
ATTENDANCE TRACKER

Total tickets: 30

✓ Confirmed: 22 people
⏳ Pending: 5 (no response)
✗ Can't attend: 3 people

Expected: ~73% attendance

[Send Reminder to Pending]
```

**Benefits:**
- Host plans better (food, materials, space)
- Reduces no-shows
- Professional event management

### Reminder Schedule (Automatic)

**Smart formula based on event timing:**

```
IF event is 2-7 DAYS AWAY:
→ 1 day before
→ 1 hour before

IF event is 8-30 DAYS AWAY:
→ 1 week before
→ 1 day before
→ 1 hour before

IF event is 30+ DAYS AWAY:
→ 2 weeks before
→ 1 week before
→ 1 day before
→ 1 hour before

IF event is SAME DAY:
→ 1 hour before only
```

**Rules:**
- Always: 1 hour before (critical)
- If time allows: 1 day before
- If far out: 1 week before
- If very far: 2 weeks before
- Maximum: 4 reminders total
- Minimum: 1 reminder (1h before)

### Adaptive Reminders

**System adjusts based on purchase timing:**

```
Event: March 30 (30 days away)

User A buys March 1:
→ 2 weeks, 1 week, 1 day, 1h reminders

User B buys March 28 (2 days before):
→ 1 day, 1h reminders only

User C buys March 30 (same day):
→ 1h reminder only

Each user gets only relevant reminders
```

### Notification Timeline

**Before Event:**

```
[X days before] (if host enabled)
"Confirm your attendance"
[Yes, I'm Coming] [Can't Make It]

[Auto-calculated]
"Beach Yoga is next week!"

[1 day before]
🔔 Beach Yoga tomorrow at 5pm!
📍 1234 Ocean Drive, Miami Beach
🚗 Street parking ($2/hr)
[View Ticket] [Get Directions]

[1 hour before]
🔔 Beach Yoga starts in 1 hour!
⏰ 5:00 PM - Don't be late!
[View Your Ticket QR]
```

**After Event:**

```
[2 hours after]
"How was Beach Yoga? ⭐"
Tap to rate: ⭐⭐⭐⭐⭐
Earn 50 points!

[24 hours after]
"Leave a review (earn 50 points)"
[Write Review]
```

### Total Notifications Per Attendee

**Minimum** (same-day event):
1. 1h before reminder
2. Rate request (2h after)
Total: **2 notifications**

**Typical** (1-week advance):
1. 1 day before reminder
2. 1h before reminder
3. Rate request (2h after)
Total: **3 notifications**

**Maximum** (1+ month advance):
1. Confirmation request (if host enabled)
2. 2 weeks before
3. 1 week before
4. 1 day before
5. 1h before
6. Rate request (2h after)
Total: **6 notifications**

### Host Notifications

**Sales Activity:**
```
"New ticket sale! 🎉"
Sarah Chen · Premium ($40)
Total: 23/30 tickets

[View Dashboard]
```

**Sold Out:**
```
"Beach Yoga is SOLD OUT! 🔥"
30/30 tickets · $1,200 gross

[View Analytics]
```

**New Review:**
```
"New 5-star review! ⭐"
Emma: "Amazing sunrise session..."

[View Review] [Respond]
```

**Payout Ready:**
```
"💰 Your payout is ready!"
$945 from Beach Yoga

[View Details]
```

### Host Controls

```
EVENT SETTINGS → Notifications

ATTENDANCE CONFIRMATION:
○ Disabled
● Enabled: Send [3] days before

REMINDER SCHEDULE:
● Automatic (smart formula) ← Default
○ Custom schedule

POST-EVENT:
☑️ Request rating (2h after)
☑️ Request review (24h after)

[Save Settings]
```

### User Notification Settings

```
NOTIFICATION PREFERENCES

Event Reminders:
☑️ Smart reminders (recommended)
☐ Disable all except 1h before

Review Requests:
☑️ After attending events
☐ Disable review requests

Friend Activity:
☑️ When friends create events
☑️ When friends buy tickets

Cannot disable:
- 1h before event (critical)

[Save Preferences]
```

### Anti-Spam Rules

**Frequency Caps:**
- Max 4 reminders per event
- No more than 2 notifications per day
- 24h+ between marketing notifications

**Smart Batching:**
If multiple events same day:
```
"You have 3 events tomorrow 🗓️"
- Beach Yoga · 5pm
- Pottery Class · 7pm
- Food Festival · 8pm

[View All Tickets]
```

---

## 13. USER JOURNEYS

### Journey 1: Host Creates Event

```
1. Emma logs in
2. [Create Event]
3. Upload 3 photos
4. Title: "Beach Yoga"
5. Category: Fitness
6. Location: ● Hybrid
7. Physical: FREE (20) + $40 (15)
8. Online: FREE (∞) + $15 (50)
9. Link products: Yoga mat, recording
10. Create coupon: "EARLY20" (20% off)
11. [Publish]
12. Share on Instagram

Time: 12 minutes
```

```
EVENT DAY:
- Generate 2 scanner links
- Send to iPad + volunteer
- 28 physical checked in
- 119 online joined
- Event ends successfully
```

```
48 HOURS LATER:
- Payout: $945 deposited
- 24 reviews (4.9 stars)
- 18 product purchases
```

### Journey 2: Attendee Discovers & Attends

```
Sarah scrolling feed
→ "Beach Yoga - Emma +8 friends going"
→ Taps event

Event page:
→ Beautiful photos
→ 4.9 stars
→ Virtual Premium: $15

Checkout:
→ Apply coupon: "EARLY20"
→ $15 → $12
→ Apple Pay
→ Done!

Event day:
→ Zoom link opens
→ 130 people online
→ 90-min session
→ Downloads recording

2 hours later:
→ "Rate ⭐"
→ 5 stars
→ Earns 150 points
→ Buys recording: $10
```

---

## 14. TECHNICAL REQUIREMENTS

### Performance

- Event page: < 2s
- Search: < 1s
- Scanner: < 500ms
- Payment: < 3s

### Scalability

- 10k concurrent users
- 1k simultaneous purchases
- 500 simultaneous scans

### Availability

- 99.9% uptime
- Zero-downtime deploys
- < 4h recovery

### Security

- OAuth login
- JWT tokens
- AES-256 encryption
- TLS 1.3
- PCI DSS compliant
- GDPR compliant
- SHA-256 QR signing

### Mobile

- Mobile-first design
- Works on 3G/4G
- Offline scanner
- PWA support
- iOS 14+, Android 8+

---

## 15. BUSINESS RULES

### Capacity

- Cannot oversell
- Atomic transactions
- Per-tier tracking
- Sold-out states

### Ticket Transfers

**Allowed:**
- Before event starts
- Unlimited transfers
- New QR for recipient

**Not allowed:**
- During/after event

### Check-In Rules

**Standard:** One scan
**Multi-day:** Same QR all days
**Recurring:** Track class usage

### Coupon Rules

**Validation checks:**
- Code exists
- Not expired
- Usage limit not reached
- Applies to selected tickets
- Can be combined? (host setting)

### Refund Policies

**Host options:**
1. Full refund anytime
2. Full until X days before
3. Partial after deadline
4. No refunds

---

## 16. SUCCESS METRICS

### Platform Growth (Year 1)

- Month 1: 50 events, 500 tickets
- Month 6: 500 events, 10k tickets
- Month 12: 2k events, 50k tickets

### Key Metrics

**Engagement:**
- View → purchase: 15%+
- Repeat attendees: 40%+
- Review rate: 40%+
- Social share: 20%+
- Coupon usage: 25%+

**Revenue:**
- GMV growth
- Platform revenue (10%)
- Avg ticket price
- Product revenue: +30%

**Quality:**
- Event rating: 4.5+
- Host retention: 60%+
- Scanner success: 99%+
- Uptime: 99.9%+

---

## 17. IMPLEMENTATION ROADMAP

### Phase 1: MVP (Months 1-3)

**Build:**
1. Event creation (all types)
2. Hybrid pricing
3. Per-tier pricing (free+paid)
4. Multiple ticket purchase
5. Unique QR per ticket
6. Gallery (3 photos min)
7. Device linking scanner
8. Product attachment (unlimited)
9. Coupon system
10. 48-hour payout
11. Basic reviews
12. Feed integration

**Success:**
- 50 events
- 500 tickets
- 4.0+ rating

### Phase 2: Enhanced (Months 4-6)

**Add:**
1. Advanced analytics
2. Ticket transfer
3. Review filters
4. Host responses
5. Attendee messaging
6. Advanced coupons (tiered, bundled)
7. Product marketplace analytics

**Success:**
- 200 events/month
- 30% hybrid adoption
- 50% review rate

### Phase 3: Scale (Months 7-12)

**Add:**
1. Class packs
2. Monthly passes
3. Multi-language
4. International currency
5. API access
6. White-label

**Success:**
- 1,000+ events/month
- 50,000+ tickets/month
- Profitable

---

## CONCLUSION

### What Makes Nexgate Different

**The Formula:**
```
Social Commerce + Events = Network Effects
```

**The Flywheel:**
```
Host creates event
→ Links products
→ Creates coupons
→ Shares to feed
→ Friends discover
→ Use coupons
→ Buy tickets
→ Shop products
→ Leave reviews
→ More social proof
→ REPEAT
```

### Key Advantages

1. **48-hour payout** - Fastest in industry
2. **Unlimited products** - Any shop on platform
3. **Coupon system** - Built-in marketing tool
4. **Device linking** - Unlimited scanners
5. **Per-tier pricing** - Mix free + paid freely
6. **Hybrid events** - Physical + online pricing
7. **Social discovery** - Friends drive attendance

### Next Steps

1. Build MVP (3 months)
2. Beta with 50 hosts
3. Iterate & improve
4. Scale to 1,000+ hosts
5. Dominate social commerce events

---

**END OF DOCUMENT**

*Version: 3.0 - Simplified & Focused*
*Last Updated: November 2024*

# Event Management System - Requirements & User Flow Documentation

## Table of Contents

1. [Overview](#overview)
2. [Event Types](#event-types)
3. [Core Features](#core-features)
4. [Event Creation Flows](#event-creation-flows)
5. [Ticket Management](#ticket-management)
6. [Registration & Attendance](#registration--attendance)
7. [User Roles & Permissions](#user-roles--permissions)
8. [Business Rules](#business-rules)
9. [Data Relationships](#data-relationships)

---

## Overview

The Event Management System supports three distinct event lifecycle types with flexible ticketing, multiple location options, and comprehensive access control. The system is designed to handle everything from simple one-time events to complex recurring series with variable pricing.

### Key Capabilities
- Multiple event types (One-time, Multi-day, Recurring)
- Flexible ticket types with varied pricing
- Multiple location modes (In-person, Online, Hybrid)
- Access control (Public, Private, Unlisted)
- Integration with online meeting providers
- Comprehensive registration and attendance tracking

---

## Event Types

### 1. One-Time Event
A single event occurring once at a specific date and time.

**Required Information:**
- Start date & time
- End date & time
- Location details
- Ticket types (optional)

**Example:**
```
Event: Tech Conference 2025
Date: March 15, 2025
Time: 9:00 AM - 5:00 PM
Location: Convention Center
```

---

### 2. Multi-Day Event
A single event spanning multiple consecutive or non-consecutive days.

**Required Information:**
- Start date
- End date
- Schedule per day (can vary)
- Location details (can vary per day)
- Single ticket for all days

**Characteristics:**
- ONE event, multiple days
- ONE ticket purchase covers ALL days
- Each day can have different start/end times
- Each day can have different locations
- User registers once, attends multiple days

**Example:**
```
Event: Music Festival 2025
Dates: June 10-12, 2025

Day 1 (June 10): 2:00 PM - 11:00 PM - Main Stage
Day 2 (June 11): 12:00 PM - 11:00 PM - Multiple Stages  
Day 3 (June 12): 1:00 PM - 10:00 PM - Main Stage

Ticket: $200 (access all 3 days)
```

---

### 3. Recurring Event
A series of separate events happening on a repeating schedule.

**Required Information:**
- Series name & description
- Recurrence pattern:
  - Frequency: Daily, Weekly, Monthly, Yearly
  - Interval: Every 1 week, every 2 weeks, etc.
  - Days of week (for weekly)
  - Day of month (for monthly)
- Date range: Start date → End date OR number of occurrences
- Default schedule for sessions
- Default ticket types

**Characteristics:**
- MULTIPLE separate events (series)
- Each session = independent event
- Users buy tickets per session
- Must buy at least ONE session to register for series
- Can buy multiple sessions at once or separately
- Each session can have own ticket types and pricing

**Example:**
```
Series: Monday Yoga Classes
Pattern: Every Monday
Time: 9:00 AM - 10:00 AM
Duration: January - March 2025 (12 sessions)

Individual Sessions:
- Session 1: Jan 6, 2025, 9-10 AM
- Session 2: Jan 13, 2025, 9-10 AM
- Session 3: Jan 20, 2025, 9-10 AM
... (12 total)

Each session has tickets: Regular ($15), Student ($10), Member ($8)
```

**Recurrence Patterns Supported:**
- Daily: Every day, every 2 days, weekdays only, etc.
- Weekly: Every Monday, Mon/Wed/Fri, every 2 weeks on Tuesday, etc.
- Monthly: First Monday, 15th of each month, last Friday, etc.
- Yearly: Same date each year

---

## Core Features

### Event Visibility & Access Control

```
┌──────────────────────────────────────────────────────────────┐
│ VISIBILITY OPTIONS                                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ○ Public                                                    │
│    • Discoverable in search                                  │
│    • Anyone can view and register                            │
│    • Appears in event listings                               │
│                                                              │
│  ○ Private                                                   │
│    • Not discoverable                                        │
│    • Invitation-only access                                  │
│    • Organizer sends invitations                             │
│    • Invitees must be explicitly added                       │
│                                                              │
│  ○ Unlisted                                                  │
│    • Not discoverable in search                              │
│    • Anyone with link can access                             │
│    • No invitation required                                  │
│    • Link-based sharing                                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

#### Public Events
- Visible to everyone
- Appear in search results and listings
- Anyone can register/buy tickets
- No access restrictions

#### Private Events
- Not visible in search
- Access by invitation only
- Organizer manages invitations

**Invitation System:**
- Organizer invites by email or username
- Invitations have optional expiration dates
- Invitation statuses: Pending, Accepted, Declined, Expired
- Invited users get notifications
- Can generate shareable invite links with tokens

**Invitation Expiration Rules:**
- Automatically expires when event ends
- Optional custom expiration date
- Token-based links can be regenerated (invalidates old tokens)

#### Unlisted Events
- Not discoverable in search
- Anyone with the link can access
- No explicit invitation needed
- Access through direct URL only

**Access Mechanism:**
- Simple approach: Direct URL with event ID
- Secure approach: URL includes access token
- Organizer can regenerate link (invalidates previous link)
- No expiration on the link itself (valid until event ends)

---

### Location Types

```
┌──────────────────────────────────────────────────────────────┐
│ LOCATION TYPE                                                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ○ In-Person Only                                            │
│    Required: Physical address, venue name                    │
│    Optional: Coordinates (lat/long), directions             │
│                                                              │
│  ○ Online Only                                               │
│    Required: Meeting platform, link/credentials             │
│    Options: Manual link OR provider integration             │
│                                                              │
│  ○ Hybrid (Both)                                             │
│    Required: ALL in-person + online details                 │
│    Attendees choose: in-person OR online                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘
```

#### In-Person Events
**Required Fields:**
- Venue name
- Full address
- City, State, Country

**Optional Fields:**
- Latitude/Longitude (for maps)
- Venue description
- Parking information
- Accessibility details

#### Online Events
**Meeting Provider Options:**

**Option 1: Manual Entry**
- Organizer provides meeting link directly
- Platform-agnostic (Zoom, Google Meet, Teams, Discord, etc.)
- Organizer manages meeting creation
- Meeting details stored:
  - Meeting URL
  - Meeting ID (optional)
  - Password/Access code (optional)

**Security for Manual Links:**
- Link hidden until 30 minutes before event
- User must be logged in
- User must have valid ticket
- Link visible until 1 hour after event ends

**Option 2: Provider Integration (Zoom)**
- OAuth connection to organizer's account
- System creates meeting automatically
- Enhanced security features available

**Zoom Integration Flow:**
```
┌─────────────────────────────────────────────────────────────┐
│ Event Creation: Online Event → Select "Connect with Zoom"  │
└──────────────────┬──────────────────────────────────────────┘
                   ↓
         ┌─────────────────────┐
         │ Has Zoom connected? │
         └─────────┬───────────┘
                   ↓
           ┌───────┴────────┐
           │                │
          NO               YES
           │                │
           ↓                ↓
┌──────────────────┐  ┌──────────────────┐
│ Redirect to Zoom │  │ Create meeting   │
│ OAuth page       │  │ immediately via  │
│                  │  │ API              │
└────────┬─────────┘  └─────────┬────────┘
         │                      │
         ↓                      │
┌──────────────────┐           │
│ User authorizes  │           │
│ on Zoom          │           │
└────────┬─────────┘           │
         │                      │
         ↓                      │
┌──────────────────┐           │
│ Save tokens to   │           │
│ user account     │           │
└────────┬─────────┘           │
         │                      │
         ↓                      │
┌──────────────────┐           │
│ Create meeting   │←──────────┘
│ via Zoom API     │
└────────┬─────────┘
         │
         ↓
┌──────────────────────────────┐
│ Each paid attendee gets      │
│ UNIQUE registration link     │
│ (cannot be shared)           │
└──────────────────────────────┘
```

**Zoom Integration Benefits:**
- Each registered attendee gets unique join URL
- URLs tied to email (not shareable)
- Waiting room can be enabled
- Attendance tracking automatic
- Registration approval control

**Zoom Account Responsibility:**
- Organizer uses their own Zoom account
- Organizer pays for Zoom subscription
- Platform creates meetings using organizer's credentials
- No cost to platform

#### Hybrid Events
**Requirements:**
- ALL in-person details (venue, address, etc.)
- ALL online details (meeting link/platform)

**Attendee Selection:**
- During registration, attendee chooses: "Attending in-person" OR "Attending online"
- Helps organizer plan:
  - Seating/catering for in-person
  - Meeting capacity for online
- Attendee can change preference before event

---

## Event Creation Flows

### Flow 1: One-Time Event Creation

```
┌──────────────────────────────────────────────────────────────┐
│ CREATE ONE-TIME EVENT                                        │
└──────────────────────┬───────────────────────────────────────┘
                       ↓
         ┌─────────────────────────────┐
         │ Step 1: Basic Information   │
         │ • Event name                │
         │ • Description               │
         │ • Category                  │
         │ • Cover image               │
         │ • Visibility (Public/Private│
         │   /Unlisted)                │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 2: Date & Time         │
         │ • Start date & time         │
         │ • End date & time           │
         │ • Timezone                  │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 3: Location Type       │
         │ Select:                     │
         │ • In-person                 │
         │ • Online                    │
         │ • Hybrid                    │
         └──────────┬──────────────────┘
                    ↓
    ┌───────────────┴────────────────┐
    │                                │
    ↓                                ↓
┌────────────────┐      ┌──────────────────────┐
│ If In-Person:  │      │ If Online:           │
│ • Venue        │      │ • Choose provider    │
│ • Address      │      │   - Manual link      │
│ • Coordinates  │      │   - Zoom integration │
└───────┬────────┘      │ • Meeting details    │
        │               └──────────┬───────────┘
        │                          │
        └────────┬─────────────────┘
                 ↓
         ┌─────────────────────────────┐
         │ Step 4: Tickets             │
         │ [Add Ticket Type] button    │
         │                             │
         │ For each ticket type:       │
         │ • Name                      │
         │ • Price                     │
         │ • Quantity                  │
         │ • Description               │
         │ • Sales start/end dates     │
         │                             │
         │ [Add Another Ticket Type]   │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 5: Review & Publish    │
         │ • Preview event details     │
         │ • Save as draft OR          │
         │ • Publish immediately       │
         └─────────────────────────────┘
```

---

### Flow 2: Multi-Day Event Creation

```
┌──────────────────────────────────────────────────────────────┐
│ CREATE MULTI-DAY EVENT                                       │
└──────────────────────┬───────────────────────────────────────┘
                       ↓
         ┌─────────────────────────────┐
         │ Steps 1-3: Same as one-time │
         │ (Basic info, Location type) │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 4: Multi-Day Schedule  │
         │ • Overall start date        │
         │ • Overall end date          │
         │                             │
         │ Configure each day:         │
         │                             │
         │ Day 1:                      │
         │ • Date: March 10, 2025      │
         │ • Start time: 9:00 AM       │
         │ • End time: 6:00 PM         │
         │ • Location (can differ)     │
         │ • Description               │
         │                             │
         │ Day 2:                      │
         │ • Date: March 11, 2025      │
         │ • Start time: 10:00 AM      │
         │ • End time: 8:00 PM         │
         │ • Location (can differ)     │
         │ • Description               │
         │                             │
         │ [Add Another Day]           │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 5: Tickets             │
         │                             │
         │ Ticket Type Options:        │
         │                             │
         │ 1. Full Event Pass (common) │
         │    • Price: $200            │
         │    • Access to all days     │
         │                             │
         │ 2. Day Passes (optional)    │
         │    • Day 1 only: $80        │
         │    • Day 2 only: $80        │
         │    • Day 3 only: $60        │
         │                             │
         │ 3. VIP/Tier options         │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 6: Review & Publish    │
         └─────────────────────────────┘
```

---

### Flow 3: Recurring Event Creation

```
┌──────────────────────────────────────────────────────────────┐
│ CREATE RECURRING EVENT                                       │
└──────────────────────┬───────────────────────────────────────┘
                       ↓
         ┌─────────────────────────────┐
         │ Step 1: Series Information  │
         │ • Series name               │
         │ • Description               │
         │ • Category                  │
         │ • Cover image               │
         │ • Visibility                │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 2: Recurrence Pattern  │
         │                             │
         │ Frequency:                  │
         │ ○ Daily                     │
         │ ○ Weekly                    │
         │ ○ Monthly                   │
         │ ○ Yearly                    │
         │                             │
         │ Interval:                   │
         │ Every [1] [week(s)]         │
         │                             │
         │ If Weekly, select days:     │
         │ ☐ Monday                    │
         │ ☐ Tuesday                   │
         │ ☐ Wednesday                 │
         │ ☐ Thursday                  │
         │ ☐ Friday                    │
         │ ☐ Saturday                  │
         │ ☐ Sunday                    │
         │                             │
         │ Session Time:               │
         │ Start: [9:00 AM]            │
         │ End: [10:00 AM]             │
         │                             │
         │ Duration:                   │
         │ ○ End date: [March 31, 2025]│
         │ ○ Number of occurrences: 12 │
         │ ○ No end date (ongoing)     │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 3: Location Type       │
         │ (Same as one-time event)    │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 4: Default Ticket Types│
         │                             │
         │ These will apply to ALL     │
         │ sessions by default:        │
         │                             │
         │ Ticket Type 1:              │
         │ • Name: Regular             │
         │ • Price: $15                │
         │ • Quantity per session: 15  │
         │ • Description               │
         │                             │
         │ Ticket Type 2:              │
         │ • Name: Student             │
         │ • Price: $10                │
         │ • Quantity per session: 3   │
         │ • Requires verification     │
         │                             │
         │ Ticket Type 3:              │
         │ • Name: Member              │
         │ • Price: $8                 │
         │ • Quantity per session: 2   │
         │                             │
         │ [Add Another Ticket Type]   │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 5: Generate Sessions   │
         │                             │
         │ System generates all        │
         │ sessions based on pattern:  │
         │                             │
         │ Preview:                    │
         │ • Session 1: Jan 6, 9-10 AM │
         │ • Session 2: Jan 13, 9-10 AM│
         │ • Session 3: Jan 20, 9-10 AM│
         │ ... (12 total sessions)     │
         │                             │
         │ Each session gets:          │
         │ • Copy of all ticket types  │
         │ • Default quantities        │
         │ • Default prices            │
         │                             │
         │ [Confirm & Generate]        │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 6: Customize Sessions  │
         │         (Optional)          │
         │                             │
         │ Organizer can edit any      │
         │ individual session:         │
         │                             │
         │ • Change prices             │
         │ • Add/remove ticket types   │
         │ • Change quantities         │
         │ • Modify time/location      │
         │ • Cancel specific session   │
         │                             │
         │ Example: Jan 20 special     │
         │ guest session               │
         │ • Increase price to $20     │
         │ • Add VIP ticket ($30)      │
         └──────────┬──────────────────┘
                    ↓
         ┌─────────────────────────────┐
         │ Step 7: Publish Series      │
         │ • All sessions now active   │
         │ • Users can start registering│
         └─────────────────────────────┘
```

---

## Ticket Management

### Ticket Types Structure

**For One-Time & Multi-Day Events:**

```
Event
 │
 ├── Ticket Type 1: Early Bird
 │    ├── Name: "Early Bird"
 │    ├── Price: $80
 │    ├── Quantity Available: 100
 │    ├── Quantity Sold: 87
 │    ├── Description: "Save $40 with early registration"
 │    ├── Sales Start: Dec 1, 2024
 │    ├── Sales End: Jan 31, 2025
 │    └── Status: Active
 │
 ├── Ticket Type 2: Regular
 │    ├── Name: "Regular"
 │    ├── Price: $120
 │    ├── Quantity Available: 200
 │    ├── Quantity Sold: 45
 │    ├── Sales Start: Feb 1, 2025
 │    ├── Sales End: March 14, 2025
 │    └── Status: Active
 │
 └── Ticket Type 3: VIP
      ├── Name: "VIP Pass"
      ├── Price: $250
      ├── Quantity Available: 50
      ├── Quantity Sold: 28
      ├── Benefits: ["VIP lounge access", "Meet speakers", "Premium seating"]
      ├── Sales Start: Dec 1, 2024
      ├── Sales End: March 14, 2025
      └── Status: Active
```

**For Recurring Events:**

```
Series: "Monday Yoga"
 │
 ├── Default Ticket Templates
 │    ├── Template 1: Regular ($15)
 │    ├── Template 2: Student ($10)
 │    └── Template 3: Member ($8)
 │
 └── Sessions
      │
      ├── Session 1: Jan 6
      │    ├── Ticket Type: Regular - $15 (15 available, 12 sold)
      │    ├── Ticket Type: Student - $10 (3 available, 2 sold)
      │    └── Ticket Type: Member - $8 (2 available, 1 sold)
      │
      ├── Session 2: Jan 13
      │    ├── Ticket Type: Regular - $15 (15 available, 8 sold)
      │    ├── Ticket Type: Student - $10 (3 available, 1 sold)
      │    └── Ticket Type: Member - $8 (2 available, 0 sold)
      │
      └── Session 3: Jan 20 (Customized - Special Guest)
           ├── Ticket Type: Regular - $20 (15 available, 5 sold) ← Price changed
           ├── Ticket Type: Student - $15 (3 available, 1 sold) ← Price changed
           └── Ticket Type: VIP - $30 (5 available, 2 sold) ← New type added
```

### Ticket Type Properties

**Required Fields:**
- Name (e.g., "Early Bird", "VIP", "Student")
- Price (can be $0 for free tickets)
- Quantity available

**Optional Fields:**
- Description
- Benefits list
- Sales start date/time
- Sales end date/time
- Minimum purchase quantity
- Maximum purchase quantity per user
- Requires verification (e.g., student ID)
- Hidden (invite-only tickets)

### Ticket Purchase Rules

**General Rules:**
1. User must select at least one ticket
2. Cannot exceed available quantity
3. Cannot purchase after sales end date
4. Cannot purchase before sales start date
5. One ticket per session per user (for recurring events)

**For Recurring Events:**
1. First purchase = automatic series registration
2. Must buy at least one session to join series
3. Can buy different ticket types for different sessions
4. Can purchase multiple sessions at once
5. Can return later to purchase more sessions

**Ticket Selection Flow:**

```
┌──────────────────────────────────────────────────────────────┐
│ EVENT: Tech Conference 2025                                  │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│ Available Tickets:                                           │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ☐ Early Bird - $80                                     │ │
│ │   Save $40 with early registration                     │ │
│ │   [Only 13 left!] Sales end Jan 31                     │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ☐ Regular - $120                                       │ │
│ │   Standard conference access                           │ │
│ │   [200 available]                                      │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ☐ VIP Pass - $250                                      │ │
│ │   ✓ All conference access                              │ │
│ │   ✓ VIP lounge access                                  │ │
│ │   ✓ Meet the speakers                                  │ │
│ │   ✓ Premium seating                                    │ │
│ │   [22 available]                                       │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ Quantity: [1] ▼                                              │
│                                                              │
│ Selected: Regular Ticket × 1 = $120                          │
│                                                              │
│                                     [Proceed to Checkout →] │
└──────────────────────────────────────────────────────────────┘
```

**For Recurring Events:**

```
┌──────────────────────────────────────────────────────────────┐
│ SERIES: Monday Yoga - January 2025                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│ Select sessions you want to attend:                          │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Session: Monday, Jan 6, 2025 · 9:00 AM - 10:00 AM     │ │
│ │                                                        │ │
│ │ ☐ Regular - $15        [15 available]                 │ │
│ │ ☐ Student - $10        [3 available]                  │ │
│ │ ☐ Member - $8          [2 available]                  │ │
│ │ ☐ First-Timer Free - $0 [5 available]                 │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Session: Monday, Jan 13, 2025 · 9:00 AM - 10:00 AM    │ │
│ │                                                        │ │
│ │ ☐ Regular - $15        [15 available]                 │ │
│ │ ☐ Student - $10        [3 available]                  │ │
│ │ ☐ Member - $8          [2 available]                  │ │
│ │ ☐ First-Timer Free - $0 [5 available]                 │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Session: Monday, Jan 20, 2025 · 9:00 AM - 10:00 AM    │ │
│ │ 🌟 SPECIAL GUEST SESSION                               │ │
│ │                                                        │ │
│ │ ☐ Regular - $20        [15 available]                 │ │
│ │ ☐ Student - $15        [3 available]                  │ │
│ │ ☐ VIP - $30            [5 available] Meet the guest!  │ │
│ └────────────────────────────────────────────────────────┘ │
│                                                              │
│ Selected:                                                    │
│ • Jan 6 - Student ticket: $10                                │
│ • Jan 13 - Student ticket: $10                               │
│ • Jan 20 - Regular ticket: $20                               │
│                                                              │
│ Total: $40 for 3 sessions                                    │
│                                                              │
│                                     [Proceed to Checkout →] │
└──────────────────────────────────────────────────────────────┘
```

---

## Registration & Attendance

### Registration Flow

**For One-Time & Multi-Day Events:**

```
User selects ticket
       ↓
Proceeds to checkout
       ↓
Payment processed
       ↓
Registration created
       ↓
Ticket issued
       ↓
Confirmation email sent
       ↓
User can view ticket in "My Events"
```

**For Recurring Events:**

```
User selects session(s) and ticket type(s)
       ↓
First purchase?
       ↓
   ┌───┴────┐
  YES      NO
   │        │
   ↓        ↓
Create    Already
Series    registered
Registration
   │        │
   └───┬────┘
       ↓
Payment processed
       ↓
Session ticket(s) issued
       ↓
Confirmation email sent
       ↓
User can:
• View registered series
• See owned session tickets
• Purchase more sessions
```

### Registration Data Tracking

**One-Time Event Registration:**
- User ID
- Event ID
- Ticket Type ID
- Payment status
- Registration date/time
- Ticket unique code/QR
- Attendance status (for check-in)

**Multi-Day Event Registration:**
- User ID
- Event ID
- Ticket Type ID
- Payment status
- Registration date/time
- Ticket unique code/QR
- Attendance per day (optional tracking):
  - Day 1: Attended/Not attended
  - Day 2: Attended/Not attended
  - Day 3: Attended/Not attended

**Recurring Event Registration:**

**Series Level:**
- User ID
- Series ID
- Registration date (first purchase date)
- Status: Active/Cancelled
- Total sessions owned

**Session Level:**
- User ID
- Session ID
- Ticket Type ID
- Payment status
- Purchase date/time
- Ticket unique code/QR
- Attendance status

### Attendance Tracking

**Check-in Methods:**
1. QR code scan
2. Manual check-in by organizer
3. Email verification
4. Unique ticket code entry

**For Recurring Events:**
- Track attendance per session
- User can have tickets for sessions 1, 3, 5 but not 2, 4
- Each session = separate check-in

**Attendance Reports:**
- Who registered vs who attended
- No-show rate
- For recurring: attendance patterns across sessions

---

## User Roles & Permissions

### Role Definitions

**1. Event Organizer (Creator)**
- Creates events
- Manages event details
- Creates/modifies ticket types
- Manages invitations (for private events)
- Views registrations and attendance
- Can edit event before it starts
- Can cancel event
- Receives payments (event revenue)

**2. Attendee (Regular User)**
- Browses public/unlisted events
- Registers for events
- Purchases tickets
- Receives invitations (for private events)
- Views owned tickets
- Can cancel registration (based on refund policy)

**3. Platform Administrator**
- Manages all events
- Can modify/delete any event
- Views all registrations
- Manages users
- Handles disputes
- Platform-level reporting

### Organizer Capabilities

**Event Management:**
- Create new events (all types)
- Edit unpublished events (full edit)
- Edit published events (limited - can't change past dates, can modify future details)
- Cancel events (with attendee notifications)
- Duplicate events

**Ticket Management:**
- Create ticket types
- Modify ticket quantities
- Modify ticket prices (before sales start)
- Enable/disable ticket types
- Set sales periods

**Recurring Event Specific:**
- Customize individual sessions
- Cancel specific sessions
- Reschedule sessions (with attendee notifications)
- Modify session-specific ticket pricing

**Registration Management:**
- View all registrations
- Export attendee lists
- Send messages to attendees
- Manually add attendees (comp tickets)
- Issue refunds
- Check-in attendees

**For Private Events:**
- Send invitations
- Approve/reject registration requests
- Regenerate invite links
- View invitation status

### Attendee Capabilities

**Event Discovery:**
- Browse public events
- Search events by category, date, location
- Filter by event type
- View event details

**Registration:**
- Register for public events
- Access unlisted events via link
- Accept invitations for private events
- Select ticket types
- Purchase multiple tickets/sessions

**Account Management:**
- View "My Events" (upcoming, past, cancelled)
- View tickets/QR codes
- Download tickets
- Cancel registration (if allowed)
- Rate/review events (after attendance)

**For Recurring Events:**
- View registered series
- See which sessions owned
- Purchase additional sessions
- View attendance history

---

## Business Rules

### Event Creation Rules

1. **Required Information:**
   - Event name (3-200 characters)
   - Start date/time (must be in future)
   - End date/time (must be after start)
   - Location details (based on type)
   - At least one ticket type OR mark as free

2. **Date Validation:**
   - Start date must be in future (at event creation)
   - End date must be after start date
   - For multi-day: Each day's dates must be sequential or specified
   - For recurring: End date must be after start date OR specify number of occurrences

3. **Location Validation:**
   - In-person: Address required
   - Online: Meeting link OR provider integration required
   - Hybrid: Both in-person AND online details required

4. **Ticket Validation:**
   - At least one ticket type OR mark event as free
   - Ticket quantity must be positive integer or unlimited
   - Price must be >= 0
   - Sales end date must be before or on event start date
   - Sales start date must be before sales end date

### Registration Rules

1. **Capacity Management:**
   - Cannot register if event/session is at capacity
   - Cannot register if ticket type is sold out
   - For recurring: Each session has independent capacity

2. **Time Restrictions:**
   - Cannot register after event starts
   - Cannot register before ticket sales start
   - Cannot register after ticket sales end
   - For recurring: Can register for future sessions even if past sessions occurred

3. **User Restrictions:**
   - One registration per user per event (one-time/multi-day)
   - One ticket per user per session (recurring)
   - Cannot purchase same ticket type twice for same session
   - Can purchase different ticket types across different sessions

4. **Private Event Rules:**
   - Must have valid invitation
   - Invitation must not be expired
   - User email must match invitation email (or logged-in account)

5. **Payment Rules:**
   - Payment required before ticket issued (except free tickets)
   - Payment amount must match ticket price at time of purchase
   - For recurring: Can pay for multiple sessions in single transaction

### Cancellation & Refund Rules

**Event Cancellation (by Organizer):**
- Can cancel event at any time
- Must notify all registered attendees
- Automatic full refund to all attendees
- For recurring: Can cancel entire series OR individual sessions

**Registration Cancellation (by Attendee):**
- Depends on organizer's refund policy
- Common policies:
  - Full refund until X days before event
  - Partial refund until Y days before event
  - No refund after Z days before event
- For recurring:
  - Can cancel individual session tickets
  - If all sessions cancelled, series registration remains (can buy more sessions later)

**Automated Actions:**
- Send cancellation notifications
- Process refunds
- Update capacities
- Mark tickets as cancelled

### Recurring Event Specific Rules

1. **Session Generation:**
   - Sessions auto-generated based on recurrence pattern
   - Each session gets copy of default ticket types
   - Sessions created in "scheduled" status

2. **Session Customization:**
   - Organizer can modify any session independently
   - Changes to one session don't affect others
   - Can cancel individual sessions

3. **Registration Requirements:**
   - Must purchase at least one session to register for series
   - First purchase = automatic series registration
   - Can purchase more sessions anytime before session starts

4. **Session Attendance:**
   - Each session = separate attendance record
   - User can own tickets for non-consecutive sessions
   - Missing a session doesn't affect future session tickets

5. **Series Management:**
   - Cannot delete series if any session has registrations
   - Can cancel future sessions
   - Past sessions remain in history

---

## Data Relationships

### Core Entities

```
┌─────────────────────────────────────────────────────────────┐
│ User                                                        │
├─────────────────────────────────────────────────────────────┤
│ • User ID (PK)                                              │
│ • Name, Email, Phone                                        │
│ • Zoom access token (optional)                              │
│ • Account created date                                      │
│ • Role (Organizer, Attendee, Admin)                         │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ creates
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Event (One-Time or Multi-Day)                               │
├─────────────────────────────────────────────────────────────┤
│ • Event ID (PK)                                             │
│ • Organizer ID (FK → User)                                  │
│ • Name, Description, Category                               │
│ • Event Type (ONE_TIME, MULTI_DAY)                          │
│ • Visibility (PUBLIC, PRIVATE, UNLISTED)                    │
│ • Location Type (IN_PERSON, ONLINE, HYBRID)                 │
│ • Start Date/Time, End Date/Time                            │
│ • Venue/Address (if in-person)                              │
│ • Meeting URL/Platform (if online)                          │
│ • Access Token (if unlisted)                                │
│ • Status (DRAFT, PUBLISHED, CANCELLED, COMPLETED)           │
│ • Created/Updated timestamps                                │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ has many
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Ticket Type                                                 │
├─────────────────────────────────────────────────────────────┤
│ • Ticket Type ID (PK)                                       │
│ • Event ID (FK → Event)                                     │
│ • Name (e.g., "Early Bird", "VIP")                          │
│ • Price                                                     │
│ • Quantity Available                                        │
│ • Quantity Sold                                             │
│ • Description, Benefits                                     │
│ • Sales Start Date, Sales End Date                          │
│ • Status (ACTIVE, SOLD_OUT, EXPIRED)                        │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ purchased via
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Event Registration                                          │
├─────────────────────────────────────────────────────────────┤
│ • Registration ID (PK)                                      │
│ • Event ID (FK → Event)                                     │
│ • User ID (FK → User)                                       │
│ • Ticket Type ID (FK → Ticket Type)                         │
│ • Unique Ticket Code / QR Code                              │
│ • Payment Status (PENDING, PAID, REFUNDED)                  │
│ • Registration Date/Time                                    │
│ • Attendance Status (NOT_ATTENDED, ATTENDED)                │
│ • Check-in Time (if attended)                               │
│ • Attendance Mode (IN_PERSON, ONLINE) - for hybrid events   │
└─────────────────────────────────────────────────────────────┘


For Multi-Day Events:
┌─────────────────────────────────────────────────────────────┐
│ Event Day Schedule                                          │
├─────────────────────────────────────────────────────────────┤
│ • Day Schedule ID (PK)                                      │
│ • Event ID (FK → Event)                                     │
│ • Day Number (1, 2, 3...)                                   │
│ • Date                                                      │
│ • Start Time, End Time                                      │
│ • Location (if different from main)                         │
│ • Description                                               │
└─────────────────────────────────────────────────────────────┘

For Private Events:
┌─────────────────────────────────────────────────────────────┐
│ Event Invitation                                            │
├─────────────────────────────────────────────────────────────┤
│ • Invitation ID (PK)                                        │
│ • Event ID (FK → Event)                                     │
│ • Invited User ID (FK → User, nullable)                     │
│ • Invited Email (if user doesn't exist)                     │
│ • Invite Token (unique)                                     │
│ • Status (PENDING, ACCEPTED, DECLINED, EXPIRED)             │
│ • Sent At, Expires At                                       │
│ • Responded At                                              │
└─────────────────────────────────────────────────────────────┘
```

### Recurring Event Relationships

```
┌─────────────────────────────────────────────────────────────┐
│ Recurring Event Series                                      │
├─────────────────────────────────────────────────────────────┤
│ • Series ID (PK)                                            │
│ • Organizer ID (FK → User)                                  │
│ • Series Name, Description                                  │
│ • Visibility, Location Type                                 │
│ • Recurrence Pattern (DAILY, WEEKLY, MONTHLY, YEARLY)       │
│ • Recurrence Interval (e.g., every 2 weeks)                 │
│ • Days of Week (if weekly)                                  │
│ • Day of Month (if monthly)                                 │
│ • Series Start Date, Series End Date                        │
│ • Default Session Duration                                  │
│ • Default Session Time                                      │
│ • Total Sessions Generated                                  │
│ • Status (DRAFT, PUBLISHED, CANCELLED, COMPLETED)           │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ has many
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Series Session (Individual Occurrence)                      │
├─────────────────────────────────────────────────────────────┤
│ • Session ID (PK)                                           │
│ • Series ID (FK → Recurring Event Series)                   │
│ • Session Number (1, 2, 3...)                               │
│ • Session Date                                              │
│ • Start Time, End Time                                      │
│ • Location (can differ from default)                        │
│ • Meeting URL (if online)                                   │
│ • Status (SCHEDULED, CANCELLED, COMPLETED)                  │
│ • Is Customized (boolean - if differs from defaults)        │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ has many
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Session Ticket Type                                         │
├─────────────────────────────────────────────────────────────┤
│ • Session Ticket Type ID (PK)                               │
│ • Session ID (FK → Series Session)                          │
│ • Name (e.g., "Regular", "Student")                         │
│ • Price (can differ per session)                            │
│ • Quantity Available                                        │
│ • Quantity Sold                                             │
│ • Description                                               │
│ • Requires Verification (boolean)                           │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ purchased via
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Series Registration (User enrolled in series)               │
├─────────────────────────────────────────────────────────────┤
│ • Series Registration ID (PK)                               │
│ • Series ID (FK → Recurring Event Series)                   │
│ • User ID (FK → User)                                       │
│ • Registration Date (first purchase date)                   │
│ • Status (ACTIVE, CANCELLED)                                │
│ • Total Sessions Owned                                      │
└──────────────┬──────────────────────────────────────────────┘
               │
               │ owns
               ↓
┌─────────────────────────────────────────────────────────────┐
│ Session Ticket (User's ticket for specific session)        │
├─────────────────────────────────────────────────────────────┤
│ • Session Ticket ID (PK)                                    │
│ • Series Registration ID (FK → Series Registration)         │
│ • Session ID (FK → Series Session)                          │
│ • Session Ticket Type ID (FK → Session Ticket Type)         │
│ • User ID (FK → User)                                       │
│ • Unique Ticket Code / QR Code                              │
│ • Payment Status (PENDING, PAID, REFUNDED)                  │
│ • Purchase Date/Time                                        │
│ • Attendance Status (NOT_ATTENDED, ATTENDED)                │
│ • Check-in Time (if attended)                               │
└─────────────────────────────────────────────────────────────┘
```

### Relationship Summary

**One-to-Many Relationships:**
- User → Events (user creates many events)
- Event → Ticket Types (event has many ticket types)
- Event → Registrations (event has many registrations)
- User → Registrations (user has many registrations)
- Event → Event Invitations (private event has many invitations)

**For Recurring Events:**
- Recurring Series → Sessions (series has many sessions)
- Session → Session Ticket Types (each session has many ticket types)
- User → Series Registrations (user registered for many series)
- Series Registration → Session Tickets (registration owns many session tickets)

**For Multi-Day Events:**
- Event → Day Schedules (event has schedule for each day)

---

## Key Technical Considerations

### Performance Optimization

1. **Capacity Checks:**
   - Cache available tickets count
   - Use optimistic locking for ticket purchases
   - Handle race conditions (multiple users buying last ticket)

2. **Recurring Event Sessions:**
   - Generate sessions in batches
   - Index sessions by date for quick lookups
   - Lazy load past sessions

3. **Search & Discovery:**
   - Index events by: date, category, location, visibility
   - Full-text search on event name/description
   - Filter by event type, price range, location type

### Security Considerations

1. **Access Control:**
   - Verify user owns event before allowing edits
   - Validate invitation tokens
   - Rate limit invitation sends
   - Sanitize all user inputs

2. **Payment Security:**
   - Never store credit card details
   - Use payment gateway webhooks for status updates
   - Verify payment before issuing tickets
   - Handle failed payments gracefully

3. **Online Meeting Links:**
   - Don't expose links before access time
   - Validate user has valid ticket before showing link
   - Log link access for security auditing
   - Support link regeneration for compromised links

### Edge Cases to Handle

1. **Event Modifications:**
   - Event date changed after registrations exist → Notify all attendees
   - Event cancelled → Auto-refund + notifications
   - Ticket quantity reduced below sold amount → Prevent or handle gracefully
   - Event visibility changed from Public to Private → Handle existing registrations

2. **Recurring Events:**
   - Session cancelled → Refund that session only, keep other tickets
   - Series cancelled → Refund all future sessions
   - User wants to change ticket type for purchased session → Depends on policy
   - Session time conflicts with another event user registered for → User's problem to manage

3. **Capacity:**
   - Event at capacity but user in checkout process → Reserve temporarily or first-come-first-served
   - Refund issued → Increase available capacity
   - Organizer increases capacity mid-sale → Allow more registrations

4. **Payment:**
   - Payment pending during checkout → Hold temporarily, expire after X minutes
   - Payment fails → Release reserved ticket, notify user
   - Refund requested → Verify eligibility, process, update capacity

---

## User Experience Flows

### Attendee: Discovering & Registering for One-Time Event

```
Homepage / Event Listing
       ↓
Browse or Search Events
       ↓
Filter by Category/Date/Location
       ↓
Click on Event
       ↓
View Event Details Page
• Description, Date, Location
• Available Tickets
• Organizer Info
       ↓
Select Ticket Type & Quantity
       ↓
Click "Register" or "Buy Ticket"
       ↓
Login / Sign Up (if not logged in)
       ↓
Review Order
• Ticket details
• Price breakdown
• Terms & conditions
       ↓
Enter Payment Information
       ↓
Complete Purchase
       ↓
Receive Confirmation
• Email with ticket
• QR code for check-in
       ↓
View in "My Events"
• Access ticket anytime
• Add to calendar
• Get event updates
       ↓
Day of Event
• Show QR code for check-in
• Access online meeting link (if applicable)
```

### Attendee: Registering for Recurring Event

```
Browse Events
       ↓
Discover Recurring Series
       ↓
View Series Details
• Pattern (every Monday)
• Dates covered
• Location & time
       ↓
View All Sessions
• Upcoming sessions listed
• Each with ticket options
       ↓
Select Session(s) & Ticket Type(s)
• Can select multiple sessions
• Choose appropriate ticket type per session
       ↓
Review Selection
• Jan 6 - Student: $10
• Jan 13 - Student: $10
• Jan 20 - Regular: $20
• Total: $40
       ↓
Checkout & Pay
       ↓
First Purchase?
   ↓
  YES → Create Series Registration
  NO → Add to existing registration
       ↓
Receive Confirmation
• Email with tickets for purchased sessions
• Series enrollment confirmed
       ↓
View in "My Events"
• See series enrollment
• View owned session tickets
• See upcoming sessions
       ↓
Later: Want to Attend More Sessions
       ↓
Go to Series Page
       ↓
Select Additional Sessions
       ↓
Purchase & Receive New Tickets
       ↓
Day of Session
• Show session-specific QR code
• Check in for that session only
```

### Organizer: Creating One-Time Event

```
Dashboard
       ↓
Click "Create New Event"
       ↓
Select Event Type: One-Time
       ↓
Step 1: Basic Info
• Name, description, category
• Upload cover image
• Select visibility
       ↓
Step 2: Date & Time
• Start date/time
• End date/time
• Timezone
       ↓
Step 3: Location
• Select type (in-person/online/hybrid)
• Enter details based on type
• If online + Zoom: Connect account or manual link
       ↓
Step 4: Tickets
• Add ticket types
• For each: name, price, quantity, description
• Set sales periods
       ↓
Step 5: Review
• Preview how event looks
• Check all details
       ↓
Save as Draft OR Publish
       ↓
If Published:
• Event goes live
• Appears in listings (if public)
• Tickets available for purchase
       ↓
Monitor Event
• View registrations
• Track ticket sales
• Send updates to attendees
       ↓
Day of Event
• Check in attendees
• View attendance list
       ↓
After Event
• View final attendance
• Download reports
• Close event
```

### Organizer: Creating Recurring Event Series

```
Dashboard
       ↓
Click "Create New Event"
       ↓
Select Event Type: Recurring
       ↓
Step 1: Series Info
• Series name & description
• Cover image
• Visibility
       ↓
Step 2: Recurrence Pattern
• Frequency (daily/weekly/monthly/yearly)
• Interval (every X weeks)
• Days of week (if weekly)
• Start & end date OR # of occurrences
       ↓
Step 3: Session Details
• Default session time
• Default duration
• Location type & details
       ↓
Step 4: Default Tickets
• Create ticket type templates
• These apply to all sessions by default
• Name, price, quantity per session
       ↓
Review Pattern & Generate Sessions
• Preview: "This will create 12 sessions"
• Show first few sessions as examples
       ↓
Confirm Generation
       ↓
System Creates All Sessions
• Each session = copy of defaults
• All sessions in "scheduled" status
       ↓
Optional: Customize Individual Sessions
• Click on any session
• Modify price, add ticket types, change time
• Example: Special guest session - increase price
       ↓
Publish Series
• All sessions go live
• Users can start registering
       ↓
Ongoing Management
• Monitor registrations per session
• Can still customize future sessions
• Cancel individual sessions if needed
• View attendance per session
       ↓
Each Session Day
• Check in attendees for that specific session
• Track attendance per session
       ↓
After Series Completes
• View overall series analytics
• Attendance patterns
• Revenue per session
• Download reports
```

---

## Additional Features & Considerations

### Waitlist Management

**When Event/Session is at Capacity:**

```
User tries to register
       ↓
Event is full
       ↓
Option to "Join Waitlist"
       ↓
User added to waitlist
       ↓
If spot opens (cancellation/refund)
       ↓
First person on waitlist notified
       ↓
Limited time to claim spot (e.g., 24 hours)
       ↓
If claimed: Registration processed
If not claimed: Offer to next person
```

**Waitlist Properties:**
- Order (first-come, first-served)
- Expiration time for offers
- Notifications when spots available
- User can leave waitlist

### Event Updates & Notifications

**Notify Attendees When:**
- Event details changed (date, time, location)
- Event cancelled
- New announcement from organizer
- Reminder before event (24 hours, 1 hour)
- Event starts soon (for online events - show link)

**For Recurring Events:**
- Notify about upcoming session (24 hours before)
- Notify if specific session cancelled
- Notify if session time/location changed

### Analytics & Reporting

**For Organizers:**
- Total registrations
- Revenue by ticket type
- Sales over time (graph)
- Attendance rate
- Demographics (if collected)
- For recurring: Per-session breakdown

**For Platform:**
- Total events created
- Event types distribution
- Popular categories
- Revenue metrics
- User engagement

### Social Features

**Event Sharing:**
- Share on social media
- Generate shareable link
- Embed event widget

**Reviews & Ratings:**
- After event, attendees can rate/review
- Displayed on event page
- Builds organizer reputation

**Following:**
- Users can follow organizers
- Get notified of new events

---

## Implementation Priority

### Phase 1: MVP (Core Features)
1. One-time event creation & management
2. Basic ticket types (free & paid)
3. Simple registration flow
4. Public event visibility
5. In-person and manual online link support
6. Basic attendee management

### Phase 2: Enhanced Features
1. Multi-day events
2. Recurring events (simple - uniform pricing)
3. Private events with invitation system
4. Unlisted events
5. Multiple ticket types per event
6. Waitlist functionality

### Phase 3: Advanced Features
1. Recurring events with variable pricing per session
2. Zoom/provider integration
3. Hybrid events with attendance mode selection
4. Advanced analytics
5. Event series customization (per-session modifications)
6. Refund automation

### Phase 4: Premium Features
1. Social features (reviews, following)
2. Event recommendations
3. Advanced reporting
4. API for third-party integrations
5. White-label options
6. Mobile app

---

## Conclusion

This document provides comprehensive requirements for an event management system supporting three distinct event types with flexible ticketing and access control. The system is designed to scale from simple one-time events to complex recurring series with variable pricing.

**Key Differentiators:**
- Support for recurring events with per-session ticket management
- Flexible visibility options (public, private, unlisted)
- Multiple location modes including hybrid
- Integration with online meeting providers
- Granular ticket type control

**Success Metrics:**
- Number of events created
- Registration conversion rate
- User satisfaction (organizers & attendees)
- Platform revenue
- Event completion rate

This system empowers organizers to create professional events while providing attendees with a smooth discovery and registration experience.

---

**Document Version:** 1.0  
**Last Updated:** November 23, 2025  
**Status:** Requirements Approved - Ready for Development

# Event Categories Management API

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Event Categories Management API provides administrative functionality for managing event categories in the Nexgate platform. This API enables authorized users (Staff Admin/Super Admin) to create, update, retrieve event categories, and seed default categories for event classification and organization.

**Hints**:

- All POST/PUT endpoints require STAFF\_ADMIN or SUPER\_ADMIN role
- GET endpoints are accessible to all authenticated users
- Category names are unique (case-insensitive)
- Slugs are auto-generated from category names and must be unique
- Slug collisions are handled automatically by appending numbers (e.g., music-1, music-2)
- Categories support featured and active status flags
- Pagination is 1-indexed (page=1 for first page), default size is 10 items
- Event count tracks number of events in each category (updated automatically)
- Color codes must be valid hex format (#FF5733 or #F57)
- Icon URLs must be valid image URLs or paths

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    // Actual response data goes here
  }
}

```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-12-11T10:30:45",
  "data": "Error description"
}

```

### Standard Response Fields

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`success`</td><td>boolean</td><td>Always `true` for successful operations, `false` for errors</td></tr><tr><td>`httpStatus`</td><td>string</td><td>HTTP status name (OK, BAD\_REQUEST, NOT\_FOUND, etc.)</td></tr><tr><td>`message`</td><td>string</td><td>Human-readable message describing the operation result</td></tr><tr><td>`action_time`</td><td>string</td><td>ISO 8601 timestamp of when the response was generated</td></tr><tr><td>`data`</td><td>object/string</td><td>Response payload for success, error details for failures</td></tr></tbody></table>

---

## HTTP Method Badge Standards

For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PUT** - <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> - Yellow (Update/replace entire resource)

---

## EventCategoryResponse Structure

This is the standard response structure returned by all category endpoints:

```json
{
  "categoryId": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Music & Concerts",
  "slug": "music-concerts",
  "description": "Live music performances, concerts, festivals, and DJ events",
  "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
  "colorCode": "#E91E63",
  "isActive": true,
  "isFeatured": true,
  "eventCount": 125,
  "createdBy": "admin_user",
  "createdAt": "2025-12-11T10:30:45",
  "updatedBy": "admin_user",
  "updatedAt": "2025-12-11T14:20:30"
}

```

### Response Field Descriptions

<table id="bkmrk-field-type-descripti-1"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>categoryId</td><td>string (UUID)</td><td>Unique identifier for the category</td></tr><tr><td>name</td><td>string</td><td>Category display name (unique, case-insensitive)</td></tr><tr><td>slug</td><td>string</td><td>URL-friendly identifier (auto-generated, unique)</td></tr><tr><td>description</td><td>string</td><td>Category description</td></tr><tr><td>iconUrl</td><td>string</td><td>URL or path to category icon image</td></tr><tr><td>colorCode</td><td>string</td><td>Hex color code for category theming (#RRGGBB or #RGB)</td></tr><tr><td>isActive</td><td>boolean</td><td>Whether category is active and visible</td></tr><tr><td>isFeatured</td><td>boolean</td><td>Whether category should be featured/highlighted</td></tr><tr><td>eventCount</td><td>integer</td><td>Number of events in this category</td></tr><tr><td>createdBy</td><td>string</td><td>Username of the admin who created the category</td></tr><tr><td>createdAt</td><td>string</td><td>ISO 8601 timestamp of creation</td></tr><tr><td>updatedBy</td><td>string</td><td>Username of the admin who last updated (null if never updated)</td></tr><tr><td>updatedAt</td><td>string</td><td>ISO 8601 timestamp of last update (null if never updated)</td></tr></tbody></table>

---

## Endpoints

## 1. Create Category

**Purpose**: Create a new event category (requires admin privileges)

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/e-events/categories`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication + Admin Role)

**Required Roles**: ROLE\_STAFF\_ADMIN or ROLE\_SUPER\_ADMIN

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "name": "Music & Concerts",
  "description": "Live music performances, concerts, festivals, and DJ events",
  "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
  "colorCode": "#E91E63",
  "isActive": true,
  "isFeatured": true
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>name</td><td>string</td><td>Yes</td><td>Category name</td><td>Min: 2, Max: 100 characters</td></tr><tr><td>description</td><td>string</td><td>No</td><td>Category description</td><td>Max: 500 characters</td></tr><tr><td>iconUrl</td><td>string</td><td>No</td><td>URL or path to icon image</td><td>Must be valid image URL (jpg, jpeg, png, gif, svg, webp) or path starting with /icons/</td></tr><tr><td>colorCode</td><td>string</td><td>No</td><td>Hex color code</td><td>Must be valid hex format: #RRGGBB or #RGB (e.g., #FF5733 or #F57)</td></tr><tr><td>isActive</td><td>boolean</td><td>Yes</td><td>Whether category is active</td><td>Required boolean value</td></tr><tr><td>isFeatured</td><td>boolean</td><td>Yes</td><td>Whether category is featured</td><td>Required boolean value</td></tr></tbody></table>

**Success Response**: Returns standard EventCategoryResponse structure (see "EventCategoryResponse Structure" section above)

**Success Response Message**: "Category created successfully"

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Category created successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "categoryId": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Music & Concerts",
    "slug": "music-concerts",
    "description": "Live music performances, concerts, festivals, and DJ events",
    "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
    "colorCode": "#E91E63",
    "isActive": true,
    "isFeatured": true,
    "eventCount": 0,
    "createdBy": "admin_user",
    "createdAt": "2025-12-11T10:30:45",
    "updatedBy": null,
    "updatedAt": null
  }
}

```

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `400 BAD_REQUEST`: Category already exists (duplicate name)
- `401 UNAUTHORIZED`: Authentication issues (empty, invalid, expired, or malformed tokens)
- `403 FORBIDDEN`: Access denied (insufficient permissions - not admin)
- `404 NOT_FOUND`: User not found or not authenticated
- `422 UNPROCESSABLE_ENTITY`: Validation errors with detailed field information
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

**Error Response Examples**:

*Already Exists - Duplicate Name (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Category already exists",
  "action_time": "2025-12-11T10:30:45",
  "data": "Category with name 'Music & Concerts' already exists"
}

```

*Access Denied - Insufficient Permissions (403):*

```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Access denied",
  "action_time": "2025-12-11T10:30:45",
  "data": "Access denied. Insufficient permissions."
}

```

*Validation Error - Invalid Name (422):*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "name": "Category name must be between 2 and 100 characters"
  }
}

```

*Validation Error - Invalid Color Code (422):*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "colorCode": "Color code must be a valid hex color (e.g., #FF5733 or #F57)"
  }
}

```

*Validation Error - Invalid Icon URL (422):*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "iconUrl": "Icon URL must be a valid image URL or path"
  }
}

```

---

## 2. Update Category

**Purpose**: Update an existing event category (requires admin privileges)

**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `{base_url}/e-events/categories/{categoryId}`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication + Admin Role)

**Required Roles**: ROLE\_STAFF\_ADMIN or ROLE\_SUPER\_ADMIN

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>categoryId</td><td>string</td><td>Yes</td><td>UUID of the category to update</td><td>Must be valid UUID format</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "name": "Live Music & Concerts",
  "description": "Updated description for live music events",
  "iconUrl": "https://cdn.nexgate.com/icons/music-updated.svg",
  "colorCode": "#FF1744",
  "isActive": true,
  "isFeatured": false
}

```

**Request Body Parameters** (All Optional):

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>name</td><td>string</td><td>No</td><td>Updated category name</td><td>Min: 2, Max: 100 characters</td></tr><tr><td>description</td><td>string</td><td>No</td><td>Updated description</td><td>Max: 500 characters</td></tr><tr><td>iconUrl</td><td>string</td><td>No</td><td>Updated icon URL/path</td><td>Must be valid image URL or path</td></tr><tr><td>colorCode</td><td>string</td><td>No</td><td>Updated hex color code</td><td>Must be valid hex format: #RRGGBB or #RGB</td></tr><tr><td>isActive</td><td>boolean</td><td>No</td><td>Updated active status</td><td></td></tr><tr><td>isFeatured</td><td>boolean</td><td>No</td><td>Updated featured status</td><td></td></tr></tbody></table>

**Success Response**: Returns standard EventCategoryResponse structure with updated values

**Success Response Message**: "Category updated successfully"

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `400 BAD_REQUEST`: Category name already exists (when changing name to duplicate)
- `401 UNAUTHORIZED`: Authentication issues
- `403 FORBIDDEN`: Access denied (insufficient permissions - not admin)
- `404 NOT_FOUND`: Category not found with given ID
- `422 UNPROCESSABLE_ENTITY`: Validation errors
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

**Error Response Examples**:

*Not Found - Invalid Category ID (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Category not found",
  "action_time": "2025-12-11T10:30:45",
  "data": "Category not found with ID: 550e8400-e29b-41d4-a716-446655440000"
}

```

*Already Exists - Duplicate Name (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Category already exists",
  "action_time": "2025-12-11T10:30:45",
  "data": "Category with name 'Sports & Fitness' already exists"
}

```

---

## 3. Get Category by ID

**Purpose**: Retrieve a single category by its UUID

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/categories/{categoryId}`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-2"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>categoryId</td><td>string</td><td>Yes</td><td>UUID of the category to retrieve</td><td>Must be valid UUID format</td></tr></tbody></table>

**Success Response**: Returns standard EventCategoryResponse structure

**Success Response Message**: "Category retrieved successfully"

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Category retrieved successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "categoryId": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Music & Concerts",
    "slug": "music-concerts",
    "description": "Live music performances, concerts, festivals, and DJ events",
    "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
    "colorCode": "#E91E63",
    "isActive": true,
    "isFeatured": true,
    "eventCount": 125,
    "createdBy": "admin_user",
    "createdAt": "2025-12-11T10:30:45",
    "updatedBy": "admin_user",
    "updatedAt": "2025-12-11T14:20:30"
  }
}

```

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `401 UNAUTHORIZED`: Authentication issues
- `404 NOT_FOUND`: Category not found with given ID
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

---

## 4. Get Category by Slug

**Purpose**: Retrieve a single category by its URL-friendly slug

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/categories/slug/{slug}`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-3"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>slug</td><td>string</td><td>Yes</td><td>URL-friendly slug of the category</td><td>Lowercase, hyphen-separated (e.g., music-concerts)</td></tr></tbody></table>

**Example Request URL**: `https://api.nexgate.com/api/v1/e-events/categories/slug/music-concerts`

**Success Response**: Returns standard EventCategoryResponse structure

**Success Response Message**: "Category retrieved successfully"

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `401 UNAUTHORIZED`: Authentication issues
- `404 NOT_FOUND`: Category not found with given slug
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

**Error Response Example**:

*Not Found - Invalid Slug (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Category not found",
  "action_time": "2025-12-11T10:30:45",
  "data": "Category not found with slug: non-existent-category"
}

```

---

## 5. Get All Categories

**Purpose**: Retrieve all event categories (unpaginated, complete list)

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/categories/all`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-4"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Categories retrieved successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": [
    {
      "categoryId": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Music & Concerts",
      "slug": "music-concerts",
      "description": "Live music performances, concerts, festivals, and DJ events",
      "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
      "colorCode": "#E91E63",
      "isActive": true,
      "isFeatured": true,
      "eventCount": 125,
      "createdBy": "admin_user",
      "createdAt": "2025-12-11T10:30:45",
      "updatedBy": null,
      "updatedAt": null
    },
    {
      "categoryId": "660e8400-e29b-41d4-a716-446655440001",
      "name": "Sports & Fitness",
      "slug": "sports-fitness",
      "description": "Yoga, gym classes, marathons, tournaments, and outdoor activities",
      "iconUrl": "https://cdn.nexgate.com/icons/sports.svg",
      "colorCode": "#4CAF50",
      "isActive": true,
      "isFeatured": true,
      "eventCount": 89,
      "createdBy": "admin_user",
      "createdAt": "2025-12-11T10:30:45",
      "updatedBy": null,
      "updatedAt": null
    }
  ]
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-da"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>data</td><td>Array of EventCategoryResponse objects (complete list, no pagination)</td></tr></tbody></table>

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `401 UNAUTHORIZED`: Authentication issues
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

---

## 6. Get Paginated Categories

**Purpose**: Retrieve event categories with pagination support

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/categories/paged`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-5"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th><th>Default</th></tr></thead><tbody><tr><td>page</td><td>integer</td><td>No</td><td>Page number (1-indexed)</td><td>Min: 1</td><td>1</td></tr><tr><td>size</td><td>integer</td><td>No</td><td>Number of items per page</td><td>Min: 1, Max: 100</td><td>10</td></tr></tbody></table>

**Example Request URL**: `https://api.nexgate.com/api/v1/e-events/categories/paged?page=1&size=10`

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Categories retrieved successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "content": [
      {
        "categoryId": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Music & Concerts",
        "slug": "music-concerts",
        "description": "Live music performances, concerts, festivals, and DJ events",
        "iconUrl": "https://cdn.nexgate.com/icons/music.svg",
        "colorCode": "#E91E63",
        "isActive": true,
        "isFeatured": true,
        "eventCount": 125,
        "createdBy": "admin_user",
        "createdAt": "2025-12-11T10:30:45",
        "updatedBy": null,
        "updatedAt": null
      }
    ],
    "pageable": {
      "pageNumber": 0,
      "pageSize": 10,
      "sort": {
        "sorted": false,
        "empty": true,
        "unsorted": true
      },
      "offset": 0,
      "paged": true,
      "unpaged": false
    },
    "totalPages": 1,
    "totalElements": 10,
    "last": true,
    "size": 10,
    "number": 0,
    "sort": {
      "sorted": false,
      "empty": true,
      "unsorted": true
    },
    "numberOfElements": 10,
    "first": true,
    "empty": false
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-co"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>content</td><td>Array of EventCategoryResponse objects for current page</td></tr><tr><td>totalPages</td><td>Total number of pages available</td></tr><tr><td>totalElements</td><td>Total number of categories across all pages</td></tr><tr><td>size</td><td>Number of items per page (requested size)</td></tr><tr><td>number</td><td>Current page number (0-indexed in response)</td></tr><tr><td>first</td><td>Boolean indicating if this is the first page</td></tr><tr><td>last</td><td>Boolean indicating if this is the last page</td></tr><tr><td>empty</td><td>Boolean indicating if the result set is empty</td></tr></tbody></table>

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `401 UNAUTHORIZED`: Authentication issues
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

---

## 7. Seed Categories

**Purpose**: Populate the database with default event categories (requires admin privileges)

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/e-events/categories/seed`

**Access Level**: 🔒 Protected (Requires Bearer Token Authentication + Admin Role)

**Required Roles**: ROLE\_STAFF\_ADMIN or ROLE\_SUPER\_ADMIN

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-6"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication (format: `Bearer <token>`)</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Request Body**: None (no body required)

**Default Categories Seeded**:

This endpoint creates the following 10 default categories (if they don't already exist):

1. **Music &amp; Concerts** - Live music performances, concerts, festivals, and DJ events (Featured)
2. **Sports &amp; Fitness** - Yoga, gym classes, marathons, tournaments, and outdoor activities (Featured)
3. **Business &amp; Networking** - Professional meetups, conferences, workshops, and networking events (Featured)
4. **Food &amp; Drink** - Food festivals, cooking classes, wine tastings, and dining experiences
5. **Arts &amp; Culture** - Art exhibitions, theater, dance, museums, and cultural events
6. **Education &amp; Learning** - Workshops, seminars, courses, bootcamps, and training sessions (Featured)
7. **Social &amp; Community** - Parties, meetups, social clubs, game nights, and community events
8. **Technology &amp; Innovation** - Tech talks, hackathons, product launches, and startup events
9. **Wellness &amp; Spirituality** - Meditation, yoga retreats, healing workshops, and mindfulness events
10. **Entertainment** - Comedy shows, movie screenings, gaming, and entertainment events

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Categories seeded successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": [
    {
      "categoryId": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Music & Concerts",
      "slug": "music-concerts",
      "description": "Live music performances, concerts, festivals, and DJ events",
      "iconUrl": "",
      "colorCode": "#E91E63",
      "isActive": true,
      "isFeatured": true,
      "eventCount": 0,
      "createdBy": "admin_user",
      "createdAt": "2025-12-11T10:30:45",
      "updatedBy": null,
      "updatedAt": null
    },
    {
      "categoryId": "660e8400-e29b-41d4-a716-446655440001",
      "name": "Sports & Fitness",
      "slug": "sports-fitness",
      "description": "Yoga, gym classes, marathons, tournaments, and outdoor activities",
      "iconUrl": "",
      "colorCode": "#4CAF50",
      "isActive": true,
      "isFeatured": true,
      "eventCount": 0,
      "createdBy": "admin_user",
      "createdAt": "2025-12-11T10:30:45",
      "updatedBy": null,
      "updatedAt": null
    }
    // ... (remaining 8 categories)
  ]
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-da-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>data</td><td>Array of EventCategoryResponse objects (newly created categories only, or all existing if none were created)</td></tr></tbody></table>

**Behavior Notes**:

- Only creates categories that don't already exist (checked by name, case-insensitive)
- If all 10 default categories already exist, returns the existing categories
- All seeded categories are marked as active (`isActive: true`)
- Categories marked as "Featured" have `isFeatured: true`
- Event count starts at 0 for newly seeded categories
- The authenticated admin user is set as `createdBy` for all new categories
- Slugs are auto-generated from category names

**Standard Error Types**:

### Application-Level Exceptions (400-499)

- `401 UNAUTHORIZED`: Authentication issues
- `403 FORBIDDEN`: Access denied (insufficient permissions - not admin)
- `404 NOT_FOUND`: User not found or not authenticated
- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors

**Error Response Example**:

*Access Denied - Not Admin (403):*

```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Access denied",
  "action_time": "2025-12-11T10:30:45",
  "data": "Access denied. Insufficient permissions."
}

```

---

## Quick Reference Guide

### Common HTTP Status Codes

- `200 OK`: Successful GET/POST/PUT request
- `400 Bad Request`: Invalid request data or business rule violation (duplicate names)
- `401 Unauthorized`: Authentication required/failed
- `403 Forbidden`: Insufficient permissions (not admin)
- `404 Not Found`: Resource not found (category, user)
- `422 Unprocessable Entity`: Validation errors
- `500 Internal Server Error`: Server error

### Authentication &amp; Authorization

- **Bearer Token**: Include `Authorization: Bearer your_token` in headers
- **Required Roles for POST/PUT**: ROLE\_STAFF\_ADMIN or ROLE\_SUPER\_ADMIN
- **GET Endpoints**: Accessible to all authenticated users

### Data Format Standards

- **Dates**: ISO 8601 format (2025-12-11T14:30:00)
- **IDs**: UUID format (e.g., 550e8400-e29b-41d4-a716-446655440000)
- **Slugs**: Lowercase, hyphen-separated (e.g., music-concerts)
- **Color Codes**: Hex format #RRGGBB or #RGB (e.g., #FF5733 or #F57)
- **Pagination**: 1-indexed pages (page=1 for first page), default size=10

### Category Name Rules

- Minimum 2 characters, maximum 100 characters
- Must be unique (case-insensitive check)
- Automatically trimmed of whitespace
- Used to auto-generate slug

### Slug Generation Rules

- Auto-generated from category name
- Lowercase only
- Special characters removed
- Spaces replaced with hyphens
- Duplicate hyphens removed
- Leading/trailing hyphens removed
- Collisions handled by appending numbers (e.g., music-1, music-2)

### Icon URL Validation

- Must be valid HTTP/HTTPS URL ending in image extension (jpg, jpeg, png, gif, svg, webp)
- OR must be path starting with `/icons/` ending in image extension
- Examples: 
    - Valid: `https://cdn.nexgate.com/icons/music.svg`
    - Valid: `/icons/music.png`
    - Invalid: `https://example.com/file.pdf`

### Color Code Validation

- Must start with `#`
- Must be 6-character hex (e.g., #FF5733) or 3-character hex (e.g., #F57)
- Case-insensitive (A-F or a-f)
- Examples: 
    - Valid: `#FF5733`, `#F57`, `#e91e63`
    - Invalid: `FF5733`, `#GG5733`, `#12345`

### Featured Categories

Categories with `isFeatured: true` are typically displayed prominently in UI:

- Music &amp; Concerts
- Sports &amp; Fitness
- Business &amp; Networking
- Education &amp; Learning

### Event Count

- Automatically updated when events are created/deleted in a category
- Starts at 0 for new categories
- Used for analytics and sorting popular categories

# Events  API

# Events  Management API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-07-09
**Version**: v1.3

**Base URL**: `https://your-api-domain.com/api/v1/e-events`

**Short Description**: The Event Core API provides full lifecycle management for events on the NextGate platform — from creating drafts and configuring schedules, to publishing, managing live events, and discovery. It is used by event organizers to build, publish, and manage events step-by-step, and by all authenticated users to browse, search, and filter published events.

**Hints**:
- All write operations (create, update, publish) require a valid Bearer token. Read operations on published events are public.
- Dates and times must always use ISO 8601 format with timezone offset (e.g., `2025-06-15T09:00:00+03:00`). The API stores and returns `ZonedDateTime`.
- Event creation follows a **staged workflow**: `BASIC_INFO → SCHEDULE → LOCATION_DETAILS → TICKETS`. All required stages must be completed before publishing.
- Slugs are auto-generated from the event title with a UUID suffix to guarantee uniqueness — do not pass a slug manually.
- Pagination uses 1-based page numbers (page=1 is the first page).
- Category seeding and management live in the **Event Categories API** (base: `/api/v1/e-events/categories`), documented separately.
- **Banner media is upload-then-reference, not a URL string.** Upload the banner image through the FileThunder upload endpoint first to obtain a `fileId`, then pass that `fileId` as `bannerFileId` when creating/updating the event. The API resolves it into a full media object (`bannerMedia`) in responses — do not construct or pass CDN URLs directly.
- On a `PUBLISHED` event, the banner can only be changed via the dedicated `PATCH /{eventId}/published/banner` endpoint (query param, not body) — it is no longer part of the "Update Published Basic Info" request.

---

### Event Creation User Journey

```
  [Organizer]
       │
       │  POST /draft
       ▼
  ┌─────────────┐
  │  BASIC INFO │  title, category, format, description, bannerFileId (optional)
  └──────┬──────┘
         │  PATCH /drafts/{id}/basic-info  (optional update)
         │
         │  PATCH /draft/{id}/schedule
         ▼
  ┌──────────────┐
  │   SCHEDULE   │  days[ date, startTime, endTime ], timezone
  └──────┬───────┘
         │
         │  PATCH /draft/{id}/location
         ▼
  ┌──────────────────┐
  │ LOCATION DETAILS │  venue (IN_PERSON/HYBRID) or virtualDetails (ONLINE/HYBRID)
  └──────┬───────────┘         or skip entirely (TBA format)
         │
         │  (Ticket creation via Tickets API — required before publish)
         ▼
  ┌──────────────┐
  │   TICKETS    │  at least one active ticket required
  └──────┬───────┘
         │
         │  (Optional enrichment)
         │  PATCH /drafts/{id}/highlights
         │  PATCH /drafts/{id}/faqs
         │  PATCH /drafts/{id}/lineup
         │  PATCH /drafts/{id}/agenda
         │  POST  /draft/{id}/products/{productId}
         │  POST  /draft/{id}/shops/{shopId}
         │
         │  PATCH /{eventId}/publish
         ▼
  ┌───────────────┐
  │   PUBLISHED   │  RSA keys generated, ctaLabel auto-derived if not set,
  │               │  category count incremented, event visible in public feed
  └───────────────┘
```

### Event Status Flow

```
  DRAFT ◄──────────────────────► PUBLISHED
    │           (unpublish           │
    │        if 0 tickets sold)      │  (unpublish — only if no tickets sold)
    │                                │
    │  (discardDraft)                │
    ▼                                ▼
  [deleted]                     CANCELLED ◄─── (cancel from any non-terminal status)
                                     │
                              [terminal state]

  PUBLISHED ──► (system / scheduled job) ──► HAPPENING ──► COMPLETED
```

> **Status Rules**:
> - `DRAFT ↔ PUBLISHED` — Free movement. Unpublish is only allowed if zero tickets have been sold.
> - `CANCELLED` — Terminal. Can be triggered from any non-terminal status. Triggers bulk refund if tickets were sold.
> - `HAPPENING` / `COMPLETED` — System-managed via scheduled jobs.

---

## Standard Response Format

All API responses follow a consistent structure using the Globe Response Builder pattern.

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": { }
}
```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, CREATED, BAD_REQUEST, etc.) |
| `message` | string | Human-readable description of the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object / string | Response payload on success; error detail on failure |

---

## Shared Response Object Definitions

The following objects are returned by multiple endpoints. They are defined once here and referenced throughout.

### A. EventResponse (Full Event Object)

Returned by all draft and event management endpoints.

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Unique event identifier |
| `title` | string | Event title |
| `slug` | string | URL-friendly identifier, auto-generated |
| `description` | string | Full event description |
| `category.categoryId` | UUID | Category identifier |
| `category.categoryName` | string | Category display name |
| `category.categorySlug` | string | Category slug |
| `eventFormat` | string | `IN_PERSON`, `ONLINE`, `HYBRID`, or `TBA` |
| `eventVisibility` | string | `PUBLIC`, `PRIVATE`, or `UNLISTED` |
| `status` | string | `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` |
| `schedule.startDateTime` | ZonedDateTime | Event start (ISO 8601 with offset) |
| `schedule.endDateTime` | ZonedDateTime | Event end (ISO 8601 with offset) |
| `schedule.timezone` | string | IANA timezone string (e.g., `Africa/Dar_es_Salaam`) |
| `schedule.days[]` | array | Day-level schedule entries (see EventDayInfo below) |
| `venue.name` | string | Venue name (IN_PERSON / HYBRID only) |
| `venue.address` | string | Venue address |
| `venue.coordinates.latitude` | string | Latitude decimal string |
| `venue.coordinates.longitude` | string | Longitude decimal string |
| `virtualDetails.meetingLink` | string | Meeting URL (ONLINE / HYBRID only). **Stripped to `null` on the public `GET /{eventId}` endpoint** — only returned to the organizer via draft/my-events endpoints |
| `virtualDetails.meetingId` | string | Platform meeting ID (same stripping rule as above) |
| `virtualDetails.passcode` | string | Meeting passcode (same stripping rule as above) |
| `bannerMedia` | object \| null | Resolved banner media object. See EventBannerMedia definition below. `null` if no banner was ever set |
| `highlights[]` | array | See HighlightEntry definition below |
| `faqs[]` | array | See FaqEntry definition below |
| `lineup[]` | array | See LineupEntry definition below |
| `agenda[]` | array | See AgendaDay definition below |
| `linkedProducts[]` | array | `{ productId, productName, productSlug }` |
| `linkedShops[]` | array | `{ shopId, shopName, shopSlug }` |
| `tickets[]` | array | See TicketSummaryInfo definition below |
| `organizer` | object | See OrganizerInfo definition below |
| `ctaLabel` | string | CTA button label (e.g., "Get Tickets", "Register for Free"). Auto-derived from ticket pricing at publish if not explicitly set |
| `hasApplicantForm` | boolean | Whether an applicant form is configured for this event |
| `applicantForm.displayTime` | string | When the form is shown: `BEFORE_CHECKOUT` or `AFTER_CHECKOUT` |
| `applicantForm.isRequired` | boolean | Whether form submission is required for online attendees |
| `applicantForm.applyToAtDoor` | boolean | Whether the form also applies to at-door check-in |
| `currentStage` | string | Current event creation stage |
| `completedStages[]` | array | List of completed stage names |
| `completionPercentage` | integer | 0–100 completion percentage |
| `canPublish` | boolean | Whether all required stages are completed |
| `createdAt` | ZonedDateTime | Creation timestamp |
| `updatedAt` | ZonedDateTime | Last update timestamp |
| `createdBy` | string | Username of creator |
| `updatedBy` | string | Username of last editor |

#### EventDayInfo

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Day entity ID |
| `date` | string | Date in `YYYY-MM-DD` format |
| `startTime` | string | Start time in `HH:mm:ss` |
| `endTime` | string | End time in `HH:mm:ss` |
| `description` | string | Optional day description |
| `dayOrder` | integer | Display order (1-based) |

#### HighlightEntry

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | `AGE_RESTRICTION`, `CHECK_IN_TIME`, `PARKING`, `DRESS_CODE`, `FOOD_DRINKS`, `ACCESSIBILITY`, `REFUND_POLICY`, `WHAT_TO_BRING`, `PROHIBITED_ITEMS`, `WEATHER_INFO`, `CUSTOM` |
| `title` | string | Display title for this highlight |
| `value` | string | Short value (e.g., "18+") |
| `description` | string | Longer explanation |

#### FaqEntry

| Field | Type | Description |
|-------|------|-------------|
| `question` | string | The FAQ question |
| `answer` | string | The answer |
| `order` | integer | Display order |

#### LineupEntry

| Field | Type | Description |
|-------|------|-------------|
| `entryType` | string | `PLATFORM_USER` or `CUSTOM` |
| `userId` | UUID | Platform user ID (only when `entryType=PLATFORM_USER`) |
| `name` | string | Display name (auto-enriched from user profile when `PLATFORM_USER`) |
| `role` | string | `HEADLINER`, `PERFORMER`, `SPEAKER`, `DJ`, `HOST`, `PANELIST`, `MODERATOR`, `GUEST` |
| `title` | string | Professional title (e.g., "Lead Vocalist") |
| `bio` | string | Short biography |
| `profileMedia` | object \| null | Resolved profile picture — **only populated for `PLATFORM_USER` entries**, auto-refreshed from the linked account on every read. Same lean shape as `EventBannerPrimaryMedia` (`fileId`, `status`, `thumbUrl`, `mimeType`). **`CUSTOM` entries have no image/media at all** — there is no way to attach a photo to a custom lineup entry |
| `performanceDay` | integer | Which day number of the event they perform |
| `performanceTime` | string | Time of performance |
| `order` | integer | Display order |

#### AgendaDay

| Field | Type | Description |
|-------|------|-------------|
| `dayNumber` | integer | Day number (1-based) |
| `date` | string | Date in `YYYY-MM-DD` format |
| `sessions[]` | array | See AgendaSession definition below |

#### AgendaSession

| Field | Type | Description |
|-------|------|-------------|
| `startTime` | string | Session start time (`HH:mm`) |
| `endTime` | string | Session end time (`HH:mm`) |
| `title` | string | Session title |
| `description` | string | Session description |
| `type` | string | `GENERAL`, `PERFORMANCE`, `CEREMONY`, `PANEL`, `WORKSHOP`, `NETWORKING`, `MEAL`, `BREAK` |
| `location` | string | Sub-location within the event venue |
| `presenterType` | string | `PLATFORM_USER` or `CUSTOM` |
| `presenterId` | UUID | Platform user ID (when `presenterType=PLATFORM_USER`) |
| `presenterName` | string | Presenter name (auto-enriched from user profile when `PLATFORM_USER`) |
| `presenterTitle` | string | Professional title |
| `presenterBio` | string | Short biography |
| `presenterMedia` | object \| null | Resolved profile picture — **only populated for `PLATFORM_USER` presenters**, auto-refreshed from the linked account on every read. Same lean shape as `EventBannerPrimaryMedia` (`fileId`, `status`, `thumbUrl`, `mimeType`). **`CUSTOM` presenters have no image/media at all** — there is no way to attach a photo to a custom presenter |

#### TicketSummaryInfo

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Ticket type ID |
| `name` | string | Ticket type name (e.g., "VIP", "General Admission") |
| `price` | BigDecimal | Ticket price (0.00 for free) |
| `salesChannel` | string | Ticket sales channel enum value |
| `ticketPricingType` | string | `FREE`, `PAID`, or `DONATION` |
| `totalTickets` | integer | Total slots allocated |
| `ticketsSold` | integer | Number sold |
| `ticketsAvailable` | integer | Remaining slots |
| `ticketsReserved` | integer | Slots currently held in an active (unpaid) checkout |
| `isSoldOut` | boolean | True when available = 0 |
| `attendanceMode` | string | Ticket attendance mode enum value |
| `status` | string | Ticket status enum value |
| `isOnSale` | boolean | Whether the ticket is currently within its sales window |
| `saleStatusMessage` | string | Human-readable explanation of the current sale status |

#### EventBannerMedia (full media object — on EventResponse)

Resolved banner media, returned on the full event detail response. Built from the `bannerFileId` supplied at creation/update.

| Field | Type | Description |
|-------|------|-------------|
| `fileId` | UUID | FileThunder file identifier |
| `status` | string | File processing status enum (e.g., `READY`, `PROCESSING`, `FAILED`) |
| `variants` | map&lt;string,string&gt; | All resolved size/format variants, keyed by variant name (e.g., `"thumb"`, `"medium"`, `"original"`) → signed/public URL |
| `mimeType` | string | MIME type of the uploaded file |

#### EventBannerPrimaryMedia (lean media object — on EventSummaryResponse)

Lightweight single-URL counterpart used in list/search results — only the `"thumb"` variant is resolved (falls back to the first available variant if no thumb exists).

| Field | Type | Description |
|-------|------|-------------|
| `fileId` | UUID | FileThunder file identifier |
| `status` | string | File processing status enum |
| `thumbUrl` | string | Single resolved thumbnail URL |
| `mimeType` | string | MIME type of the uploaded file |

#### OrganizerInfo (on EventResponse only)

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Organizer account ID |
| `name` | string | Organizer full name (`firstName + " " + lastName`) |
| `username` | string | Organizer system username |
| `profileMedia` | object \| null | Organizer's profile picture, lean shape — same fields as EventBannerPrimaryMedia (`fileId`, `status`, `thumbUrl`, `mimeType`) |

> **Note**: `EventSummaryResponse` (list/search results) does **not** use this nested object — it still exposes flat `organizerId` / `organizerName` / `organizerUsername` fields with no profile picture at all. This is a known inconsistency versus the full `EventResponse` shape (which was updated to include `profileMedia`); the summary shape has not been migrated yet.

---

### B. EventSummaryResponse (Lightweight List Object)

Returned by all paginated list and search endpoints.

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Event ID |
| `title` | string | Event title |
| `slug` | string | URL slug |
| `shortDescription` | string | First 150 characters of description |
| `categoryId` | UUID | Category ID |
| `categoryName` | string | Category display name |
| `eventFormat` | string | `IN_PERSON`, `ONLINE`, `HYBRID`, `TBA` |
| `eventVisibility` | string | `PUBLIC`, `PRIVATE`, `UNLISTED` |
| `status` | string | Event status |
| `startDateTime` | ZonedDateTime | Start date/time with offset |
| `endDateTime` | ZonedDateTime | End date/time with offset |
| `timezone` | string | IANA timezone |
| `locationSummary` | string | Human-readable location (e.g., "Dar es Salaam, TZ", "Online Event", "Location To Be Announced") |
| `bannerMedia` | object \| null | Lean resolved banner media — see [EventBannerPrimaryMedia](#eventbannerprimarymedia-lean-media-object--on-eventsummaryresponse) above. `null` if no banner was ever set |
| `hasApplicantForm` | boolean | Whether an applicant form is configured |
| `ctaLabel` | string | CTA button label |
| `pricing.pricingType` | string | `FREE`, `PAID`, `DONATION`, or `MIXED` — frontend uses this directly for the display label |
| `pricing.minPrice` | BigDecimal \| null | Lowest available ticket price (PAID/FREE tickets only, excludes DONATION). `null` if pricing is FREE-only or DONATION-only |
| `pricing.maxPrice` | BigDecimal \| null | Highest available ticket price (same exclusion rule as `minPrice`) |
| `pricing.hasFreeTickets` | boolean | True when at least one active FREE ticket type exists |
| `pricing.hasPaidTickets` | boolean | True when at least one active PAID ticket type exists |
| `pricing.hasDonationTickets` | boolean | True when at least one active DONATION ticket type exists |
| `organizerId` | UUID | Organizer ID (flat field — no nested media/profile picture on this lightweight object, see note above) |
| `organizerName` | string | Organizer full name |
| `organizerUsername` | string | Organizer username |
| `stats.totalTickets` | integer | Sum of all ticket slots |
| `stats.ticketsSold` | integer | Total tickets sold |
| `stats.ticketsAvailable` | integer | Remaining tickets |
| `stats.isSoldOut` | boolean | True when no tickets remain |
| `stats.attendeeCount` | integer | Same as ticketsSold |
| `createdAt` | ZonedDateTime | Creation timestamp |

---

### C. Standard Paginated Response Wrapper

All list endpoints return data inside a Spring `Page` wrapper.

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ ],
    "pageable": {
      "pageNumber": 0,
      "pageSize": 10
    },
    "totalElements": 42,
    "totalPages": 5,
    "last": false,
    "first": true,
    "empty": false
  }
}
```

> **Note**: All list endpoints accept `page` (1-based, default `1`) and `size` (default `10`) as query parameters. Spring internally converts to 0-based before querying.

---

### D. EventStatusResponse (Lightweight Status Object)

Returned by the publish/unpublish/cancel transition endpoints. These no longer return the full `EventResponse` — only the minimal fields needed to confirm the transition.

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Event ID |
| `slug` | string | Event URL slug |
| `status` | string | New status after the transition (`PUBLISHED`, `DRAFT`, or `CANCELLED`) |

> If you need the full updated event (media, tickets, organizer, etc.) after a publish/unpublish/cancel call, follow up with `GET /{eventId}` ([Endpoint 25](#25-get-event-by-id)) or `GET /drafts/{draftId}` ([Endpoint 3](#3-get-draft-by-id)).

---

## HTTP Method Badge Standards

- **GET** — Green (`#28a745`) — Safe, read-only operations
- **POST** — Blue (`#007bff`) — Create new resources
- **PUT** — Yellow (`#ffc107`, black text) — Replace entire resource
- **PATCH** — Orange (`#fd7e14`) — Partial update
- **DELETE** — Red (`#dc3545`) — Remove resource

---

## Standard Error Types

### Application-Level Exceptions (400–499)

| Code | Name | When it occurs |
|------|------|---------------|
| `400` | `BAD_REQUEST` | Invalid request data, already published event, duplicate product/shop, unpublish blocked due to ticket sales |
| `401` | `UNAUTHORIZED` | Missing, expired, or malformed Bearer token |
| `403` | `FORBIDDEN` | Authenticated but not the event organizer, or accessing a draft belonging to another user |
| `404` | `NOT_FOUND` | Event, category, product, or shop ID not found |
| `422` | `UNPROCESSABLE_ENTITY` | Bean validation failures with per-field detail |

### Server-Level Exceptions (500+)

| Code | Name | When it occurs |
|------|------|---------------|
| `500` | `INTERNAL_SERVER_ERROR` | RSA key generation failure, unexpected runtime error |

---

## Shared Error Response Examples

> All endpoints may return these error shapes. Each endpoint section references them rather than repeating the full JSON.

**401 — Unauthorized:**
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2025-02-17T10:30:45",
  "data": "Token has expired"
}
```

**403 — Forbidden:**
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Access denied: Insufficient permissions",
  "action_time": "2025-02-17T10:30:45",
  "data": "Access denied: Insufficient permissions"
}
```

**404 — Not Found:**
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Event not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "action_time": "2025-02-17T10:30:45",
  "data": "Event not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

**422 — Validation Error:**
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "title": "size must be between 3 and 200",
    "categoryId": "must not be null",
    "eventFormat": "must not be null"
  }
}
```

---

## Endpoints

---

## 1. Create Event Draft

**Purpose**: Creates a new event in `DRAFT` status as the first step of the event creation workflow. Marks `BASIC_INFO` stage as completed automatically.

**Endpoint**: `POST` `/api/v1/e-events/drafts`

**Access Level**: 🔒 Protected (Any authenticated user — becomes event organizer)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Request JSON Sample**:

```json
{
  "title": "Dar es Salaam Jazz Festival 2025",
  "categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "eventFormat": "IN_PERSON",
  "eventVisibility": "PUBLIC",
  "description": "An annual celebration of jazz and live music at the heart of the city.",
  "bannerFileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `title` | string | Yes | Event title | Min: 3, Max: 200 characters |
| `categoryId` | UUID | Yes | ID of an active event category | Must exist and be active |
| `eventFormat` | string | Yes | Event format | Enum: `IN_PERSON`, `ONLINE`, `HYBRID`, `TBA` |
| `eventVisibility` | string | No | Who can see the event | Enum: `PUBLIC`, `PRIVATE`, `UNLISTED`. Defaults to `PUBLIC` |
| `description` | string | No | Full event description | Max: 5000 characters |
| `bannerFileId` | UUID | No | ID of a file already uploaded via FileThunder to use as the banner | Must reference an existing uploaded file |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Event draft created",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "title": "Dar es Salaam Jazz Festival 2025",
    "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
    "description": "An annual celebration of jazz...",
    "category": {
      "categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "categoryName": "Music & Concerts",
      "categorySlug": "music-concerts"
    },
    "eventFormat": "IN_PERSON",
    "eventVisibility": "PUBLIC",
    "status": "DRAFT",
    "currentStage": "BASIC_INFO",
    "completedStages": ["BASIC_INFO"],
    "completionPercentage": 25,
    "canPublish": false,
    "schedule": null,
    "venue": null,
    "virtualDetails": null,
    "bannerMedia": {
      "fileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
      "status": "READY",
      "variants": {
        "thumb": "https://cdn.example.com/ft/6b1a2c3d/thumb.jpg",
        "medium": "https://cdn.example.com/ft/6b1a2c3d/medium.jpg",
        "original": "https://cdn.example.com/ft/6b1a2c3d/original.jpg"
      },
      "mimeType": "image/jpeg"
    },
    "highlights": null,
    "faqs": null,
    "lineup": null,
    "agenda": null,
    "linkedProducts": [],
    "linkedShops": [],
    "tickets": [],
    "organizer": {
      "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "name": "Amina Hassan",
      "username": "amina.hassan",
      "profileMedia": {
        "fileId": "9f8e7d6c-5b4a-3c2d-1e0f-a1b2c3d4e5f6",
        "status": "READY",
        "thumbUrl": "https://cdn.example.com/ft/9f8e7d6c/thumb.jpg",
        "mimeType": "image/jpeg"
      }
    },
    "ctaLabel": null,
    "hasApplicantForm": false,
    "applicantForm": null,
    "createdAt": "2025-02-17T10:30:45+03:00",
    "updatedAt": null,
    "createdBy": "amina.hassan",
    "updatedBy": null
  }
}
```

**Success Response Fields**: See [Shared Response Object A — EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token — see [Shared Error: 401](#shared-error-response-examples) |
| `404` | Category ID not found |
| `422` | Missing required fields (title, categoryId, eventFormat) — see [Shared Error: 422](#shared-error-response-examples) |

---

## 2. Get My Drafts

**Purpose**: Returns a paginated list of all DRAFT events owned by the authenticated organizer.

**Endpoint**: `GET` `/api/v1/e-events/drafts`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Drafts retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "title": "Dar es Salaam Jazz Festival 2025",
        "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
        "shortDescription": "An annual celebration of jazz and live music...",
        "categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        "categoryName": "Music & Concerts",
        "eventFormat": "IN_PERSON",
        "eventVisibility": "PUBLIC",
        "status": "DRAFT",
        "startDateTime": null,
        "endDateTime": null,
        "timezone": null,
        "locationSummary": null,
        "bannerMedia": {
          "fileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
          "status": "READY",
          "thumbUrl": "https://cdn.example.com/ft/6b1a2c3d/thumb.jpg",
          "mimeType": "image/jpeg"
        },
        "hasApplicantForm": false,
        "ctaLabel": null,
        "pricing": {
          "pricingType": "FREE",
          "minPrice": null,
          "maxPrice": null,
          "hasFreeTickets": false,
          "hasPaidTickets": false,
          "hasDonationTickets": false
        },
        "organizerId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
        "organizerName": "Amina Hassan",
        "organizerUsername": "amina.hassan",
        "stats": { "totalTickets": 0, "ticketsSold": 0, "ticketsAvailable": 0, "isSoldOut": false },
        "createdAt": "2025-02-17T10:30:45+03:00"
      }
    ],
    "totalElements": 3,
    "totalPages": 1,
    "first": true,
    "last": true,
    "empty": false
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects. Pagination fields follow [Standard Paginated Response Wrapper](#c-standard-paginated-response-wrapper).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token — see [Shared Error: 401](#shared-error-response-examples) |

---

## 3. Get Draft by ID

**Purpose**: Retrieves the full detail of a specific draft. Only the draft's organizer can access it.

**Endpoint**: `GET` `/api/v1/e-events/drafts/{draftId}`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The ID of the draft event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Draft retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Authenticated but not the owner of this draft |
| `404` | Draft not found with given ID |

---

## 4. Discard Draft

**Purpose**: Permanently deletes a draft event and all its associated day schedules, linked products, and linked shops. This action is irreversible.

**Endpoint**: `DELETE` `/api/v1/e-events/drafts/{draftId}`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The ID of the draft to discard | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Draft discarded",
  "action_time": "2025-02-17T10:30:45",
  "data": null
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Authenticated but not the owner of this draft |
| `404` | Draft not found with given ID |

---

## 5. Update Draft — Basic Info

**Purpose**: Updates the basic information of a draft. All fields are optional — only provided fields are updated. Advances `currentStage` to `SCHEDULE` upon completion.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/basic-info`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "title": "Dar es Salaam Jazz Festival 2025 — Updated",
  "description": "The biggest jazz event in East Africa returns for its 10th edition with over 30 artists.",
  "categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "eventVisibility": "PUBLIC",
  "eventFormat": "IN_PERSON",
  "ctaLabel": "Get Tickets",
  "bannerFileId": "6b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `title` | string | No | Updated event title | Min: 3, Max: 200 characters |
| `description` | string | No | Updated description | Min: 15, Max: 5000 characters |
| `categoryId` | UUID | No | New category | Must exist and be active |
| `eventVisibility` | string | No | Visibility change | Enum: `PUBLIC`, `PRIVATE`, `UNLISTED` |
| `eventFormat` | string | No | Format change | Enum: `IN_PERSON`, `ONLINE`, `HYBRID`, `TBA` |
| `ctaLabel` | string | No | CTA button label override | Max: 50 characters. If omitted, existing value is kept; auto-derived at publish if never set |
| `bannerFileId` | UUID | No | ID of a file already uploaded via FileThunder to replace the banner | Must reference an existing uploaded file |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Basic info updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). Note that `completedStages` will now include `"BASIC_INFO"` and `currentStage` will be `"SCHEDULE"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft or category not found |
| `422` | Validation failure on provided fields |

---

## 6. Update Draft — Schedule

**Purpose**: Sets the event's day-by-day schedule. Supports multi-day events. Each day must have a unique date in chronological order. Advances `currentStage` to `LOCATION_DETAILS`. The overall `startDateTime` and `endDateTime` on the event are derived automatically from the first and last day.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/schedule`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "timezone": "Africa/Dar_es_Salaam",
  "days": [
    {
      "date": "2025-07-18",
      "startTime": "18:00:00",
      "endTime": "23:00:00",
      "description": "Opening Night",
      "dayOrder": 1
    },
    {
      "date": "2025-07-19",
      "startTime": "16:00:00",
      "endTime": "23:59:00",
      "description": "Main Concert Day",
      "dayOrder": 2
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `timezone` | string | No | IANA timezone identifier | Must be a valid IANA zone ID (e.g., `Africa/Dar_es_Salaam`). Defaults to `UTC` |
| `days` | array | Yes | List of event days | Min 1 day required |
| `days[].date` | string | Yes | Date of the day | `YYYY-MM-DD` format; must not be in the past |
| `days[].startTime` | string | Yes | Day start time | `HH:mm:ss` format |
| `days[].endTime` | string | Yes | Day end time | `HH:mm:ss`; must be after `startTime` |
| `days[].description` | string | No | Optional day description | — |
| `days[].dayOrder` | integer | No | Display order | Defaults to position in array if omitted |

> **Notes**:
> - All existing days are replaced on each call. To update the schedule, resend the full days array.
> - Dates must be unique — duplicate dates in the same request are rejected with `422`.
> - Days must be provided in chronological order (sorted ascending by date).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Schedule updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `schedule.days` will be populated, and `schedule.startDateTime` / `schedule.endDateTime` will be set from the first and last day.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found |
| `422` | Days out of order, duplicate dates, date in the past, missing time fields |

---

## 7. Update Draft — Location

**Purpose**: Sets the physical venue and/or virtual meeting details for the event. Required fields depend on `eventFormat`. For `TBA` format, this endpoint can be called but no fields are required — the stage is automatically marked complete. Advances `currentStage` to `TICKETS`.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/location`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample** (IN_PERSON):

```json
{
  "venue": {
    "name": "Mlimani City Arena",
    "address": "Sam Nujoma Road, Dar es Salaam",
    "coordinates": {
      "latitude": -6.7724,
      "longitude": 39.2083
    }
  }
}
```

**Request JSON Sample** (ONLINE):

```json
{
  "virtualDetails": {
    "meetingLink": "https://zoom.us/j/123456789",
    "meetingId": "123 456 789",
    "passcode": "jazz2025"
  }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `venue` | object | Conditional | Required when format is `IN_PERSON` or `HYBRID` | — |
| `venue.name` | string | Yes (if venue) | Venue name | Max: 200 characters |
| `venue.address` | string | No | Full address | Max: 500 characters |
| `venue.coordinates.latitude` | BigDecimal | No | GPS latitude | — |
| `venue.coordinates.longitude` | BigDecimal | No | GPS longitude | — |
| `virtualDetails` | object | Conditional | Required when format is `ONLINE` or `HYBRID` | — |
| `virtualDetails.meetingLink` | string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
| `virtualDetails.meetingId` | string | No | Platform meeting ID | Max: 100 characters |
| `virtualDetails.passcode` | string | No | Meeting passcode | Max: 100 characters |

> **Format-based rules**:
> - `IN_PERSON` → `venue.name` is required; `virtualDetails` is ignored
> - `ONLINE` → `virtualDetails.meetingLink` is required; `venue` is ignored
> - `HYBRID` → both `venue.name` and `virtualDetails.meetingLink` are required
> - `TBA` → no fields required; stage is immediately marked complete

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Location updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `completedStages` will include `"LOCATION_DETAILS"` when requirements are met and `currentStage` will advance to `"TICKETS"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found |
| `422` | Missing required venue/virtual fields for the event format |

---

## 8. Update Draft — Highlights

**Purpose**: Replaces the full list of event highlights (key attendee-facing info such as age restriction, dress code, parking, etc.). Sending an empty array clears all highlights.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/highlights`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "highlights": [
    {
      "type": "AGE_RESTRICTION",
      "title": "Age Limit",
      "value": "18+",
      "description": "This event is strictly for adults aged 18 and above."
    },
    {
      "type": "DRESS_CODE",
      "title": "Dress Code",
      "value": "Smart Casual",
      "description": "No sportswear or flip flops allowed."
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `highlights` | array | Yes | Full list of highlights | Empty array is valid (clears existing) |
| `highlights[].type` | string | Yes | Highlight category | Enum: `AGE_RESTRICTION`, `CHECK_IN_TIME`, `PARKING`, `DRESS_CODE`, `FOOD_DRINKS`, `ACCESSIBILITY`, `REFUND_POLICY`, `WHAT_TO_BRING`, `PROHIBITED_ITEMS`, `WEATHER_INFO`, `CUSTOM` |
| `highlights[].title` | string | Yes | Display title | — |
| `highlights[].value` | string | No | Short value summary | — |
| `highlights[].description` | string | No | Longer explanation | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Highlights updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.highlights` will reflect the new list.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found |

---

## 9. Update Draft — FAQs

**Purpose**: Replaces the full list of frequently asked questions for the event. Sending an empty array clears all FAQs.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/faqs`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "faqs": [
    {
      "question": "Is this event suitable for children?",
      "answer": "No. This event is strictly 18+ only.",
      "order": 1
    },
    {
      "question": "Is parking available?",
      "answer": "Yes, free parking is available on site.",
      "order": 2
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `faqs` | array | Yes | Full list of FAQs | Empty array is valid (clears existing) |
| `faqs[].question` | string | Yes | FAQ question text | — |
| `faqs[].answer` | string | Yes | FAQ answer text | — |
| `faqs[].order` | integer | No | Display order | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "FAQs updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.faqs` will reflect the new list.

**Possible Error Responses**: Same as Endpoint 8 (401, 403, 404).

---

## 10. Update Draft — Lineup

**Purpose**: Replaces the full event lineup. Supports both platform users (whose profile data is auto-enriched) and custom entries for external performers or speakers. Sending an empty array clears the lineup.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/lineup`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "lineup": [
    {
      "entryType": "PLATFORM_USER",
      "userId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "role": "HEADLINER",
      "performanceDay": 1,
      "performanceTime": "21:00",
      "order": 1
    },
    {
      "entryType": "CUSTOM",
      "name": "DJ Afrobeat",
      "role": "DJ",
      "title": "International Guest DJ",
      "bio": "Award-winning DJ from Lagos with 10 years of experience.",
      "performanceDay": 2,
      "performanceTime": "22:00",
      "order": 2
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `lineup` | array | Yes | Full lineup list | Empty array is valid |
| `lineup[].entryType` | string | Yes | Entry source | Enum: `PLATFORM_USER`, `CUSTOM` |
| `lineup[].userId` | UUID | Conditional | Platform user ID | Required when `entryType=PLATFORM_USER`; must exist in the system |
| `lineup[].name` | string | Conditional | Display name | Required when `entryType=CUSTOM`; auto-set from user profile when `PLATFORM_USER` |
| `lineup[].role` | string | No | Lineup role | Enum: `HEADLINER`, `PERFORMER`, `SPEAKER`, `DJ`, `HOST`, `PANELIST`, `MODERATOR`, `GUEST` |
| `lineup[].title` | string | No | Professional title | — |
| `lineup[].bio` | string | No | Biography text | Auto-set from user profile when `PLATFORM_USER` |
| `lineup[].performanceDay` | integer | No | Day number of performance | — |
| `lineup[].performanceTime` | string | No | Performance start time (`HH:mm`) | — |
| `lineup[].order` | integer | No | Display order | — |

> **Auto-enrichment**: When `entryType=PLATFORM_USER`, the system automatically fetches and populates `name`, `bio`, and `profileMedia` (resolved profile picture object) from the user's profile. These are also refreshed on every `GET` of the event. **There is no `image` field on this request** — `CUSTOM` entries cannot have a photo at all; only `PLATFORM_USER` entries get one, sourced from the linked account.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Lineup updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.lineup` will contain enriched entries.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found, or a `userId` in lineup does not exist |

---

## 11. Update Draft — Agenda

**Purpose**: Replaces the full event agenda organized by day and session. Supports both platform users and custom entries as session presenters. Sending an empty array clears the agenda.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/agenda`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "agenda": [
    {
      "dayNumber": 1,
      "date": "2025-07-18",
      "sessions": [
        {
          "startTime": "18:00",
          "endTime": "19:00",
          "title": "Welcome & Opening Ceremony",
          "type": "CEREMONY",
          "location": "Main Stage",
          "presenterType": "PLATFORM_USER",
          "presenterId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
        },
        {
          "startTime": "20:00",
          "endTime": "22:00",
          "title": "Headliner Performance",
          "type": "PERFORMANCE",
          "presenterType": "CUSTOM",
          "presenterName": "The Sauti Sol Band",
          "presenterTitle": "Headline Act",
          "presenterBio": "East Africa's most celebrated group."
        }
      ]
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `agenda` | array | Yes | Full agenda list (by day) | Empty array is valid |
| `agenda[].dayNumber` | integer | Yes | Day number (1-based) | — |
| `agenda[].date` | string | No | Date string (`YYYY-MM-DD`) | — |
| `agenda[].sessions` | array | Yes | Sessions for this day | — |
| `sessions[].startTime` | string | Yes | Session start (`HH:mm`) | — |
| `sessions[].endTime` | string | Yes | Session end (`HH:mm`) | — |
| `sessions[].title` | string | Yes | Session name | — |
| `sessions[].description` | string | No | Session description | — |
| `sessions[].type` | string | No | Session type | Enum: `GENERAL`, `PERFORMANCE`, `CEREMONY`, `PANEL`, `WORKSHOP`, `NETWORKING`, `MEAL`, `BREAK` |
| `sessions[].location` | string | No | Sub-location within venue | — |
| `sessions[].presenterType` | string | No | Presenter source | Enum: `PLATFORM_USER`, `CUSTOM` |
| `sessions[].presenterId` | UUID | Conditional | Platform user ID | Required when `presenterType=PLATFORM_USER` |
| `sessions[].presenterName` | string | Conditional | Presenter name | Required when `presenterType=CUSTOM`; auto-set when `PLATFORM_USER` |
| `sessions[].presenterTitle` | string | No | Professional title | Auto-enriched for platform users |
| `sessions[].presenterBio` | string | No | Biography | Auto-enriched for platform users |

> **There is no `presenterImage` field on this request** — `CUSTOM` presenters cannot have a photo at all; only `PLATFORM_USER` presenters get one (`presenterMedia`, resolved profile picture object), sourced from the linked account.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agenda updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.agenda` will contain enriched sessions.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found, or a `presenterId` does not exist |

---

## 12. Attach Product to Event

**Purpose**: Links an existing active product from the PRODUCT domain to an event. Allowed on both `DRAFT` and `PUBLISHED` events. Marks the `LINKS` stage as completed.

**Endpoint**: `POST` `/api/v1/e-events/{eventId}/products/{productId}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event (DRAFT or PUBLISHED) | Must be a valid UUID |
| `productId` | UUID | Yes | The product to attach | Must exist and have status `ACTIVE` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product attached",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.linkedProducts` will include the newly attached product.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event or product not found |
| `400` | Product is not active, already attached, or event is not in DRAFT/PUBLISHED status |

---

## 13. Remove Product from Event

**Purpose**: Detaches a previously linked product from the event. Allowed on both `DRAFT` and `PUBLISHED` events.

**Endpoint**: `DELETE` `/api/v1/e-events/{eventId}/products/{productId}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event (DRAFT or PUBLISHED) | Must be a valid UUID |
| `productId` | UUID | Yes | The product to remove | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product removed",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or product not currently attached |

---

## 14. Attach Shop to Event

**Purpose**: Links an existing active shop from the PRODUCT domain to an event. Allowed on both `DRAFT` and `PUBLISHED` events. Marks the `LINKS` stage as completed.

**Endpoint**: `POST` `/api/v1/e-events/{eventId}/shops/{shopId}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event (DRAFT or PUBLISHED) | Must be a valid UUID |
| `shopId` | UUID | Yes | The shop to attach | Must exist and have status `ACTIVE` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop attached",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.linkedShops` will include the newly attached shop.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event or shop not found |
| `400` | Shop is not active, already attached, or event is not in DRAFT/PUBLISHED status |

---

## 15. Remove Shop from Event

**Purpose**: Detaches a previously linked shop from the event. Allowed on both `DRAFT` and `PUBLISHED` events.

**Endpoint**: `DELETE` `/api/v1/e-events/{eventId}/shops/{shopId}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event (DRAFT or PUBLISHED) | Must be a valid UUID |
| `shopId` | UUID | Yes | The shop to remove | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop removed",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**: Same as Endpoint 13 (401, 403, 404).

---

## 16. Publish Event

**Purpose**: Transitions a draft event to `PUBLISHED` status. This is the final step of the creation workflow. The system validates all required stages are complete, performs a duplicate event check against existing published events, generates an RSA key pair used for secure ticket QR code signing, and auto-derives the `ctaLabel` from ticket pricing if the organizer has not set one.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/publish`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to publish | Must be a valid UUID |

> **Pre-publish checklist** (all must pass or the request is rejected with `422`):
> - `BASIC_INFO` stage completed ✓
> - `SCHEDULE` stage completed (at least one day, not in the past) ✓
> - `LOCATION_DETAILS` stage completed ✓
> - At least one active ticket exists for the event ✓
> - Event start date is not in the past ✓
> - No duplicate event detected with ≥85% similarity score to another organizer's public event ✓

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event published successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
    "status": "PUBLISHED"
  }
}
```

**Success Response Fields**: `data` is an [EventStatusResponse](#d-eventstatusresponse-lightweight-status-object) — **not** the full `EventResponse`.

**Error Response Sample** (duplicate detected):

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This event appears to be a duplicate of 'Dar es Salaam Jazz Night' by user123. Please make the title, date, or location more distinct.",
  "action_time": "2025-02-17T10:30:45",
  "data": "This event appears to be a duplicate..."
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is already published, or duplicate event detected |
| `422` | One or more required stages are incomplete, or no active tickets exist |
| `500` | RSA key generation failed |

---

## 17. Unpublish Event

**Purpose**: Reverts a `PUBLISHED` event back to `DRAFT` status. Only allowed if zero tickets have been sold across all ticket types. If tickets have been sold, the organizer must cancel the event instead.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/unpublish`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to unpublish | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event unpublished successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
    "status": "DRAFT"
  }
}
```

**Success Response Fields**: `data` is an [EventStatusResponse](#d-eventstatusresponse-lightweight-status-object) — **not** the full `EventResponse`.

**Error Response Sample** (tickets sold):

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot unpublish: tickets have already been sold. Please cancel the event instead.",
  "action_time": "2025-02-17T10:30:45",
  "data": "Cannot unpublish: tickets have already been sold. Please cancel the event instead."
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not currently PUBLISHED, or tickets have been sold |

---

## 18. Cancel Event

**Purpose**: Cancels an event. Can be triggered from any non-terminal status (`DRAFT`, `PUBLISHED`, `HAPPENING`). This action is irreversible. If the event was published and tickets were sold, a bulk refund process is triggered.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/cancel`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to cancel | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event cancelled successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
    "status": "CANCELLED"
  }
}
```

**Success Response Fields**: `data` is an [EventStatusResponse](#d-eventstatusresponse-lightweight-status-object) — **not** the full `EventResponse`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is already `CANCELLED` or `COMPLETED` |

---

## 19. Update Published Event — Basic Info

**Purpose**: Updates the description and/or CTA label of a published event. Title, category, and format changes are blocked on published events as the slug has already been shared publicly. **Banner updates are no longer part of this endpoint** — use [Endpoint 19a — Update Published Event — Banner](#19a-update-published-event--banner) instead.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/basic-info`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "description": "Updated: The biggest jazz event in East Africa. Now featuring 35 artists across 3 stages.",
  "ctaLabel": "Get Tickets Now"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `description` | string | No | Updated event description | Min: 15, Max: 5000 characters |
| `ctaLabel` | string | No | CTA button label override | Max: 50 characters. If provided and non-blank → saved as-is. If omitted or null → re-derived from current ticket pricing |

> **Excluded fields**: `title`, `categoryId`, and `eventFormat` are not present on this request at all (too breaking for a published event whose slug is already shared). `eventVisibility` and banner media are also not accepted here — banner updates now go through [Endpoint 19a](#19a-update-published-event--banner) as a separate call.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event info updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not in PUBLISHED status |
| `422` | Validation failure on provided fields |

---

## 19a. Update Published Event — Banner

**Purpose**: Replaces the banner image on a published event. Split out from the basic-info endpoint since the banner is a FileThunder-managed upload, not a plain string field.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/banner`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `bannerFileId` | UUID | Yes | ID of a file already uploaded via FileThunder to use as the new banner | Must reference an existing uploaded file |

> **Note**: Unlike every other update endpoint in this API, this one takes its input as a query parameter, not a JSON request body — there is no request body for this call.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event banner updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.bannerMedia` reflects the newly resolved banner.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or `bannerFileId` does not reference an existing file |
| `400` | Event is not in PUBLISHED status |

---

## 20. Update Published Event — Highlights

**Purpose**: Replaces the full list of highlights for a published event. Behavior is identical to the draft equivalent — sending an empty array clears all highlights.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/highlights`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 8 — Update Draft Highlights](#8-update-draft--highlights).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Highlights updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not PUBLISHED |

---

## 21. Update Published Event — FAQs

**Purpose**: Replaces the full list of FAQs for a published event. Sending an empty array clears all FAQs.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/faqs`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 9 — Update Draft FAQs](#9-update-draft--faqs).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "FAQs updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**: Same as Endpoint 20 (401, 403, 404, 400).

---

## 22. Update Published Event — Lineup

**Purpose**: Replaces the full event lineup on a published event. Supports the same platform user enrichment as the draft equivalent.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/lineup`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 10 — Update Draft Lineup](#10-update-draft--lineup).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Lineup updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or a `userId` in lineup does not exist |
| `400` | Event is not PUBLISHED |

---

## 22a. Update Check-in Settings

**Purpose**: Configures the check-in window for attendee entry — how early before start, and how late after start, attendees are allowed to check in.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/checkin-settings`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "checkInBeforeValue": 2,
  "checkInBeforeUnit": "HOURS",
  "checkInAfterValue": 30,
  "checkInAfterUnit": "MINUTES"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `checkInBeforeValue` | integer | Yes | How long before start check-in opens | Min: 1 |
| `checkInBeforeUnit` | string | Yes | Unit for `checkInBeforeValue` | Enum: `MINUTES`, `HOURS`, `DAYS` |
| `checkInAfterValue` | integer | Yes | How long after start check-in remains open | Min: 0 |
| `checkInAfterUnit` | string | Yes | Unit for `checkInAfterValue` | Enum: `MINUTES`, `HOURS`, `DAYS` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Check-in settings updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `422` | Missing/invalid check-in value or unit |

---

## 23. Update Published Event — Agenda

**Purpose**: Replaces the full event agenda on a published event. Supports the same platform user enrichment as the draft equivalent.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/agenda`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 11 — Update Draft Agenda](#11-update-draft--agenda).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agenda updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or a `presenterId` does not exist |
| `400` | Event is not PUBLISHED |

---

## 24. Reveal Location (TBA → Actual)

**Purpose**: Reveals the actual location for an event that was originally published with `eventFormat=TBA`. The new format cannot be `TBA`. Required fields depend on the new format chosen.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/reveal-location`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published TBA event | Must be a valid UUID |

**Request JSON Sample** (revealing as IN_PERSON):

```json
{
  "eventFormat": "IN_PERSON",
  "venue": {
    "name": "Mlimani City Arena",
    "address": "Sam Nujoma Road, Dar es Salaam",
    "coordinates": {
      "latitude": -6.7724,
      "longitude": 39.2083
    }
  }
}
```

**Request JSON Sample** (revealing as ONLINE):

```json
{
  "eventFormat": "ONLINE",
  "virtualDetails": {
    "meetingLink": "https://zoom.us/j/123456789",
    "meetingId": "123 456 789",
    "passcode": "jazz2025"
  }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventFormat` | string | Yes | The actual event format | Enum: `IN_PERSON`, `ONLINE`, `HYBRID` — **cannot be `TBA`** |
| `venue` | object | Conditional | Physical venue details | Required when new format is `IN_PERSON` or `HYBRID` |
| `venue.name` | string | Yes (if venue) | Venue name | Max: 200 characters |
| `venue.address` | string | No | Full address | Max: 500 characters |
| `venue.coordinates.latitude` | BigDecimal | No | GPS latitude | — |
| `venue.coordinates.longitude` | BigDecimal | No | GPS longitude | — |
| `virtualDetails` | object | Conditional | Virtual meeting details | Required when new format is `ONLINE` or `HYBRID` |
| `virtualDetails.meetingLink` | string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
| `virtualDetails.meetingId` | string | No | Platform meeting ID | Max: 100 characters |
| `virtualDetails.passcode` | string | No | Meeting passcode | Max: 100 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Location revealed successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not PUBLISHED, current format is not TBA, or new format is TBA |
| `422` | Missing required venue/virtual fields for the chosen format |

---

## 25. Get Event by ID

**Purpose**: Retrieves full event details. Published events are publicly accessible without authentication. Draft events can only be viewed by their organizer.

**Endpoint**: `GET` `/api/v1/e-events/{eventId}`

**Access Level**: 🌐 Public (for PUBLISHED events) | 🔒 Protected (for DRAFT events — organizer only)

**Authentication**: Bearer Token (required only for DRAFT access)

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Conditional | Required when accessing a DRAFT event |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to retrieve | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). **`data.virtualDetails` is always `null` on this endpoint** — it uses the public-response mapping, which strips meeting link/ID/passcode even when the requester is the event's own organizer. While the event is still `DRAFT`, virtual meeting details can be read via `GET /drafts/{draftId}` ([Endpoint 3](#3-get-draft-by-id)); there is currently no endpoint that returns `virtualDetails` for an already-`PUBLISHED` event, even to its organizer.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `403` | Attempting to access a DRAFT event without being its organizer |
| `404` | Event not found or is soft-deleted |

---

## 26. Get My Events

**Purpose**: Returns a paginated list of all events (any status) created by the authenticated organizer.

**Endpoint**: `GET` `/api/v1/e-events/my-events`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 12,
    "totalPages": 2
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects. Pagination follows [Standard Paginated Response Wrapper](#c-standard-paginated-response-wrapper).

**Possible Error Responses**: 401 — see [Shared Error: 401](#shared-error-response-examples).

---

## 27. Get My Events by Status

**Purpose**: Returns a paginated list of the authenticated organizer's events filtered by a specific status.

**Endpoint**: `GET` `/api/v1/e-events/my-events/status/{status}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `status` | string | Yes | Event status filter | Enum: `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 5,
    "totalPages": 1
  }
}
```

**Success Response Fields**: Same as Endpoint 26.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | Invalid `status` enum value |

---

## 28. Get Events Feed

**Purpose**: Returns a paginated list of all published events for the public discovery feed. Results are ordered by creation date descending. This endpoint will eventually incorporate a recommendation algorithm.

**Endpoint**: `GET` `/api/v1/e-events/events-feed`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events feed retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects — PUBLISHED only ]" ],
    "totalElements": 87,
    "totalPages": 9
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects (PUBLISHED status only).

**Possible Error Responses**: None expected (no authentication required).

---

## 28a. Get Past Events

**Purpose**: Returns a paginated list of `PUBLISHED` events whose end date/time has already passed (e.g., for an "Events you missed" / past-events archive view). Note that this is distinct from `COMPLETED` status — an event moves to `COMPLETED` via a separate scheduled job, so there can be a lag where a past-dated event still shows here as `PUBLISHED`.

**Endpoint**: `GET` `/api/v1/e-events/events-feed/past`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Past events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects — PUBLISHED, end date in the past ]" ],
    "totalElements": 23,
    "totalPages": 3
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects.

**Possible Error Responses**: None expected (no authentication required).

---

## 29. Search Events

**Purpose**: Full-text search across published event titles. Results are ordered by start date ascending.

**Endpoint**: `GET` `/api/v1/e-events/search`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | Yes | Search keyword(s) | Non-empty string | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Search results retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 4,
    "totalPages": 1
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects.

**Possible Error Responses**: None expected for normal queries (empty results return an empty `content` array, not a 404).

---

## 30. Filter Events by Date Range

**Purpose**: Returns published events whose schedule overlaps with the provided date range. An event is included if it starts before `endDate` AND ends after `startDate` (overlap logic, not exact range match).

**Endpoint**: `GET` `/api/v1/e-events/filter/date`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `startDate` | ZonedDateTime | Yes | Range start | ISO 8601 with offset (e.g., `2025-07-01T00:00:00+03:00`) | — |
| `endDate` | ZonedDateTime | Yes | Range end | ISO 8601 with offset; must be after `startDate` | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Filtered events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 7,
    "totalPages": 1
  }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `400` | `startDate` is after `endDate` |

---

## 31. Search and Filter Events (Combined)

**Purpose**: Combines keyword search with optional date range filtering in a single call. All parameters are optional — calling with no parameters is equivalent to getting the full published feed.

**Endpoint**: `GET` `/api/v1/e-events/filter`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | No | Title keyword search | — | — |
| `startDate` | ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
| `endDate` | ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after `startDate` if both provided | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `400` | `startDate` is after `endDate` (when both are provided) |

---

## 32. Search My Events (Organizer)

**Purpose**: Allows an organizer to search and filter within their own events across all statuses. Supports keyword, status filter, and date range simultaneously.

**Endpoint**: `GET` `/api/v1/e-events/my-events/search`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | No | Keyword search on title | — | — |
| `status` | string | No | Filter by status | Enum: `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` | — |
| `startDate` | ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
| `endDate` | ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after `startDate` if both provided | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | `startDate` is after `endDate` |

---

## 33. Get Event OG Meta by Slug

**Purpose**: Returns lightweight Open Graph metadata for a published event, for use by link-preview crawlers (social media unfurling, messaging apps) when someone shares an event URL.

**Endpoint**: `GET` `/api/v1/e-events/slug/{slug}/og-meta`

**Access Level**: 🌐 Public

**Authentication**: None required

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `slug` | string | Yes | The event's URL slug | Must match an existing event |

> **⚠️ Non-standard response shape**: Unlike every other endpoint in this API, this one returns the `EventOGMetaResponse` object **directly** as the HTTP body — it is **not** wrapped in the standard `{ success, httpStatus, message, action_time, data }` envelope described at the top of this document. Build OG-meta consumers accordingly.

**Success Response JSON Sample**:

```json
{
  "title": "Dar es Salaam Jazz Festival 2025",
  "eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "description": "An annual celebration of jazz and live music at the heart of the city.",
  "banner": "https://cdn.example.com/ft/6b1a2c3d/medium.jpg",
  "startDateTime": "2025-07-18T18:00:00+03:00",
  "locationSummary": "Dar es Salaam, TZ"
}
```

**Success Response Fields**:

| Field | Type | Description |
|-------|------|--------------|
| `title` | string | Event title |
| `eventId` | UUID | Event ID |
| `description` | string | Full event description |
| `banner` | string \| null | Resolved banner image URL (single string, not the full `bannerMedia` object) |
| `startDateTime` | ZonedDateTime | Event start date/time with offset |
| `locationSummary` | string | Human-readable location summary |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `404` | No event found with the given slug |

---

## Data Format Standards

| Concern | Standard |
|---------|----------|
| Timestamps | ISO 8601 with timezone offset: `2025-07-18T18:00:00+03:00` |
| Dates | `YYYY-MM-DD` format: `2025-07-18` |
| Times | `HH:mm:ss` (24-hour): `18:00:00` |
| IDs | UUID v4: `3fa85f64-5717-4562-b3fc-2c963f66afa6` |
| Pagination | 1-based `page` query parameter, Spring `Page` wrapper in response |
| Prices | `BigDecimal` with 2 decimal places; `0.00` for free |
| Enums | Uppercase strings as defined (e.g., `IN_PERSON`, `PUBLISHED`) |

# Ticket Management API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-02-20
**Version**: v1.2

**Base URL**: `https://your-api-domain.com/api/v1/e-events/tickets`

**Short Description**: The Ticket Management API allows event organizers to define and manage ticket types for their events on NextGate. Organizers can create multiple ticket tiers (e.g. VIP, Early Bird, General Admission), control pricing, capacity, sales periods, visibility, and sales channels. Tickets are scoped to a specific event and follow the event's lifecycle from DRAFT through to PUBLISHED.

**Hints**:
- Tickets can be created while the event is in **DRAFT or PUBLISHED** status. Full edits (name, price, type, etc.) are only allowed in **DRAFT**. After publishing, use the dedicated published-ticket endpoint for limited updates.
- For **HYBRID** events, you must create at least one `IN_PERSON` ticket and one `ONLINE` ticket before the event can be published.
- Ticket names must be unique per event per attendance mode — you cannot have two `IN_PERSON` tickets both named "VIP Pass" on the same event.
- **DONATION** tickets are restricted to `ONLINE_ONLY` sales channel and a maximum of 1 ticket per order and per user. They have no fixed price — the buyer freely chooses their donation amount at checkout.
- The ticket sales window must fall **within the event's registration window**. Sales cannot start before registration opens or end after registration closes.
- The minimum gap between `salesStartDateTime` and `salesEndDateTime` is **30 minutes**.
- Soft deletion is used — a ticket can only be deleted if zero tickets have been sold. Otherwise, close it using the status endpoint.
- All datetimes must be in **ISO 8601 / ZonedDateTime format** (e.g. `2025-08-10T09:00:00+03:00`).

---

## User Journey

```
  [Organizer creates event in DRAFT status]
                    |
                    | (Event must be DRAFT or PUBLISHED for ticket work)
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   TICKET SETUP PHASE (DRAFT or PUBLISHED)  .
  .                                             .
  .   [Create ticket types]                     .
  .      |-- General Admission (PAID)           .
  .      |-- VIP Pass (PAID)                    .
  .      |-- Student Discount (PAID)            .
  .      |-- Free Entry (FREE)                  .
  .      '-- Support the Artist (DONATION)      .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
                    |
                    v
  [Review all ticket types via Get All Tickets]
                    |
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   ADJUSTMENTS PHASE (DRAFT only)            .
  .                                             .
  .   Need to fix details?                      .
  .      --> Update Ticket (name, price, etc.)  .
  .                                             .
  .   Wrong capacity?                           .
  .      --> Update Ticket Capacity             .
  .                                             .
  .   Ticket no longer needed?                  .
  .      --> Delete Ticket (only if 0 sold)     .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
                    |
                    v
  [Event published — tickets go live for buyers]
                    |
                    v
  . . . . . . . . . . . . . . . . . . . . . . .
  .                                             .
  .   LIVE EVENT PHASE (PUBLISHED)              .
  .                                             .
  .   Need a new ticket tier?                   .
  .      --> Create Ticket (allowed on PUBLISHED).
  .                                             .
  .   Need to adjust visibility/status?         .
  .      --> Update Published Ticket            .
  .                                             .
  .   Need to shift the sales window?           .
  .      --> Update Sales Window                .
  .                                             .
  .   Ticket sells out?                         .
  .      --> System auto-sets SOLD_OUT          .
  .      --> Organizer can increase capacity    .
  .          to reactivate it                   .
  .                                             .
  .   Want to pause sales temporarily?          .
  .      --> Update Status to INACTIVE          .
  .                                             .
  .   Want to stop sales permanently?           .
  .      --> Update Status to CLOSED            .
  .                                             .
  . . . . . . . . . . . . . . . . . . . . . . .
```

---

## Sales Window Rules

Ticket sales must respect three nested time windows:

```
Event:          [eventStartDateTime ─────────────── eventEndDateTime]
Registration:         [registrationOpensAt ──── registrationClosesAt]
Ticket Sales:              [salesStartDateTime ── salesEndDateTime]
```

**Rules enforced:**
- `salesStartDateTime` cannot be in the past
- `salesEndDateTime` cannot be in the past
- `salesEndDateTime` must be after `salesStartDateTime`
- Minimum gap between sales start and end is **30 minutes**
- `salesStartDateTime` must be on or after `registrationOpensAt`
- `salesStartDateTime` cannot be after `registrationClosesAt`
- `salesEndDateTime` cannot be after `registrationClosesAt`
- Neither date can be after `eventEndDateTime`

---

## Standard Response Format

All API responses follow a consistent structure:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
| `message` | string | Human-readable description of the operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload on success; error detail on failure |

---

## HTTP Method Badge Standards

- **GET** — Green (`#28a745`) — Safe, read-only operations
- **POST** — Blue (`#007bff`) — Create new resources
- **PUT** — Yellow (`#ffc107`, black text) — Replace entire resource
- **PATCH** — Orange (`#fd7e14`) — Partial update
- **DELETE** — Red (`#dc3545`) — Remove resource

---

## Enum Reference

### TicketPricingType
| Value | Description |
|-------|-------------|
| `PAID` | Standard paid ticket. Price must be greater than `0.00` |
| `FREE` | Free entry. Price must be exactly `0.00` |
| `DONATION` | Supporter ticket. No fixed price — buyer freely enters their donation amount at checkout. Restricted to `ONLINE_ONLY` channel, max 1 per order and per user. `price` is `null` in responses |

### SalesChannel
| Value | Description |
|-------|-------------|
| `EVERYWHERE` | Available both online and at the door |
| `ONLINE_ONLY` | Available for purchase online only |
| `AT_DOOR_ONLY` | Available for purchase at the venue door only |

### AttendanceMode
| Value | Description |
|-------|-------------|
| `IN_PERSON` | Ticket grants physical entry to the venue |
| `ONLINE` | Ticket grants access to the online/virtual stream |

> For `IN_PERSON` events, only `IN_PERSON` tickets are allowed. For `ONLINE` events, only `ONLINE` tickets are allowed. For `HYBRID` events, both are permitted and at least one of each is required before publishing.

### TicketVisibility
| Value | Description |
|-------|-------------|
| `VISIBLE` | Always shown to the public |
| `HIDDEN` | Never shown to buyers (organizer use only) |
| `HIDDEN_WHEN_NOT_ON_SALE` | Only visible while the ticket is actively on sale |
| `CUSTOM_SCHEDULE` | Shown only within a defined date/time window. Requires `visibilityStartDate` and `visibilityEndDate` |

### TicketStatus
| Value | Description |
|-------|-------------|
| `ACTIVE` | Ticket is live and available for purchase |
| `INACTIVE` | Temporarily paused. Organizer can reactivate |
| `CLOSED` | Permanently stopped. Cannot be reopened |
| `SOLD_OUT` | System-managed. Set automatically when `ticketsSold >= totalTickets`. Reverts to `ACTIVE` if capacity is increased |
| `DELETED` | Soft-deleted. Only possible if zero tickets were sold. Use the Delete endpoint — cannot be set via status update |

---

## Endpoints

---

## 1. Create Ticket

**Purpose**: Creates a new ticket type for a specific event. The event must be in `DRAFT` or `PUBLISHED` status. The authenticated user must be the event organizer.

**Endpoint**: `POST` `{base_url}/{eventId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to add the ticket to | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `name` | string | Yes | Ticket name (e.g. "VIP Pass", "Early Bird") | Min: 2, Max: 100 characters. Must be unique per event per attendance mode |
| `description` | string | No | Optional description of what the ticket includes | Max: 500 characters |
| `price` | decimal | Conditional | Ticket price. Use `0.00` for FREE tickets. Omit or send `0.00` for DONATION tickets | Min: 0.00. PAID → must be > 0.00. FREE → must be 0.00. DONATION → ignored |
| `ticketPricingType` | string | Yes | Pricing model | Enum: `PAID`, `FREE`, `DONATION` |
| `salesChannel` | string | Yes | Where the ticket can be purchased. DONATION must use `ONLINE_ONLY` | Enum: `EVERYWHERE`, `ONLINE_ONLY`, `AT_DOOR_ONLY`. Defaults to `EVERYWHERE` |
| `totalQuantity` | integer | Yes | Total number of tickets available | Min: 1, Max: 1,000,000 |
| `salesStartDateTime` | datetime | No | When sales open. Must be within registration window | ISO 8601 ZonedDateTime. Cannot be before `registrationOpensAt` or after `registrationClosesAt` |
| `salesEndDateTime` | datetime | No | When sales close. Must be within registration window | ISO 8601 ZonedDateTime. Must be after `salesStartDateTime` with at least 30 minutes gap |
| `minQuantityPerOrder` | integer | No | Minimum tickets per order | Min: 1. Defaults to 1 |
| `maxQuantityPerOrder` | integer | No | Maximum tickets per order. DONATION tickets must be 1 | Min: 1, Max: 100. Must be ≥ `minQuantityPerOrder` |
| `maxQuantityPerUser` | integer | No | Maximum tickets a single user can purchase across all orders. DONATION tickets must be 1 | Min: 1, Max: 1000. Must be ≥ `maxQuantityPerOrder` |
| `visibility` | string | Yes | Controls whether the ticket is shown to buyers | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE`. Defaults to `VISIBLE` |
| `visibilityStartDate` | datetime | No | When the ticket becomes visible. Required if `visibility` is `CUSTOM_SCHEDULE` | ISO 8601 ZonedDateTime |
| `visibilityEndDate` | datetime | No | When the ticket stops being visible. Required if `visibility` is `CUSTOM_SCHEDULE` | ISO 8601 ZonedDateTime. Must be after `visibilityStartDate` |
| `attendanceMode` | string | Yes | Whether this ticket is for physical or online attendance | Enum: `IN_PERSON`, `ONLINE`. Must match the event format |
| `inclusiveItems` | array of strings | No | List of perks included with this ticket (e.g. "Free T-Shirt", "Meet & Greet") | Max: 50 items. Each item: max 200 characters, cannot be blank |

**Request JSON Sample (PAID)**:
```json
{
  "name": "VIP Pass",
  "description": "Full weekend access with backstage entry and a complimentary gift bag.",
  "price": 150.00,
  "ticketPricingType": "PAID",
  "salesChannel": "EVERYWHERE",
  "totalQuantity": 200,
  "salesStartDateTime": "2026-03-18T08:00:00+03:00",
  "salesEndDateTime": "2026-04-17T23:59:00+03:00",
  "minQuantityPerOrder": 1,
  "maxQuantityPerOrder": 4,
  "maxQuantityPerUser": 4,
  "visibility": "VISIBLE",
  "attendanceMode": "IN_PERSON",
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating"
  ]
}
```

**Request JSON Sample (DONATION)**:
```json
{
  "name": "Support the Artist",
  "description": "Show your support — donate any amount you choose at checkout.",
  "price": 0.00,
  "ticketPricingType": "DONATION",
  "salesChannel": "ONLINE_ONLY",
  "totalQuantity": 500,
  "salesStartDateTime": "2026-03-18T08:00:00+03:00",
  "salesEndDateTime": "2026-04-17T23:59:00+03:00",
  "minQuantityPerOrder": 1,
  "maxQuantityPerOrder": 1,
  "maxQuantityPerUser": 1,
  "visibility": "VISIBLE",
  "attendanceMode": "IN_PERSON"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Ticket created successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 0,
    "ticketsRemaining": 200,
    "ticketsAvailable": 200,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": false,
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "VISIBLE",
    "visibilityStartDate": null,
    "visibilityEndDate": null,
    "isCurrentlyVisible": true,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating"
    ],
    "status": "ACTIVE",
    "saleStatusMessage": "On sale until Apr 17, 2026",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": null,
    "createdBy": "john_organizer",
    "updatedBy": null
  }
}
```

> **Note on DONATION response**: For `DONATION` tickets, the `price` field is `null` in the response. The buyer enters their own amount at checkout.

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `id` | Unique identifier for this ticket type |
| `eventId` | The event this ticket belongs to |
| `name` | Ticket name |
| `description` | Ticket description |
| `price` | Ticket price. `null` for DONATION tickets |
| `ticketPricingType` | Pricing model: `PAID`, `FREE`, or `DONATION` |
| `salesChannel` | Where the ticket can be purchased |
| `totalTickets` | Total number of tickets created |
| `ticketsSold` | Number of tickets purchased so far |
| `ticketsRemaining` | `totalTickets - ticketsSold` |
| `ticketsAvailable` | Same as `ticketsRemaining` |
| `isSoldOut` | `true` if `ticketsSold >= totalTickets` |
| `salesStartDateTime` | When ticket sales open |
| `salesEndDateTime` | When ticket sales close |
| `isOnSale` | `true` if ticket is ACTIVE and currently within the sales window |
| `saleStatusMessage` | Human-readable message describing the current sale state (e.g. `"On sale until Apr 17, 2026"`, `"Sales start Mar 18, 2026"`, `"Sales ended"`, `"Sold out"`) |
| `minQuantityPerOrder` | Minimum per order |
| `maxQuantityPerOrder` | Maximum per order (`null` = no limit) |
| `maxQuantityPerUser` | Maximum per user across all orders (`null` = no limit) |
| `visibility` | Visibility setting |
| `visibilityStartDate` | Start of custom visibility window |
| `visibilityEndDate` | End of custom visibility window |
| `isCurrentlyVisible` | Whether the ticket is currently visible to buyers |
| `attendanceMode` | `IN_PERSON` or `ONLINE` |
| `inclusiveItems` | List of perks included with the ticket |
| `status` | Ticket status: `ACTIVE`, `INACTIVE`, `CLOSED`, `SOLD_OUT`, `DELETED` |
| `createdAt` | Timestamp when the ticket was created |
| `updatedAt` | Timestamp of last update (`null` if never updated) |
| `createdBy` | Username of the organizer who created the ticket |
| `updatedBy` | Username of last person who updated the ticket |

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Authenticated user is not the event organizer |
| `404` | Event not found |
| `400` | Event is not in DRAFT or PUBLISHED status |
| `400` | Ticket name already exists for this event and attendance mode |
| `422` | Validation errors (missing required fields, invalid price, sales window outside registration window, gap less than 30 minutes, etc.) |

**Error Response Examples**:

*Event in invalid status (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Tickets can only be created for DRAFT or PUBLISHED events. Current status: CANCELLED",
  "action_time": "2025-02-18T10:30:45",
  "data": "Tickets can only be created for DRAFT or PUBLISHED events. Current status: CANCELLED"
}
```

*Duplicate Ticket Name (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "A ticket with name 'VIP Pass' and attendance mode 'IN_PERSON' already exists for this event",
  "action_time": "2025-02-18T10:30:45",
  "data": "A ticket with name 'VIP Pass' and attendance mode 'IN_PERSON' already exists for this event"
}
```

---

## 2. Update Ticket

**Purpose**: Updates the full details of an existing ticket type. The event must still be in `DRAFT` status. All fields are optional — only the fields you send will be updated.

**Endpoint**: `PUT` `{base_url}/{ticketId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The ticket to update | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `name` | string | No | Updated ticket name | Min: 2, Max: 100 characters. Must remain unique per event per attendance mode |
| `description` | string | No | Updated description | Max: 500 characters |
| `price` | decimal | No | Updated price. Ignored for DONATION tickets | Min: 0.00. Must be consistent with `ticketPricingType` |
| `ticketPricingType` | string | No | Updated pricing model | Enum: `PAID`, `FREE`, `DONATION` |
| `salesChannel` | string | No | Updated sales channel. DONATION tickets must be `ONLINE_ONLY` | Enum: `EVERYWHERE`, `ONLINE_ONLY`, `AT_DOOR_ONLY` |
| `totalQuantity` | integer | No | Updated total capacity | Min: 1, Max: 1,000,000 |
| `salesStartDateTime` | datetime | No | Updated sales open time | ISO 8601 ZonedDateTime. Cannot be before `registrationOpensAt` |
| `salesEndDateTime` | datetime | No | Updated sales close time | ISO 8601 ZonedDateTime. At least 30 minutes after `salesStartDateTime` |
| `minQuantityPerOrder` | integer | No | Updated minimum per order | Min: 1 |
| `maxQuantityPerOrder` | integer | No | Updated maximum per order. DONATION must be 1 | Min: 1, Max: 100 |
| `maxQuantityPerUser` | integer | No | Updated maximum per user. DONATION must be 1 | Min: 1, Max: 1000 |
| `attendanceMode` | string | No | Updated attendance mode | Enum: `IN_PERSON`, `ONLINE`. Must match event format rules |
| `inclusiveItems` | array of strings | No | Full replacement list of inclusive perks | Max: 50 items. Each: max 200 characters, cannot be blank |
| `visibility` | string | No | Updated visibility | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE` |
| `visibilityStartDate` | datetime | No | Updated visibility start | Required if changing to `CUSTOM_SCHEDULE` |
| `visibilityEndDate` | datetime | No | Updated visibility end | Required if changing to `CUSTOM_SCHEDULE`. Must be after start |

**Request JSON Sample**:
```json
{
  "name": "VIP Weekend Pass",
  "price": 175.00,
  "maxQuantityPerOrder": 2,
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating",
    "Artist meet & greet"
  ]
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket updated successfully",
  "action_time": "2025-02-18T11:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Weekend Pass",
    "price": 175.00,
    "maxQuantityPerOrder": 2,
    "status": "ACTIVE",
    "updatedAt": "2025-02-18T11:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Success Response Fields**: Same as [Create Ticket response fields](#1-create-ticket).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Event is not in DRAFT status |
| `400` | Updated name conflicts with an existing ticket on the same event |
| `422` | Validation errors (invalid price, quantity inconsistencies, sales window violations, etc.) |

---

## 3. Get All Tickets for Event

**Purpose**: Retrieves a lightweight summary list of all active (non-deleted) tickets for a given event, ordered by creation date ascending.

**Endpoint**: `GET` `{base_url}/{eventId}`

**Access Level**: 🌐 Public

**Authentication**: None required

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to retrieve tickets for | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Tickets retrieved successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "General Admission",
      "price": 25.00,
      "ticketPricingType": "PAID",
      "salesChannel": "EVERYWHERE",
      "visibility": "VISIBLE",
      "totalTickets": 1000,
      "ticketsSold": 342,
      "ticketsAvailable": 658,
      "isSoldOut": false,
      "attendanceMode": "IN_PERSON",
      "status": "ACTIVE",
      "isOnSale": true
    },
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "VIP Pass",
      "price": 150.00,
      "ticketPricingType": "PAID",
      "salesChannel": "EVERYWHERE",
      "visibility": "VISIBLE",
      "totalTickets": 200,
      "ticketsSold": 200,
      "ticketsAvailable": 0,
      "isSoldOut": true,
      "attendanceMode": "IN_PERSON",
      "status": "SOLD_OUT",
      "isOnSale": false
    }
  ]
}
```

**Success Response Fields** (per item):
| Field | Description |
|-------|-------------|
| `id` | Unique identifier for the ticket type |
| `name` | Ticket name |
| `price` | Ticket price. `null` for DONATION tickets |
| `ticketPricingType` | Pricing model |
| `salesChannel` | Where it can be purchased |
| `visibility` | Visibility setting |
| `totalTickets` | Total quantity created |
| `ticketsSold` | Quantity sold so far |
| `ticketsAvailable` | Quantity still available |
| `isSoldOut` | Whether the ticket is sold out |
| `attendanceMode` | `IN_PERSON` or `ONLINE` |
| `status` | Current ticket status |
| `isOnSale` | Whether the ticket is currently purchasable |
| `saleStatusMessage` | Human-readable message describing the current sale state |

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `404` | Event not found |

---

## 4. Get Ticket by ID

**Purpose**: Retrieves the full details of a single ticket type by its ID.

**Endpoint**: `GET` `{base_url}/{eventId}/{ticketId}`

**Access Level**: 🌐 Public

**Authentication**: None required

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event the ticket belongs to | Must be a valid UUID |
| `ticketId` | UUID | Yes | The specific ticket to retrieve | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket retrieved successfully",
  "action_time": "2025-02-18T10:30:45",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 45,
    "ticketsRemaining": 155,
    "ticketsAvailable": 155,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": true,
    "saleStatusMessage": "On sale until Apr 17, 2026",
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "VISIBLE",
    "visibilityStartDate": null,
    "visibilityEndDate": null,
    "isCurrentlyVisible": true,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating"
    ],
    "status": "ACTIVE",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": "2025-02-18T11:00:00+03:00",
    "createdBy": "john_organizer",
    "updatedBy": "john_organizer"
  }
}
```

**Success Response Fields**: Same as [Create Ticket response fields](#1-create-ticket).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `404` | Event not found or ticket not found |

---

## 5. Update Ticket Capacity

**Purpose**: Updates the total quantity (capacity) of a ticket. Allowed on both DRAFT and PUBLISHED events. The new capacity must be greater than or equal to the number of tickets already sold.

**Endpoint**: `PATCH` `{base_url}/{eventId}/{ticketId}/capacity`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to update capacity for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `newTotalQuantity` | integer | Yes | The new total capacity | Min: 1, Max: 1,000,000. Must be ≥ `ticketsSold` |

**Request JSON Sample**:
```json
{
  "newTotalQuantity": 300
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket capacity updated successfully",
  "action_time": "2025-02-18T12:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Pass",
    "totalTickets": 300,
    "ticketsSold": 200,
    "ticketsRemaining": 100,
    "ticketsAvailable": 100,
    "isSoldOut": false,
    "status": "ACTIVE",
    "updatedAt": "2025-02-18T12:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

> **Note:** If a ticket was previously `SOLD_OUT` and the new capacity exceeds tickets sold, the status is automatically reset to `ACTIVE`.

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | New capacity is less than the number of tickets already sold |
| `422` | `newTotalQuantity` is missing or below minimum |

*Capacity Below Sold Count (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot reduce capacity to 100 because 200 tickets have already been sold",
  "action_time": "2025-02-18T12:00:00",
  "data": "Cannot reduce capacity to 100 because 200 tickets have already been sold"
}
```

---

## 6. Update Ticket Status

**Purpose**: Manually changes the status of a ticket type. Use this to pause sales (`INACTIVE`), permanently stop sales (`CLOSED`), or reactivate a paused ticket (`ACTIVE`). Works on both DRAFT and PUBLISHED events. The system automatically manages `SOLD_OUT` status — it cannot be set manually.

**Endpoint**: `PATCH` `{base_url}/{eventId}/{ticketId}/status`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to update status for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `status` | string | Yes | The new status to set | Enum: `ACTIVE`, `INACTIVE`, `CLOSED`. Cannot set `SOLD_OUT` or `DELETED` manually |

**Status Transition Rules**:
| Current Status | Allowed Transitions | Notes |
|----------------|---------------------|-------|
| `ACTIVE` | `INACTIVE`, `CLOSED` | Normal operations |
| `INACTIVE` | `ACTIVE`, `CLOSED` | Can be reactivated |
| `SOLD_OUT` | `ACTIVE`, `CLOSED` | `ACTIVE` only if capacity was increased first |
| `CLOSED` | None | Permanent. Cannot be changed |
| `DELETED` | None | Permanent. Cannot be changed |

**Request JSON Sample**:
```json
{
  "status": "INACTIVE"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket status updated successfully",
  "action_time": "2025-02-18T13:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "VIP Pass",
    "status": "INACTIVE",
    "isOnSale": false,
    "updatedAt": "2025-02-18T13:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Attempted to set `SOLD_OUT` or `DELETED` manually |
| `400` | Attempted to change status of a `CLOSED` or `DELETED` ticket |
| `422` | `status` field is missing |

---

## 7. Delete Ticket

**Purpose**: Soft-deletes a ticket type. The ticket is marked as deleted and hidden from all listings. **Deletion is only allowed if zero tickets have been sold.** If tickets have already been sold, close the ticket using the status endpoint instead.

**Endpoint**: `DELETE` `{base_url}/{eventId}/{ticketId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `ticketId` | UUID | Yes | The ticket to delete | Must be a valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket deleted successfully",
  "action_time": "2025-02-18T14:00:00",
  "data": null
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Ticket cannot be deleted because tickets have already been sold |

*Tickets Already Sold (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot delete ticket 'VIP Pass' because 45 tickets have been sold. You can close the ticket instead to stop sales.",
  "action_time": "2025-02-18T14:00:00",
  "data": "Cannot delete ticket 'VIP Pass' because 45 tickets have been sold. You can close the ticket instead to stop sales."
}
```

---

## 8. Update Sales Window

**Purpose**: Updates the sales start and/or end datetime of a ticket on a PUBLISHED event. Both fields are optional — omitting one preserves its current value. All existing sales period rules apply (30-minute minimum gap, must fall within registration window, etc.).

**Endpoint**: `PATCH` `{base_url}/{ticketId}/sales-window`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The ticket to update the sales window for | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `salesStartDateTime` | datetime | No | New sales open time | ISO 8601 ZonedDateTime. Must be on or after `registrationOpensAt`. Cannot be after `registrationClosesAt` |
| `salesEndDateTime` | datetime | No | New sales close time | ISO 8601 ZonedDateTime. Must be after `salesStartDateTime` with at least 30 minutes gap. Cannot be after `registrationClosesAt` |

> **At least one of `salesStartDateTime` or `salesEndDateTime` must be provided.**

**Request JSON Sample**:
```json
{
  "salesEndDateTime": "2026-04-25T23:59:00+03:00"
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket sales window updated successfully",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-25T20:59:00Z",
    "isOnSale": true,
    "status": "ACTIVE",
    "updatedAt": "2026-02-20T10:00:00+03:00",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Ticket is deleted or closed |
| `422` | Sales window violates registration window, gap less than 30 minutes, or no fields provided |

*Sales Window Outside Registration Window (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Sales end date cannot be after registration closes (2026-04-17T20:59:00Z)",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "stage": "TICKETS",
    "message": "Sales end date cannot be after registration closes (2026-04-17T20:59:00Z)"
  }
}
```

---

## 9. Update Published Ticket

**Purpose**: Performs a limited update on a ticket belonging to a PUBLISHED event. Only three fields are allowed: `visibility` (and its schedule dates), `status` (ACTIVE, INACTIVE, or CLOSED), and `inclusiveItems`. All other fields must be updated while the event is still in DRAFT.

**Endpoint**: `PATCH` `{base_url}/{ticketId}/published`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketId` | UUID | Yes | The published ticket to update | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `visibility` | string | No | Updated visibility setting | Enum: `VISIBLE`, `HIDDEN`, `HIDDEN_WHEN_NOT_ON_SALE`, `CUSTOM_SCHEDULE` |
| `visibilityStartDate` | datetime | No | Start of custom visibility window | Required if `visibility` is `CUSTOM_SCHEDULE`. ISO 8601 ZonedDateTime |
| `visibilityEndDate` | datetime | No | End of custom visibility window | Required if `visibility` is `CUSTOM_SCHEDULE`. Must be after `visibilityStartDate`. ISO 8601 ZonedDateTime |
| `status` | string | No | Updated ticket status | Enum: `ACTIVE`, `INACTIVE`, `CLOSED`. Cannot set `SOLD_OUT` or `DELETED` |
| `inclusiveItems` | array of strings | No | Full replacement list of perks | Max: 50 items. Each: max 200 characters, cannot be blank |

> **This endpoint is specifically for PUBLISHED events.** For DRAFT events, use the full `PUT /{ticketId}` endpoint instead.

**Request JSON Sample**:
```json
{
  "visibility": "CUSTOM_SCHEDULE",
  "visibilityStartDate": "2026-03-01T00:00:00+03:00",
  "visibilityEndDate": "2026-04-17T23:59:00+03:00",
  "inclusiveItems": [
    "Backstage access",
    "Complimentary gift bag",
    "Priority seating",
    "Artist meet & greet",
    "Exclusive after-party entry"
  ]
}
```

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Ticket updated successfully",
  "action_time": "2026-02-20T10:00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "eventId": "f1e2d3c4-b5a6-7890-fedc-ba9876543210",
    "name": "VIP Pass",
    "description": "Full weekend access with backstage entry and a complimentary gift bag.",
    "price": 150.00,
    "ticketPricingType": "PAID",
    "salesChannel": "EVERYWHERE",
    "totalTickets": 200,
    "ticketsSold": 87,
    "ticketsRemaining": 113,
    "ticketsAvailable": 113,
    "isSoldOut": false,
    "salesStartDateTime": "2026-03-18T05:00:00Z",
    "salesEndDateTime": "2026-04-17T20:59:00Z",
    "isOnSale": true,
    "minQuantityPerOrder": 1,
    "maxQuantityPerOrder": 4,
    "maxQuantityPerUser": 4,
    "visibility": "CUSTOM_SCHEDULE",
    "visibilityStartDate": "2026-03-01T00:00:00+03:00",
    "visibilityEndDate": "2026-04-17T23:59:00+03:00",
    "isCurrentlyVisible": false,
    "attendanceMode": "IN_PERSON",
    "inclusiveItems": [
      "Backstage access",
      "Complimentary gift bag",
      "Priority seating",
      "Artist meet & greet",
      "Exclusive after-party entry"
    ],
    "status": "ACTIVE",
    "createdAt": "2025-02-18T10:30:45+03:00",
    "updatedAt": "2026-02-20T10:00:00+03:00",
    "createdBy": "john_organizer",
    "updatedBy": "john_organizer"
  }
}
```

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Ticket not found |
| `400` | Event is not in PUBLISHED status — use the draft update endpoint instead |
| `400` | Ticket is deleted |
| `422` | Invalid visibility schedule dates, attempted to set `SOLD_OUT` or `DELETED`, or `CUSTOM_SCHEDULE` missing required date fields |

*Event Not Published (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This endpoint is only for published events. Use the draft ticket update endpoint instead.",
  "action_time": "2026-02-20T10:00:00",
  "data": "This endpoint is only for published events. Use the draft ticket update endpoint instead."
}
```

---

## Quick Reference

### Endpoint Summary
| # | Method | Path | Description | Auth | Event Status |
|---|--------|------|-------------|------|--------------|
| 1 | POST | `/{eventId}` | Create a ticket type | 🔒 Organizer | DRAFT or PUBLISHED |
| 2 | PUT | `/{ticketId}` | Full update of ticket details | 🔒 Organizer | DRAFT only |
| 3 | GET | `/{eventId}` | Get all tickets for an event | 🌐 Public | Any |
| 4 | GET | `/{eventId}/{ticketId}` | Get a single ticket by ID | 🌐 Public | Any |
| 5 | PATCH | `/{eventId}/{ticketId}/capacity` | Update ticket capacity | 🔒 Organizer | DRAFT or PUBLISHED |
| 6 | PATCH | `/{eventId}/{ticketId}/status` | Update ticket status | 🔒 Organizer | DRAFT or PUBLISHED |
| 7 | DELETE | `/{eventId}/{ticketId}` | Delete a ticket | 🔒 Organizer | DRAFT or PUBLISHED (0 sold) |
| 8 | PATCH | `/{ticketId}/sales-window` | Update sales window dates | 🔒 Organizer | PUBLISHED |
| 9 | PATCH | `/{ticketId}/published` | Limited update (visibility, status, perks) | 🔒 Organizer | PUBLISHED only |

### Common HTTP Status Codes
| Code | Meaning |
|------|---------|
| `200 OK` | Successful GET, PATCH, PUT, or DELETE |
| `201 Created` | Successful POST (ticket created) |
| `400 Bad Request` | Business rule violated (wrong status, already sold, duplicate name) |
| `401 Unauthorized` | Missing or invalid token |
| `403 Forbidden` | Authenticated but not the event organizer |
| `404 Not Found` | Event or ticket does not exist |
| `422 Unprocessable Entity` | Validation errors on request fields |

### Business Rule Cheat Sheet
| Rule | Detail |
|------|--------|
| Create ticket | Allowed on DRAFT and PUBLISHED events |
| Full update (`PUT`) | DRAFT events only |
| Published update (`PATCH /published`) | PUBLISHED events only. Visibility, status, and inclusiveItems only |
| Sales window update | PUBLISHED events. Validates against registration window |
| Capacity update | DRAFT and PUBLISHED events |
| Status update | DRAFT and PUBLISHED events |
| FREE ticket | Price must be exactly `0.00` |
| PAID ticket | Price must be greater than `0.00` |
| DONATION ticket | `ONLINE_ONLY` channel. Max 1 per order and per user. No fixed price — buyer sets amount at checkout. `price` is `null` in response |
| Sales window | Must fall within the event's registration window |
| Sales period gap | Minimum 30 minutes between `salesStartDateTime` and `salesEndDateTime` |
| HYBRID event | Must have ≥ 1 `IN_PERSON` ticket and ≥ 1 `ONLINE` ticket to publish |
| Delete | Only allowed if `ticketsSold = 0` |
| SOLD_OUT | System-managed. Cannot be set manually. Auto-cleared when capacity is increased |
| CLOSED | Permanent. Cannot be reversed |

# Event Checkout & Payment API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-23
**Version**: v2.0

**Base URL**: `https://api.nextgate.co.tz/api/v1`

**Short Description**: The NextGate Checkout API manages the complete ticket purchasing lifecycle on the NextGate event platform. It supports two distinct checkout flows: online checkout for registered attendees and at-door ticket sales for event organizers and scanner devices. This API should be used by frontend clients, mobile applications, and authorized scanner hardware integrations.

**Hints**:
- All monetary values are in **TZS (Tanzanian Shilling)** unless otherwise stated
- Checkout sessions expire after **15 minutes** for online checkout and **1 hour** for at-door sales — always check `expiresAt` before processing payment
- Bearer token authentication is required for all endpoints
- **Wallet balance is validated at session creation time for PAID tickets** — if insufficient, the session is never created and a structured `422` response is returned with `shortfall` and `recommendedTopUp` so the frontend can direct the user to top up immediately
- For FREE tickets, payment is auto-processed immediately upon session creation — no separate payment step is needed
- For DONATION tickets, maximum 1 ticket per order and cannot be purchased for other attendees
- Ticket holds are applied immediately on session creation; cancelling the session releases the hold
- Scanner devices must have the `SELL_TICKETS` permission assigned and a valid `deviceFingerprint` registered before calling the scanner sale endpoint

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": {}
}
```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Insufficient Balance Error Structure (422)

Returned when wallet balance is insufficient **at session creation time** for a PAID ticket. No session is created. The `data` field carries rich balance details so the frontend can guide the user directly to top up.

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Insufficient wallet balance to complete checkout",
  "action_time": "2025-09-23T10:30:45",
  "data": {
    "walletBalance": 50000.00,
    "sessionTotal": 150000.00,
    "shortfall": 100000.00,
    "hasSufficientBalance": false,
    "recommendedTopUp": 100000.00,
    "pspMinimum": 500.00,
    "currency": "TZS"
  }
}
```

| Field | Description |
|-------|-------------|
| `walletBalance` | Current wallet balance of the user in TZS |
| `sessionTotal` | Total amount required for this checkout |
| `shortfall` | Amount missing (`sessionTotal - walletBalance`) |
| `hasSufficientBalance` | Always `false` when this error is returned |
| `recommendedTopUp` | Suggested top-up amount — at minimum equals `shortfall`, rounded up to `pspMinimum` if needed |
| `pspMinimum` | Minimum top-up amount accepted by the payment provider |
| `currency` | Always `TZS` |

---

### Standard Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
| `message` | string | Human-readable message describing the operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload for success, error details for failures |

---

## HTTP Method Badge Standards

- **GET** — Green: Safe, read-only operations
- **POST** — Blue: Create new resources
- **PUT** — Yellow: Update/replace entire resource
- **PATCH** — Orange: Partial updates
- **DELETE** — Red: Remove resources

---

## User Journey Flows

### Flow A — Online Checkout (Registered Attendee)

```
  [ Attendee browses event & selects ticket ]
                      |
                      v
  [ POST /checkout  — Create checkout session ]
                      |
          ............|............
          .                       .
          v                       v
  [ FREE / DONATION ticket ]   [ PAID ticket ]
          |                       |
          |                       v
          |           [ Wallet balance check ]
          |           ....................
          |           . INSUFFICIENT     .
          |           ....................
          |                   |
          |           [ 422 response — no session created ]
          |           [ data: { shortfall, recommendedTopUp, ... } ]
          |
          |           ....................
          |           . SUFFICIENT       .
          |           ....................
          |                       |
          v                       v
  [ Auto-processed immediately ]  [ Ticket hold applied ]
          |                       [ Session = PENDING_PAYMENT ]
          |                       |
          |                       v
          |           [ POST /checkout/{sessionId}/payment ]
          |           [ — Deduct from wallet, create escrow ]
          |                       |
          `----------->-----------'
                      |
                      v
  [ Booking order created asynchronously ]
  [ QR codes generated & sent to attendees ]
                      |
                      v
  [ Session status = COMPLETED ]


  At any point before payment:
  [ POST /checkout/{sessionId}/cancel ]
  [ — Releases ticket hold            ]
```

---

### Flow B — At-Door Sale via Scanner Device

```
  [ Customer arrives at event venue ]
                      |
                      v
  [ Scanner device sends sale request ]
  [ POST /checkout/sell-at-door-ticket/scanner ]
                      |
          ............|............
          .                       .
          v                       v
  [ Validate scanner ID ]   [ Validate device fingerprint ]
  [ & permissions       ]   [ & SELL_TICKETS permission   ]
          .                       .
          `..........v............'
                      |
                      v
  [ Validate ticket type belongs to event ]
  [ Check sales channel = AT_DOOR or BOTH ]
                      |
                      v
  [ Cash payment processed (no wallet deduction) ]
  [ Booking order created immediately            ]
                      |
                      v
  [ QR codes returned in response ]
  [ immediateCheckIn = true → ticket marked as checked-in ]
```

---

### Flow C — At-Door Sale via Organizer

```
  [ Organizer is authenticated & accesses event ]
                      |
                      v
  [ POST /checkout/{eventId}/organizer ]
  [ — Organizer sells ticket at their counter ]
                      |
                      v
  [ System verifies organizer owns the event ]
                      |
                      v
  [ Validate ticket type & attendee count ]
                      |
                      v
  [ Cash payment recorded (NEUTRAL transaction) ]
  [ Booking order created                       ]
                      |
                      v
  [ QR codes returned in response ]
  [ immediateCheckIn flag respected ]
```

---

## Endpoints

---

## 1. Create Checkout Session

**Purpose**: Creates a new online checkout session for a registered attendee purchasing event tickets, holding the requested quantity and initializing the payment intent.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `/api/v1/e-events/checkout`

**Access Level**: 🔒 Protected (Requires valid Bearer token — authenticated attendee)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token of the authenticated attendee |
| `Content-Type` | string | Yes | Must be `application/json` |

**Request JSON Sample**:

```json
{
  "eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
  "ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
  "ticketsForMe": 2,
  "donationAmount": null,
  "otherAttendees": [
    {
      "name": "Jane Doe",
      "email": "jane.doe@example.com",
      "phone": "+255712345678",
      "quantity": 1
    }
  ],
  "sendTicketsToAttendees": true,
  "paymentMethodId": null
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The ID of the event being booked | Must be a valid published event that has not yet started |
| `ticketTypeId` | UUID | Yes | The ID of the ticket type being purchased | Must belong to the specified event and be active and on sale |
| `ticketsForMe` | integer | Yes | Number of tickets for the buyer themselves. Use `0` if the buyer is not attending | Min: 0 |
| `donationAmount` | decimal | No | Donation amount in TZS. Only applicable for `DONATION` type tickets | Only used when ticket pricing type is `DONATION` |
| `otherAttendees` | array | No | List of other attendees to purchase tickets for | Each attendee must have valid name, email, and Tanzanian phone number |
| `otherAttendees[].name` | string | Yes (if array provided) | Full name of the attendee | Min: 2, Max: 100 characters |
| `otherAttendees[].email` | string | Yes (if array provided) | Email address of the attendee | Valid email format; no duplicate emails in the array |
| `otherAttendees[].phone` | string | Yes (if array provided) | Phone number of the attendee | Must match Tanzania format: `+255[67]XXXXXXXX` |
| `otherAttendees[].quantity` | integer | Yes (if array provided) | Number of tickets for this attendee | Min: 1 |
| `sendTicketsToAttendees` | boolean | No | If `true`, QR tickets are sent to each attendee's email. If `false`, all QR codes are sent to the buyer only | Default: `true` |
| `paymentMethodId` | UUID | No | ID of a saved payment method. Defaults to wallet if not provided | Optional |

**Business Rules**:
- Total quantity = `ticketsForMe` + sum of all `otherAttendees[].quantity` — must be at least 1
- `DONATION` tickets: maximum 1 per order, cannot be bought for other attendees, online only
- `AT_DOOR_ONLY` tickets cannot be purchased through this endpoint
- Wallet balance is validated upfront for `PAID` tickets
- If the event has a required questionnaire set to `BEFORE_CHECKOUT`, it must be submitted before calling this endpoint
- `FREE` tickets are auto-processed immediately — the response will already show `PAYMENT_COMPLETED`

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Checkout session created successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": {
    "sessionId": "a1b2c3d4-0000-0000-0000-000000000001",
    "status": "PENDING_PAYMENT",
    "customerId": "f1e2d3c4-0000-0000-0000-000000000010",
    "customerUserName": "john_doe",
    "eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
    "eventTitle": "Kilimanjaro Jazz Night 2025",
    "ticketDetails": {
      "ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
      "ticketTypeName": "VIP",
      "unitPrice": 50000.00,
      "ticketsForBuyer": 2,
      "otherAttendees": [
        {
          "name": "Jane Doe",
          "email": "jane.doe@example.com",
          "phone": "+255712345678",
          "quantity": 1
        }
      ],
      "sendTicketsToAttendees": true,
      "totalQuantity": 3,
      "subtotal": 150000.00
    },
    "pricing": {
      "subtotal": 150000.00,
      "total": 150000.00
    },
    "paymentIntent": {
      "provider": "WALLET",
      "clientSecret": null,
      "paymentMethods": ["WALLET"],
      "status": "PENDING"
    },
    "ticketsHeld": true,
    "ticketHoldExpiresAt": "2025-09-23T10:45:45",
    "expiresAt": "2025-09-23T10:45:45",
    "createdAt": "2025-09-23T10:30:45",
    "updatedAt": "2025-09-23T10:30:45",
    "completedAt": null,
    "createdBookingOrderId": null,
    "isExpired": false,
    "canRetryPayment": false
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `sessionId` | Unique identifier for this checkout session. Use it for all subsequent actions |
| `status` | Current session status. Values: `PENDING_PAYMENT`, `PAYMENT_COMPLETED`, `COMPLETED`, `PAYMENT_FAILED`, `CANCELLED`, `EXPIRED` |
| `customerId` | Account ID of the buyer |
| `customerUserName` | Username of the buyer |
| `eventId` | ID of the event being booked |
| `eventTitle` | Human-readable event title |
| `ticketDetails.ticketTypeId` | ID of the chosen ticket type |
| `ticketDetails.ticketTypeName` | Name of the ticket type (e.g., VIP, Regular) |
| `ticketDetails.unitPrice` | Price per single ticket in TZS |
| `ticketDetails.ticketsForBuyer` | Number of tickets allocated to the buyer |
| `ticketDetails.otherAttendees` | List of other attendees and their ticket quantities |
| `ticketDetails.sendTicketsToAttendees` | Whether QR codes will be emailed to each attendee |
| `ticketDetails.totalQuantity` | Total tickets across buyer and all attendees |
| `ticketDetails.subtotal` | Total price before any discounts (TZS) |
| `pricing.subtotal` | Subtotal amount in TZS |
| `pricing.total` | Final payable amount in TZS |
| `paymentIntent.provider` | Payment provider (e.g., `WALLET`) |
| `paymentIntent.paymentMethods` | Available payment methods for this session |
| `paymentIntent.status` | Payment intent status (`PENDING`, `COMPLETED`) |
| `ticketsHeld` | Whether the tickets are currently being held in reserve |
| `ticketHoldExpiresAt` | Timestamp when the ticket hold expires |
| `expiresAt` | Timestamp when the entire session expires |
| `createdBookingOrderId` | Populated after payment is completed — the resulting booking order ID |
| `isExpired` | Computed flag indicating whether the session has passed its expiry time |
| `canRetryPayment` | Whether the session allows another payment attempt (true if status is `PAYMENT_FAILED` and attempts < 5) |

**Error Response JSON Sample**:

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Please complete the event questionnaire before purchasing tickets",
  "action_time": "2025-09-23T10:30:45",
  "data": "Please complete the event questionnaire before purchasing tickets"
}
```

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `400 BAD_REQUEST` | Event is not published, event has already started, ticket not on sale, AT_DOOR_ONLY ticket purchased online, DONATION rules violated, questionnaire not submitted |
| `401 UNAUTHORIZED` | Missing, expired, or invalid Bearer token |
| `404 NOT_FOUND` | Event or ticket type not found |
| `422 UNPROCESSABLE_ENTITY` | Insufficient wallet balance (returns rich `data` with `shortfall` and `recommendedTopUp`); or validation failed on request fields |
| `500 INTERNAL_SERVER_ERROR` | Unexpected server error |

---

## 2. Get Checkout Session

**Purpose**: Retrieves the current state of an existing checkout session belonging to the authenticated user.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/api/v1/e-events/checkout/{sessionId}`

**Access Level**: 🔒 Protected (Authenticated attendee — only the session owner can retrieve it)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token of the authenticated attendee |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `sessionId` | UUID | Yes | The ID of the checkout session to retrieve | Must be a valid UUID belonging to the authenticated user |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Checkout session retrieved successfully",
  "action_time": "2025-09-23T10:35:00",
  "data": {
    "sessionId": "a1b2c3d4-0000-0000-0000-000000000001",
    "status": "PENDING_PAYMENT",
    "customerId": "f1e2d3c4-0000-0000-0000-000000000010",
    "customerUserName": "john_doe",
    "eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
    "eventTitle": "Kilimanjaro Jazz Night 2025",
    "ticketDetails": {
      "ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
      "ticketTypeName": "VIP",
      "unitPrice": 50000.00,
      "ticketsForBuyer": 2,
      "otherAttendees": [
        {
          "name": "Jane Doe",
          "email": "jane.doe@example.com",
          "phone": "+255712345678",
          "quantity": 1
        }
      ],
      "sendTicketsToAttendees": true,
      "totalQuantity": 3,
      "subtotal": 150000.00
    },
    "pricing": {
      "subtotal": 150000.00,
      "total": 150000.00
    },
    "paymentIntent": {
      "provider": "WALLET",
      "clientSecret": null,
      "paymentMethods": ["WALLET"],
      "status": "PENDING"
    },
    "paymentAttempts": [],
    "ticketsHeld": true,
    "ticketHoldExpiresAt": "2025-09-23T10:45:45",
    "expiresAt": "2025-09-23T10:45:45",
    "createdAt": "2025-09-23T10:30:45",
    "updatedAt": "2025-09-23T10:30:45",
    "completedAt": null,
    "createdBookingOrderId": null,
    "isExpired": false,
    "canRetryPayment": false
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `sessionId` | Unique session identifier |
| `status` | Current session status |
| `paymentAttempts` | List of all payment attempts made on this session, including failures |
| `paymentAttempts[].attemptNumber` | Sequential attempt number (1-indexed) |
| `paymentAttempts[].paymentMethod` | Payment method used for this attempt |
| `paymentAttempts[].status` | Result of the attempt: `SUCCESS` or `FAILED` |
| `paymentAttempts[].errorMessage` | Failure reason if the attempt failed |
| `paymentAttempts[].attemptedAt` | Timestamp of the attempt |
| `paymentAttempts[].transactionId` | External or internal transaction reference |
| All other fields | Same as Create Checkout Session response |

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `401 UNAUTHORIZED` | Missing, expired, or invalid Bearer token |
| `404 NOT_FOUND` | Session not found or does not belong to the authenticated user |
| `500 INTERNAL_SERVER_ERROR` | Unexpected server error |

---

## 3. Process Payment

**Purpose**: Initiates wallet payment for a pending checkout session, creating an escrow account and triggering asynchronous booking order creation upon success.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `/api/v1/e-events/checkout/{sessionId}/payment`

**Access Level**: 🔒 Protected (Session owner only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token of the authenticated attendee |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `sessionId` | UUID | Yes | The ID of the checkout session to pay for | Must be a valid UUID; session must be in `PENDING_PAYMENT` status and not expired |

**Business Rules**:
- Session must be in `PENDING_PAYMENT` status
- Session must not be expired
- Cannot call this on FREE tickets (auto-processed on creation)
- Wallet must have sufficient balance at time of payment call
- Maximum 5 payment attempts per session; after that `canRetryPayment` becomes `false`
- On success, escrow is created, session moves to `PAYMENT_COMPLETED`, and a booking order is created asynchronously

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Payment completed successfully. Your booking is being processed.",
  "action_time": "2025-09-23T10:38:00",
  "data": {
    "success": true,
    "status": "SUCCESS",
    "message": "Payment completed successfully. Your booking is being processed.",
    "checkoutSessionId": "a1b2c3d4-0000-0000-0000-000000000001",
    "escrowId": "e9f8a7b6-0000-0000-0000-000000000020",
    "escrowNumber": "ESC-2025-000001",
    "orderId": null,
    "orderNumber": null,
    "paymentMethod": "WALLET",
    "amountPaid": 150000.00,
    "platformFee": 7500.00,
    "sellerAmount": 142500.00,
    "currency": "TZS"
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `status` | Payment result status: `SUCCESS`, `FAILED`, or `PENDING` |
| `checkoutSessionId` | The checkout session this payment belongs to |
| `escrowId` | ID of the escrow account holding the funds |
| `escrowNumber` | Human-readable escrow reference number (format: ESC-YYYY-NNNNNN) |
| `orderId` | Booking order ID — may be `null` immediately after payment as booking is created asynchronously |
| `orderNumber` | Human-readable order reference — `null` until order is created |
| `paymentMethod` | Payment method used: `WALLET` |
| `amountPaid` | Total amount deducted from buyer's wallet in TZS |
| `platformFee` | Platform fee amount (5% of total) in TZS |
| `sellerAmount` | Amount that will be released to the event organizer in TZS |
| `currency` | Currency code, always `TZS` |

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `400 BAD_REQUEST` | Session is not in PENDING_PAYMENT status, or session is expired |
| `401 UNAUTHORIZED` | Missing, expired, or invalid Bearer token |
| `404 NOT_FOUND` | Session not found or does not belong to the authenticated user |
| `500 INTERNAL_SERVER_ERROR` | Unexpected payment processing or booking creation error |

---

## 4. Cancel Checkout Session

**Purpose**: Cancels an active checkout session and releases any held tickets back to available inventory.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `/api/v1/e-events/checkout/{sessionId}/cancel`

**Access Level**: 🔒 Protected (Session owner only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token of the authenticated attendee |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `sessionId` | UUID | Yes | The ID of the checkout session to cancel | Must be a valid UUID belonging to the authenticated user |

**Business Rules**:
- Cannot cancel a session that is in `COMPLETED` status
- Cannot cancel a session that is in `PAYMENT_COMPLETED` status (payment already processed)
- Cancelling releases the ticket hold immediately
- Cancellation does not trigger a refund — refunds are handled separately through the escrow system

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Checkout session cancelled successfully",
  "action_time": "2025-09-23T10:40:00",
  "data": null
}
```

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `400 BAD_REQUEST` | Session is already completed or payment has been completed |
| `401 UNAUTHORIZED` | Missing, expired, or invalid Bearer token |
| `404 NOT_FOUND` | Session not found or does not belong to the authenticated user |
| `500 INTERNAL_SERVER_ERROR` | Unexpected server error |

---

## 5. Scanner — Sell Ticket at Door

**Purpose**: Allows an authorized scanner device to sell tickets at the venue entrance, processing a cash payment and optionally checking in the attendee immediately.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `/api/v1/e-events/checkout/sell-at-door-ticket/scanner`

**Access Level**: 🔒 Protected (Scanner device authentication via `scannerId` + `deviceFingerprint`)

**Authentication**: Bearer Token (of scanner's linked account) + Scanner credentials in body

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token linked to the scanner's registered account |
| `Content-Type` | string | Yes | Must be `application/json` |

**Request JSON Sample**:

```json
{
  "scannerId": "SCN-2025-001",
  "deviceFingerprint": "a3f1b2c4d5e6f7890abc1234def56789",
  "ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
  "quantity": 2,
  "attendees": [
    {
      "fullName": "John Mbeki",
      "email": "john.mbeki@example.com",
      "phoneNumber": "+255789123456"
    },
    {
      "fullName": "Amina Hassan",
      "email": "amina.hassan@example.com",
      "phoneNumber": "+255754321987"
    }
  ],
  "immediateCheckIn": true
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `scannerId` | string | Yes | Unique identifier of the registered scanner device | Must match a registered, active scanner with `SELL_TICKETS` permission |
| `deviceFingerprint` | string | Yes | Hardware fingerprint of the scanner device | Must match the fingerprint registered for this scanner |
| `ticketTypeId` | UUID | Yes | ID of the ticket type to sell | Must belong to the event this scanner is assigned to; must not be `ONLINE_ONLY`; must be on sale |
| `quantity` | integer | Yes | Total number of tickets to sell | Min: 1; must equal the number of attendees in the `attendees` array |
| `attendees` | array | Yes | List of attendee details, one entry per ticket | Min: 1 entry; count must match `quantity` |
| `attendees[].fullName` | string | No | Full name of the attendee | Optional — if blank, a generated name like `ATTENDEE-XXXX` is assigned |
| `attendees[].email` | string | No | Email address of the attendee | Valid email format if provided |
| `attendees[].phoneNumber` | string | No | Phone number of the attendee | Optional |
| `immediateCheckIn` | boolean | Yes | If `true`, the ticket is marked as checked-in immediately upon sale | Required |

**Business Rules**:
- The scanner must be active, not expired, and have the `SELL_TICKETS` permission
- The `deviceFingerprint` must exactly match the registered fingerprint for this scanner
- The number of attendees must equal `quantity` — a 1-to-1 mapping is enforced
- Payment method is always `CASH` — no wallet deduction occurs
- The ticket type must not be `ONLINE_ONLY`
- `immediateCheckIn = true` automatically marks each generated ticket as checked-in

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Tickets sold successfully at door",
  "action_time": "2025-09-23T18:00:00",
  "data": {
    "bookingId": "c9d8e7f6-0000-0000-0000-000000000030",
    "bookingReference": "BK-2025-000042",
    "eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
    "eventName": "Kilimanjaro Jazz Night 2025",
    "tickets": [
      {
        "ticketInstanceId": "aa11bb22-0000-0000-0000-000000000050",
        "ticketSeries": "VIP-0042-A",
        "ticketTypeName": "VIP",
        "attendeeName": "John Mbeki",
        "attendeeEmail": "john.mbeki@example.com",
        "checkedIn": true,
        "checkInTime": "2025-09-23T18:00:05Z",
        "qrCode": "eyJhbGciOiJIUzI1NiJ9..."
      },
      {
        "ticketInstanceId": "cc33dd44-0000-0000-0000-000000000051",
        "ticketSeries": "VIP-0042-B",
        "ticketTypeName": "VIP",
        "attendeeName": "Amina Hassan",
        "attendeeEmail": "amina.hassan@example.com",
        "checkedIn": true,
        "checkInTime": "2025-09-23T18:00:05Z",
        "qrCode": "eyJhbGciOiJIUzI1NiJ9..."
      }
    ],
    "totalAmount": 100000.00,
    "currency": "TZS",
    "paymentMethod": "CASH",
    "soldBy": "Gate-A Scanner",
    "soldAt": "Main Entrance",
    "saleTime": "2025-09-23T18:00:05Z"
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `bookingId` | UUID of the created booking order |
| `bookingReference` | Human-readable booking reference number |
| `eventId` | ID of the event |
| `eventName` | Name of the event |
| `tickets` | Array of issued ticket instances — one per attendee |
| `tickets[].ticketInstanceId` | Unique ID of this specific ticket instance |
| `tickets[].ticketSeries` | Ticket serial number (e.g., VIP-0042-A) |
| `tickets[].ticketTypeName` | The type of the sold ticket |
| `tickets[].attendeeName` | Name of the attendee this ticket is assigned to |
| `tickets[].attendeeEmail` | Email of the attendee |
| `tickets[].checkedIn` | Whether the attendee has been checked in |
| `tickets[].checkInTime` | Timestamp of check-in if `immediateCheckIn` was `true` |
| `tickets[].qrCode` | JWT-encoded QR code string for this ticket |
| `totalAmount` | Total cash amount collected in TZS |
| `currency` | Always `TZS` |
| `paymentMethod` | Always `CASH` for at-door sales |
| `soldBy` | Name of the scanner that processed the sale |
| `soldAt` | Location label of the scanner (e.g., "Main Entrance") |
| `saleTime` | ISO 8601 timestamp of when the sale occurred |

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `400 BAD_REQUEST` | Attendee count does not match quantity, ticket is ONLINE_ONLY, ticket not on sale |
| `401 UNAUTHORIZED` | Missing or invalid Bearer token |
| `403 FORBIDDEN` | Scanner does not have SELL_TICKETS permission, device fingerprint mismatch, scanner is inactive or expired |
| `404 NOT_FOUND` | Scanner ID not found, ticket type not found |
| `422 UNPROCESSABLE_ENTITY` | Validation errors on request fields |
| `500 INTERNAL_SERVER_ERROR` | Payment processing or booking creation failure |

---

## 6. Organizer — Sell Ticket at Door

**Purpose**: Allows the authenticated event organizer to sell tickets directly at their event counter, processing a cash payment and optionally checking in the attendee immediately.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `/api/v1/e-events/checkout/sell-at-door-ticket/{eventId}/organizer`

**Access Level**: 🔒 Protected (Must be the organizer of the specified event)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | Bearer token of the authenticated event organizer |
| `Content-Type` | string | Yes | Must be `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The ID of the event to sell tickets for | Must be an existing, non-deleted event; authenticated user must be the organizer |

**Request JSON Sample**:

```json
{
  "ticketTypeId": "d4e5f6a7-1234-5678-abcd-000000000002",
  "quantity": 2,
  "attendees": [
    {
      "fullName": "Peter Salim",
      "email": "peter.salim@example.com",
      "phoneNumber": "+255711223344"
    },
    {
      "fullName": "Grace Mwangi",
      "email": "grace.mwangi@example.com",
      "phoneNumber": null
    }
  ],
  "immediateCheckIn": false,
  "location": "VIP Gate"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ticketTypeId` | UUID | Yes | ID of the ticket type to sell | Must belong to the event in the path; must not be `ONLINE_ONLY`; must be on sale |
| `quantity` | integer | Yes | Total number of tickets to sell | Min: 1; must equal the number of attendees in the `attendees` array |
| `attendees` | array | Yes | List of attendee details — one entry per ticket | Min: 1 entry; count must match `quantity` |
| `attendees[].fullName` | string | No | Full name of the attendee | Optional — auto-generated if blank |
| `attendees[].email` | string | No | Email of the attendee | Valid email format if provided |
| `attendees[].phoneNumber` | string | No | Phone number of the attendee | Optional |
| `immediateCheckIn` | boolean | Yes | Whether to mark attendees as checked-in immediately | Required |
| `location` | string | No | Description of the sale point, e.g., "VIP Gate", "Main Counter" | Max: 200 characters; defaults to `"Organizer Counter"` if not provided |

**Business Rules**:
- Only the event organizer (the user who created the event) can call this endpoint
- Number of entries in `attendees` must equal `quantity`
- Payment is always `CASH` — no wallet or ledger deduction
- Ticket type must not be `ONLINE_ONLY`
- `immediateCheckIn = true` marks each ticket as checked-in at time of sale

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Tickets sold successfully at door",
  "action_time": "2025-09-23T17:30:00",
  "data": {
    "bookingId": "d7e6f5a4-0000-0000-0000-000000000035",
    "bookingReference": "BK-2025-000043",
    "eventId": "b3f1a2c4-1234-5678-abcd-000000000001",
    "eventName": "Kilimanjaro Jazz Night 2025",
    "tickets": [
      {
        "ticketInstanceId": "ee55ff66-0000-0000-0000-000000000060",
        "ticketSeries": "VIP-0043-A",
        "ticketTypeName": "VIP",
        "attendeeName": "Peter Salim",
        "attendeeEmail": "peter.salim@example.com",
        "checkedIn": false,
        "checkInTime": null,
        "qrCode": "eyJhbGciOiJIUzI1NiJ9..."
      },
      {
        "ticketInstanceId": "gg77hh88-0000-0000-0000-000000000061",
        "ticketSeries": "VIP-0043-B",
        "ticketTypeName": "VIP",
        "attendeeName": "Grace Mwangi",
        "attendeeEmail": "grace.mwangi@example.com",
        "checkedIn": false,
        "checkInTime": null,
        "qrCode": "eyJhbGciOiJIUzI1NiJ9..."
      }
    ],
    "totalAmount": 100000.00,
    "currency": "TZS",
    "paymentMethod": "CASH",
    "soldBy": "organizer_username",
    "soldAt": "VIP Gate",
    "saleTime": "2025-09-23T17:30:05Z"
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `bookingId` | UUID of the created booking order |
| `bookingReference` | Human-readable booking reference number |
| `eventId` | ID of the event |
| `eventName` | Name of the event |
| `tickets` | Array of issued ticket instances — one per attendee |
| `tickets[].ticketInstanceId` | Unique ID of this specific ticket instance |
| `tickets[].ticketSeries` | Ticket serial number |
| `tickets[].ticketTypeName` | The type of ticket sold |
| `tickets[].attendeeName` | Assigned attendee name |
| `tickets[].attendeeEmail` | Attendee email |
| `tickets[].checkedIn` | Whether immediately checked in |
| `tickets[].checkInTime` | Check-in timestamp, `null` if not checked in |
| `tickets[].qrCode` | JWT-encoded QR code string for this ticket |
| `totalAmount` | Total cash amount in TZS |
| `currency` | Always `TZS` |
| `paymentMethod` | Always `CASH` |
| `soldBy` | Username of the organizer who made the sale |
| `soldAt` | Location label provided in the request |
| `saleTime` | ISO 8601 timestamp of the sale |

**Possible Errors**:

| HTTP Status | Scenario |
|-------------|----------|
| `400 BAD_REQUEST` | Attendee count does not match quantity, ticket is ONLINE_ONLY, ticket not on sale |
| `401 UNAUTHORIZED` | Missing or invalid Bearer token |
| `403 FORBIDDEN` | Authenticated user is not the organizer of the specified event |
| `404 NOT_FOUND` | Event not found, ticket type not found |
| `422 UNPROCESSABLE_ENTITY` | Validation errors on request fields |
| `500 INTERNAL_SERVER_ERROR` | Payment processing or booking creation failure |

---

## Standard Error Response Examples

*Bad Request — General (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Ticket is not currently on sale",
  "action_time": "2025-09-23T10:30:45",
  "data": "Ticket is not currently on sale"
}
```

*Unauthorized — Token Issues (401):*

```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2025-09-23T10:30:45",
  "data": "Token has expired"
}
```

*Forbidden — Access Denied (403):*

```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Only the event organizer can sell tickets at door",
  "action_time": "2025-09-23T10:30:45",
  "data": "Only the event organizer can sell tickets at door"
}
```

*Not Found (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Event not found",
  "action_time": "2025-09-23T10:30:45",
  "data": "Event not found"
}
```

*Validation Error (422):*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-09-23T10:30:45",
  "data": {
    "ticketTypeId": "must not be null",
    "quantity": "must be greater than or equal to 1",
    "immediateCheckIn": "must not be null"
  }
}
```

---

## Standard Error Types Reference

### Application-Level Exceptions (400–499)

- `400 BAD_REQUEST`: General invalid request, business rule violations, or item already exists
- `401 UNAUTHORIZED`: Authentication issues (missing, invalid, expired, or malformed token)
- `403 FORBIDDEN`: Access denied, scanner permission issues, organizer mismatch
- `404 NOT_FOUND`: Event, ticket type, session, or scanner not found
- `422 UNPROCESSABLE_ENTITY`: Bean validation errors with per-field details
- `429 TOO_MANY_REQUESTS`: Rate limit exceeded

### Server-Level Exceptions (500+)

- `500 INTERNAL_SERVER_ERROR`: Unexpected server errors, payment orchestration failures

---

## Ticket Pricing Types — Detailed Behaviour

This section explains exactly how the system handles each pricing type end-to-end: from checkout creation through payment, booking order creation, and ticket serial assignment. Understanding this is critical for integrating correctly with the checkout API.

---

### FREE Tickets

**What they are**: Tickets with a price of `0 TZS`. No money changes hands.

**Checkout flow**:

```
  [ POST /checkout — session created ]
              |
              v
  [ System detects price = 0 TZS ]
              |
              v
  [ Payment auto-processed immediately ]
  [ No wallet deduction               ]
  [ No escrow created                 ]
              |
              v
  [ PaymentCompletedEvent published (escrow = null) ]
              |
              v
  [ Booking order created asynchronously ]
  [ Ticket serials assigned              ]
              |
              v
  [ Session status = COMPLETED ]
  [ Response returned to caller ]
```

**Key rules**:
- The caller does **not** need to call `POST /{sessionId}/payment` — this is skipped entirely
- The session is returned from the create endpoint already in `PAYMENT_COMPLETED` status (may shift to `COMPLETED` once the booking is written)
- No escrow record exists for this transaction; `escrowId` will be `null` in all responses
- A `NEUTRAL` transaction history entry is recorded for audit purposes
- Ticket holds are still applied on creation and released naturally upon booking completion
- FREE tickets can be `ONLINE_ONLY` or `BOTH` depending on the sales channel configuration — `AT_DOOR_ONLY` FREE tickets go through the scanner/organizer at-door flows instead

---

### PAID Tickets

**What they are**: Tickets with a price greater than `0 TZS`. Wallet payment is required.

**Checkout flow**:

```
  [ POST /checkout — session created ]
              |
              v
  [ assertSufficientBalanceForCheckout(total) ]
  ............................................
  . INSUFFICIENT BALANCE                      .
  ............................................
          |
  [ 422 returned immediately ]
  [ No session created       ]
  [ data: {                  ]
  [   walletBalance,         ]
  [   sessionTotal,          ]
  [   shortfall,             ]
  [   recommendedTopUp       ]
  [ }                        ]

  ............................................
  . SUFFICIENT BALANCE                        .
  ............................................
          |
          v
  [ Ticket hold applied               ]
              |
              v
  [ Session status = PENDING_PAYMENT ]
  [ paymentIntent.provider = WALLET  ]
              |
              v
  [ POST /{sessionId}/payment called by client ]
              |
              v
  [ Wallet deducted via double-entry ledger ]
  [ Escrow account created (ESC-YYYY-NNNNNN)]
  [ platformFee = 5% of total              ]
  [ sellerAmount = total - platformFee     ]
              |
              v
  [ PaymentCompletedEvent published (escrow != null) ]
              |
              v
  [ Booking order created asynchronously ]
  [ Ticket serials assigned              ]
  [ QR codes generated                   ]
              |
              v
  [ Session status = COMPLETED ]
  [ Escrow status = HELD        ]
  [ (Released to organizer on event completion) ]
```

**Key rules**:
- Wallet balance is checked **at session creation** — if insufficient, a `422` is returned with rich balance data (`shortfall`, `recommendedTopUp`) and no session is created. A second safety-net check runs at actual payment time in case the balance changed between the two calls
- Maximum **5 payment attempts** per session; after that `canRetryPayment = false` and a new session must be created
- If a payment attempt fails, the session moves to `PAYMENT_FAILED` but the ticket hold remains active until the session expires
- Escrow holds funds in a separate ledger account — the organizer does **not** receive the money until the platform releases it after the event
- `orderId` in the payment response may be `null` immediately after payment since booking creation is asynchronous — poll `GET /checkout/{sessionId}` and check `createdBookingOrderId` to confirm

---

### DONATION Tickets

**What they are**: Tickets where the attendee voluntarily chooses the amount they pay. A minimum may or may not be set by the organizer.

**Checkout flow**:

```
  [ POST /checkout — session created ]
  [ donationAmount provided in body  ]
              |
              v
  [ System validates DONATION rules  ]
  .....................................
  . totalQuantity must be exactly 1  .
  . otherAttendees must be empty     .
  . salesChannel must be ONLINE_ONLY .
  .....................................
              |
              v
  [ Treated as PAID internally                    ]
  [ donationAmount used as the ticket price       ]
  [ Wallet deducted for the donation amount       ]
  [ Escrow created for the donation amount        ]
              |
              v
  [ Booking order created asynchronously ]
  [ Ticket serial assigned               ]
  [ QR code generated                    ]
              |
              v
  [ Session status = COMPLETED ]
```

**Key rules**:
- **Strictly 1 ticket per order** — the system rejects any request with `ticketsForMe > 1` or any `otherAttendees`
- **Online-only** — DONATION tickets cannot be sold at the door through any channel
- The `donationAmount` in the request body is the amount that will be charged; the system uses it as the effective unit price
- Despite being a donation, the standard 5% platform fee still applies and an escrow account is created
- If `donationAmount` is `null` or `0`, the system may treat it as a FREE ticket depending on the ticket's configured minimum — confirm with the organizer's ticket setup

---

## Ticket Serials — How They Are Assigned

Every ticket instance issued by the system receives a unique **ticket serial** (also called `ticketSeries` in the response). This serial is the human-readable identifier printed on physical tickets, displayed in QR codes, and used for manual verification at the door.

### Serial Format

```
  [TICKET_TYPE_CODE]-[BOOKING_NUMBER]-[POSITION_LETTER]

  Examples:
    VIP-0042-A      ← First VIP ticket in booking #42
    VIP-0042-B      ← Second VIP ticket in booking #42
    REG-0199-A      ← First Regular ticket in booking #199
    GENERAL-0001-A  ← First General Admission ticket in booking #1
```

### How Serials Are Generated

```
  [ Booking order created ]
              |
              v
  [ System reads totalQuantity from checkout session ]
              |
              v
  [ For each ticket in the order: ]
  .......................................
  .  ticketSeries = TYPE_CODE         .
  .               + "-"               .
  .               + BOOKING_NUMBER    .  ← zero-padded (e.g., 0042)
  .               + "-"               .
  .               + POSITION_LETTER   .  ← A, B, C, D ... per ticket
  .......................................
              |
              v
  [ Each serial stored on the TicketInstance entity ]
  [ JWT-encoded QR token generated per ticket       ]
  [ QR token embeds: ticketSeries + ticketInstanceId ]
              |
              v
  [ Serials returned in: ]
  .  POST /sell-at-door-ticket response (tickets[].ticketSeries) ]
  .  Booking order details endpoint (separate booking API)       ]
```

### Serial Assignment per Pricing Type

| Pricing Type | When Serials Are Assigned | Who Appears in `attendeeName` |
|--------------|--------------------------|-------------------------------|
| `FREE` | Asynchronously after `PaymentCompletedEvent` | Buyer for `ticketsForBuyer` tickets; each named attendee for their tickets |
| `PAID` | Asynchronously after payment escrow is created | Buyer for `ticketsForBuyer` tickets; each named attendee for their tickets |
| `DONATION` | Asynchronously after payment (always 1 ticket) | Always the buyer only |
| At-Door (any type) | Synchronously — returned immediately in the sale response | Each attendee in the `attendees` array; auto-generated name if blank |

### Attendee-to-Serial Mapping

When a buyer purchases tickets for themselves and other attendees, each serial maps to exactly one person:

```
  Buyer purchases:
    ticketsForMe = 2
    otherAttendees = [ { name: "Jane", quantity: 1 } ]
    totalQuantity = 3

  Serials assigned:
    VIP-0042-A  →  Buyer (ticket 1 of 2 for buyer)
    VIP-0042-B  →  Buyer (ticket 2 of 2 for buyer)
    VIP-0042-C  →  Jane  (her 1 ticket)
```

### QR Code & Serial Relationship

Each ticket's `qrCode` field in the response is a **JWT token** that encodes the ticket serial and instance ID. Scanners decode this JWT at check-in time to verify the ticket. The serial alone is readable by humans; the JWT is what the scanner hardware validates cryptographically.

```
  QR Code (JWT) decodes to:
  ...........................................
  .  ticketInstanceId  (UUID)             .
  .  ticketSeries      (e.g. VIP-0042-A)  .
  .  eventId           (UUID)             .
  .  issuedAt          (timestamp)        .
  ...........................................
              |
              v
  [ Scanner validates JWT signature ]
  [ Marks ticket as CHECKED_IN      ]
  [ Returns check-in confirmation   ]
```

---

## Quick Reference

### Endpoint Summary

| # | Method | Path | Description |
|---|--------|------|-------------|
| 1 | POST | `/api/v1/e-events/checkout` | Create online checkout session |
| 2 | GET | `/api/v1/e-events/checkout/{sessionId}` | Get checkout session details |
| 3 | POST | `/api/v1/e-events/checkout/{sessionId}/payment` | Process wallet payment |
| 4 | POST | `/api/v1/e-events/checkout/{sessionId}/cancel` | Cancel checkout session |
| 5 | POST | `/api/v1/e-events/checkout/sell-at-door-ticket/scanner` | Scanner at-door sale |
| 6 | POST | `/api/v1/e-events/checkout/sell-at-door-ticket/{eventId}/organizer` | Organizer at-door sale |

### Session Status Reference

| Status | Meaning |
|--------|---------|
| `PENDING_PAYMENT` | Session created, awaiting payment |
| `PAYMENT_PROCESSING` | External payment initiated, awaiting confirmation |
| `PAYMENT_COMPLETED` | Payment succeeded, booking being created |
| `PAYMENT_FAILED` | Payment attempt failed (retry may be possible) |
| `COMPLETED` | Booking fully created and confirmed |
| `CANCELLED` | Session cancelled by user |
| `EXPIRED` | Session timed out before payment |

### Ticket Pricing Type Behaviour

| Pricing Type | Payment Required | At-Door Allowed | Notes |
|--------------|-----------------|-----------------|-------|
| `FREE` | No | Depends on sales channel | Auto-processed on session creation |
| `PAID` | Yes (Wallet) | Yes | Escrow created on payment |
| `DONATION` | Optional amount | No (Online only) | Max 1 ticket per order; no other attendees |

### Sales Channel Rules

| Sales Channel | Online Checkout | At-Door (Scanner) | At-Door (Organizer) |
|---------------|----------------|-------------------|---------------------|
| `ONLINE_ONLY` | ✅ Allowed | ❌ Blocked | ❌ Blocked |
| `AT_DOOR_ONLY` | ❌ Blocked | ✅ Allowed | ✅ Allowed |
| `BOTH` | ✅ Allowed | ✅ Allowed | ✅ Allowed |

### Authentication Reference

- **Bearer Token**: Include `Authorization: Bearer <token>` in all request headers
- All endpoints require authentication
- For scanner endpoints, the Bearer token must belong to the account linked to the scanner device

### Data Format Standards

- **Dates**: ISO 8601 format (`2025-09-23T10:30:45`)
- **Currency**: TZS (Tanzanian Shilling) — decimal values with 2 decimal places
- **UUIDs**: Standard UUID v4 format (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
- **Phone Numbers**: Tanzania format only — `+255[67]XXXXXXXX`

# Event Booking Orders API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-23
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Event Booking Orders API manages confirmed ticket purchases for events on the Nexgate platform. It allows customers to view their bookings with JWT-signed QR codes, track multi-day check-in history, download ticket PDFs, and enables event organizers to monitor all orders for their events with filtering and revenue stats.

**Hints**:
- Bookings are created automatically after successful checkout payment — never created directly via this API
- QR codes are RSA-signed JWTs containing full ticket and event data; they cannot be forged
- Event details (title, venue, organizer) are snapshotted at booking time and never change on a booking record
- Ticket series are auto-generated (e.g., `VIP-0001`, `GENER-0042`) using a per-ticket-type counter
- Multi-day events store a full `checkIns` array per ticket — one record per day attended
- Customers see their own bookings only; organizers see bookings for their events; admins see all
- The PDF endpoint returns a binary file, not a JSON response

---

## Standard Response Format

All JSON endpoints follow a consistent structure using the Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-05-23T10:30:45",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Booking not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "Booking not found"
}
```

### Standard Response Fields
| Field         | Type          | Description                                                         |
|---------------|---------------|---------------------------------------------------------------------|
| `success`     | boolean       | `true` for successful operations, `false` for errors               |
| `httpStatus`  | string        | HTTP status name (OK, NOT_FOUND, FORBIDDEN, etc.)                  |
| `message`     | string        | Human-readable description of the result                           |
| `action_time` | string        | ISO 8601 timestamp of when the response was generated              |
| `data`        | object/string | Response payload on success; error message string on failure       |

---

## HTTP Method Badge Standards

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PUT** - <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> - Yellow (Update/replace entire resource)
- **PATCH** - <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> - Orange (Partial updates)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - Red (Remove resources)

---

## Endpoints

## 1. Get Booking by ID

**Purpose**: Retrieve complete booking details including all tickets, check-in history, and event snapshot for a specific booking.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/booking-orders/{bookingId}`

**Access Level**: 🔒 Protected (Booking Owner, Event Organizer, or Admin)

**Authentication**: Bearer Token

**Request Headers**:
| Header          | Type   | Required | Description                               |
|-----------------|--------|----------|-------------------------------------------|
| `Authorization` | string | Yes      | Bearer token (format: `Bearer <token>`)   |

**Path Parameters**:
| Parameter   | Type         | Required | Description            | Validation       |
|-------------|--------------|----------|------------------------|------------------|
| `bookingId` | string (UUID)| Yes      | Unique booking order ID | Must be valid UUID |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Booking retrieved successfully",
  "action_time": "2026-05-23T10:30:45",
  "data": {
    "bookingId": "550e8400-e29b-41d4-a716-446655440000",
    "bookingReference": "EVT-A3F4B21C",
    "status": "CONFIRMED",
    "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "event": {
      "eventId": "770e8400-e29b-41d4-a716-446655440002",
      "title": "East African Tech Summit 2025",
      "startDateTime": "2025-12-15T09:00:00",
      "endDateTime": "2025-12-17T18:00:00",
      "timezone": "Africa/Nairobi",
      "location": "KICC Nairobi, Harambee Avenue, Nairobi",
      "format": "HYBRID",
      "hasApplicantForm": true,
      "virtualDetails": {
        "meetingLink": "https://zoom.us/j/123456789",
        "meetingId": "123 456 789",
        "passcode": "summit2025"
      }
    },
    "organizer": {
      "name": "TechEvents Kenya",
      "email": "organizer@techevents.ke",
      "phone": "+254712345678"
    },
    "customer": {
      "customerId": "660e8400-e29b-41d4-a716-446655440001",
      "name": "johndoe",
      "email": "john@example.com"
    },
    "tickets": [
      {
        "ticketInstanceId": "880e8400-e29b-41d4-a716-446655440010",
        "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "ticketTypeName": "VIP Pass",
        "ticketSeries": "VIP-0001",
        "ticketNumber": "VIP-0001",
        "price": 150.00,
        "qrCode": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "attendanceMode": "IN_PERSON",
        "attendee": {
          "name": "John Doe",
          "email": "john@example.com",
          "phone": "+255712345678"
        },
        "buyer": {
          "name": "John Doe",
          "email": "john@example.com",
          "buyerType": "SYSTEM_USER"
        },
        "checkIns": [
          {
            "checkInTime": "2025-12-15T09:15:00+03:00",
            "checkInLocation": "Main Gate",
            "checkedInBy": "Scanner Operator 1",
            "dayName": "Day 1 - Opening Day",
            "scannerId": "SCANNER-001",
            "checkInMethod": "QR_SCAN"
          },
          {
            "checkInTime": "2025-12-16T08:45:00+03:00",
            "checkInLocation": "VIP Entrance",
            "checkedInBy": "Scanner Operator 2",
            "dayName": "Day 2 - Conference Day",
            "scannerId": "SCANNER-003",
            "checkInMethod": "QR_SCAN"
          }
        ],
        "hasBeenCheckedIn": true,
        "lastCheckedInAt": "2025-12-16T08:45:00+03:00",
        "lastCheckedInBy": "Scanner Operator 2",
        "lastCheckInLocation": "VIP Entrance",
        "lastCheckInDayName": "Day 2 - Conference Day",
        "status": "USED",
        "validFrom": "2025-12-15T09:00:00+03:00",
        "validUntil": "2025-12-17T18:00:00+03:00"
      },
      {
        "ticketInstanceId": "880e8400-e29b-41d4-a716-446655440011",
        "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "ticketTypeName": "General Admission",
        "ticketSeries": "GENER-0042",
        "ticketNumber": "GENER-0042",
        "price": 50.00,
        "qrCode": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "attendanceMode": "IN_PERSON",
        "attendee": {
          "name": "Jane Smith",
          "email": "jane@example.com",
          "phone": "+255723456789"
        },
        "buyer": {
          "name": "John Doe",
          "email": "john@example.com",
          "buyerType": "SYSTEM_USER"
        },
        "checkIns": [],
        "hasBeenCheckedIn": false,
        "status": "ACTIVE",
        "validFrom": "2025-12-15T09:00:00+03:00",
        "validUntil": "2025-12-17T18:00:00+03:00"
      }
    ],
    "totalTickets": 2,
    "checkedInTicketsCount": 1,
    "subtotal": 200.00,
    "total": 200.00,
    "bookedAt": "2025-12-11T10:30:45",
    "cancelledAt": null
  }
}
```

**Success Response Fields**:
| Field | Description |
|---|---|
| `bookingId` | Unique booking UUID |
| `bookingReference` | Short readable code (e.g., `EVT-A3F4B21C`) |
| `status` | Booking status: `CONFIRMED`, `CANCELLED` |
| `formResponseId` | UUID of the buyer's applicant form response — `null` if the event had no form |
| `event.eventId` | Event UUID |
| `event.title` | Event title at time of booking (immutable snapshot) |
| `event.startDateTime` | Event start — LocalDateTime (no timezone) |
| `event.endDateTime` | Event end — LocalDateTime (no timezone) |
| `event.timezone` | IANA timezone string (e.g., `Africa/Nairobi`) |
| `event.location` | Full venue address snapshotted at booking |
| `event.format` | `IN_PERSON`, `ONLINE`, or `HYBRID` |
| `event.hasApplicantForm` | `true` if the event had an applicant form at time of booking |
| `event.virtualDetails` | Virtual meeting info — only present for `ONLINE`/`HYBRID` events |
| `event.virtualDetails.meetingLink` | Full meeting URL |
| `event.virtualDetails.meetingId` | Meeting ID (optional) |
| `event.virtualDetails.passcode` | Meeting passcode (optional) |
| `organizer.name` | Organizer name snapshotted at booking |
| `organizer.email` | Organizer email |
| `organizer.phone` | Organizer phone |
| `customer.customerId` | Customer account UUID |
| `customer.name` | Customer username |
| `customer.email` | Customer email |
| `tickets` | Array of all booked ticket instances |
| `tickets[].ticketInstanceId` | Unique ticket instance UUID |
| `tickets[].formResponseId` | UUID of the applicant form response linked to this ticket — same as order-level `formResponseId` |
| `tickets[].ticketTypeName` | Ticket type name (e.g., `VIP Pass`) |
| `tickets[].ticketSeries` | Auto-generated series (e.g., `VIP-0001`) |
| `tickets[].ticketNumber` | Same as ticketSeries — used for PDF labeling |
| `tickets[].price` | Price paid for this ticket |
| `tickets[].qrCode` | RSA-signed JWT — used for gate check-in scanning |
| `tickets[].attendanceMode` | `IN_PERSON` or `ONLINE` (relevant for hybrid events) |
| `tickets[].attendee` | Person this ticket is for |
| `tickets[].attendee.name` | Attendee full name |
| `tickets[].attendee.email` | Attendee email |
| `tickets[].attendee.phone` | Attendee phone |
| `tickets[].buyer` | Person who purchased this ticket |
| `tickets[].buyer.name` | Buyer name |
| `tickets[].buyer.email` | Buyer email (null for AT_DOOR purchases) |
| `tickets[].buyer.buyerType` | `SYSTEM_USER` (online) or `AT_DOOR` (sold at gate) |
| `tickets[].checkIns` | Full check-in history — one record per day attended |
| `tickets[].checkIns[].checkInTime` | ZonedDateTime of check-in |
| `tickets[].checkIns[].checkInLocation` | Where checked in (e.g., `Main Gate`, `VIP Entrance`) |
| `tickets[].checkIns[].checkedInBy` | Staff/scanner operator name |
| `tickets[].checkIns[].dayName` | Event day label (e.g., `Day 1 - Opening Day`) |
| `tickets[].checkIns[].scannerId` | Scanner device ID |
| `tickets[].checkIns[].checkInMethod` | `QR_SCAN` (default), `MANUAL`, or `NFC` |
| `tickets[].hasBeenCheckedIn` | `true` if ticket has at least one check-in |
| `tickets[].lastCheckedInAt` | Most recent check-in time (ZonedDateTime, null if none) |
| `tickets[].lastCheckedInBy` | Who performed the most recent check-in (null if none) |
| `tickets[].lastCheckInLocation` | Location of most recent check-in (null if none) |
| `tickets[].lastCheckInDayName` | Day label of most recent check-in (null if none) |
| `tickets[].status` | `ACTIVE`, `USED`, or `CANCELLED` |
| `tickets[].validFrom` | Ticket validity start — ZonedDateTime |
| `tickets[].validUntil` | Ticket validity end — ZonedDateTime |
| `totalTickets` | Total ticket count in this booking |
| `checkedInTicketsCount` | Count of tickets with at least one check-in |
| `subtotal` | Subtotal before any deductions |
| `total` | Total amount paid |
| `bookedAt` | When the booking was created — LocalDateTime |
| `cancelledAt` | When cancelled — LocalDateTime (null if still active) |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Booking not found: 550e8400-e29b-41d4-a716-446655440000",
  "action_time": "2026-05-23T10:30:45",
  "data": "Booking not found: 550e8400-e29b-41d4-a716-446655440000"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or expired Bearer token
- `403 FORBIDDEN`: Authenticated user is not the booking owner, event organizer, or admin
- `404 NOT_FOUND`: Booking with given ID does not exist

---

## 2. Get My Bookings

**Purpose**: Retrieve a summary list of all bookings for the authenticated user, sorted newest first.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/booking-orders/my-bookings`

**Access Level**: 🔒 Protected (Authenticated Users)

**Authentication**: Bearer Token

**Request Headers**:
| Header          | Type   | Required | Description                             |
|-----------------|--------|----------|-----------------------------------------|
| `Authorization` | string | Yes      | Bearer token (format: `Bearer <token>`) |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Bookings retrieved successfully",
  "action_time": "2026-05-23T10:30:45",
  "data": [
    {
      "bookingId": "550e8400-e29b-41d4-a716-446655440000",
      "bookingReference": "EVT-A3F4B21C",
      "status": "CONFIRMED",
      "eventTitle": "East African Tech Summit 2025",
      "eventStartDateTime": "2025-12-15T09:00:00",
      "eventLocation": "KICC Nairobi, Harambee Avenue, Nairobi",
      "totalTickets": 2,
      "checkedInTickets": 1,
      "total": 200.00,
      "bookedAt": "2025-12-11T10:30:45",
      "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
    },
    {
      "bookingId": "550e8400-e29b-41d4-a716-446655440001",
      "bookingReference": "EVT-B5D2E12F",
      "status": "CONFIRMED",
      "eventTitle": "Dar es Salaam Food Festival",
      "eventStartDateTime": "2025-12-20T11:00:00",
      "eventLocation": "Mlimani City, Sam Nujoma Road, Dar es Salaam",
      "totalTickets": 4,
      "checkedInTickets": 0,
      "total": 150.00,
      "bookedAt": "2025-12-10T14:20:30",
      "formResponseId": null
    }
  ]
}
```

**Success Response Fields**:
| Field | Description |
|---|---|
| `bookingId` | Unique booking UUID |
| `bookingReference` | Short readable code (e.g., `EVT-A3F4B21C`) |
| `status` | `CONFIRMED` or `CANCELLED` |
| `eventTitle` | Event title snapshotted at booking time |
| `eventStartDateTime` | Event start date/time — LocalDateTime (no timezone) |
| `eventLocation` | Venue address snapshotted at booking time |
| `totalTickets` | Total number of tickets in this booking |
| `checkedInTickets` | Number of tickets with at least one check-in |
| `total` | Total amount paid |
| `bookedAt` | When booking was created — LocalDateTime |
| `formResponseId` | UUID of the applicant form response — `null` if the event had no form |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "User not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "User not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or expired Bearer token
- `404 NOT_FOUND`: Authenticated user account not found

---

## 3. Download Ticket PDF

**Purpose**: Download or preview the PDF ticket for a specific ticket instance belonging to the authenticated user.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/booking-orders/tickets/{ticketInstanceId}/pdf`

**Access Level**: 🔒 Protected (Ticket Owner)

**Authentication**: Bearer Token

**Request Headers**:
| Header          | Type   | Required | Description                             |
|-----------------|--------|----------|-----------------------------------------|
| `Authorization` | string | Yes      | Bearer token (format: `Bearer <token>`) |

**Path Parameters**:
| Parameter          | Type          | Required | Description                        | Validation         |
|--------------------|---------------|----------|------------------------------------|--------------------|
| `ticketInstanceId` | string (UUID) | Yes      | Unique ID of the ticket to download | Must be valid UUID |

**Query Parameters**:
| Parameter | Type   | Required | Description                                                                    | Default    |
|-----------|--------|----------|--------------------------------------------------------------------------------|------------|
| `mode`    | string | No       | `download` prompts a save dialog; `inline` opens the PDF in the browser tab   | `download` |

**Success Response**: Binary PDF file (not JSON)

**Success Response Headers**:
| Header                | Value                                                                   |
|-----------------------|-------------------------------------------------------------------------|
| `Content-Type`        | `application/pdf`                                                       |
| `Content-Disposition` | `attachment; filename="ticket-{series}.pdf"` or `inline; ...` by mode  |

**PDF Content Includes**:
- Event name, date, and venue
- Attendee name
- Ticket series and type
- QR code (RSA-signed JWT) for gate scanning
- Ticket validity period

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You don't have permission to access this ticket",
  "action_time": "2026-05-23T10:30:45",
  "data": "You don't have permission to access this ticket"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or expired Bearer token
- `403 FORBIDDEN`: Ticket does not belong to the authenticated user
- `404 NOT_FOUND`: Ticket with given ID does not exist
- `500 INTERNAL_SERVER_ERROR`: PDF generation failed

---

## 4. Get Event Orders (Organizer)

**Purpose**: Retrieve a paginated list of all booking orders for a specific event with filtering by status, buyer type, and ticket type, plus aggregated revenue and order stats.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/e-events/booking-orders/event/{eventId}`

**Access Level**: 🔒 Protected (Event Organizer or Admin)

**Authentication**: Bearer Token

**Request Headers**:
| Header          | Type   | Required | Description                             |
|-----------------|--------|----------|-----------------------------------------|
| `Authorization` | string | Yes      | Bearer token (format: `Bearer <token>`) |

**Path Parameters**:
| Parameter | Type          | Required | Description       | Validation         |
|-----------|---------------|----------|-------------------|--------------------|
| `eventId` | string (UUID) | Yes      | Target event UUID | Must be valid UUID |

**Query Parameters**:
| Parameter      | Type          | Required | Description                                        | Validation                              | Default |
|----------------|---------------|----------|----------------------------------------------------|-----------------------------------------|---------|
| `status`       | string (enum) | No       | Filter by booking status                          | `CONFIRMED`, `CANCELLED`                | —       |
| `buyerType`    | string (enum) | No       | Filter by how the ticket was purchased            | `SYSTEM_USER`, `AT_DOOR`                | —       |
| `ticketTypeId` | string (UUID) | No       | Filter orders that include a specific ticket type | Must be valid UUID                      | —       |
| `search`       | string        | No       | Search by buyer name, email, or booking reference | —                                       | —       |
| `page`         | integer       | No       | Zero-based page index                             | Min: 0                                  | `0`     |
| `size`         | integer       | No       | Number of orders per page                         | Min: 1                                  | `20`    |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event orders retrieved",
  "action_time": "2026-05-23T10:30:45",
  "data": {
    "eventId": "770e8400-e29b-41d4-a716-446655440002",
    "eventTitle": "East African Tech Summit 2025",
    "appliedFilters": {
      "status": "CONFIRMED",
      "buyerType": null,
      "ticketTypeId": null,
      "ticketTypeName": null,
      "search": null
    },
    "stats": {
      "totalOrders": 142,
      "totalTicketsSold": 318,
      "totalRevenue": 47500.00,
      "confirmedOrders": 139,
      "cancelledOrders": 3,
      "refundedOrders": 0,
      "onlineOrders": 128,
      "atDoorOrders": 14
    },
    "orders": [
      {
        "bookingId": "550e8400-e29b-41d4-a716-446655440000",
        "bookingReference": "EVT-A3F4B21C",
        "status": "CONFIRMED",
        "bookedAt": "2025-12-11T10:30:45",
        "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "buyer": {
          "buyerName": "John Doe",
          "buyerEmail": "john@example.com",
          "buyerType": "SYSTEM_USER"
        },
        "totalTickets": 2,
        "checkedInCount": 1,
        "total": 200.00,
        "ticketTypes": [
          { "ticketTypeName": "VIP Pass", "quantity": 1 },
          { "ticketTypeName": "General Admission", "quantity": 1 }
        ]
      },
      {
        "bookingId": "550e8400-e29b-41d4-a716-446655440099",
        "bookingReference": "EVT-C7E1F34A",
        "status": "CONFIRMED",
        "bookedAt": "2025-12-10T18:15:00",
        "formResponseId": null,
        "buyer": {
          "buyerName": "Grace Njeri",
          "buyerEmail": null,
          "buyerType": "AT_DOOR"
        },
        "totalTickets": 1,
        "checkedInCount": 1,
        "total": 50.00,
        "ticketTypes": [
          { "ticketTypeName": "General Admission", "quantity": 1 }
        ]
      }
    ],
    "pagination": {
      "currentPage": 0,
      "pageSize": 20,
      "totalPages": 8,
      "totalElements": 142,
      "hasNext": true,
      "hasPrevious": false
    }
  }
}
```

**Success Response Fields**:
| Field                          | Description                                                                 |
|--------------------------------|-----------------------------------------------------------------------------|
| `eventId`                      | UUID of the queried event                                                   |
| `eventTitle`                   | Event title                                                                 |
| `appliedFilters`               | Echo of the active filter values (null = not filtered)                     |
| `appliedFilters.status`        | Active booking status filter                                                |
| `appliedFilters.buyerType`     | Active buyer type filter                                                    |
| `appliedFilters.ticketTypeId`  | Active ticket type UUID filter                                              |
| `appliedFilters.ticketTypeName`| Resolved name of the filtered ticket type                                  |
| `appliedFilters.search`        | Active search string                                                        |
| `stats`                        | Aggregated stats across ALL orders for the event (not just current page)   |
| `stats.totalOrders`            | Total booking count                                                         |
| `stats.totalTicketsSold`       | Total individual tickets across all bookings                                |
| `stats.totalRevenue`           | Sum of all order totals                                                     |
| `stats.confirmedOrders`        | Orders with status `CONFIRMED`                                              |
| `stats.cancelledOrders`        | Orders with status `CANCELLED`                                              |
| `stats.refundedOrders`         | Orders with status `REFUNDED`                                               |
| `stats.onlineOrders`           | Orders placed online (`SYSTEM_USER` buyer type)                            |
| `stats.atDoorOrders`           | Orders sold at the gate (`AT_DOOR` buyer type)                             |
| `orders`                       | Paginated list of order rows                                                |
| `orders[].bookingId`           | Booking UUID                                                                |
| `orders[].bookingReference`    | Short readable code (e.g., `EVT-A3F4B21C`)                                 |
| `orders[].status`              | Booking status string                                                       |
| `orders[].bookedAt`            | When booking was created — LocalDateTime                                    |
| `orders[].formResponseId`      | UUID of the buyer's applicant form response — `null` if no form or not yet submitted |
| `orders[].buyer.buyerName`     | Buyer's name                                                                |
| `orders[].buyer.buyerEmail`    | Buyer's email (null for AT_DOOR)                                            |
| `orders[].buyer.buyerType`     | `SYSTEM_USER` or `AT_DOOR`                                                 |
| `orders[].totalTickets`        | Number of tickets in this order                                             |
| `orders[].checkedInCount`      | Number of tickets checked in at least once                                  |
| `orders[].total`               | Order total amount                                                          |
| `orders[].ticketTypes`         | Breakdown of ticket types in this order                                     |
| `orders[].ticketTypes[].ticketTypeName` | Name of the ticket type                                          |
| `orders[].ticketTypes[].quantity`       | How many of this type are in the order                           |
| `pagination.currentPage`       | Zero-based current page index                                               |
| `pagination.pageSize`          | Number of items per page                                                    |
| `pagination.totalPages`        | Total number of pages                                                       |
| `pagination.totalElements`     | Total matching orders                                                       |
| `pagination.hasNext`           | `true` if a next page exists                                                |
| `pagination.hasPrevious`       | `true` if a previous page exists                                            |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Access denied: you are not the organizer of this event",
  "action_time": "2026-05-23T10:30:45",
  "data": "Access denied: you are not the organizer of this event"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or expired Bearer token
- `403 FORBIDDEN`: Authenticated user is not the event organizer or an admin
- `404 NOT_FOUND`: Event with given ID does not exist

---

## Ticket Series Generation

**Format**: `{TICKET_CODE}-{COUNTER}`

The ticket code is the first 5 characters of the ticket type name (uppercased, spaces removed). The counter is a zero-padded incrementing integer per ticket type, managed with pessimistic locking to prevent duplicates.

**Examples**:
- `VIP Pass` → `VIP-0001`, `VIP-0002`, `VIP-0003`
- `General Admission` → `GENER-0001`, `GENER-0042`
- `Early Bird` → `EARLY-0001`

The counter never resets — ticket series are globally unique per type forever.

---

## JWT-Signed QR Codes

Each ticket's `qrCode` field is a full RSA-signed JWT (2048-bit key pair generated when the event is published). The JWT payload contains:

- `ticketInstanceId`, `ticketTypeId`, `ticketTypeName`, `ticketSeries`
- `eventId`, `eventName`, `eventStartDateTime`
- `attendeeName`, `attendeeEmail`, `attendeePhone`, `attendanceMode`
- `bookingReference`
- `eventSchedules` — array of day objects for multi-day events:
```json
[
  {
    "dayName": "Day 1 - Opening Day",
    "startDateTime": "2025-12-15T09:00:00+03:00",
    "endDateTime": "2025-12-15T18:00:00+03:00",
    "description": "Conference Opening & Keynotes"
  }
]
```
- `validFrom`, `validUntil`

**Why JWT?** Scanners can verify the ticket offline using the public key without a database lookup. Any tampering breaks the signature. The embedded `eventSchedules` lets scanners enforce per-day check-in rules without an internet connection.

---

## Multi-Day Check-Ins

For multi-day events, each ticket can be checked in once per day. The `checkIns` array stores the full history. The `hasBeenCheckedIn`, `lastCheckedInAt`, `lastCheckedInBy`, `lastCheckInLocation`, and `lastCheckInDayName` convenience fields reflect the most recent entry.

- `dayName` in a check-in record must match one of the `eventSchedules` day names embedded in the JWT
- A ticket cannot be checked in twice on the same day
- Ticket status stays `ACTIVE` across days and transitions to `USED` after all expected check-ins

---

## Event Snapshots

Event details (title, venue, times, organizer contacts) are captured into the booking record at the moment of purchase and never change. This means:

- PDF tickets always show what the customer actually bought
- Bookings remain accurate even if the organizer later renames or reschedules the event
- The `event` and `organizer` blocks in `BookingOrderResponse` always reflect booking-time state

---

## Booking Creation Flow

Bookings are created automatically by `EventPaymentCompletedListener` after a successful payment — never through a direct API call. The flow:

1. Fetch checkout session, event, and ticket type from the database
2. Generate a unique booking reference (`EVT-{8-char-UUID}`)
3. Create ticket instances (one per quantity unit, tagged with attendee and buyer info)
4. Sign a JWT QR code for each ticket using the event's RSA private key
5. Snapshot current event and organizer details into the booking record
6. Save the booking and mark the checkout session as `COMPLETED`
7. Publish a `BookingCreatedEvent` to trigger email notifications to buyer, attendees, and organizer

The entire flow runs in a single transaction — no partial bookings are created.

---

## Access Control Rules

| Actor                         | Endpoint                        | Allowed? |
|-------------------------------|---------------------------------|----------|
| Booking owner (customer)      | `GET /booking-orders/{id}`      | Yes      |
| Event organizer               | `GET /booking-orders/{id}`      | Yes      |
| Admin (SUPER_ADMIN/STAFF_ADMIN)| `GET /booking-orders/{id}`     | Yes      |
| Other authenticated user      | `GET /booking-orders/{id}`      | No — 403 |
| Any authenticated user        | `GET /my-bookings`              | Yes (own bookings only) |
| Ticket owner                  | `GET /tickets/{id}/pdf`         | Yes      |
| Other authenticated user      | `GET /tickets/{id}/pdf`         | No — 403 |
| Event organizer               | `GET /booking-orders/event/{id}`| Yes      |
| Admin                         | `GET /booking-orders/event/{id}`| Yes      |
| Non-organizer user            | `GET /booking-orders/event/{id}`| No — 403 |

---

## Booking and Ticket Status

**Booking Status**:
| Status      | Description                       |
|-------------|-----------------------------------|
| `CONFIRMED` | Active booking, tickets are valid |
| `CANCELLED` | Booking cancelled (placeholder — cancellation logic not yet implemented) |

**Ticket Instance Status**:
| Status      | Description                                                         |
|-------------|---------------------------------------------------------------------|
| `ACTIVE`    | Ticket ready for use; can be scanned                               |
| `USED`      | Ticket has been checked in; still scannable for additional days     |
| `CANCELLED` | Ticket cancelled; cannot be used for entry                         |

---

## Date/Time Formats

| Type            | Format                        | Example                        | Used For                                    |
|-----------------|-------------------------------|--------------------------------|---------------------------------------------|
| `LocalDateTime` | `YYYY-MM-DDTHH:mm:ss`         | `2025-12-15T09:00:00`          | `bookedAt`, `cancelledAt`, event snapshots  |
| `ZonedDateTime` | `YYYY-MM-DDTHH:mm:ss±HH:mm`   | `2025-12-15T09:00:00+03:00`    | `validFrom`, `validUntil`, `checkInTime`    |

`ZonedDateTime` fields carry the event's local timezone offset and display correctly regardless of the viewer's locale.

---

## Endpoint Summary

| # | Endpoint | Access |
|---|---|---|
| 1 | `GET /e-events/booking-orders/{bookingId}` | Owner / Organizer / Admin |
| 2 | `GET /e-events/booking-orders/my-bookings` | Any authenticated user |
| 3 | `GET /e-events/booking-orders/tickets/{ticketInstanceId}/pdf` | Ticket owner |
| 4 | `GET /e-events/booking-orders/event/{eventId}` | Event organizer / Admin |

# Event Check-In System API

**Author**: Josh, Lead Backend Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Event Check-In System API provides secure ticket validation and scanner management for event entry. This API enables organizers to generate registration tokens (like WhatsApp's "Link Device"), scanners to register for events, and perform real-time ticket validation with JWT signature verification. The system supports multi-day events with per-day check-in tracking, device fingerprinting for security, automatic scanner revocation, and comprehensive duplicate detection.

**Hints**: 
- **Registration Flow**: Organizer generates token → Scanner scans QR → Scanner registers → Automatic revocation of old scanners
- **Device Security**: Fingerprint validation prevents credential theft
- **JWT Validation**: RSA-signed tickets verified offline-capable
- **Multi-Day Events**: Separate check-in per event day
- **One Device Rule**: One ACTIVE scanner per device across all events
- **Check-In Strategies**: 5 strategies (HOURS_BEFORE, SPECIFIC_TIME, ALL_DAY, EXACT_TIME, AS_DAY_START)
- **Duplicate Detection**: Prevents same-day re-entry per event day
- **Scanner Stats**: Tracks successful/failed scans automatically
- **Auto-Revocation**: Old scanners revoked when device registers for new event
- **Organizer Only**: Only event organizers can generate tokens and manage scanners

---

## API Overview

### Registration Token Endpoints
1. **POST** `/check-in/tokens/generate` - Generate registration token (organizer)
2. **GET** `/check-in/tokens/validate/{token}` - Validate registration token

### Scanner Management Endpoints
3. **POST** `/check-in/scanners/register` - Register scanner device
4. **GET** `/check-in/scanners/event/{eventId}` - Get all scanners for event
5. **GET** `/check-in/scanners/event/{eventId}/active` - Get active scanners
6. **POST** `/check-in/scanners/{scannerId}/revoke` - Revoke scanner

### Ticket Validation Endpoint
7. **POST** `/check-in/validate` - Validate ticket and check-in

---

## Response Structures

### RegistrationTokenResponse
```json
{
  "tokenId": "uuid",
  "token": "REG-ABC12345-XYZ67890",
  "eventId": "uuid",
  "eventName": "East African Tech Summit 2025",
  "scannerName": "Gate A - Main Entrance",
  "expiresAt": "2025-12-11T10:35:00Z",
  "validityMinutes": 5,
  "remainingSeconds": 240,
  "qrCodeData": "scannerapp://register?token=REG-ABC12345-XYZ67890",
  "isValid": true,
  "used": false
}
```

### ScannerResponse
```json
{
  "scannerId": "uuid-string",
  "name": "Gate A - Main Entrance",
  "eventId": "uuid",
  "eventName": "East African Tech Summit 2025",
  "status": "ACTIVE",
  "deviceFingerprint": "abc123def456...",
  "createdAt": "2025-12-11T10:30:00Z",
  "credentials": "eyJhbGc...",
  "publicKey": "MIIBIjANBgkqhkiG9w0...",
  "revocationReason": null
}
```

### ValidateTicketResponse
```json
{
  "valid": true,
  "status": "VALID",
  "message": "✅ Entry granted for Day 1 - Opening Day. Welcome!",
  "ticketInstanceId": "uuid",
  "ticketTypeName": "VIP Pass",
  "ticketSeries": "VIP-0001",
  "attendeeName": "John Doe",
  "attendeeEmail": "john@example.com",
  "eventName": "East African Tech Summit 2025",
  "bookingReference": "EVT-A3F4B21C",
  "alreadyCheckedIn": false,
  "previousCheckInTime": null,
  "previousCheckInLocation": null,
  "currentCheckInTime": "2025-12-15T09:15:00+03:00",
  "validationMode": "ONLINE",
  "scannerName": "Gate A - Main Entrance",
  "dayName": "Day 1 - Opening Day"
}
```

---

## Endpoints

## 1. Generate Registration Token
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px;">POST</span> `/check-in/tokens/generate`

**Access**: 🔒 Event Organizer Only

**Request**:
```json
{
  "eventId": "uuid",
  "scannerName": "Gate A - Main Entrance"
}
```

**Success Response**: Returns RegistrationTokenResponse

**Behavior**:
- Validates user is event organizer
- Validates event has RSA keys (must be published)
- Generates token: `REG-{8-UUID}-{8-UUID}`
- Sets expiry: 5 minutes (configurable)
- Returns QR code data for scanner app

**Errors**:
- `403 FORBIDDEN`: Not event organizer
- `404 NOT_FOUND`: Event not found
- `422`: Event not published or no RSA keys

---

## 2. Validate Registration Token
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/check-in/tokens/validate/{token}`

**Access**: 🔓 Public (for scanner apps)

**Success Response**: Returns RegistrationTokenResponse with validity status

**Use Case**: Scanner app validates token before registration

---

## 3. Register Scanner
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px;">POST</span> `/check-in/scanners/register`

**Access**: 🔓 Public (uses registration token)

**Request**:
```json
{
  "registrationToken": "REG-ABC12345-XYZ67890",
  "deviceFingerprint": "abc123def456hash",
  "scannerName": "Gate A - Main Entrance",
  "deviceInfo": "{\"model\":\"iPhone 13\",\"os\":\"iOS 15\"}"
}
```

**Success Response**: Returns ScannerResponse with credentials

**Behavior**:
1. Validates device fingerprint (10-255 chars)
2. Validates token (not used, not expired)
3. **Auto-revokes** any ACTIVE scanner with same device fingerprint
4. Generates scanner ID (UUID)
5. Generates JWT credentials (1 year validity)
6. Marks token as used (one-time use)

**Key Rule**: One device → One ACTIVE scanner (across all events)

**Errors**:
- `400 BAD_REQUEST`: Invalid fingerprint, token used/expired
- `404 NOT_FOUND`: Token not found

---

## 4. Get Scanners for Event
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/check-in/scanners/event/{eventId}`

**Access**: 🔒 Event Organizer Only

**Success Response**: Returns array of ScannerResponse

**Includes**: All scanners (ACTIVE + REVOKED)

---

## 5. Get Active Scanners
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/check-in/scanners/event/{eventId}/active`

**Access**: 🔒 Event Organizer Only

**Success Response**: Returns array of ScannerResponse (ACTIVE only)

---

## 6. Revoke Scanner
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px;">POST</span> `/check-in/scanners/{scannerId}/revoke?reason=Suspicious activity`

**Access**: 🔒 Event Organizer Only

**Success Response**: 200 OK with confirmation message

**Behavior**:
- Changes status to REVOKED (permanent)
- Records revocation reason and timestamp
- Scanner can no longer validate tickets

---

## 7. Validate Ticket and Check-In
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px;">POST</span> `/check-in/validate`

**Access**: 🔒 Scanner credentials required

**Request**:
```json
{
  "jwtToken": "eyJhbGc...",
  "scannerId": "uuid-string",
  "deviceFingerprint": "abc123def456hash",
  "checkInLocation": "Gate A"
}
```

**Success Response**: Returns ValidateTicketResponse

**Validation Flow**:
1. **Validate Scanner**: Active status, fingerprint match
2. **Verify JWT**: RSA signature with event's public key
3. **Find Current Day**: Match current time to event schedules
4. **Find Booking**: Locate ticket in database
5. **Check Duplicate**: Already checked in for this day?
6. **Create Check-In**: Add check-in record for current day
7. **Update Stats**: Increment scanner counters

**Check-In Strategies**:
- **HOURS_BEFORE**: X hours before event + late grace period
- **SPECIFIC_TIME**: Daily window (e.g., 08:00-23:00)
- **ALL_DAY**: Anytime on event date (00:00-23:59)
- **EXACT_TIME**: Only during event start-end
- **AS_DAY_START**: From day start (00:00) until event end + grace

**Validation Statuses**:
- **VALID**: ✅ Entry granted
- **DUPLICATE**: ❌ Already checked in for this day
- **INVALID_SIGNATURE**: ❌ JWT signature failed
- **EXPIRED**: ❌ Ticket validity expired
- **NOT_FOUND**: ❌ Ticket not in database
- **REVOKED**: ❌ Scanner revoked

**Response Examples**:

*Success (VALID)*:
```json
{
  "success": true,
  "message": "✅ Entry granted for Day 1 - Opening Day. Welcome!",
  "data": {
    "valid": true,
    "status": "VALID",
    "attendeeName": "John Doe",
    "dayName": "Day 1 - Opening Day",
    "currentCheckInTime": "2025-12-15T09:15:00+03:00"
  }
}
```

*Duplicate Check-In*:
```json
{
  "success": false,
  "message": "❌ Ticket already used for Day 1 - Opening Day. Entry denied.",
  "data": {
    "valid": false,
    "status": "DUPLICATE",
    "alreadyCheckedIn": true,
    "previousCheckInTime": "2025-12-15T09:00:00+03:00",
    "previousCheckInLocation": "Gate A"
  }
}
```

---

## System Flows

### Registration Flow

**Step 1: Organizer Generates Token**
```
Organizer → POST /tokens/generate
← Returns: Token + QR code data
```

**Step 2: Scanner Scans QR Code**
```
Scanner App → Scans QR code
Extracts: "scannerapp://register?token=REG-..."
```

**Step 3: Scanner Validates Token (Optional)**
```
Scanner App → GET /tokens/validate/{token}
← Confirms: Token valid, event details
```

**Step 4: Scanner Registers**
```
Scanner App → POST /scanners/register
Sends: Token, device fingerprint, name
← Receives: Scanner credentials (JWT)
```

**Step 5: Scanner Stores Credentials**
```
Scanner App → Saves: credentials, publicKey
Ready to validate tickets offline
```

### Ticket Validation Flow

**Step 1: Scanner Scans Ticket QR**
```
Scanner → Reads JWT from QR code
```

**Step 2: Offline Validation (Optional)**
```
Scanner → Verifies JWT signature with public key
Checks: Expiry, event match
```

**Step 3: Online Check-In**
```
Scanner → POST /validate
Sends: JWT, scannerId, fingerprint
```

**Step 4: System Validates**
```
System → Validates scanner status
System → Verifies JWT signature
System → Finds current event day
System → Checks duplicate
System → Records check-in
```

**Step 5: Response to Scanner**
```
System → Returns validation result
Scanner → Shows success/error to staff
```

---

## Multi-Day Event Support

### How It Works

**Single-Day Event**:
- One check-in expected
- Status: ACTIVE → USED after check-in

**Multi-Day Event** (e.g., 3-day festival):
- Multiple check-ins allowed (one per day)
- Each day tracked separately
- Ticket stays ACTIVE throughout

**Example: 3-Day Festival**

JWT contains schedules:
```json
{
  "eventSchedules": [
    {
      "dayName": "Day 1 - Friday Night",
      "startDateTime": "2025-12-15T18:00:00+03:00",
      "endDateTime": "2025-12-15T23:59:00+03:00"
    },
    {
      "dayName": "Day 2 - Saturday",
      "startDateTime": "2025-12-16T10:00:00+03:00",
      "endDateTime": "2025-12-16T23:59:00+03:00"
    },
    {
      "dayName": "Day 3 - Sunday",
      "startDateTime": "2025-12-17T10:00:00+03:00",
      "endDateTime": "2025-12-17T20:00:00+03:00"
    }
  ]
}
```

**Check-In Timeline**:
```
Friday 18:30 → Check-in for "Day 1 - Friday Night" ✅
Friday 19:00 → Duplicate for "Day 1" ❌
Saturday 11:00 → Check-in for "Day 2 - Saturday" ✅
Saturday 15:00 → Duplicate for "Day 2" ❌
Sunday 12:00 → Check-in for "Day 3 - Sunday" ✅
```

**Validation Logic**:
1. System finds current time: Saturday 11:00
2. Matches to event schedule: "Day 2 - Saturday"
3. Checks if ticket already checked in for "Day 2"
4. If no → Allow check-in
5. If yes → Reject as duplicate

---

## Check-In Window Strategies

### 1. HOURS_BEFORE (Default)
**Configuration**:
- earlyCheckInHours: 2 (default)
- lateCheckInMinutes: 30 (default)

**Window**: 2 hours before event until 30 minutes after event ends

**Example**:
```
Event: 09:00 - 18:00
Check-in allowed: 07:00 - 18:30
```

**Use Case**: Conferences, concerts

### 2. SPECIFIC_TIME
**Configuration**:
- checkInOpensAt: "08:00"
- checkInClosesAt: "23:00"

**Window**: Same time window each event day

**Example**:
```
3-day event (Dec 15-17)
Check-in allowed: 08:00-23:00 each day
```

**Use Case**: Multi-day festivals with consistent entry hours

### 3. ALL_DAY
**Configuration**: None needed

**Window**: Entire event date (00:00 - 23:59)

**Example**:
```
Event date: Dec 15
Check-in allowed: Dec 15 00:00 - Dec 15 23:59
```

**Use Case**: All-day events, exhibitions

### 4. EXACT_TIME
**Configuration**: None

**Window**: Only during event start-end times

**Example**:
```
Event: 14:00 - 17:00
Check-in allowed: 14:00 - 17:00 only
```

**Use Case**: Strict timing events

### 5. AS_DAY_START
**Configuration**:
- lateCheckInMinutes: 30 (default)

**Window**: From start of event day (00:00) until event end + grace

**Example**:
```
Event: Dec 15 18:00 - 23:00
Check-in allowed: Dec 15 00:00 - 23:30
```

**Use Case**: Evening events with early arrival

---

## Security Features

### Device Fingerprinting
**Purpose**: Prevent credential theft

**How It Works**:
1. Scanner generates fingerprint from device hardware
2. Fingerprint sent with every request
3. System validates fingerprint matches registered device
4. Mismatch → Reject (credentials stolen)

**Fingerprint Components** (example):
```javascript
const fingerprint = SHA256(
  deviceModel + 
  osVersion + 
  hardwareId + 
  appInstallId
);
```

### JWT Credentials
**Scanner Credentials** (1 year validity):
```json
{
  "scannerId": "uuid",
  "eventId": "uuid",
  "type": "scanner_credential",
  "iat": 1702300000,
  "exp": 1733836000
}
```

**Signed with**: Event's RSA private key

**Used for**: Scanner authentication to API

### Ticket JWT Verification
**Process**:
1. Scanner receives ticket JWT (QR code)
2. Scanner verifies signature with event's public key
3. Scanner can validate offline (no internet needed)
4. Scanner sends to API for check-in recording

**Benefits**:
- Offline validation capability
- Cannot forge tickets
- Cannot reuse tokens (duplicate tracking)
- Tamper-proof (signature validation)

---

## Auto-Revocation System

### The Rule
**One device → One ACTIVE scanner at a time (across ALL events)**

### Scenarios

**Scenario 1: Device Registers for Different Event**
```
Device ABC has ACTIVE scanner for Event 1
Device ABC registers for Event 2
→ System revokes Event 1 scanner automatically
→ Creates new scanner for Event 2
```

**Scenario 2: Same Event Re-Registration**
```
Device ABC has ACTIVE scanner for Event 1
Device ABC registers again for Event 1
→ System revokes old scanner
→ Creates new scanner (new credentials)
```

**Revocation Message**:
```
"Automatically revoked: Device registered as new scanner for event 'East African Tech Summit 2025'"
```

### Why This Rule?

**Prevents**:
- Device scanning tickets for multiple events simultaneously
- Confusion about which event device is working
- Credential misuse across events

**Allows**:
- Device switching between events (auto-handled)
- Fresh start for each event
- Clean scanner sessions

---

## Scanner Statistics

### Auto-Tracked Metrics
- **totalScans**: All scan attempts
- **successfulScans**: Valid entries
- **failedScans**: Duplicates, invalid tickets
- **lastScanAt**: Most recent scan timestamp
- **lastSyncedAt**: Last API contact

### Success Rate Calculation
```
successRate = (successfulScans / totalScans) * 100
```

### Updated Automatically
- Each validation attempt updates counters
- Successful → successfulScans++, totalScans++
- Failed → failedScans++, totalScans++

---

## Error Codes Summary

### Registration Token Errors
- **400**: Token expired/used, invalid format
- **403**: Not event organizer
- **404**: Token/event not found
- **422**: Event not published, no RSA keys

### Scanner Registration Errors
- **400**: Invalid fingerprint (too short/long)
- **400**: Token expired/used
- **404**: Token not found
- **422**: Invalid scanner name

### Ticket Validation Errors
- **INVALID_SIGNATURE**: JWT signature failed
- **DUPLICATE**: Already checked in for this day
- **EXPIRED**: Ticket validity expired
- **NOT_FOUND**: Ticket not in database
- **REVOKED**: Scanner revoked

---

## Best Practices

### For Organizers
✅ Generate tokens right before scanner setup  
✅ Use descriptive scanner names (e.g., "Gate A - Main")  
✅ Monitor scanner activity via dashboard  
✅ Revoke suspicious scanners immediately  
✅ Test scanner before event starts  

### For Scanner App Developers
✅ Generate stable device fingerprint  
✅ Store credentials securely (encrypted)  
✅ Implement offline validation first  
✅ Sync check-ins when online  
✅ Show clear success/error messages  
✅ Handle device fingerprint mismatch gracefully  

### For Event Staff
✅ Keep scanners charged  
✅ Verify scanner name matches gate  
✅ Watch for duplicate warnings  
✅ Report technical issues immediately  
✅ Use backup manual verification if needed  

---

## Quick Reference

### Token Lifetime
- Registration token: **5 minutes**
- Scanner credentials: **1 year**
- Ticket JWT: **Event validity period**

### Device Fingerprint
- Min length: **10 characters**
- Max length: **255 characters**
- Format: **Stable hash of device properties**

### Scanner Name
- Min length: **3 characters**
- Max length: **200 characters**
- Examples: "Gate A", "VIP Entrance", "Main Hall Scanner"

### Status Flow
```
Registration Token: unused → used (one-time)
Scanner: ACTIVE → REVOKED (permanent)
Validation: VALID | DUPLICATE | INVALID_SIGNATURE | EXPIRED | NOT_FOUND | REVOKED
```

---

## Integration Guide

### Organizer Dashboard Integration
```javascript
// 1. Generate token
POST /check-in/tokens/generate
{
  eventId: "uuid",
  scannerName: "Gate A"
}

// 2. Display QR code
<QRCode value={response.qrCodeData} />

// 3. Show token expiry countdown
remainingTime = response.remainingSeconds

// 4. List active scanners
GET /check-in/scanners/event/{eventId}/active
```

### Scanner App Integration
```javascript
// 1. Scan registration QR code
const token = extractTokenFromQR(qrData);

// 2. Register device
POST /check-in/scanners/register
{
  registrationToken: token,
  deviceFingerprint: generateFingerprint(),
  scannerName: "Gate A",
  deviceInfo: JSON.stringify(deviceDetails)
}

// 3. Store credentials
secureStorage.save('credentials', response.credentials);
secureStorage.save('publicKey', response.publicKey);
secureStorage.save('scannerId', response.scannerId);

// 4. Validate tickets
while (eventActive) {
  const ticketJWT = scanTicketQR();
  
  // Offline validation
  const offlineValid = verifyJWT(ticketJWT, publicKey);
  
  if (offlineValid) {
    // Online check-in
    POST /check-in/validate
    {
      jwtToken: ticketJWT,
      scannerId: scannerId,
      deviceFingerprint: generateFingerprint(),
      checkInLocation: "Gate A"
    }
  }
}
```

---

## Conclusion

The Event Check-In System provides enterprise-grade security with:

✅ **WhatsApp-Style Registration**: Scan QR to link device  
✅ **Device Security**: Fingerprint validation prevents theft  
✅ **Offline Capable**: JWT verification without internet  
✅ **Multi-Day Support**: Per-day check-in tracking  
✅ **Auto-Revocation**: Clean scanner sessions per event  
✅ **Flexible Timing**: 5 check-in window strategies  
✅ **Duplicate Prevention**: Same-day re-entry blocked  
✅ **Real-Time Stats**: Automatic success/fail tracking

# Organizer Analytics API

**Author**: Josh, Lead Backend Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Organizer Analytics API provides comprehensive financial and performance insights for event organizers. This API enables organizers to track total collections across all events, analyze revenue by event with filters, monitor individual event performance with detailed metrics, and visualize revenue trends over time with monthly/yearly breakdowns. The system calculates escrow holdings, released payments, ticket sales, attendance rates, and sell-out percentages automatically.

**Hints**: 
- **Organizer Only**: All endpoints restricted to event organizers
- **Auto-Calculations**: Revenue, attendance, sell-out rates computed automatically
- **Escrow Tracking**: Separate tracking for held vs released funds
- **Multi-Event**: Aggregates data across all organizer's events
- **Time Filters**: Filter by status, date range, year
- **Pagination**: Event lists paginated (default 20 per page)
- **Top Performer**: Identifies highest revenue event
- **Trend Analysis**: Monthly/yearly revenue patterns
- **Real-Time**: Updates reflect latest bookings and check-ins

---

## Response Structures

### CollectionSummaryResponse
```json
{
  "eventMetrics": {
    "totalEvents": 15,
    "upcomingEvents": 5,
    "ongoingEvents": 1,
    "completedEvents": 8,
    "cancelledEvents": 1
  },
  "collectionMetrics": {
    "totalTicketsSold": 2500,
    "totalRevenue": 5000000.00,
    "inEscrow": 1200000.00,
    "released": 3800000.00,
    "refunded": 0.00,
    "pendingRefunds": 0.00
  },
  "topEvent": {
    "eventId": "uuid",
    "eventTitle": "East African Tech Summit 2025",
    "revenue": 1500000.00,
    "ticketsSold": 500,
    "attendanceRate": 92.5
  }
}
```

### EventRevenueResponse
```json
{
  "events": [
    {
      "eventId": "uuid",
      "eventTitle": "East African Tech Summit 2025",
      "eventDate": "2025-12-15T09:00:00",
      "status": "PUBLISHED",
      "ticketsSold": 500,
      "totalRevenue": 1500000.00,
      "inEscrow": 1500000.00,
      "released": 0.00,
      "refunded": 0.00,
      "attendanceRate": 0.0,
      "totalCapacity": 1000,
      "sellOutPercentage": 50.0
    }
  ],
  "pagination": {
    "currentPage": 0,
    "pageSize": 20,
    "totalPages": 3,
    "totalElements": 45,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

### EventPerformanceResponse
```json
{
  "eventId": "uuid",
  "eventTitle": "East African Tech Summit 2025",
  "eventDate": "2025-12-15T09:00:00",
  "status": "COMPLETED",
  "financials": {
    "totalRevenue": 1500000.00,
    "inEscrow": 0.00,
    "released": 1500000.00,
    "refunded": 0.00,
    "averageTicketPrice": 3000.00
  },
  "ticketMetrics": {
    "totalCapacity": 1000,
    "totalSold": 500,
    "totalRemaining": 500,
    "sellOutPercentage": 50.0
  },
  "attendanceMetrics": {
    "totalTickets": 500,
    "checkedIn": 462,
    "noShows": 38,
    "attendanceRate": 92.4
  },
  "timeline": {
    "createdAt": "2025-10-01T10:00:00",
    "publishedAt": "2025-10-05T14:30:00",
    "firstSaleAt": "2025-10-06T09:15:00",
    "eventDate": "2025-12-15T09:00:00",
    "completedAt": "2025-12-15T18:00:00"
  }
}
```

### RevenueTrendResponse
```json
{
  "period": "MONTHLY",
  "totalEvents": 12,
  "trends": [
    {
      "label": "JAN",
      "year": 2025,
      "month": 1,
      "eventsCount": 2,
      "ticketsSold": 350,
      "revenue": 875000.00,
      "inEscrow": 0.00,
      "released": 875000.00,
      "averageAttendanceRate": 88.5,
      "averageSellOutRate": 62.0
    }
  ]
}
```

---

## Endpoints

## 1. Get Collection Summary
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/analytics/collections/summary`

**Access**: 🔒 Organizer Only

**Success Response**: Returns CollectionSummaryResponse

**Success Response Message**: "Collection summary retrieved"

**Behavior**:
- Aggregates ALL organizer's events
- Calculates escrow (PUBLISHED, HAPPENING events)
- Calculates released (COMPLETED events)
- Identifies top performer by revenue
- Real-time metrics from latest data

**Metrics Included**:
- Event counts by status
- Total tickets sold
- Total revenue (all time)
- Money in escrow (upcoming/ongoing)
- Money released (completed)
- Refunds (placeholder)
- Top performing event

---

## 2. Get Event Revenue
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/analytics/collections/by-event?status=PUBLISHED&startDate=2025-01-01&endDate=2025-12-31&page=0&size=20`

**Access**: 🔒 Organizer Only

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| status | string | No | Filter by event status (PUBLISHED, COMPLETED, etc.) |
| startDate | date (ISO) | No | Filter events from this date (2025-01-01) |
| endDate | date (ISO) | No | Filter events until this date (2025-12-31) |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 20) |

**Success Response**: Returns EventRevenueResponse with pagination

**Success Response Message**: "Event revenue retrieved"

**Behavior**:
- Lists organizer's events with revenue details
- Filters by status and date range
- Sorted by event date (newest first)
- Paginated results
- Includes capacity and sell-out metrics

---

## 3. Get Event Performance
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/analytics/performance/{eventId}`

**Access**: 🔒 Event Organizer Only

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Success Response**: Returns EventPerformanceResponse

**Success Response Message**: "Event performance retrieved"

**Behavior**:
- Validates organizer owns event
- Calculates comprehensive metrics
- Shows financial breakdown
- Displays ticket sales vs capacity
- Tracks attendance from check-ins
- Provides event timeline

**Metrics Sections**:
- **Financials**: Revenue, escrow, released, average price
- **Tickets**: Capacity, sold, remaining, sell-out %
- **Attendance**: Total, checked-in, no-shows, rate
- **Timeline**: Key dates from creation to completion

**Errors**:
- `403 FORBIDDEN`: Not event organizer
- `404 NOT_FOUND`: Event not found

---

## 4. Get Revenue Trends
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/analytics/trends?period=MONTHLY&year=2025`

**Access**: 🔒 Organizer Only

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| period | string | No | MONTHLY or YEARLY (default: MONTHLY) |
| year | integer | No | Year to analyze (default: current year) |

**Success Response**: Returns RevenueTrendResponse

**Success Response Message**: "Revenue trends retrieved"

**Behavior**:

**MONTHLY Period**:
- Shows all 12 months for specified year
- Data per month: events, tickets, revenue, escrow, released
- Calculates average attendance and sell-out rates

**YEARLY Period**:
- Shows all years organizer has events
- Data per year: total events, tickets, revenue
- Year-over-year comparison

**Each Period Includes**:
- Event count
- Tickets sold
- Total revenue
- Money in escrow
- Money released
- Average attendance rate
- Average sell-out rate

---

## Key Calculations

### Escrow vs Released

**In Escrow** (Not yet paid to organizer):
- PUBLISHED events (upcoming)
- HAPPENING events (ongoing)
- Formula: Sum of booking totals for non-completed events

**Released** (Paid to organizer):
- COMPLETED events only
- Formula: Sum of booking totals for completed events

**Example**:
```
Event A: PUBLISHED, Revenue: 500,000 TZS → In Escrow
Event B: HAPPENING, Revenue: 300,000 TZS → In Escrow  
Event C: COMPLETED, Revenue: 1,200,000 TZS → Released

Total Revenue: 2,000,000 TZS
In Escrow: 800,000 TZS
Released: 1,200,000 TZS
```

### Attendance Rate
```
Attendance Rate = (Checked-In Tickets / Total Tickets) × 100
```

**Example**:
- Total tickets: 500
- Checked-in: 462
- Attendance: 92.4%

### Sell-Out Percentage
```
Sell-Out % = (Tickets Sold / Total Capacity) × 100
```

**Example**:
- Capacity: 1000
- Sold: 500
- Sell-out: 50%

### Average Ticket Price
```
Average Price = Total Revenue / Total Tickets
```

**Example**:
- Revenue: 1,500,000 TZS
- Tickets: 500
- Average: 3,000 TZS

---

## Event Status Flow

```
DRAFT → PUBLISHED → HAPPENING → COMPLETED
   ↓                               ↓
CANCELLED                     (Revenue Released)
```

**Financial Impact by Status**:
- **DRAFT**: No bookings, no revenue
- **PUBLISHED**: Bookings allowed, revenue in escrow
- **HAPPENING**: Ongoing, revenue still in escrow
- **COMPLETED**: Revenue released to organizer
- **CANCELLED**: Refunds processed (if applicable)

---

## Top Performer Logic

**Selection Criteria**: Highest total revenue

**Metrics Included**:
- Event ID and title
- Total revenue (highest wins)
- Tickets sold
- Attendance rate

**Use Case**: Dashboard highlight showing best-performing event

---

## Use Cases

### Dashboard Overview
```
GET /analytics/collections/summary

Shows:
- Total events across all statuses
- Total revenue (all time)
- Current escrow balance
- Released payments
- Top performing event
```

### Event List with Filters
```
GET /analytics/collections/by-event?status=COMPLETED&page=0&size=20

Shows:
- All completed events
- Revenue details per event
- Attendance and sell-out rates
- Paginated for easy navigation
```

### Deep Dive on Specific Event
```
GET /analytics/performance/550e8400-e29b-41d4-a716-446655440000

Shows:
- Complete financial breakdown
- Ticket sales metrics
- Attendance statistics
- Event timeline
```

### Monthly Revenue Analysis
```
GET /analytics/trends?period=MONTHLY&year=2025

Shows:
- Revenue per month in 2025
- Event count trends
- Attendance patterns
- Sell-out trends
```

### Year-Over-Year Comparison
```
GET /analytics/trends?period=YEARLY

Shows:
- Revenue by year
- Growth trends
- Performance evolution
```

---

## Best Practices

### For Organizers
✅ Check collection summary regularly  
✅ Monitor escrow balance (upcoming payouts)  
✅ Track attendance rates to improve future events  
✅ Use trends to identify peak seasons  
✅ Review individual event performance post-event  

### For Developers
✅ Cache collection summary (refresh hourly)  
✅ Paginate event lists (default 20 items)  
✅ Display financial amounts clearly (currency formatting)  
✅ Show percentage metrics with 1 decimal (92.4%)  
✅ Provide export functionality for trends  

---

## Quick Reference

### HTTP Status Codes
- `200 OK`: Successful request
- `401 UNAUTHORIZED`: Authentication required
- `403 FORBIDDEN`: Not event organizer
- `404 NOT_FOUND`: Event not found

### Date Formats
- **Event Date**: LocalDateTime (2025-12-15T09:00:00)
- **Query Params**: ISO Date (2025-01-01)

### Currency
- All amounts in **TZS** (Tanzanian Shilling)
- Format: 1500000.00 (2 decimals)

### Percentage Format
- Format: 92.4 (1 decimal)
- Range: 0.0 - 100.0

### Pagination
- Zero-indexed pages (0, 1, 2...)
- Default size: 20
- Max size: 100 (recommended)

---

## Conclusion

The Organizer Analytics API provides comprehensive insights with:

✅ **Collection Summary**: Total revenue, escrow, and top performers  
✅ **Event Revenue**: Filterable, paginated event list  
✅ **Event Performance**: Deep dive into individual event metrics  
✅ **Revenue Trends**: Monthly/yearly pattern analysis  
✅ **Real-Time**: Auto-calculated from latest bookings  
✅ **Organizer-Focused**: Restricted to event owners only

# Events  Applicant Form API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-22
**Version**: v1.2

**Base URL**: `https://your-api-domain.com/api/v1/e-events/applicant-form`

**Short Description**: The Applicant Form API allows event organizers to attach an optional custom multi-page form to their event. When enabled, attendees are prompted to fill in the form as part of the registration flow. The form is fully scoped to the event — all endpoints use `eventId` as the primary key, so the organizer never needs to track a separate `formId`. The entire feature is optional; if never enabled, the event registration proceeds with no form step.

**Hints**:
- All endpoints require a valid Bearer token.
- **Form setup and management** (enable, pages, fields, options) require the authenticated user to be the **event organizer**. Any other user gets `403`.
- **Attendee submission** endpoints (`/start`, `/pages/{pageId}/save`, `/submit`, `/my-response`) require the event to be **PUBLISHED** and within its registration window (`registrationOpensAt` → `registrationClosesAt`).
- `enableForm` creates the underlying form with **no pages**. Pages must be added by the organizer via the page endpoints. The form must have at least one page before the event can be published.
- Use `GET /events/{eventId}/full-form` to load the complete form state (all pages, fields, and options) in a single call — intended for the form builder UI.
- `startSubmission` is idempotent — if the attendee already has a draft response it returns the existing one instead of creating a new one.
- `savePage` accepts a `?moveToNextPage=false/true` query parameter that controls validation behaviour. When `false` (default), answers are written to the draft with **no validation** — use this for background auto-save. When `true`, the page is validated first; if any field fails, errors are returned and nothing is saved; if all fields pass, answers are saved, the page is marked complete, and the index advances to the next page. This is the "Next" button action.
- `submit` requires **every page** to have been completed via `?moveToNextPage=true` at least once. Pages that were only auto-saved (without passing "Next" validation) will block submission with a `PAGE_INCOMPLETE` error. This ensures every page was validated before the form is accepted.
- Delete endpoints accept a `?hard=false` query parameter. Soft delete (`hard=false`, default) preserves historical response data. Hard delete (`hard=true`) permanently removes the record.
- `DROPDOWN`, `RADIO`, and `CHECKBOX` field types require options to be added separately after the field is created.
- The `HEADER` field type is a display-only section divider — it has no `required` flag and stores no answer data.

---


## Form Structure — Layers

```
┌─────────────────────────────────────────────────────────────────────────┐
│  FORM                                                                   │
│  title · description · settings · cover page                            │
│                                                                         │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  PAGE 1                                                           │  │
│  │  title · description · action button text                         │  │
│  │                                                                   │  │
│  │   ┌─────────────────────────────────────────────────────────┐     │  │
│  │   │ FIELD  (e.g. TEXT, EMAIL, DATE, FILE, RATING …)         │     │  │
│  │   │ label · placeholder · required · validation rules       │     │  │
│  │   └─────────────────────────────────────────────────────────┘     │  │
│  │                                                                   │  │
│  │   ┌─────────────────────────────────────────────────────────┐     │  │
│  │   │ FIELD  (DROPDOWN / RADIO / CHECKBOX)                    │     │  │
│  │   │ label · required                                        │     │  │
│  │   │   ┌──────────┐  ┌──────────┐  ┌──────────┐              │     │  │
│  │   │   │ Option 1 │  │ Option 2 │  │ Option 3 │  …           │     │  │
│  │   │   └──────────┘  └──────────┘  └──────────┘              │     │  │
│  │   └─────────────────────────────────────────────────────────┘     │  │
│  │                                                                   │  │
│  │   ┌─────────────────────────────────────────────────────────┐     │  │
│  │   │ FIELD  (HEADER — display only, no answer stored)        │     │  │
│  │   │ label (section divider text)                            │     │  │
│  │   └─────────────────────────────────────────────────────────┘     │  │
│  │                                                                   │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  PAGE 2                                                           │  │
│  │   ┌──────────────────────────┐  ┌──────────────────────────┐      │  │
│  │   │ FIELD                    │  │ FIELD                    │  …   │  │
│  │   └──────────────────────────┘  └──────────────────────────┘      │  │
│  └───────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  PAGE N  …                                                        │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘
```

**Rules at a glance:**
- A form has one or more **pages** displayed in `displayOrder`.
- Each page has one or more **fields** displayed in `displayOrder`.
- Fields of type `DROPDOWN`, `RADIO`, or `CHECKBOX` must have **options** added after the field is created — no other field type has options.
- `HEADER` is the only field type that stores no answer and cannot be marked required. It acts purely as a section label between other fields.
- Every other field type (`TEXT`, `TEXTAREA`, `EMAIL`, `PHONE`, `NUMBER`, `URL`, `DATE`, `TIME`, `DATETIME`, `RATING`, `FILE`) stores exactly one answer per attendee.

---

### Organizer Workflow

```
  [Organizer]
       │
       │  POST /events/{eventId}/enable
       ▼
  ┌─────────────────────────────────────┐
  │   Form ENABLED (empty)              │
  │   - Form created internally         │
  │   - No pages yet (pages: [])        │
  │   - Config saved (displayTime etc.) │
  └──────────────┬──────────────────────┘
                 │
                 │  Load builder state (on re-open):
                 │  GET  /events/{eventId}/full-form
                 │
                 │  Build the form structure:
                 │
                 │  POST /events/{eventId}/pages
                 │  POST /events/{eventId}/pages/{pageId}/fields
                 │  POST /events/{eventId}/fields/{fieldId}/options
                 │         (DROPDOWN / RADIO / CHECKBOX only)
                 │
                 │  (Optional bulk / clone shortcuts)
                 │  POST /events/{eventId}/pages/bulk
                 │  POST /events/{eventId}/pages/{pageId}/fields/bulk
                 │  POST /events/{eventId}/pages/{pageId}/clone
                 │  POST /events/{eventId}/fields/{fieldId}/clone
                 │
                 │  Preview before publishing:
                 │  GET  /events/{eventId}/preview/metadata
                 │  GET  /events/{eventId}/preview/pages/{pageNumber}
                 │  POST /events/{eventId}/preview/pages/{pageNumber}/validate
                 ▼
  ┌─────────────────────────────────────┐
  │   Form ready — event can publish    │
  │   ⚠ Publish blocked if 0 pages     │
  └─────────────────────────────────────┘
```

### Attendee Submission Journey

```
  [Attendee — event must be PUBLISHED and within registration window]
         │
         │  POST /events/{eventId}/start
         ▼
  ┌──────────────────────────────────────────────────┐
  │  Response created (DRAFT)                        │
  │  or existing draft returned (idempotent)         │
  │  status: DRAFT, completedPageIds: [], index: 0   │
  └──────────────┬───────────────────────────────────┘
                 │
                 │  ┌──────────────────────────────────────────────────────────┐
                 │  │  For each page (repeat until all pages completed):       │
                 │  │                                                          │
                 │  │  [Background auto-save while typing]                     │
                 │  │  PUT /pages/{pageId}/save?moveToNextPage=false           │
                 │  │  → No validation. Saves whatever is there. Always 200.  │
                 │  │  → completedPageIds unchanged.                           │
                 │  │                                                          │
                 │  │  [User clicks "Next"]                                    │
                 │  │  PUT /pages/{pageId}/save?moveToNextPage=true            │
                 │  │       ├─ Validation FAILS → returns errors, nothing      │
                 │  │       │  saved, page stays, success: false               │
                 │  │       └─ Validation PASSES → answers saved, page        │
                 │  │          added to completedPageIds, index advances       │
                 │  └──────────────────────────────────────────────────────────┘
                 │
                 │  POST /events/{eventId}/submit
                 │       ├─ Any page NOT in completedPageIds → 422 PAGE_INCOMPLETE
                 │       └─ All pages complete + required fields present → SUBMITTED
                 ▼
  ┌──────────────────────────────────────────────────┐
  │  Response SUBMITTED                              │
  │  status: SUBMITTED, completionTimeSeconds set    │
  └──────────────────────────────────────────────────┘
```

### Response Status Flow

```
  DRAFT ──────────────────────────────► SUBMITTED
    │                                        │
    │  (withdraw — not in this API)          │  (organizer action — outside scope)
    ▼                                        ▼
  WITHDRAWN                           UNDER_REVIEW → APPROVED / REJECTED
```

---

## Standard Response Format

All API responses follow a consistent structure using the Globe Response Builder pattern.

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { }
}
```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-02-17T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, CREATED, BAD_REQUEST, etc.) |
| `message` | string | Human-readable description of the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object / string | Response payload on success; error detail on failure |

---

## HTTP Method Badge Standards

- **GET** — Green (`#28a745`) — Safe, read-only operations
- **POST** — Blue (`#007bff`) — Create new resources
- **PUT** — Yellow (`#ffc107`, black text) — Update / replace
- **PATCH** — Orange (`#fd7e14`) — Partial update
- **DELETE** — Red (`#dc3545`) — Remove resource

---

## Shared Response Object Definitions

### A. ApplicantFormSetupResponse

Returned only by the Enable Form endpoint (Endpoint 1).

| Field | Type | Description |
|-------|------|-------------|
| `config` | object | The saved `EventApplicantFormEntity` — see config fields below |
| `config.id` | UUID | Config record ID |
| `config.formId` | UUID | Internal Form Builder form ID (not needed for subsequent API calls) |
| `config.displayTime` | string | `BEFORE_CHECKOUT` or `AFTER_CHECKOUT` |
| `config.isRequiredOnline` | boolean | Whether online buyers must complete the form |
| `config.applyToAtDoor` | boolean | Whether walk-in attendees see the form |
| `config.createdAt` | ZonedDateTime | Config creation timestamp |
| `config.updatedAt` | ZonedDateTime | Last update timestamp |
| `form` | object | Full FormResponse — see FormResponse definition below |

### B. EventApplicantFormEntity (Config Object)

Returned by the Update Settings endpoint.

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Config record ID |
| `formId` | UUID | Internal Form Builder form ID |
| `displayTime` | string | `BEFORE_CHECKOUT` or `AFTER_CHECKOUT` |
| `isRequiredOnline` | boolean | Whether online buyers must complete the form |
| `applyToAtDoor` | boolean | Whether walk-in attendees see the form |
| `createdAt` | ZonedDateTime | ISO 8601 with offset |
| `updatedAt` | ZonedDateTime | ISO 8601 with offset |

### C. FormResponse (Full Form Object)

| Field | Type | Description |
|-------|------|-------------|
| `formId` | UUID | Form identifier (Form Builder internal ID) |
| `title` | string | Auto-set to `"Attendee Questions - {event title}"` on enable |
| `description` | string | Form description |
| `settings` | object | Form open/close settings (see FormSettings below) |
| `settings.acceptResponses` | boolean | Whether the form is accepting responses |
| `settings.allowMultipleSubmissions` | boolean | Whether a user can submit more than once |
| `settings.responseStartTime` | Instant | UTC datetime when responses open |
| `settings.responseDeadline` | Instant | UTC datetime when responses close |
| `settings.allowSaveDraft` | boolean | Whether respondents can save progress |
| `coverPage` | object | Optional cover page config |
| `createdBy` | string | Username of creator |
| `createdAt` | LocalDateTime | Creation timestamp |
| `pages[]` | array | Ordered list of PageResponse objects |

### D. PageResponse

| Field | Type | Description |
|-------|------|-------------|
| `pageId` | UUID | Page identifier |
| `title` | string | Page heading shown to attendee |
| `description` | string | Subheading or instruction text |
| `displayOrder` | integer | 1-based position within the form |
| `actionButtonText` | string | Label for the next/submit button |
| `fields[]` | array | Ordered list of FieldResponse objects (soft-deleted excluded) |

### E. FieldResponse

| Field | Type | Description |
|-------|------|-------------|
| `fieldId` | UUID | Field identifier |
| `type` | string | `TEXT`, `TEXTAREA`, `EMAIL`, `PHONE`, `NUMBER`, `URL`, `DATE`, `TIME`, `DATETIME`, `DROPDOWN`, `RADIO`, `CHECKBOX`, `FILE`, `RATING`, `HEADER` |
| `label` | string | Field label. For `HEADER` type this is the section title |
| `description` | string | Helper text shown below the field |
| `placeholder` | string | Input placeholder text |
| `displayOrder` | integer | 1-based order within the page |
| `required` | boolean | Whether the field must be answered. Always `false` for `HEADER` |
| `validation` | object | See FieldValidation below |
| `options[]` | array | OptionResponse entries — only for `DROPDOWN`, `RADIO`, `CHECKBOX` |

#### FieldValidation Object

| Field | Applicable To | Description |
|-------|--------------|-------------|
| `minLength` / `maxLength` | `TEXT`, `TEXTAREA` | Character count constraints |
| `pattern` / `patternMessage` | `TEXT` | Regex pattern and custom error message |
| `min` / `max` | `NUMBER` | Numeric range |
| `minDate` / `maxDate` | `DATE` | Date range (`YYYY-MM-DD`) |
| `minSelections` / `maxSelections` | `CHECKBOX` | Selection count constraints |
| `maxSizeMb` / `accept` | `FILE` | File size limit and accepted MIME types |

### F. OptionResponse

| Field | Type | Description |
|-------|------|-------------|
| `optionId` | UUID | Option identifier |
| `label` | string | Display label shown to attendee |
| `displayOrder` | integer | 1-based order within the field |

### G. FormResponseObject (Attendee Submission)

| Field | Type | Description |
|-------|------|-------------|
| `responseId` | UUID | Unique response identifier |
| `formId` | UUID | Internal form ID |
| `submittedBy` | string | Username of the attendee |
| `status` | string | `DRAFT`, `SUBMITTED`, `UNDER_REVIEW`, `APPROVED`, `REJECTED`, `WITHDRAWN` |
| `completedPageIds` | array | UUIDs of pages marked complete |
| `currentPageIndex` | integer | 0-based index of current page |
| `startedAt` | LocalDateTime | When the attendee started the form |
| `submittedAt` | LocalDateTime | When the response was submitted |
| `completionTimeSeconds` | integer | Total seconds from start to submit |
| `answers[]` | array | See AnswerResponse below |

#### AnswerResponse

| Field | Type | Description |
|-------|------|-------------|
| `answerId` | UUID | Answer identifier |
| `fieldId` | UUID | Field ID (null if field was hard-deleted) |
| `fieldLabel` | string | Label snapshot — preserved even if field is later deleted |
| `fieldType` | string | Type snapshot — preserved even if field is later deleted |
| `fieldDeleted` | boolean | `true` when the source field has been soft-deleted |
| `value` | any | Submitted value. String for text/single-choice, array of strings for checkbox, number for NUMBER/RATING |
| `answeredAt` | LocalDateTime | When this answer was saved |
| `fileUrl` / `fileName` / `fileSize` / `fileType` | various | File upload metadata (FILE fields only) |

### H. BulkOperationResult

| Field | Type | Description |
|-------|------|-------------|
| `successCount` | integer | Number of items successfully processed |
| `failureCount` | integer | Number of items that failed |
| `errors[]` | array | Error message per failed item |
| `createdFields[]` | array | FieldResponse objects (bulk field operations) |
| `createdPages[]` | array | PageResponse objects (bulk page operations) |

### I. BulkDeleteResult

| Field | Type | Description |
|-------|------|-------------|
| `successCount` | integer | Number of items successfully deleted |
| `failureCount` | integer | Number of items that failed |
| `errors[]` | array | Error message per failed item |
| `deletedIds[]` | array | UUIDs of successfully deleted items |

### J. SavePageResponse

Returned by the Save Page Answers endpoint (Endpoint 27). The shape of the response is the same whether the save succeeded or validation failed — the client always reads `saved` and `isValid` to decide what to do next.

| Field | Type | Description |
|-------|------|-------------|
| `eventId` | UUID | The event ID |
| `saved` | boolean | `true` if answers were actually written to the database. `false` if validation failed and nothing was saved |
| `savedAt` | LocalDateTime | Timestamp of the save. `null` when `saved` is `false` |
| `isValid` | boolean | `true` if all fields on the page passed validation. Mirrors `saved` |
| `errors` | array / null | `null` when `isValid` is `true`. Array of `FieldValidationError` when validation failed |
| `errors[].fieldId` | UUID | The field that failed validation |
| `errors[].fieldLabel` | string | Label of the failing field (e.g. `"Email"`) |
| `errors[].errorCode` | string | Machine-readable error type: `REQUIRED`, `INVALID_FORMAT`, `INVALID_TYPE`, `VALIDATION_FAILED` |
| `errors[].message` | string | Human-readable error description (e.g. `"must be valid email"`) |

> **Note**: `totalFieldsOnPage`, `answeredFieldsOnPage`, and `overallProgress` are reserved fields in the response object but are not currently populated — they will be `null` and omitted from the JSON (`@JsonInclude(NON_NULL)`).

### K. FormAnalytics

| Field | Type | Description |
|-------|------|-------------|
| `formId` | UUID | Form identifier |
| `formTitle` | string | Form title |
| `stats.totalStarted` | integer | Total responses started (including drafts) |
| `stats.totalDrafts` | integer | Responses still in DRAFT |
| `stats.totalSubmitted` | integer | Responses with SUBMITTED status |
| `stats.totalWithdrawn` | integer | Withdrawn responses |
| `stats.completionRate` | double | Submitted / totalStarted × 100 |
| `stats.dropOffRate` | double | Inverse of completion rate |
| `stats.avgCompletionTimeSeconds` | double | Mean time from start to submit |
| `stats.fastestTimeSeconds` | integer | Fastest submission time |
| `stats.slowestTimeSeconds` | integer | Slowest submission time |
| `fieldAnalytics[]` | array | Per-field breakdown |
| `fieldAnalytics[].fieldId` | UUID | Field ID |
| `fieldAnalytics[].fieldLabel` | string | Field label snapshot |
| `fieldAnalytics[].fieldType` | string | Field type snapshot |
| `fieldAnalytics[].fieldDeleted` | boolean | Whether field is soft-deleted |
| `fieldAnalytics[].totalResponses` | integer | Total answers for this field |
| `fieldAnalytics[].choiceDistribution[]` | array | `{ option, count, percentage }` — choice fields |
| `fieldAnalytics[].numericStats` | object | `{ min, max, avg, median }` — NUMBER/RATING fields |
| `fieldAnalytics[].textResponses[]` | array | Raw text answers — TEXT/TEXTAREA fields |
| `dailySubmissions[]` | array | `{ date, count }` — submissions per day |

---

## Standard Error Types

### Application-Level Exceptions (400–499)

- `400 BAD_REQUEST` — Form already enabled for this event, field type does not support options, already submitted
- `401 UNAUTHORIZED` — Missing, expired, or malformed Bearer token
- `403 FORBIDDEN` — Authenticated but not the event organizer; or registration window not open/already closed
- `404 NOT_FOUND` — Event not found, form not enabled, page/field/option not found, no response found
- `422 UNPROCESSABLE_ENTITY` — Bean validation failures; or `submit` called when one or more pages were not completed via `?moveToNextPage=true` (`PAGE_INCOMPLETE` error type); or required field still unanswered at submit time (`REQUIRED` error type). For save validation failures (`?moveToNextPage=true`), the response returns HTTP `200` with `success: false` — not a 422 — because the client needs to display inline errors without treating it as a hard failure

### Server-Level Exceptions (500+)

- `500 INTERNAL_SERVER_ERROR` — Unexpected runtime error

---

## Shared Error Response Examples

**401 — Unauthorized:**
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2025-02-17T10:30:45",
  "data": "Token has expired"
}
```

**403 — Forbidden (not organizer):**
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Only organizer can manage form",
  "action_time": "2025-02-17T10:30:45",
  "data": "Only organizer can manage form"
}
```

**403 — Forbidden (registration closed):**
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Registration closed",
  "action_time": "2025-02-17T10:30:45",
  "data": "Registration closed"
}
```

**404 — Form not enabled:**
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Form not enabled",
  "action_time": "2025-02-17T10:30:45",
  "data": "Form not enabled"
}
```

---

## Endpoints

---

## Form Setup

---

## 1. Enable Applicant Form

**Purpose**: Enables the applicant form for an event. Internally creates a Form Builder form titled `"Attendee Questions - {event title}"` with no pages. Pages are added separately by the organizer. Only the event organizer can call this. Returns the empty form structure alongside the config.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/enable`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to enable the form for | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "displayTime": "BEFORE_CHECKOUT",
  "isRequiredOnline": true,
  "applyToAtDoor": false
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `displayTime` | string | No | When attendees see the form in the UI flow | Enum: `BEFORE_CHECKOUT`, `AFTER_CHECKOUT`. Defaults to `BEFORE_CHECKOUT` |
| `isRequiredOnline` | boolean | No | Whether online ticket buyers must complete the form before proceeding | Defaults to `false` |
| `applyToAtDoor` | boolean | No | Whether at-door / walk-in attendees also see the form | Defaults to `false` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Applicant form enabled",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "config": {
      "id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "displayTime": "BEFORE_CHECKOUT",
      "isRequiredOnline": true,
      "applyToAtDoor": false,
      "createdAt": "2025-02-17T10:30:45+03:00",
      "updatedAt": null
    },
    "form": {
      "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "title": "Attendee Questions - Dar es Salaam Jazz Festival 2025",
      "description": null,
      "settings": null,
      "coverPage": null,
      "createdBy": "amina.hassan",
      "createdAt": "2025-02-17T10:30:45",
      "pages": []
    }
  }
}
```

**Success Response Fields**: `data` is an [ApplicantFormSetupResponse](#a-applicantformsetupresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer, or form is already enabled for this event |
| `404` | Event not found |

---

## 2. Update Form Settings

**Purpose**: Updates the form configuration for the event — `displayTime`, `isRequiredOnline`, and `applyToAtDoor`. Does not affect the form structure (pages/fields). All fields are optional.

**Endpoint**: `PUT` `/api/v1/e-events/applicant-form/events/{eventId}/settings`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "displayTime": "AFTER_CHECKOUT",
  "isRequiredOnline": false,
  "applyToAtDoor": true
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `displayTime` | string | No | Updated display time | Enum: `BEFORE_CHECKOUT`, `AFTER_CHECKOUT` |
| `isRequiredOnline` | boolean | No | Updated required flag for online buyers | — |
| `applyToAtDoor` | boolean | No | Updated at-door flag | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Form settings updated",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "displayTime": "AFTER_CHECKOUT",
    "isRequiredOnline": false,
    "applyToAtDoor": true,
    "createdAt": "2025-02-17T10:30:45+03:00",
    "updatedAt": "2025-02-17T11:00:00+03:00"
  }
}
```

**Success Response Fields**: `data` is an [EventApplicantFormEntity config object](#b-eventapplicantformentity-config-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## 3. Disable Form

**Purpose**: Disables and permanently removes the applicant form from the event. The underlying Form Builder form is soft-deleted. Existing submitted responses are preserved. This action cannot be undone — to re-enable a form, call Endpoint 1 again which will create a fresh form.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/disable`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Applicant form disabled",
  "action_time": "2025-02-17T10:30:45",
  "data": null
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## 4. Get Full Form

**Purpose**: Returns the complete form structure — all pages, fields, and options — in a single call. Intended for the form builder UI to load the current state when the organizer opens the builder. Only the event organizer can call this.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/full-form`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Form retrieved",
  "action_time": "2026-05-21T10:30:45",
  "data": {
    "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "title": "Attendee Questions - Dar es Salaam Jazz Festival 2025",
    "description": null,
    "settings": null,
    "coverPage": null,
    "createdBy": "amina.hassan",
    "createdAt": "2026-05-21T10:30:45",
    "pages": [
      {
        "pageId": "1a2b3c4d-...",
        "title": "Personal Details",
        "description": null,
        "displayOrder": 1,
        "actionButtonText": "Next",
        "fields": [
          {
            "fieldId": "c3d4e5f6-...",
            "type": "TEXT",
            "label": "Full Name",
            "description": "As it appears on your ID.",
            "placeholder": "e.g. Amina Hassan",
            "displayOrder": 1,
            "required": true,
            "validation": { "minLength": 2, "maxLength": 100 },
            "options": []
          },
          {
            "fieldId": "d4e5f6a7-...",
            "type": "DROPDOWN",
            "label": "T-Shirt Size",
            "displayOrder": 2,
            "required": true,
            "validation": null,
            "options": [
              { "optionId": "a1b2c3d4-...", "label": "Small (S)", "displayOrder": 1 },
              { "optionId": "b2c3d4e5-...", "label": "Medium (M)", "displayOrder": 2 }
            ]
          }
        ]
      },
      {
        "pageId": "2b3c4d5e-...",
        "title": "Dietary Requirements",
        "displayOrder": 2,
        "actionButtonText": "Submit",
        "fields": []
      }
    ]
  }
}
```

**Success Response Fields**: `data` is a [FormResponse](#c-formresponse-full-form-object) with all pages, fields, and options fully nested. Soft-deleted items are excluded. Pages and fields are ordered by `displayOrder`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## Page Management

---

## 4. Add Page

**Purpose**: Appends a new blank page to the form. `displayOrder` is auto-assigned as the next position. Fields are added to the page separately.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/pages`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "title": "Emergency Contact",
  "description": "In case we need to reach someone on your behalf.",
  "actionButtonText": "Next"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `title` | string | Yes | Page heading shown to attendee | Max: 255 characters |
| `description` | string | No | Subheading or instruction text | Max: 500 characters |
| `actionButtonText` | string | No | Label for the page's action button | Max: 50 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Page added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "pageId": "2b3c4d5e-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
    "title": "Emergency Contact",
    "description": "In case we need to reach someone on your behalf.",
    "displayOrder": 2,
    "actionButtonText": "Next",
    "fields": []
  }
}
```

**Success Response Fields**: `data` is a [PageResponse](#d-pageresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |
| `422` | `title` is blank |

---

## 5. Update Page

**Purpose**: Updates the title, description, or action button label of an existing page.

**Endpoint**: `PUT` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "title": "Your Background",
  "actionButtonText": "Continue"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `title` | string | No | Updated page title | Max: 255 characters |
| `description` | string | No | Updated description | Max: 500 characters |
| `actionButtonText` | string | No | Updated button label | Max: 50 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "PageResponse object" }
}
```

**Success Response Fields**: `data` is a [PageResponse](#d-pageresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or page not found |

---

## 6. Delete Page

**Purpose**: Deletes a page. Pass `?hard=false` (default) for soft delete — the page is hidden but historical answer data is preserved. Pass `?hard=true` to permanently remove the page and all its fields.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page to delete | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete, `false` = soft delete | — | `false` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page deleted",
  "action_time": "2025-02-17T10:30:45",
  "data": null
}
```

> When `hard=true`, `message` is `"Page permanently deleted"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or page not found |

---

## 7. Bulk Add Pages (with Fields)

**Purpose**: Creates multiple pages in one call. Each page can optionally include an inline list of fields.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/pages/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "pages": [
    {
      "title": "Personal Details",
      "actionButtonText": "Next",
      "fields": [
        { "type": "TEXT", "label": "Full Name", "required": true },
        { "type": "EMAIL", "label": "Email", "required": true }
      ]
    },
    {
      "title": "Dietary Requirements",
      "actionButtonText": "Submit",
      "fields": [
        { "type": "DROPDOWN", "label": "Dietary preference", "required": false }
      ]
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `pages` | array | Yes | Pages to create | Min 1 item |
| `pages[].title` | string | Yes | Page title | Max: 255 characters |
| `pages[].description` | string | No | Page description | Max: 500 characters |
| `pages[].actionButtonText` | string | No | Button label | Max: 50 characters |
| `pages[].fields` | array | No | Fields to create inline | See CreateField below |
| `pages[].fields[].type` | string | Yes | Field type | See FieldType enum |
| `pages[].fields[].label` | string | Yes | Field label | Max: 255 characters |
| `pages[].fields[].description` | string | No | Helper text | Max: 500 characters |
| `pages[].fields[].placeholder` | string | No | Placeholder text | Max: 255 characters |
| `pages[].fields[].required` | boolean | No | Required flag | Defaults to `false` |
| `pages[].fields[].validation` | object | No | Validation rules | See FieldValidation |

> **Note**: For `DROPDOWN`, `RADIO`, and `CHECKBOX` fields created via bulk, options must still be added separately using the option endpoints after this call.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "2 pages added, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": [],
    "createdPages": [
      { "...": "PageResponse for page 1" },
      { "...": "PageResponse for page 2" }
    ]
  }
}
```

**Success Response Fields**: `data` is a [BulkOperationResult](#h-bulkoperationresult).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## 8. Bulk Delete Pages

**Purpose**: Deletes multiple pages in one call. Supports both soft delete (default) and hard delete via `?hard` query param. Items are processed individually — a failure on one does not stop the rest.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/pages/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete | — | `false` |

**Request JSON Sample**:

```json
{
  "ids": ["pageId-1", "pageId-2"]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ids` | array | Yes | UUIDs of pages to delete | Min 1 item |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "2 pages deleted, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": [],
    "deletedIds": ["pageId-1", "pageId-2"]
  }
}
```

**Success Response Fields**: `data` is a [BulkDeleteResult](#i-bulkdeleteresult).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found. Individual page errors reported in `data.errors[]` |

---

## 9. Clone Page

**Purpose**: Creates an exact copy of an existing page including all its active fields and options. The clone is appended at the end of the form with "(Copy)" added to the title.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/clone`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page to clone | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Page cloned successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "PageResponse for the cloned page" }
}
```

**Success Response Fields**: `data` is a [PageResponse](#d-pageresponse) for the new clone.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or page not found |

---

## Field Management

---

## 10. Add Field
**Purpose**: Adds a single field to a page. `displayOrder` is auto-assigned. For `DROPDOWN`, `RADIO`, and `CHECKBOX` types, options can be passed inline in the same request or added separately via the Add Option endpoint.
**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields`
**Access Level**: 🔒 Protected (Event organizer only)
**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page to add the field to | Must be a valid UUID |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `type` | string | Yes | Field type | Enum: `TEXT`, `TEXTAREA`, `EMAIL`, `PHONE`, `NUMBER`, `URL`, `DATE`, `TIME`, `DATETIME`, `DROPDOWN`, `RADIO`, `CHECKBOX`, `FILE`, `RATING`, `HEADER` |
| `label` | string | Yes | Field label (or section title for `HEADER`) | Max: 255 characters |
| `description` | string | No | Helper text | Max: 500 characters |
| `placeholder` | string | No | Input placeholder | Max: 255 characters |
| `required` | boolean | No | Required flag. Forced `false` for `HEADER` | Defaults to `false` |
| `validation` | object | No | Validation rules | See FieldValidation |
| `options` | array | No | Inline options for `DROPDOWN`, `RADIO`, `CHECKBOX`. Ignored for other types. Blank labels are skipped | — |
| `options[].label` | string | Yes (if options provided) | Option label | Max: 255 characters |

---

### Example 1 — TEXT field with validation

**Request**:
```json
{
  "type": "TEXT",
  "label": "Full Name",
  "description": "As it appears on your ID.",
  "placeholder": "e.g. Amina Hassan",
  "required": true,
  "validation": {
    "minLength": 2,
    "maxLength": 100
  }
}
```

**Response**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "fieldId": "c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f",
    "type": "TEXT",
    "label": "Full Name",
    "description": "As it appears on your ID.",
    "placeholder": "e.g. Amina Hassan",
    "displayOrder": 1,
    "required": true,
    "validation": { "minLength": 2, "maxLength": 100 },
    "options": []
  }
}
```

---

### Example 2 — DROPDOWN field with inline options

**Request**:
```json
{
  "type": "DROPDOWN",
  "label": "T-Shirt Size",
  "description": "Select your preferred size.",
  "placeholder": "Choose a size",
  "required": true,
  "options": [
    { "label": "Small (S)" },
    { "label": "Medium (M)" },
    { "label": "Large (L)" },
    { "label": "Extra Large (XL)" }
  ]
}
```

**Response**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "fieldId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
    "type": "DROPDOWN",
    "label": "T-Shirt Size",
    "description": "Select your preferred size.",
    "placeholder": "Choose a size",
    "displayOrder": 2,
    "required": true,
    "validation": null,
    "options": [
      { "optionId": "a1b2c3d4-...", "label": "Small (S)", "displayOrder": 1 },
      { "optionId": "b2c3d4e5-...", "label": "Medium (M)", "displayOrder": 2 },
      { "optionId": "c3d4e5f6-...", "label": "Large (L)", "displayOrder": 3 },
      { "optionId": "d4e5f6a7-...", "label": "Extra Large (XL)", "displayOrder": 4 }
    ]
  }
}
```

---

### Example 3 — RADIO field with inline options

**Request**:
```json
{
  "type": "RADIO",
  "label": "What is your attendance mode?",
  "description": "Select how you will attend the event.",
  "required": true,
  "options": [
    { "label": "In Person" },
    { "label": "Online" }
  ]
}
```

**Response**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "fieldId": "e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b",
    "type": "RADIO",
    "label": "What is your attendance mode?",
    "description": "Select how you will attend the event.",
    "placeholder": null,
    "displayOrder": 3,
    "required": true,
    "validation": null,
    "options": [
      { "optionId": "e5f6a7b8-...", "label": "In Person", "displayOrder": 1 },
      { "optionId": "f6a7b8c9-...", "label": "Online", "displayOrder": 2 }
    ]
  }
}
```

---

### Example 4 — CHECKBOX field with inline options and validation

**Request**:
```json
{
  "type": "CHECKBOX",
  "label": "Which sessions will you attend?",
  "description": "Select all that apply. You may choose up to 3.",
  "required": false,
  "validation": {
    "minSelections": 1,
    "maxSelections": 3
  },
  "options": [
    { "label": "Morning Session (9am - 12pm)" },
    { "label": "Afternoon Session (1pm - 4pm)" },
    { "label": "Evening Session (6pm - 9pm)" }
  ]
}
```

**Response**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "fieldId": "f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c",
    "type": "CHECKBOX",
    "label": "Which sessions will you attend?",
    "description": "Select all that apply. You may choose up to 3.",
    "placeholder": null,
    "displayOrder": 4,
    "required": false,
    "validation": { "minSelections": 1, "maxSelections": 3 },
    "options": [
      { "optionId": "g7a8b9c0-...", "label": "Morning Session (9am - 12pm)", "displayOrder": 1 },
      { "optionId": "h8b9c0d1-...", "label": "Afternoon Session (1pm - 4pm)", "displayOrder": 2 },
      { "optionId": "i9c0d1e2-...", "label": "Evening Session (6pm - 9pm)", "displayOrder": 3 }
    ]
  }
}
```

---

### Example 5 — HEADER field (section divider)

**Request**:
```json
{
  "type": "HEADER",
  "label": "Emergency Contact Information"
}
```

**Response**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "fieldId": "a0b1c2d3-e4f5-6a7b-8c9d-0e1f2a3b4c5d",
    "type": "HEADER",
    "label": "Emergency Contact Information",
    "description": null,
    "placeholder": null,
    "displayOrder": 5,
    "required": false,
    "validation": null,
    "options": []
  }
}
```

---

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or page not found |
| `422` | `type` is null, or `label` is blank |

---

## 11. Update Field
**Purpose**: Updates the metadata of an existing field. All fields are optional. **Field type cannot be changed** — if a different type is needed, delete the field and create a new one. Options are managed via dedicated option endpoints.
**Endpoint**: `PUT` `/api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}`
**Access Level**: 🔒 Protected (Event organizer only)
**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `fieldId` | UUID | Yes | The field to update | Must be a valid UUID |

**Request JSON Sample**:
```json
{
  "label": "Legal Full Name",
  "required": true,
  "validation": {
    "minLength": 3,
    "maxLength": 150
  }
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `label` | string | No | Updated label | Max: 255 characters |
| `description` | string | No | Updated helper text | Max: 500 characters |
| `placeholder` | string | No | Updated placeholder | Max: 255 characters |
| `required` | boolean | No | Updated required flag. Forced `false` if type is `HEADER` | — |
| `validation` | object | No | Updated validation rules (full replacement) | See FieldValidation |

> ⚠️ **Note:** `type` cannot be changed via this endpoint. Attempting to pass a different `type` will return a `400` error with the message: `"Field type cannot be changed. Delete this field and create a new one with the correct type."` To change the field type, delete this field and create a new one.

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Field updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "FieldResponse object" }
}
```

**Success Response Fields**: `data` is a [FieldResponse](#e-fieldresponse).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or field not found |
| `400` | Attempting to change field `type` |

---

## 12. Delete Field

**Purpose**: Deletes a field. Soft delete (default) preserves historical answer snapshots. Hard delete permanently removes the field record.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `fieldId` | UUID | Yes | The field to delete | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete | — | `false` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Field deleted",
  "action_time": "2025-02-17T10:30:45",
  "data": null
}
```

> When `hard=true`, `message` is `"Field permanently deleted"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or field not found |

---

## 13. Bulk Add Fields
**Purpose**: Adds multiple fields to a page in one call. Fields are appended after existing fields in array order. For `DROPDOWN`, `RADIO`, and `CHECKBOX` types, options can be passed inline per field.
**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields/bulk`
**Access Level**: 🔒 Protected (Event organizer only)
**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page to add fields to | Must be a valid UUID |

**Request JSON Sample**:
```json
{
  "fields": [
    { "type": "EMAIL", "label": "Email Address", "required": true },
    { "type": "PHONE", "label": "Phone Number", "required": false },
    { "type": "HEADER", "label": "Travel Details" },
    {
      "type": "DROPDOWN",
      "label": "Arrival Method",
      "required": true,
      "options": [
        { "label": "Flight" },
        { "label": "Bus" },
        { "label": "Car" }
      ]
    }
  ]
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `fields` | array | Yes | Fields to create | Min 1 item |
| `fields[].type` | string | Yes | Field type | See FieldType enum |
| `fields[].label` | string | Yes | Field label | Max: 255 characters |
| `fields[].description` | string | No | Helper text | Max: 500 characters |
| `fields[].placeholder` | string | No | Placeholder text | Max: 255 characters |
| `fields[].required` | boolean | No | Required flag | Defaults to `false` |
| `fields[].validation` | object | No | Validation rules | See FieldValidation |
| `fields[].options` | array | No | Inline options for `DROPDOWN`, `RADIO`, `CHECKBOX`. Ignored for other types. Blank labels are skipped | — |
| `fields[].options[].label` | string | Yes (if options provided) | Option label | Max: 255 characters |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "4 fields added, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 4,
    "failureCount": 0,
    "errors": [],
    "createdFields": [ "[ FieldResponse objects ]" ]
  }
}
```

**Success Response Fields**: `data` is a [BulkOperationResult](#h-bulkoperationresult).

**Possible Error Responses**:
| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or page not found |

---

## 14. Bulk Delete Fields

**Purpose**: Deletes multiple fields from a page in one call. Supports soft and hard delete via `?hard`.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/fields/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page containing the fields | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete | — | `false` |

**Request JSON Sample**:

```json
{
  "ids": ["fieldId-1", "fieldId-2"]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ids` | array | Yes | UUIDs of fields to delete | Min 1 item |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "2 fields deleted, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": [],
    "deletedIds": ["fieldId-1", "fieldId-2"]
  }
}
```

**Success Response Fields**: `data` is a [BulkDeleteResult](#i-bulkdeleteresult).

**Possible Error Responses**: Same as Endpoint 8 (401, 403, 404).

---

## 15. Bulk Update Fields

**Purpose**: Updates `required` flag and/or `validation` rules across multiple fields at once.

**Endpoint**: `PATCH` `/api/v1/e-events/applicant-form/events/{eventId}/fields/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "fieldIds": ["fieldId-1", "fieldId-2"],
  "required": true,
  "validation": { "maxLength": 200 }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `fieldIds` | array | Yes | UUIDs of fields to update | Min 1 item |
| `required` | boolean | No | New required value for all listed fields | — |
| `validation` | object | No | New validation rules for all listed fields (full replacement) | See FieldValidation |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "2 fields updated, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": [],
    "createdFields": [ "[ Updated FieldResponse objects ]" ]
  }
}
```

**Success Response Fields**: `data` is a [BulkOperationResult](#h-bulkoperationresult).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | One or more field IDs not found — reported in `data.errors[]` |

---

## 16. Clone Field

**Purpose**: Creates a copy of a field (with its options) and appends it to a target page. Label gets "(Copy)" appended.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/clone`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `fieldId` | UUID | Yes | The field to clone | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `targetPageId` | UUID | Yes | The page to clone the field into | Must be a valid UUID | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field cloned successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "FieldResponse for the cloned field" }
}
```

**Success Response Fields**: `data` is a [FieldResponse](#e-fieldresponse) for the new clone.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, field not found, or target page not found |

---

## Option Management

---

## 17. Add Option

**Purpose**: Adds a choice option to a `DROPDOWN`, `RADIO`, or `CHECKBOX` field. Calling this on any other field type returns `400`.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/options`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `fieldId` | UUID | Yes | The field to add an option to | Must be `DROPDOWN`, `RADIO`, or `CHECKBOX` type |

**Request JSON Sample**:

```json
{
  "label": "By Car"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `label` | string | Yes | Display label for this option | Max: 255 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Option added",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "optionId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
    "label": "By Car",
    "displayOrder": 1
  }
}
```

**Success Response Fields**: `data` is an [OptionResponse](#f-optionresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | Field type does not support options |
| `403` | Not the event organizer |
| `404` | Event not found or field not found |
| `422` | `label` is blank |

---

## 18. Update Option

**Purpose**: Updates the label of an existing option.

**Endpoint**: `PUT` `/api/v1/e-events/applicant-form/events/{eventId}/options/{optionId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `optionId` | UUID | Yes | The option to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "label": "Public Transport"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `label` | string | No | Updated label | Max: 255 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Option updated",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "optionId": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a",
    "label": "Public Transport",
    "displayOrder": 1
  }
}
```

**Success Response Fields**: `data` is an [OptionResponse](#f-optionresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or option not found |

---

## 19. Delete Option

**Purpose**: Deletes an option. Soft delete (default) preserves previously selected answer values. Hard delete permanently removes the option record.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/options/{optionId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `optionId` | UUID | Yes | The option to delete | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete | — | `false` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Option deleted",
  "action_time": "2025-02-17T10:30:45",
  "data": null
}
```

> When `hard=true`, `message` is `"Option permanently deleted"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or option not found |

---

## 20. Bulk Delete Options

**Purpose**: Deletes multiple options from a field in one call. Supports soft and hard delete via `?hard`.

**Endpoint**: `DELETE` `/api/v1/e-events/applicant-form/events/{eventId}/fields/{fieldId}/options/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `fieldId` | UUID | Yes | The field containing the options | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `hard` | boolean | No | `true` = permanent delete | — | `false` |

**Request JSON Sample**:

```json
{
  "ids": ["optionId-1", "optionId-2"]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `ids` | array | Yes | UUIDs of options to delete | Min 1 item |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "2 options deleted, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": [],
    "deletedIds": ["optionId-1", "optionId-2"]
  }
}
```

**Success Response Fields**: `data` is a [BulkDeleteResult](#i-bulkdeleteresult).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or field not found. Individual errors in `data.errors[]` |

---

## 21. Bulk Update Options

**Purpose**: Updates the labels of multiple options in one call.

**Endpoint**: `PATCH` `/api/v1/e-events/applicant-form/events/{eventId}/options/bulk`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "options": [
    { "optionId": "optionId-1", "label": "Train" },
    { "optionId": "optionId-2", "label": "Bus" }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `options` | array | Yes | List of option updates | Min 1 item |
| `options[].optionId` | UUID | Yes | The option to update | Must be a valid UUID |
| `options[].label` | string | No | New label | Max: 255 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "2 options updated, 0 failed",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "successCount": 2,
    "failureCount": 0,
    "errors": []
  }
}
```

**Success Response Fields**: `data` is a [BulkOperationResult](#h-bulkoperationresult).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | An option ID not found — reported in `data.errors[]` |

---

## Preview

---

## 23. Get Preview Metadata

**Purpose**: Returns a lightweight summary of the form structure — total pages, page numbers, and titles — without loading full field data. Used by the attendee to render a progress indicator before filling. Also returns the attendee's current response state (`responseId`, `currentPageIndex`, `completedPageIds`) if they have an existing draft.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/preview/metadata`

**Access Level**: 🔒 Protected (Organizer always; attendee when event is PUBLISHED)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Metadata retrieved",
  "data": {
    "formId": "uuid",
    "formTitle": "Attendee Questions - Tech Summit 2025",
    "totalPages": 3,
    "pageNumbers": [
      { "pageNumber": 1, "pageTitle": "Personal Info" },
      { "pageNumber": 2, "pageTitle": "Experience" },
      { "pageNumber": 3, "pageTitle": "Preferences" }
    ],
    "responseId": "uuid",
    "responseStatus": "DRAFT",
    "currentPageIndex": 1,
    "completedPageIds": ["page-uuid-1"]
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `formId` | Internal form ID |
| `formTitle` | Form title |
| `totalPages` | Total active pages |
| `pageNumbers[].pageNumber` | 1-based page number |
| `pageNumbers[].pageTitle` | Page heading |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## 24. Preview Page by Number

**Purpose**: Returns a specific page by its 1-based position number, including all its active fields and options. Used by attendees to load each page as they fill the form. Also accessible to the organizer for previewing before publishing.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}`

**Access Level**: 🔒 Protected (Organizer always; attendee when event is PUBLISHED)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageNumber` | integer | Yes | 1-based page position | Must be ≥ 1 and ≤ total active pages |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Preview page retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "PageResponse object" }
}
```

**Success Response Fields**: `data` is a [PageResponse](#d-pageresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, form not enabled, or `pageNumber` out of range |

---

## 25. Validate Preview Page Answers

**Purpose**: Runs validation on a set of answers for a specific page number without creating or saving any data. Lets the organizer test field validation rules before the form goes live.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}/validate`

**Access Level**: 🔒 Protected (Organizer always; attendee when event is PUBLISHED)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageNumber` | integer | Yes | 1-based page number to validate against | Must be ≥ 1 and ≤ total active pages |

**Request JSON Sample**:

```json
{
  "fieldId-fullname": { "value": "" },
  "fieldId-email":    { "value": "not-an-email" }
}
```

**Request Body**: A flat map of `fieldId (UUID) → AnswerValue`. `AnswerValue` has `value` (any), plus optional `fileUrl`, `fileName`, `fileSize`, `fileType` for FILE fields.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Validation complete",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "valid": false,
    "errors": [
      {
        "pageId": "1a2b3c4d-...",
        "pageTitle": "Attendee Information",
        "fieldId": "fieldId-fullname",
        "fieldLabel": "Full Name",
        "errorMessage": "Full Name is required",
        "errorType": "REQUIRED"
      },
      {
        "pageId": "1a2b3c4d-...",
        "pageTitle": "Attendee Information",
        "fieldId": "fieldId-email",
        "fieldLabel": "Email",
        "errorMessage": "Email must be valid email",
        "errorType": "INVALID_FORMAT"
      }
    ]
  }
}
```

**Success Response Fields**:

| Field | Description |
|-------|-------------|
| `data.valid` | `true` if all fields passed validation |
| `data.errors[].pageId` | Page UUID |
| `data.errors[].pageTitle` | Page title |
| `data.errors[].fieldId` | Field UUID |
| `data.errors[].fieldLabel` | Field label |
| `data.errors[].errorMessage` | Human-readable error |
| `data.errors[].errorType` | `REQUIRED`, `INVALID_FORMAT`, `INVALID_TYPE`, `VALIDATION_FAILED` |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, form not enabled, or `pageNumber` out of range |

---

## Attendee Submission

---

## 26. Start Submission

**Purpose**: Initiates the attendee's form response session. Creates a new `DRAFT` response tied to this event's form. If the attendee already has a draft or submitted response for this event's form, the existing response is returned — this endpoint is idempotent. Requires the event to be `PUBLISHED` and within its registration window.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/start`

**Access Level**: 🔒 Protected (Any authenticated attendee)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Form submission started",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "responseId": "e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b",
    "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "submittedBy": "john.doe",
    "status": "DRAFT",
    "completedPageIds": [],
    "currentPageIndex": 0,
    "startedAt": "2025-02-17T10:30:45",
    "submittedAt": null,
    "completionTimeSeconds": null,
    "answers": []
  }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Event not published, registration not yet open, or registration already closed |
| `404` | Event not found or form not enabled |

---

## 27. Save Page Answers

**Purpose**: Saves the attendee's answers for a specific page. Behaviour is controlled by the `?moveToNextPage` query parameter which maps directly to two distinct client actions — background auto-save and the "Next" button.

**Endpoint**: `PUT` `/api/v1/e-events/applicant-form/events/{eventId}/pages/{pageId}/save`

**Access Level**: 🔒 Protected (Attendee — response owner)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `pageId` | UUID | Yes | The page being answered | Must be a valid UUID belonging to this event's form |

**Query Parameters**:

| Parameter | Type | Required | Description | Default |
|-----------|------|----------|-------------|---------|
| `moveToNextPage` | boolean | No | Controls validation and page advancement. See behaviour table below | `false` |

#### `moveToNextPage` Behaviour

| Value | Intended Use | Validation | What Happens on Success | What Happens on Failure |
|-------|-------------|-----------|------------------------|------------------------|
| `false` | Background auto-save while the user is typing | None — answers are stored as-is regardless of format or required state | Answers written. `completedPageIds` unchanged. Always `success: true` | N/A — cannot fail |
| `true` | User clicks "Next" to advance to the next page | Full — all fields on the page are validated (required + format + type) | Answers written, page added to `completedPageIds`, `currentPageIndex` advances. `success: true` | Nothing saved, nothing advanced. `success: false` with `errors[]` in `data` |

> **Key rule**: A page only enters `completedPageIds` through a successful `?moveToNextPage=true` call. `submit` will reject the form if any page is missing from `completedPageIds`.

**Request Body**: A flat `Map<UUID, AnswerValue>` where each key is a `fieldId`.

| Key | Type | Description |
|-----|------|-------------|
| `{fieldId}` | UUID (map key) | The field being answered |
| `.value` | any | Answer value. See answer value types below |
| `.fileUrl` | string | FILE fields — uploaded file URL |
| `.fileName` | string | FILE fields — original file name |
| `.fileSize` | long | FILE fields — file size in bytes |
| `.fileType` | string | FILE fields — MIME type |

#### Answer Value Types

| Field Type | Expected Value |
|------------|---------------|
| `TEXT`, `TEXTAREA`, `EMAIL`, `PHONE`, `URL` | String |
| `NUMBER` | Number (integer or decimal) |
| `DATE` | String — `YYYY-MM-DD` |
| `TIME` | String — `HH:mm` |
| `DATETIME` | String — ISO 8601 datetime |
| `DROPDOWN`, `RADIO` | String — UUID of selected option |
| `CHECKBOX` | Array of strings — UUIDs of selected options |
| `RATING` | Integer — `1` to `5` |
| `FILE` | `null` — metadata goes in `fileUrl`, `fileName`, etc. |
| `HEADER` | Omit entirely — no answer needed |

---

### Example 1 — Auto-save (`moveToNextPage=false`)

**Request**:
```
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=false
```
```json
{
  "091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
  "6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "notanemail@@bad" }
}
```

**Response** — always `200 OK`, always `success: true`, no validation applied:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page saved",
  "action_time": "2026-05-22T14:30:00",
  "data": {
    "eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
    "saved": true,
    "savedAt": "2026-05-22T14:30:00",
    "isValid": true,
    "errors": null
  }
}
```

---

### Example 2 — User clicks "Next", validation fails (`moveToNextPage=true`)

**Request**:
```
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=true
```
```json
{
  "091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
  "6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "notanemail@@bad" },
  "956b1e2e-3e47-4ba9-8248-a6668305ca42": { "value": "" },
  "7bfb4439-1d4d-43e5-a6df-a7dba86f45ea": { "value": "Male" }
}
```

**Response** — `200 OK` but `success: false`. Nothing saved. Client shows inline errors and keeps user on the same page:
```json
{
  "success": false,
  "httpStatus": "OK",
  "message": "Validation failed",
  "action_time": "2026-05-22T14:31:00",
  "data": {
    "eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
    "saved": false,
    "savedAt": null,
    "isValid": false,
    "errors": [
      {
        "fieldId": "6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e",
        "fieldLabel": "Email",
        "errorCode": "INVALID_FORMAT",
        "message": "must be valid email"
      },
      {
        "fieldId": "956b1e2e-3e47-4ba9-8248-a6668305ca42",
        "fieldLabel": "Phone Number",
        "errorCode": "REQUIRED",
        "message": "Phone Number is required"
      }
    ]
  }
}
```

---

### Example 3 — User clicks "Next", validation passes (`moveToNextPage=true`)

**Request**:
```
PUT /events/{eventId}/pages/{pageId}/save?moveToNextPage=true
```
```json
{
  "091467dd-50f9-413f-aa5f-0feb92682945": { "value": "Eddie Murphy" },
  "6246ef54-a1b1-4b06-bd7c-cc1bc7950b8e": { "value": "graphereddy@gmail.com" },
  "956b1e2e-3e47-4ba9-8248-a6668305ca42": { "value": "0627489964" },
  "7bfb4439-1d4d-43e5-a6df-a7dba86f45ea": { "value": "Male" }
}
```

**Response** — `200 OK`, `success: true`. Page written, added to `completedPageIds`, index advances:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page saved",
  "action_time": "2026-05-22T14:32:00",
  "data": {
    "eventId": "d7c52ae3-627f-42b4-a069-dd7c2d7cd78c",
    "saved": true,
    "savedAt": "2026-05-22T14:32:00",
    "isValid": true,
    "errors": null
  }
}
```

---

#### Error Codes in `errors[].errorCode`

| errorCode | Meaning |
|-----------|---------|
| `REQUIRED` | Field is marked required and no value was provided |
| `INVALID_FORMAT` | Value format is wrong (e.g. bad email, invalid phone pattern) |
| `INVALID_TYPE` | Value is the wrong data type for the field (e.g. passing a string to a DROPDOWN expecting a UUID) |
| `VALIDATION_FAILED` | Value fails a custom rule (minLength, maxLength, min, max, minSelections, etc.) |

**Success Response Fields**: `data` is a [SavePageResponse](#j-savepageresponse).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Registration window closed or event not published |
| `404` | Event not found, form not enabled, or no active draft found — call `/start` first |

---

## 28. Get My Response

**Purpose**: Returns the authenticated attendee's current response for the event's form — whether still in draft or already submitted.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/my-response`

**Access Level**: 🔒 Protected (Any authenticated attendee)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Response retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full FormResponseObject" }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `404` | No response found for this attendee on this event's form |

---

## 29. Submit

**Purpose**: Finalises and submits the attendee's response. The server performs a two-stage validation before accepting: first it checks that every page is in `completedPageIds` (meaning the attendee clicked "Next" and passed validation for each page), then it checks that all required fields still have answers. If either check fails the response is rejected with `422` and a list of field-level errors in `data`. On success, `status` transitions to `SUBMITTED` and `completionTimeSeconds` is recorded.

**Endpoint**: `POST` `/api/v1/e-events/applicant-form/events/{eventId}/submit`

**Access Level**: 🔒 Protected (Attendee — response owner)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Form submitted successfully",
  "action_time": "2026-05-22T14:35:00",
  "data": {
    "responseId": "8a067ebe-5eff-496c-a7eb-e3f8ad853b9f",
    "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "submittedBy": "john.doe",
    "status": "SUBMITTED",
    "completedPageIds": ["a64a2f29-428e-41fc-8d14-043841d857a2"],
    "currentPageIndex": 1,
    "startedAt": "2026-05-22T14:30:00",
    "submittedAt": "2026-05-22T14:35:00",
    "completionTimeSeconds": 312,
    "answers": [ "..." ]
  }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission). `data.status` will be `"SUBMITTED"`.

**Error Response — page not completed via "Next" (`422`)**:
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Please complete all required pages",
  "action_time": "2026-05-22T14:34:00",
  "data": [
    {
      "pageId": "a64a2f29-428e-41fc-8d14-043841d857a2",
      "pageTitle": "Personal Info",
      "fieldId": null,
      "fieldLabel": null,
      "errorType": "PAGE_INCOMPLETE",
      "errorMessage": "Page not completed — please review all fields and click Next"
    }
  ]
}
```

**Error Response — required field still unanswered (`422`)**:
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Please complete all required pages",
  "action_time": "2026-05-22T14:34:00",
  "data": [
    {
      "pageId": "a64a2f29-428e-41fc-8d14-043841d857a2",
      "pageTitle": "Personal Info",
      "fieldId": "956b1e2e-3e47-4ba9-8248-a6668305ca42",
      "fieldLabel": "Phone Number",
      "errorType": "REQUIRED",
      "errorMessage": "This field is required"
    }
  ]
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Already submitted, or registration window closed |
| `404` | Event not found, form not enabled, or no draft response found |
| `422` | One or more pages not in `completedPageIds` (`PAGE_INCOMPLETE`); or a required field has no answer (`REQUIRED`). Error list is in `data[]` |

---

## Organizer Response Management

---

## 30. Get Response by ID

**Purpose**: Retrieves a specific attendee response by ID. Available to the event organizer only.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/responses/{responseId}`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |
| `responseId` | UUID | Yes | The response to retrieve | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Response retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full FormResponseObject" }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or response not found |

---

## 31. Get All Responses

**Purpose**: Returns a paginated list of all attendee responses for the event's form. For organizer review and export.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/responses`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (0-based) | Min: 0 | `0` |
| `size` | integer | No | Items per page | Min: 1 | `20` |

> **Note**: This endpoint uses **0-based** pagination (`page=0` is the first page), matching the Spring `Pageable` default passed from the controller.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Responses retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ FormResponseObject entries ]" ],
    "totalElements": 48,
    "totalPages": 3,
    "first": true,
    "last": false,
    "empty": false
  }
}
```

**Success Response Fields**: `data.content[]` contains [FormResponseObject](#g-formresponseobject-attendee-submission) entries.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## 32. Get Analytics

**Purpose**: Returns full submission analytics for the event's form — stats overview, per-field distributions, and daily submission trends.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/analytics`

**Access Level**: 🔒 Protected (Event organizer only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Analytics retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "formId": "9f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
    "formTitle": "Attendee Questions - Dar es Salaam Jazz Festival 2025",
    "stats": {
      "totalStarted": 120,
      "totalDrafts": 18,
      "totalSubmitted": 95,
      "totalWithdrawn": 7,
      "completionRate": 79.2,
      "dropOffRate": 20.8,
      "avgCompletionTimeSeconds": 312.0,
      "fastestTimeSeconds": 45,
      "slowestTimeSeconds": 1890
    },
    "fieldAnalytics": [
      {
        "fieldId": "c3d4e5f6-...",
        "fieldLabel": "Full Name",
        "fieldType": "TEXT",
        "fieldDeleted": false,
        "totalResponses": 95,
        "uniqueResponses": 95,
        "textResponses": ["Amina Hassan", "John Doe", "..."]
      },
      {
        "fieldId": "e5f6a7b8-...",
        "fieldLabel": "Arrival method",
        "fieldType": "DROPDOWN",
        "fieldDeleted": false,
        "totalResponses": 95,
        "choiceDistribution": [
          { "option": "By Car", "count": 42, "percentage": 44.2 },
          { "option": "Public Transport", "count": 35, "percentage": 36.8 },
          { "option": "On Foot", "count": 18, "percentage": 18.9 }
        ]
      }
    ],
    "dailySubmissions": [
      { "date": "2025-07-15", "count": 12 },
      { "date": "2025-07-16", "count": 28 }
    ]
  }
}
```

**Success Response Fields**: `data` is a [FormAnalytics](#k-formanalytics).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found or form not enabled |

---

## Quick Reference — Endpoint Summary

| # | Method | Path | Auth | Who | Description |
|---|--------|------|------|-----|-------------|
| **Form Setup** |
| 1 | POST | `/events/{eventId}/enable` | 🔒 | Organizer | Enable form (empty — no pages) |
| 2 | PUT | `/events/{eventId}/settings` | 🔒 | Organizer | Update form display settings |
| 3 | DELETE | `/events/{eventId}/disable` | 🔒 | Organizer | Disable and remove form |
| 4 | GET | `/events/{eventId}/full-form` | 🔒 | Organizer | Load full form (all pages + fields) for builder |
| **Page Management** |
| 5 | POST | `/events/{eventId}/pages` | 🔒 | Organizer | Add a page |
| 6 | PUT | `/events/{eventId}/pages/{pageId}` | 🔒 | Organizer | Update a page |
| 7 | DELETE | `/events/{eventId}/pages/{pageId}?hard=false` | 🔒 | Organizer | Delete a page (soft or hard) |
| 8 | POST | `/events/{eventId}/pages/bulk` | 🔒 | Organizer | Bulk add pages with fields |
| 9 | DELETE | `/events/{eventId}/pages/bulk?hard=false` | 🔒 | Organizer | Bulk delete pages |
| 10 | POST | `/events/{eventId}/pages/{pageId}/clone` | 🔒 | Organizer | Clone a page |
| **Field Management** |
| 11 | POST | `/events/{eventId}/pages/{pageId}/fields` | 🔒 | Organizer | Add a field to a page |
| 12 | PUT | `/events/{eventId}/fields/{fieldId}` | 🔒 | Organizer | Update a field |
| 13 | DELETE | `/events/{eventId}/fields/{fieldId}?hard=false` | 🔒 | Organizer | Delete a field (soft or hard) |
| 14 | POST | `/events/{eventId}/pages/{pageId}/fields/bulk` | 🔒 | Organizer | Bulk add fields |
| 15 | DELETE | `/events/{eventId}/pages/{pageId}/fields/bulk?hard=false` | 🔒 | Organizer | Bulk delete fields |
| 16 | PATCH | `/events/{eventId}/fields/bulk` | 🔒 | Organizer | Bulk update fields |
| 17 | POST | `/events/{eventId}/fields/{fieldId}/clone` | 🔒 | Organizer | Clone a field |
| **Option Management** |
| 18 | POST | `/events/{eventId}/fields/{fieldId}/options` | 🔒 | Organizer | Add option to a field |
| 19 | PUT | `/events/{eventId}/options/{optionId}` | 🔒 | Organizer | Update an option |
| 20 | DELETE | `/events/{eventId}/options/{optionId}?hard=false` | 🔒 | Organizer | Delete an option (soft or hard) |
| 21 | DELETE | `/events/{eventId}/fields/{fieldId}/options/bulk?hard=false` | 🔒 | Organizer | Bulk delete options |
| 22 | PATCH | `/events/{eventId}/options/bulk` | 🔒 | Organizer | Bulk update option labels |
| **Preview** |
| 23 | GET | `/events/{eventId}/preview/metadata` | 🔒 | Organizer + Attendee* | Form page metadata + attendee progress |
| 24 | GET | `/events/{eventId}/preview/pages/{pageNumber}` | 🔒 | Organizer + Attendee* | Load a page by number (fields + options) |
| 25 | POST | `/events/{eventId}/preview/pages/{pageNumber}/validate` | 🔒 | Organizer + Attendee* | Validate answers without saving |
| **Attendee Submission** |
| 26 | POST | `/events/{eventId}/start` | 🔒 | Attendee | Start form / get existing draft |
| 27 | PUT | `/events/{eventId}/pages/{pageId}/save?moveToNextPage=false` | 🔒 | Attendee | Auto-save answers (no validation) |
| 27 | PUT | `/events/{eventId}/pages/{pageId}/save?moveToNextPage=true` | 🔒 | Attendee | "Next" — validate page and advance if clean |
| 28 | GET | `/events/{eventId}/my-response` | 🔒 | Attendee | Get my response |
| 29 | POST | `/events/{eventId}/submit` | 🔒 | Attendee | Submit form (requires all pages completed via "Next") |
| **Organizer — Responses & Analytics** |
| 30 | GET | `/events/{eventId}/responses/{responseId}` | 🔒 | Organizer | Get response by ID |
| 31 | GET | `/events/{eventId}/responses` | 🔒 | Organizer | Get all responses (paginated) |
| 32 | GET | `/events/{eventId}/analytics` | 🔒 | Organizer | Get form analytics |

> \* Attendee access to preview endpoints requires the event to be **PUBLISHED**.

> All paths are prefixed with `/api/v1/e-events/applicant-form`.

---

## Data Format Standards

| Concern | Standard |
|---------|----------|
| Timestamps | `ZonedDateTime` — ISO 8601 with offset: `2025-02-17T10:30:45+03:00` |
| Local timestamps | `LocalDateTime` — ISO 8601 no offset: `2025-02-17T10:30:45` |
| Dates | `YYYY-MM-DD` — `2025-07-18` |
| Times | `HH:mm` — `18:00` |
| IDs | UUID v4 — `3fa85f64-5717-4562-b3fc-2c963f66afa6` |
| Pagination (responses) | 0-based `page` param, Spring `Page` wrapper |
| Enums | Uppercase: `BEFORE_CHECKOUT`, `SUBMITTED`, `DROPDOWN`, `REQUIRED` |

# Attendance Analytics API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-23
**Version**: v2.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Attendance Management API provides comprehensive attendee tracking and analytics for event organizers. Organizers can view overall attendance statistics with per-day and per-ticket-type breakdowns, browse a unified attendance list (present, absent, and partially attended in one view) with rich filtering, and drill into the full check-in history for any individual ticket. The system supports both single-day and multi-day events.

**Hints**:
- **Organizer Only**: All endpoints restricted to the event organizer
- **Unified List**: Present and absent tickets are in one list — use `?status=PRESENT|ABSENT|PARTIALLY_ATTENDED` to filter
- **Ticket-Based Rows**: Each row in the list is one ticket, not one person — one buyer can appear multiple times
- **Attendee Number**: Unique human-readable ticket ID = `bookingReference + "-" + ticketSeries` (e.g. `EVT-A3F4B21C-VIP-0042`)
- **Buyer vs Attendee**: `buyer` = who paid, `attendee` = who holds the ticket — can be different people
- **Buyer Types**: `SYSTEM_USER` = online purchase with account, `AT_DOOR` = walk-in name only (no account)
- **Day Filtering**: `?dayNumber` is optional — omit for overall view, provide for per-day status
- **Partial Attendance**: `PARTIALLY_ATTENDED` only applies when no `dayNumber` filter is set on multi-day events
- **Pagination**: Default 20 per page
- **Form Response**: `formResponseId` on each attendee/ticket row links to the buyer's applicant form submission — `null` if the event had no form or the form was not yet submitted

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-05-23T10:30:45",
  "data": {
  }
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2026-05-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|---------------|---------|------------------------------------------------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, NOT_FOUND, FORBIDDEN, etc.) |
| `message` | string | Human-readable message describing the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object | Response payload for success, error string for failures |

---

## HTTP Method Badge Standards

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PUT** - <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> - Yellow (Update/replace entire resource)
- **PATCH** - <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> - Orange (Partial updates)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - Red (Remove resources)

---

## Endpoints

## 1. Get Attendance Summary
**Purpose**: Returns overall attendance statistics for an event including per-day and per-ticket-type breakdowns. Always covers the full event — no filtering applied.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/e-events/attendance/{eventId}/summary`

**Access Level**: 🔒 Protected — Event Organizer Only

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|-----------------|--------|----------|----------------------------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | Event identifier | Must be a valid UUID of an existing event owned by the authenticated organizer |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendance summary retrieved",
  "action_time": "2026-05-23T10:30:45",
  "data": {
    "eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "eventTitle": "East African Tech Summit 2026",
    "totalDays": 3,
    "eventSchedule": [
      { "dayNumber": 1, "dayName": "Day 1 - Opening",    "date": "2026-12-15" },
      { "dayNumber": 2, "dayName": "Day 2 - Conference", "date": "2026-12-16" },
      { "dayNumber": 3, "dayName": "Day 3 - Closing",    "date": "2026-12-17" }
    ],
    "overallStats": {
      "totalTickets": 500,
      "present": 310,
      "partiallyAttended": 45,
      "absent": 145,
      "attendanceRate": 71.0,
      "byDay": [
        {
          "dayNumber": 1,
          "dayName": "Day 1 - Opening",
          "date": "2026-12-15",
          "totalTickets": 500,
          "checkedIn": 400,
          "absent": 100,
          "attendanceRate": 80.0,
          "status": "COMPLETED"
        },
        {
          "dayNumber": 2,
          "dayName": "Day 2 - Conference",
          "date": "2026-12-16",
          "totalTickets": 500,
          "checkedIn": 355,
          "absent": 145,
          "attendanceRate": 71.0,
          "status": "ONGOING"
        },
        {
          "dayNumber": 3,
          "dayName": "Day 3 - Closing",
          "date": "2026-12-17",
          "totalTickets": 500,
          "checkedIn": 0,
          "absent": 500,
          "attendanceRate": 0.0,
          "status": "UPCOMING"
        }
      ]
    },
    "byTicketType": [
      {
        "ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
        "ticketTypeName": "VIP Pass",
        "totalSold": 100,
        "present": 75,
        "partiallyAttended": 10,
        "absent": 15,
        "attendanceRate": 85.0,
        "byDay": [
          {
            "dayNumber": 1,
            "dayName": "Day 1 - Opening",
            "checkedIn": 90,
            "absent": 10,
            "attendanceRate": 90.0
          }
        ]
      }
    ]
  }
}
```

**Success Response Fields**:
| Field | Description |
|-----------------------------------------------|--------------------------------------------------------------|
| `data.eventId` | Event UUID |
| `data.eventTitle` | Event name |
| `data.totalDays` | Number of event days |
| `data.eventSchedule` | List of days with `dayNumber`, `dayName`, `date` |
| `data.overallStats.totalTickets` | Total tickets sold |
| `data.overallStats.present` | Tickets checked in for all days |
| `data.overallStats.partiallyAttended` | Tickets checked in for some days (multi-day only) |
| `data.overallStats.absent` | Tickets never checked in |
| `data.overallStats.attendanceRate` | `((present + partiallyAttended) / total) × 100`, 2 dp |
| `data.overallStats.byDay[].status` | `COMPLETED`, `ONGOING`, or `UPCOMING` |
| `data.byTicketType` | Same stats broken down per ticket type |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Event not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "Event not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event does not exist

---

## 2. Get Attendance List
**Purpose**: Returns a paginated, filterable list of all tickets for an event. Each row is one ticket. Present and absent tickets are unified in one list — use the `status` filter to narrow down.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/e-events/attendance/{eventId}/list`

**Access Level**: 🔒 Protected — Event Organizer Only

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|-----------------|--------|----------|-----------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | Event identifier | Must be a valid UUID of an existing event owned by the authenticated organizer |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|--------------|--------|----------|-----------------------------------------------------------|--------------------------------------------|---------|
| `status` | enum | No | Filter by attendance status | `PRESENT`, `ABSENT`, `PARTIALLY_ATTENDED` | All |
| `dayNumber` | integer | No | Filter for a specific event day | Must be between `1` and `totalDays` | None (overall) |
| `ticketTypeId` | UUID | No | Filter by ticket type | Must belong to this event | All types |
| `buyerType` | enum | No | Filter by how the ticket was purchased | `SYSTEM_USER`, `AT_DOOR` | All |
| `search` | string | No | Search by attendee name, email, or phone | Partial match, case-insensitive | None |
| `page` | integer | No | Page number (0-indexed) | Min: `0` | `0` |
| `size` | integer | No | Items per page | Min: `1` | `20` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendance list retrieved",
  "action_time": "2026-05-23T10:30:45",
  "data": {
    "eventId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "eventTitle": "East African Tech Summit 2026",
    "appliedFilters": {
      "ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
      "ticketTypeName": "VIP Pass",
      "status": "ALL",
      "dayNumber": 1,
      "dayName": "Day 1 - Opening",
      "buyerType": "ALL",
      "search": null
    },
    "summary": {
      "totalTickets": 100,
      "present": 90,
      "absent": 10,
      "partiallyAttended": null,
      "attendanceRate": 90.0
    },
    "attendees": [
      {
        "ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa8",
        "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "attendeeNumber": "EVT-A3F4B21C-VIP-0042",
        "ticketTypeId": "3fa85f64-5717-4562-b3fc-2c963f66afa7",
        "ticketType": "VIP Pass",
        "ticketSeries": "VIP-0042",
        "attendeeName": "John Doe",
        "attendeeEmail": "john@example.com",
        "attendeePhone": "+255712345678",
        "buyer": {
          "buyerName": "Jane Doe",
          "buyerEmail": "jane@example.com",
          "buyerId": "3fa85f64-5717-4562-b3fc-2c963f66afa9",
          "buyerType": "SYSTEM_USER"
        },
        "bookingReference": "EVT-A3F4B21C",
        "pricePaid": 150.00,
        "attendanceStatus": "PRESENT",
        "checkInTime": "2026-12-15T09:15:00+03:00",
        "checkInLocation": "Main Gate",
        "checkedInBy": "Scanner Operator 1",
        "scannerId": "3fa85f64-5717-4562-b3fc-2c963f66afb0"
      },
      {
        "ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afb1",
        "formResponseId": null,
        "attendeeNumber": "EVT-B5D2E12F-GENE-0101",
        "ticketType": "General Admission",
        "ticketSeries": "GENE-0101",
        "attendeeName": "Ali Hassan",
        "attendeeEmail": null,
        "attendeePhone": null,
        "buyer": {
          "buyerName": "Ali Hassan",
          "buyerEmail": null,
          "buyerId": null,
          "buyerType": "AT_DOOR"
        },
        "bookingReference": "EVT-B5D2E12F",
        "pricePaid": 50.00,
        "attendanceStatus": "ABSENT",
        "checkInTime": null,
        "checkInLocation": null,
        "checkedInBy": null,
        "scannerId": null
      }
    ],
    "pagination": {
      "currentPage": 0,
      "pageSize": 20,
      "totalPages": 5,
      "totalElements": 100,
      "hasNext": true,
      "hasPrevious": false
    }
  }
}
```

**Success Response Fields**:
| Field | Description |
|---------------------------------------|----------------------------------------------------------------|
| `data.appliedFilters` | Echo of all active filters so the client knows what was applied |
| `data.summary.totalTickets` | Total tickets matching the ticket type filter (unaffected by status/search) |
| `data.summary.present` | Tickets with `PRESENT` status under current filters |
| `data.summary.absent` | Tickets with `ABSENT` status under current filters |
| `data.summary.partiallyAttended` | `null` when `dayNumber` is active; count when viewing overall multi-day |
| `data.summary.attendanceRate` | `((present + partiallyAttended) / total) × 100`, 2 dp |
| `data.attendees[].ticketInstanceId` | Unique ticket instance UUID |
| `data.attendees[].formResponseId` | UUID of the buyer's applicant form response — `null` if the event had no form |
| `data.attendees[].attendeeNumber` | Unique readable ID = `bookingReference + "-" + ticketSeries` |
| `data.attendees[].attendanceStatus` | `PRESENT`, `ABSENT`, or `PARTIALLY_ATTENDED` |
| `data.attendees[].buyer.buyerType` | `SYSTEM_USER` (online) or `AT_DOOR` (walk-in) |
| `data.attendees[].buyer.buyerId` | Populated for `SYSTEM_USER`, `null` for `AT_DOOR` |
| `data.attendees[].checkInTime` | `null` when `attendanceStatus` is `ABSENT` |
| `data.pagination` | Standard pagination envelope |

**Notes**:
- When `dayNumber` is provided: `attendanceStatus` is `PRESENT` or `ABSENT` for that specific day only
- When `dayNumber` is omitted: `attendanceStatus` reflects the overall across all days (`PARTIALLY_ATTENDED` possible)
- `summary` reflects the current filter scope, not the whole event
- `partiallyAttended` in `summary` is `null` when a `dayNumber` filter is active

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Invalid day 5. Valid: 1-3",
  "action_time": "2026-05-23T10:30:45",
  "data": "Invalid day 5. Valid: 1-3"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event not found / day number out of range / ticket type not found

---

## 3. Get Attendee Detail
**Purpose**: Returns the full check-in history for a single ticket across all event days. Shows per-day status including upcoming days.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `/e-events/attendance/{eventId}/attendees/{ticketInstanceId}`

**Access Level**: 🔒 Protected — Event Organizer Only

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|-----------------|--------|----------|------------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-------------------|------|----------|--------------------------------------|---------------------------------------------------|
| `eventId` | UUID | Yes | Event identifier | Must be owned by the authenticated organizer |
| `ticketInstanceId` | UUID | Yes | Individual ticket instance identifier | Must belong to a booking under this event |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendee detail retrieved",
  "action_time": "2026-05-23T10:30:45",
  "data": {
    "ticketInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa8",
    "formResponseId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "attendeeNumber": "EVT-A3F4B21C-VIP-0042",
    "attendeeName": "John Doe",
    "attendeeEmail": "john@example.com",
    "attendeePhone": "+255712345678",
    "ticketType": "VIP Pass",
    "ticketSeries": "VIP-0042",
    "bookingReference": "EVT-A3F4B21C",
    "pricePaid": 150.00,
    "overallStatus": "PARTIALLY_ATTENDED",
    "daysAttended": 2,
    "daysTotal": 3,
    "checkInsByDay": [
      {
        "dayNumber": 1,
        "dayName": "Day 1 - Opening",
        "dayDate": "2026-12-15",
        "status": "CHECKED_IN",
        "checkInTime": "2026-12-15T09:15:00+03:00",
        "checkInLocation": "Main Gate",
        "checkedInBy": "Scanner Operator 1",
        "scannerId": "3fa85f64-5717-4562-b3fc-2c963f66afb0"
      },
      {
        "dayNumber": 2,
        "dayName": "Day 2 - Conference",
        "dayDate": "2026-12-16",
        "status": "NOT_CHECKED_IN",
        "checkInTime": null,
        "checkInLocation": null,
        "checkedInBy": null,
        "scannerId": null
      },
      {
        "dayNumber": 3,
        "dayName": "Day 3 - Closing",
        "dayDate": "2026-12-17",
        "status": "UPCOMING",
        "checkInTime": null,
        "checkInLocation": null,
        "checkedInBy": null,
        "scannerId": null
      }
    ]
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------------------------------|---------------------------------------------------------------|
| `data.ticketInstanceId` | Unique ticket instance UUID |
| `data.formResponseId` | UUID of the buyer's applicant form response — `null` if the event had no form |
| `data.attendeeNumber` | Unique readable ID = `bookingReference + "-" + ticketSeries` |
| `data.overallStatus` | `PRESENT` (all days), `PARTIALLY_ATTENDED` (some), `ABSENT` (none) |
| `data.daysAttended` | Count of days this ticket was checked in |
| `data.daysTotal` | Total number of event days |
| `data.checkInsByDay[].status` | `CHECKED_IN`, `NOT_CHECKED_IN`, or `UPCOMING` |
| `data.checkInsByDay[].checkInTime` | `null` when not checked in or day is upcoming |
| `data.checkInsByDay[].checkedInBy` | Name of the scanner operator who processed the check-in |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Ticket not found",
  "action_time": "2026-05-23T10:30:45",
  "data": "Ticket not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Missing or invalid token
- `403 FORBIDDEN`: Authenticated user is not the event organizer
- `404 NOT_FOUND`: Event not found or ticket instance does not belong to this event

---

## Quick Reference Guide

### Attendance Status Values
| Value | Meaning | Available when |
|----------------------|-----------------------------------------|----------------------------------------------|
| `PRESENT` | Checked in (all days if no day filter) | Always |
| `ABSENT` | Never checked in | Always |
| `PARTIALLY_ATTENDED` | Checked in some days but not all | No `dayNumber` filter, multi-day events only |

### Day Status Values
| Value | Meaning |
|------------------|--------------------------------|
| `CHECKED_IN` | Ticket was scanned on that day |
| `NOT_CHECKED_IN` | Day passed, ticket not scanned |
| `UPCOMING` | Day has not started yet |

### Buyer Type Values
| Value | `buyerEmail` | `buyerId` | Meaning |
|---------------|--------------|-----------|------------------------------------------|
| `SYSTEM_USER` | Populated | Populated | Bought online, has platform account |
| `AT_DOOR` | `null` | `null` | Walk-in sale, name only captured at door |

### Common HTTP Status Codes
- `200 OK`: Successful request
- `401 UNAUTHORIZED`: Authentication required or token invalid
- `403 FORBIDDEN`: Not the event organizer
- `404 NOT_FOUND`: Event, ticket, or day number not found
- `500 INTERNAL_SERVER_ERROR`: Unexpected server error

# Event Feedback API

**Author**: Josh, Lead Backend Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Event Feedback API enables attendees to rate and review events after attendance. This API provides a simple 5-star rating system with optional comments, prevents duplicate feedback, ensures only attendees (non-organizers) can submit reviews, and allows anyone to view event feedback with pagination. The system supports escrow release decisions based on feedback quality and helps organizers improve future events.

**Hints**: 
- **One Feedback Per User**: Each user can only submit one feedback per event
- **Attendee Only**: Event organizers cannot review their own events
- **5-Star Rating**: Required rating from 1-5 stars
- **Optional Comments**: Text reviews up to 1000 characters
- **Public Viewing**: Anyone can view event feedback (paginated)
- **Chronological Order**: Newest feedback first
- **Escrow Integration**: Feedback may influence escrow release (future)
- **Simple Model**: Focus on rating quality over complex metrics

---

## Response Structure

### EventFeedbackResponse
```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "eventId": "770e8400-e29b-41d4-a716-446655440002",
  "eventTitle": "East African Tech Summit 2025",
  "userId": "660e8400-e29b-41d4-a716-446655440001",
  "userName": "johndoe",
  "rating": 5,
  "comment": "Amazing event! The speakers were excellent and the venue was perfect. Definitely attending next year!",
  "createdAt": "2025-12-18T10:30:45Z"
}
```

### Paginated Response
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Feedbacks retrieved successfully",
  "action_time": "2025-12-11T10:30:45",
  "data": {
    "content": [
      {
        "id": "uuid",
        "eventId": "uuid",
        "eventTitle": "East African Tech Summit 2025",
        "userId": "uuid",
        "userName": "johndoe",
        "rating": 5,
        "comment": "Great event!",
        "createdAt": "2025-12-18T10:30:45Z"
      }
    ],
    "pageable": {
      "pageNumber": 0,
      "pageSize": 20,
      "offset": 0
    },
    "totalElements": 145,
    "totalPages": 8,
    "last": false,
    "first": true,
    "numberOfElements": 20,
    "size": 20,
    "number": 0,
    "empty": false
  }
}
```

---

## Endpoints

## 1. Create Feedback
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px;">POST</span> `/feedbacks/event/{eventId}`

**Access**: 🔒 Authenticated Users (Non-Organizers)

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token |
| Content-Type | string | Yes | application/json |

**Request Body**:
```json
{
  "rating": 5,
  "comment": "Amazing event! The speakers were excellent and the venue was perfect. Definitely attending next year!"
}
```

**Request Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| rating | integer | Yes | Star rating | Min: 1, Max: 5 |
| comment | string | No | Text review | Max: 1000 characters |

**Success Response**: Returns EventFeedbackResponse

**Success Response Message**: "Feedback submitted successfully"

**HTTP Status Code**: 201 CREATED

**Behavior**:
1. Validates user is authenticated
2. Validates event exists
3. Checks user is NOT the event organizer
4. Checks user hasn't already submitted feedback
5. Creates feedback record
6. Returns created feedback

**Validation Rules**:
- ✅ Rating must be 1-5 (integer)
- ✅ Comment optional, max 1000 chars
- ✅ One feedback per user per event
- ✅ Organizers cannot review own events
- ✅ Event must exist

**Standard Error Types**:
- `400 BAD_REQUEST`: Invalid rating (not 1-5)
- `401 UNAUTHORIZED`: Not authenticated
- `403 FORBIDDEN`: Organizer trying to review own event
- `404 NOT_FOUND`: Event not found
- `409 CONFLICT`: Already submitted feedback

**Error Response Examples**:

*Invalid Rating (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Rating must be at least 1",
  "action_time": "2025-12-11T10:30:45",
  "data": "Rating must be at least 1"
}
```

*Organizer Cannot Review (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Event organizers cannot submit feedback for their own events.",
  "action_time": "2025-12-11T10:30:45",
  "data": "Event organizers cannot submit feedback for their own events."
}
```

*Already Submitted (409):*
```json
{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "You have already provided feedback for this event",
  "action_time": "2025-12-11T10:30:45",
  "data": "You have already provided feedback for this event"
}
```

---

## 2. Get Event Feedbacks
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/feedbacks/event/{eventId}?page=0&size=20`

**Access**: 🔓 Public (No Authentication Required)

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 20) |

**Success Response**: Returns Spring Page object with EventFeedbackResponse items

**Success Response Message**: "Feedbacks retrieved successfully"

**HTTP Status Code**: 200 OK

**Behavior**:
- Returns paginated list of feedback
- Sorted by creation date (newest first)
- No authentication required (public access)
- Validates event exists

**Pagination Details**:
- Zero-indexed pages (0, 1, 2...)
- Default page size: 20
- Max page size: 100 (recommended)
- Includes total elements and pages

**Standard Error Types**:
- `404 NOT_FOUND`: Event not found

---

## Rating System

### Star Ratings (1-5)

| Rating | Meaning | Emoji | Description |
|--------|---------|-------|-------------|
| 5 ⭐⭐⭐⭐⭐ | Excellent | 😍 | Outstanding event, exceeded expectations |
| 4 ⭐⭐⭐⭐ | Very Good | 😊 | Great event, minor improvements possible |
| 3 ⭐⭐⭐ | Good | 🙂 | Satisfactory event, met expectations |
| 2 ⭐⭐ | Fair | 😐 | Below expectations, needs improvement |
| 1 ⭐ | Poor | 😞 | Disappointing event, significant issues |

### Average Rating Calculation

```
Average = Sum of all ratings / Number of feedbacks
```

**Example**:
```
Ratings: 5, 5, 4, 5, 3, 4, 5, 5, 4, 5
Total: 45
Count: 10
Average: 4.5 stars
```

**Display Format**: `4.5 ⭐ (10 reviews)`

---

## Comment Guidelines

### Recommended Comment Content

**Good Comments Include**:
- ✅ What you enjoyed
- ✅ Specific highlights (speakers, venue, activities)
- ✅ Suggestions for improvement
- ✅ Overall experience summary

**Example Good Comments**:
```
"Amazing event! The keynote speakers were excellent and provided valuable insights. 
The venue was perfect and well-organized. Only suggestion would be to have longer 
lunch breaks. Will definitely attend next year!"

"Great networking opportunities and diverse range of topics. Sound system could 
be improved in the main hall."

"Well organized but sessions felt rushed. Would prefer fewer sessions with more 
time for Q&A."
```

**Avoid**:
- ❌ Offensive language
- ❌ Personal attacks
- ❌ Spam or promotional content
- ❌ Off-topic comments

### Character Limit
- Maximum: **1000 characters**
- Recommended: 100-500 characters
- Minimum: None (optional field)

---

## Access Control

### Who Can Submit Feedback?

**Allowed**:
- ✅ Any authenticated user
- ✅ Attendees (purchased tickets)
- ✅ Non-attendees (also allowed currently)

**Not Allowed**:
- ❌ Event organizers (for their own events)
- ❌ Users who already submitted feedback
- ❌ Unauthenticated users

**Future Restriction** (Recommended):
- Only users who purchased tickets
- Requires checking booking history
- Prevents fake reviews

### Who Can View Feedback?

**Anyone** (Public access):
- ✅ No authentication required
- ✅ Potential attendees researching events
- ✅ Event organizers viewing their reviews
- ✅ Platform admins

---

## Use Cases

### Submit Feedback After Event
```
POST /feedbacks/event/{eventId}
{
  "rating": 5,
  "comment": "Great event, highly recommend!"
}

User provides honest review after attending
```

### View Event Reviews Before Booking
```
GET /feedbacks/event/{eventId}?page=0&size=20

Potential attendee checks reviews before purchasing tickets
Sees average rating and recent comments
```

### Organizer Checks Feedback
```
GET /feedbacks/event/{eventId}?page=0&size=20

Organizer views all feedback for their event
Identifies areas for improvement
Plans better future events
```

### Platform Quality Monitoring
```
GET /feedbacks/event/{eventId}?page=0&size=20

Platform admin reviews feedback
Identifies low-rated events
May influence escrow release decisions
```

---

## Future Enhancements

### Planned Features

**1. Verified Attendee Badge**
- Only show "Verified Attendee" if user has booking
- Increases trust in reviews
- Current: Anyone can review (not implemented)

**2. Average Rating Endpoint**
```
GET /feedbacks/event/{eventId}/summary
{
  "averageRating": 4.5,
  "totalReviews": 145,
  "ratingDistribution": {
    "5": 90,
    "4": 35,
    "3": 15,
    "2": 3,
    "1": 2
  }
}
```

**3. Helpful Votes**
- Users can mark reviews as helpful
- Sort by most helpful
- Bubble up quality reviews

**4. Organizer Response**
- Organizers can reply to feedback
- Shows engagement and care
- Builds trust with future attendees

**5. Escrow Integration**
- Average rating influences escrow release
- Events <3 stars may require manual review
- Automatic release for 4+ stars
- Current: Not implemented

---

## Escrow Release Logic (Future)

### How Feedback May Affect Escrow

**High Ratings** (4-5 stars average):
- ✅ Automatic escrow release
- Indicates successful event
- No manual review needed

**Medium Ratings** (3-3.9 stars average):
- ⚠️ Manual review triggered
- Platform checks for issues
- May contact organizer
- Usually released after review

**Low Ratings** (<3 stars average):
- ❌ Escrow release delayed
- Investigation required
- May require organizer explanation
- Possible refunds if serious issues

**Example**:
```
Event with 4.5 average rating (90% 5-star):
→ Escrow automatically released 24 hours after event

Event with 2.8 average rating (50% 1-2 star):
→ Escrow held, manual review, contact organizer
→ Possible partial refunds if issues confirmed
```

**Current Status**: Not implemented (all events release automatically)

---

## Best Practices

### For Attendees
✅ Submit honest, constructive feedback  
✅ Mention specific positives and negatives  
✅ Wait until after event to review  
✅ Be respectful in comments  
✅ Update review if organizer addresses issues (future)  

### For Organizers
✅ Read all feedback carefully  
✅ Identify patterns in complaints  
✅ Thank reviewers for positive feedback (future)  
✅ Address concerns in organizer response (future)  
✅ Use feedback to improve future events  

### For Platform
✅ Monitor feedback quality  
✅ Flag suspicious reviews  
✅ Use ratings in escrow decisions (future)  
✅ Display average ratings prominently  
✅ Encourage verified attendee reviews (future)  

---

## Quick Reference

### HTTP Status Codes
- `200 OK`: Feedbacks retrieved successfully
- `201 CREATED`: Feedback submitted successfully
- `400 BAD_REQUEST`: Invalid rating value
- `401 UNAUTHORIZED`: Not authenticated
- `403 FORBIDDEN`: Organizer reviewing own event
- `404 NOT_FOUND`: Event not found
- `409 CONFLICT`: Already submitted feedback

### Rating Range
- **Minimum**: 1 star (Poor)
- **Maximum**: 5 stars (Excellent)
- **Type**: Integer only (no half stars)

### Comment Length
- **Minimum**: 0 (optional)
- **Maximum**: 1000 characters
- **Recommended**: 100-500 characters

### Pagination
- **Default Page**: 0 (first page)
- **Default Size**: 20 items
- **Max Size**: 100 (recommended)
- **Sort Order**: Newest first (createdAt DESC)

### Data Formats
- **Event ID**: UUID format
- **User ID**: UUID format
- **Created At**: ISO 8601 timestamp (Instant)
- **Rating**: Integer (1-5)

---

## Integration Examples

### Submit Feedback Form
```javascript
const submitFeedback = async (eventId, rating, comment) => {
  const response = await fetch(`/api/v1/e-events/feedbacks/event/${eventId}`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ rating, comment })
  });
  
  if (response.status === 201) {
    alert('Thank you for your feedback!');
  } else if (response.status === 409) {
    alert('You have already reviewed this event');
  } else if (response.status === 403) {
    alert('Organizers cannot review their own events');
  }
};
```

### Display Feedback List
```javascript
const loadFeedback = async (eventId, page = 0) => {
  const response = await fetch(
    `/api/v1/e-events/feedbacks/event/${eventId}?page=${page}&size=20`
  );
  const data = await response.json();
  
  const feedbacks = data.data.content;
  const averageRating = calculateAverage(feedbacks);
  
  displayFeedbacks(feedbacks, averageRating);
};

const calculateAverage = (feedbacks) => {
  if (feedbacks.length === 0) return 0;
  const sum = feedbacks.reduce((acc, f) => acc + f.rating, 0);
  return (sum / feedbacks.length).toFixed(1);
};
```

### Star Rating Component
```javascript
const StarRating = ({ rating, onChange }) => {
  const stars = [1, 2, 3, 4, 5];
  
  return (
    <div className="star-rating">
      {stars.map(star => (
        <span
          key={star}
          className={star <= rating ? 'star filled' : 'star'}
          onClick={() => onChange(star)}
        >
          ⭐
        </span>
      ))}
    </div>
  );
};
```

---

## Conclusion

The Event Feedback API provides simple yet effective review system with:

✅ **5-Star Ratings**: Simple and universally understood  
✅ **Optional Comments**: Detailed written feedback  
✅ **One Per User**: Prevents spam and duplicate reviews  
✅ **Organizer Protection**: Can't review own events  
✅ **Public Access**: Anyone can view to inform decisions  
✅ **Pagination**: Handles large numbers of reviews  
✅ **Future Integration**: Ready for escrow release logic

# Event Fund Claims

**Author**: Josh S. Sakweli, Backend Lead — QBIT SPARK CO LIMITED  
**Version**: v1.0  
**Base URL**: `/api/v1/e-events/claims`

---

## Overview

When an event organizer sells tickets, revenue is held in escrow — one escrow entry per buyer checkout session. A **Fund Claim** is a formal request to release **all currently `HELD` escrows** for an event into the organizer's wallet. Every claim goes through a `PENDING → APPROVED / REJECTED` lifecycle, and funds only move on explicit admin approval.

Claim approval is an all-or-nothing escrow operation — it releases every `HELD` escrow for that event at the moment of approval. Partial escrow release is not supported by design (see [Financial Safety](#financial-safety-refund--claim-concurrency)). What the organizer does with the wallet balance after release is handled by the separate `DisbursementService`.

**Authentication**: All endpoints require `Authorization: Bearer <jwt_token>`

---

## Business Rules

| Rule | Detail |
|------|--------|
| One pending per event | At most ONE `PENDING` claim per event at any time |
| Claimable amount formula | `totalRevenue − totalRefunded − totalClaimed − totalPendingClaims` |
| Admin-only for active events | If event has not ended, only ADMIN can initiate a claim |
| Refund deadline | 3 days before event start. Past this → no refunds, organizer may claim early |
| Escrow release | Approval releases ALL `HELD` escrows for the event atomically → organizer wallet. No partial release |
| No partial amounts | Escrows are atomic per buyer. "80% release" has no business meaning at the escrow layer — staging happens via `DisbursementService` after funds land in wallet |
| Admin claims require note | `adminNote` is mandatory on admin-initiated claims |
| Refund safety | Refunds and approvals use pessimistic write lock on EscrowAccount to prevent race conditions |

---

## Claim Status Lifecycle

| Status | Meaning |
|--------|---------|
| `PENDING` | Submitted, awaiting admin review. Escrow NOT yet released |
| `APPROVED` | Admin approved. Escrow atomically released to organizer wallet |
| `REJECTED` | Admin rejected. No funds moved. Organizer may submit a new claim |
| `CANCELLED` | Organizer cancelled before admin action. No funds moved |

---

## Standard Response Format

### Success
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim submitted successfully",
  "action_time": "2026-04-27T10:30:45",
  "data": { }
}
```

### Error
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "A pending claim already exists for this event",
  "action_time": "2026-04-27T10:30:45",
  "data": "A pending claim already exists for this event"
}
```

---

## Endpoints

---

### 1. List All Claims (Admin)

**Endpoint**: `GET /api/v1/e-events/claims`  
**Access**: 🔒 `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`  
**Purpose**: Returns all fund claims across all events. Optionally filter by status.

**Query Parameters**:

| Parameter | Type | Required | Description | Default |
|-----------|------|----------|-------------|---------|
| `status` | `ClaimStatus` enum | No | Filter by: `PENDING`, `APPROVED`, `REJECTED`, `CANCELLED` | — (all returned) |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claims retrieved",
  "data": [
    {
      "claimId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "claimNumber": "EFC-2026-000001",
      "eventId": "uuid",
      "eventTitle": "Dar Jazz Night",
      "eventStatus": "PUBLISHED",
      "organizerId": "uuid",
      "organizerName": "Amina Hassan",
      "status": "PENDING",
      "claimedAmount": 50000.00,
      "totalRevenueSnapshot": 80000.00,
      "totalRefundedSnapshot": 5000.00,
      "totalPreviouslyClaimedSnapshot": 0.00,
      "totalPendingAtSubmission": 0.00,
      "currency": "TZS",
      "adminInitiated": false,
      "adminId": null,
      "adminNote": null,
      "organizerNote": "Requesting first partial claim",
      "reviewedById": null,
      "reviewerName": null,
      "reviewNote": null,
      "reviewedAt": null,
      "escrowsReleasedCount": 0,
      "escrowsSkippedCount": 0,
      "actualReleasedAmount": null,
      "initiatedAt": "2026-04-20T08:00:00",
      "updatedAt": "2026-04-20T08:00:00"
    }
  ]
}
```

**Response Fields**:

| Field | Type | Description |
|-------|------|-------------|
| `claimId` | `UUID` | Unique claim identifier |
| `claimNumber` | `string` | Human-readable reference, e.g. `EFC-2026-000001` |
| `eventId` | `UUID` | The event this claim belongs to |
| `eventTitle` | `string` | Display name of the event |
| `eventStatus` | `EventStatus` | Current event status |
| `organizerId` | `UUID` | Organizer's account ID |
| `organizerName` | `string` | Organizer's display name |
| `status` | `ClaimStatus` | Current claim status |
| `claimedAmount` | `BigDecimal` | Amount the organizer requested |
| `totalRevenueSnapshot` | `BigDecimal` | Organizer's net revenue at time of submission |
| `totalRefundedSnapshot` | `BigDecimal` | Total refunds issued at time of submission |
| `totalPreviouslyClaimedSnapshot` | `BigDecimal` | Already released to wallet before this claim |
| `totalPendingAtSubmission` | `BigDecimal` | Other pending claim amounts at submission time |
| `currency` | `string` | Currency code, e.g. `TZS` |
| `adminInitiated` | `boolean` | `true` if an admin submitted on behalf of organizer |
| `adminId` | `UUID` | Admin's account ID (null if organizer-initiated) |
| `adminNote` | `string` | Admin's reason for initiating (null if organizer-initiated) |
| `organizerNote` | `string` | Optional note from organizer |
| `reviewedById` | `UUID` | ID of admin who reviewed (null if pending) |
| `reviewerName` | `string` | Name of reviewing admin (null if pending) |
| `reviewNote` | `string` | Admin's review note (null if pending) |
| `reviewedAt` | `LocalDateTime` | Timestamp of review (null if pending) |
| `escrowsReleasedCount` | `Integer` | Number of escrow entries released on approval |
| `escrowsSkippedCount` | `Integer` | Escrow entries skipped (e.g. already refunded) |
| `actualReleasedAmount` | `BigDecimal` | Actual amount credited to wallet on approval |
| `initiatedAt` | `LocalDateTime` | When the claim was submitted |
| `updatedAt` | `LocalDateTime` | Last updated timestamp |

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Caller does not have admin role |

---

### 2. Get My Claims (Organizer)

**Endpoint**: `GET /api/v1/e-events/claims/my-claims`  
**Access**: 🔒 Authenticated organizer (any role)  
**Purpose**: Returns all fund claims submitted by the currently authenticated organizer, across all their events.

**No query parameters.**

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "My fund claims retrieved",
  "data": [ /* array of EventFundClaimResponse — same structure as endpoint 1 */ ]
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |

---

### 3. Get Claim by ID

**Endpoint**: `GET /api/v1/e-events/claims/{claimId}`  
**Access**: 🔒 Authenticated — admin sees any claim; organizer sees only their own  
**Purpose**: Returns full details of a single fund claim.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `claimId` | `UUID` | Yes | The claim's unique identifier |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Claim retrieved",
  "data": { /* EventFundClaimResponse */ }
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Organizer attempting to view another organizer's claim |
| `404 NOT_FOUND` | Claim with given ID does not exist |

---

### 4. Get Event Claims

**Endpoint**: `GET /api/v1/e-events/claims/event/{eventId}`  
**Access**: 🔒 Authenticated — organizer (own events) or admin (any event)  
**Purpose**: Returns all claims ever submitted for a specific event.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `eventId` | `UUID` | Yes | The event's unique identifier |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event claims retrieved",
  "data": [ /* array of EventFundClaimResponse */ ]
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Organizer does not own this event |
| `404 NOT_FOUND` | Event not found |

---

### 5. Get Claimable Amount

**Endpoint**: `GET /api/v1/e-events/claims/event/{eventId}/claimable-amount`  
**Access**: 🔒 Authenticated — organizer (own events) or admin  
**Purpose**: Returns the current claimable amount breakdown for an event, including eligibility status and the active pending claim if one exists.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `eventId` | `UUID` | Yes | The event's unique identifier |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Claimable amount retrieved",
  "data": {
    "eventId": "uuid",
    "eventTitle": "Dar Jazz Night",
    "totalRevenue": 80000.00,
    "totalRefunded": 5000.00,
    "totalClaimed": 0.00,
    "totalPendingClaims": 0.00,
    "claimableAmount": 60000.00,
    "currency": "TZS",
    "eligible": true,
    "ineligibilityReason": null,
    "activePendingClaimId": null,
    "refundDeadline": "2026-05-10T00:00:00+03:00",
    "pastRefundDeadline": false
  }
}
```

> **Note on claimable amount**: `claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims`. This is a **snapshot estimate** used to indicate eligibility — the `actualReleasedAmount` on approval is the real number, as it reflects the live `HELD` escrow balance at the moment of approval.

**Response Fields**:

| Field | Type | Description |
|-------|------|-------------|
| `totalRevenue` | `BigDecimal` | Net organizer share of all non-refunded tickets |
| `totalRefunded` | `BigDecimal` | Gross amount returned to buyers |
| `totalClaimed` | `BigDecimal` | Already released to organizer wallet |
| `totalPendingClaims` | `BigDecimal` | Locked by current `PENDING` claims |
| `claimableAmount` | `BigDecimal` | What the organizer can claim right now |
| `eligible` | `boolean` | Whether a new claim can be submitted |
| `ineligibilityReason` | `string` | Human-readable reason if `eligible = false` |
| `activePendingClaimId` | `UUID` | ID of the existing pending claim, or `null` |
| `refundDeadline` | `ZonedDateTime` | `eventStartDate − 3 days` |
| `pastRefundDeadline` | `boolean` | `true` = refund window closed, full amount claimable |

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Organizer does not own this event |
| `404 NOT_FOUND` | Event not found |

---

### 6. Get Revenue Summary (Admin)

**Endpoint**: `GET /api/v1/e-events/claims/event/{eventId}/revenue-summary`  
**Access**: 🔒 `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`  
**Purpose**: Returns a full financial summary of an event including gross sales, platform fees, refunds, claimed amounts, and escrow balance. Used for admin audit and oversight.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `eventId` | `UUID` | Yes | The event's unique identifier |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event revenue summary retrieved",
  "data": {
    "eventId": "uuid",
    "eventTitle": "Dar Jazz Night",
    "grossRevenue": 100000.00,
    "totalRefunded": 5000.00,
    "platformFees": 10000.00,
    "netOrganizerRevenue": 85000.00,
    "totalClaimed": 0.00,
    "totalPendingClaims": 0.00,
    "escrowBalance": 85000.00,
    "currency": "TZS"
  }
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Caller does not have admin role |
| `404 NOT_FOUND` | Event not found |

---

### 7. Submit Claim (Organizer)

**Endpoint**: `POST /api/v1/e-events/claims/event/{eventId}`  
**Access**: 🔒 Authenticated organizer — must own the event  
**Purpose**: Organizer submits a fund claim for the full current claimable amount. The claim amount is computed server-side — organizer cannot override it.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `eventId` | `UUID` | Yes | The event to submit a claim for |

**Request Body** (optional):
```json
{
  "organizerNote": "Requesting first partial claim before event ends"
}
```

**Request Body Fields**:

| Field | Type | Required | Description | Validation |
|-------|------|----------|-------------|------------|
| `organizerNote` | `string` | No | Optional note from the organizer | Max 1000 characters |

**Guard Rules Applied Server-Side**:
- Event must belong to the authenticated organizer
- Event must have ended **OR** `pastRefundDeadline = true` (otherwise only admin can claim)
- `claimableAmount` must be > 0
- No existing `PENDING` claim for this event

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim submitted successfully",
  "data": { /* EventFundClaimResponse with status: PENDING */ }
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `400 BAD_REQUEST` | A pending claim already exists for this event |
| `400 BAD_REQUEST` | Claimable amount is zero — nothing to claim |
| `400 BAD_REQUEST` | Event has not ended and refund deadline has not passed — only admin can claim |
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Authenticated user does not own this event |
| `404 NOT_FOUND` | Event not found |

---

### 8. Admin-Initiate Claim

**Endpoint**: `POST /api/v1/e-events/claims/event/{eventId}/admin-initiate`  
**Access**: 🔒 `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`  
**Purpose**: Admin submits a fund claim on behalf of the organizer. Used when the event is still active (ticket sales ongoing). The claim is flagged `adminInitiated = true` with a mandatory audit note.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `eventId` | `UUID` | Yes | The event to claim funds for |

**Request Body**:
```json
{
  "adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds."
}
```

**Request Body Fields**:

| Field | Type | Required | Description | Validation |
|-------|------|----------|-------------|------------|
| `adminNote` | `string` | **Yes** | Mandatory reason for admin-initiated claim | Not blank |

**Guard Rules Applied Server-Side**:
- `claimableAmount` must be > 0
- No existing `PENDING` claim for this event
- Claim is capped at `claimableAmount` (never full `totalRevenue`)

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Admin-initiated claim created",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000003",
    "status": "PENDING",
    "adminInitiated": true,
    "adminId": "uuid-of-admin",
    "adminNote": "Organizer requested early release. Event ending tomorrow, no pending refunds.",
    "claimedAmount": 64000.00,
    "currency": "TZS"
  }
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `400 BAD_REQUEST` | A pending claim already exists for this event |
| `400 BAD_REQUEST` | Claimable amount is zero — nothing to claim |
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Caller does not have admin role |
| `404 NOT_FOUND` | Event not found |
| `422 UNPROCESSABLE_ENTITY` | `adminNote` is blank |

---

### 9. Approve Claim (Admin)

**Endpoint**: `POST /api/v1/e-events/claims/{claimId}/approve`  
**Access**: 🔒 `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`  
**Purpose**: Admin approves a pending claim. This atomically acquires a pessimistic write lock on the `EscrowAccount`, fetches **all escrows currently in `HELD` status** for the event, releases every one of them, and credits the full released amount to the organizer's wallet.

> **Design note**: Approval releases ALL `HELD` escrows — not just the `claimedAmount` subset. Each escrow is atomic (one buyer's payment for one checkout session) so partial release has no valid business meaning. The `claimedAmount` on the claim is a snapshot estimate taken at submission time. The `actualReleasedAmount` stored on approval is the authoritative number — it reflects the live `HELD` balance at the moment the admin approves, and may differ if refunds were processed in between.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `claimId` | `UUID` | Yes | The claim to approve |

**Request Body** (optional):
```json
{
  "reviewNote": "Verified escrow balance. Approved for release."
}
```

**Request Body Fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reviewNote` | `string` | No | Optional note from reviewing admin |

> **Critical**: Approval acquires a `PESSIMISTIC_WRITE` lock on the `EscrowAccount` row. Concurrent refund operations for the same event will block until the approval transaction completes, preventing escrow from going negative.

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim approved and funds released",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000001",
    "status": "APPROVED",
    "claimedAmount": 50000.00,
    "actualReleasedAmount": 48000.00,
    "escrowsReleasedCount": 12,
    "escrowsSkippedCount": 1,
    "reviewedById": "admin-uuid",
    "reviewerName": "Admin John",
    "reviewNote": "Verified escrow balance. Approved for release.",
    "reviewedAt": "2026-04-27T14:30:00"
  }
}
```

> **Note on `actualReleasedAmount`**: May be less than `claimedAmount` if one or more escrow entries were refunded between claim submission and admin approval. The `escrowsSkippedCount` indicates how many entries were skipped for this reason.

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `400 BAD_REQUEST` | Claim is not in `PENDING` status |
| `400 BAD_REQUEST` | Escrow balance insufficient to cover claim amount |
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Caller does not have admin role |
| `404 NOT_FOUND` | Claim not found |

---

### 10. Reject Claim (Admin)

**Endpoint**: `POST /api/v1/e-events/claims/{claimId}/reject`  
**Access**: 🔒 `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`  
**Purpose**: Admin rejects a pending claim. No funds are moved. The organizer may submit a new claim after rejection.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `claimId` | `UUID` | Yes | The claim to reject |

**Request Body** (optional):
```json
{
  "reviewNote": "Pending dispute investigation. Please resubmit after resolution."
}
```

**Request Body Fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reviewNote` | `string` | No | Reason for rejection (shown to organizer) |

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim rejected",
  "data": {
    "claimId": "uuid",
    "claimNumber": "EFC-2026-000001",
    "status": "REJECTED",
    "reviewedById": "admin-uuid",
    "reviewerName": "Admin John",
    "reviewNote": "Pending dispute investigation. Please resubmit after resolution.",
    "reviewedAt": "2026-04-27T14:35:00"
  }
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `400 BAD_REQUEST` | Claim is not in `PENDING` status |
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Caller does not have admin role |
| `404 NOT_FOUND` | Claim not found |

---

### 11. Cancel Claim (Organizer)

**Endpoint**: `DELETE /api/v1/e-events/claims/{claimId}`  
**Access**: 🔒 Authenticated organizer — must be the claim owner  
**Purpose**: Organizer cancels their own pending claim before any admin action. This frees up the pending amount, allowing a new claim to be submitted.

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `claimId` | `UUID` | Yes | The claim to cancel |

**No request body.**

**Success Response**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fund claim cancelled",
  "data": null
}
```

**Possible Errors**:

| HTTP Status | Description |
|-------------|-------------|
| `400 BAD_REQUEST` | Claim is not in `PENDING` status (already reviewed) |
| `401 UNAUTHORIZED` | Invalid or expired JWT |
| `403 FORBIDDEN` | Organizer does not own this claim |
| `404 NOT_FOUND` | Claim not found |

---

## Financial Safety: Refund & Claim Concurrency

This section explains how the system prevents money from being over-released when a refund and a claim approval happen simultaneously, and why partial escrow release is not supported.

### Why Partial Release Is Not Supported

Each escrow is atomic — it represents one buyer's payment for one checkout session. To release "80% of claimable funds" the service would have to arbitrarily pick specific escrow entries to release and leave others `HELD`. There is no business logic that justifies which individual buyer's payment stays locked — the organizer delivered the event to all of them equally.

```
10 escrows × 950 TZS = 9,500 TZS claimable
"Release 80%" = 7,600 TZS = release 8 escrows, hold 2
→ WHY are those 2 buyers' payments still locked?
→ No valid answer exists at the escrow layer.
```

The correct model is:

```
Claim approval → ALL HELD escrows released → Organizer wallet (full amount lands)
                                                      ↓
                                              Organizer controls staged
                                              payouts via DisbursementService
```

The organizer's wallet is the right layer for staged payouts. The claim domain's job is simply: verify eligibility, get admin approval, atomically convert all `HELD` escrows into wallet balance.

### The Race Condition Risk

```
Revenue = 10,000 TZS in HELD escrows
PENDING claim submitted
→ Refund of 2,000 processed concurrently
→ Claim approved → releases all HELD (now 8,000)
→ Refund deducts 2,000 from already-decremented escrow
→ Escrow goes negative 💀
```

### Protections in Place

| Layer | Mechanism |
|-------|-----------|
| Refund deadline | No refunds allowed within 3 days of event. Eliminates the race entirely post-deadline. |
| Pessimistic DB lock | Both `approveClaim()` and `processRefund()` acquire `PESSIMISTIC_WRITE` lock on the `EscrowAccount` row — serializing the two operations. |
| Balance check | Before any deduction, service verifies `escrow.balance >= amount`. Throws `InsufficientEscrowException` if not. |
| Snapshot vs actual | `claimedAmount` is a snapshot estimate at submission time. `actualReleasedAmount` is the real released amount at approval time — reflects live `HELD` balance after any intervening refunds. |
| `escrowsSkippedCount` | Tracks how many escrow entries were already `REFUNDED` or `DISPUTED` at approval time and therefore skipped. |

### Claimable Amount Formula

```
claimableAmount = totalRevenue − totalRefunded − totalClaimed − totalPendingClaims
```

This formula gives an estimate of what's claimable. The actual amount released on approval equals the live sum of all `HELD` escrow `sellerAmount` values at that moment.

---

## Endpoint Quick Reference

| # | Method | Path | Access | Description |
|---|--------|------|--------|-------------|
| 1 | `GET` | `/claims` | Admin | List all claims (filter by status) |
| 2 | `GET` | `/claims/my-claims` | Organizer | Get my own claims |
| 3 | `GET` | `/claims/{claimId}` | Organizer / Admin | Get single claim by ID |
| 4 | `GET` | `/claims/event/{eventId}` | Organizer / Admin | Get all claims for an event |
| 5 | `GET` | `/claims/event/{eventId}/claimable-amount` | Organizer / Admin | Get claimable amount breakdown |
| 6 | `GET` | `/claims/event/{eventId}/revenue-summary` | Admin | Full event revenue summary |
| 7 | `POST` | `/claims/event/{eventId}` | Organizer | Submit a new claim |
| 8 | `POST` | `/claims/event/{eventId}/admin-initiate` | Admin | Admin-initiate claim for active event |
| 9 | `POST` | `/claims/{claimId}/approve` | Admin | Approve claim + release escrow |
| 10 | `POST` | `/claims/{claimId}/reject` | Admin | Reject claim |
| 11 | `DELETE` | `/claims/{claimId}` | Organizer | Cancel pending claim |

---

## Standard Error Reference

| HTTP Status | Scenario |
|-------------|---------|
| `400 BAD_REQUEST` | Business rule violation (duplicate pending, zero claimable, wrong status) |
| `401 UNAUTHORIZED` | Missing, expired, or malformed JWT |
| `403 FORBIDDEN` | Insufficient role or ownership violation |
| `404 NOT_FOUND` | Event or claim does not exist |
| `422 UNPROCESSABLE_ENTITY` | Validation failure (e.g. blank `adminNote`) |
| `500 INTERNAL_SERVER_ERROR` | Unexpected server error |

# Exproling

# Attendee Questions API Guide

## What Is It?

A customizable questionnaire system that event organizers attach to their events to collect information from attendees during registration. Think of it like Google Forms, but built specifically for events and integrated into the ticket purchase flow.

---

## The Two Sides

### Organizer Side (Building the Form)

When an organizer creates an event, they can optionally enable "Attendee Questions." The system automatically creates an empty form with one page. The organizer then builds their questionnaire by:

1. **Adding pages** - to organize questions into logical sections (e.g., "Personal Info", "Dietary Needs", "Travel Details")

2. **Adding fields to pages** - choosing from various field types like text, email, phone, dropdowns, checkboxes, ratings, file uploads, etc.

3. **Adding options** - for choice-based fields (dropdown, radio, checkbox), the organizer adds the available choices

4. **Configuring settings** - deciding when to show the form (before or after checkout), whether it's required, and if walk-in attendees should also complete it

The organizer can reorder pages, fields, and options. They can edit labels, add descriptions, set validation rules (minimum length, date ranges, file size limits), and mark fields as required or optional.

---

### Attendee Side (Filling the Form)

When someone registers for a published event, they see the questionnaire. The system works page-by-page:

1. **View a page** - attendee sees one page of questions at a time with navigation showing progress (Page 1 of 3)

2. **Fill and save** - as the attendee completes a page and moves to the next, their answers are validated and saved automatically. If any answer is invalid (wrong format, too short, invalid selection), the page won't save and they see specific error messages for each problematic field.

3. **Submit** - when ready, the attendee clicks submit. The system checks ALL pages to ensure every required field is answered and all answers are valid. If everything passes, the submission is finalized. If not, they see a summary showing which pages have problems.

The key principle: **validate before saving, never store invalid data**. This keeps the database clean and gives immediate feedback.

---

## The Analytics Side (Viewing Results)

Once attendees start submitting, the organizer can view comprehensive analytics:

- **Summary statistics** - total submissions, completion rates, average time to complete, submission trends over time, and which pages have the highest drop-off rates

- **Field-by-field breakdown** - for each question, see response rates and detailed analysis. For choice fields, see how many people selected each option with percentages. For numeric fields, see averages, ranges, and distributions. For text fields, see common words and length statistics.

- **Spreadsheet view** - all responses in a table format, like Google Forms' response spreadsheet, with each row being one submission and each column being one question

- **Export** - download all data as CSV or Excel for external analysis

---

## How Is It Useful?

### For Event Organizers

**Logistics planning:**
- Collect t-shirt sizes to order the right quantities
- Gather dietary requirements for catering
- Know arrival times for transportation planning
- Get emergency contact information for safety

**Attendee profiling:**
- Ask about interests for personalized experiences
- Collect company/role info for networking features
- Understand how attendees heard about the event for marketing

**Compliance and safety:**
- Collect required documents (ID uploads)
- Gather health declarations
- Get consent for photography/recording
- Verify age requirements

**Engagement:**
- Ask what sessions attendees plan to attend
- Collect questions for Q&A panels
- Gather song requests for performers
- Get feedback on previous events

### For Attendees

**Smoother experience:**
- Complete registration in one flow instead of separate forms
- Save progress and return later (draft mode)
- Clear validation messages prevent submission errors
- Know exactly what's required vs optional

**Personalization:**
- Their preferences are captured for a tailored experience
- Dietary needs are communicated without awkward conversations
- Special requirements are noted in advance

---

## Real-World Example

**Music Festival Scenario:**

An organizer creates a 3-day festival. They enable attendee questions with two pages:

**Page 1: Essential Info**
- Emergency contact name (required, text)
- Emergency contact phone (required, phone validation)
- T-shirt size (required, dropdown: S/M/L/XL)
- Camping or day pass? (required, radio)

**Page 2: Preferences**
- Dietary restrictions (optional, checkbox: vegetarian, vegan, gluten-free, halal, kosher)
- Other dietary notes (optional, textarea)
- Which headliner are you most excited for? (optional, dropdown with artist names)

When 500 people register, the organizer sees:
- 65% chose Medium or Large t-shirts
- 23% have dietary restrictions (42% of those are vegetarian)
- Artist X is the most anticipated headliner with 45% of votes
- Page 2 has 15% drop-off (people skipping optional questions)

They export the data to:
- Order t-shirts: 50 S, 180 M, 170 L, 100 XL
- Tell catering: 115 vegetarian meals, 45 vegan, 60 gluten-free
- Adjust sound check priority based on headliner popularity

---

## Why This Design?

**Page-by-page saves** prevent data loss. If someone fills 3 pages then loses internet, they don't lose everything.

**Strict validation before saving** means the database only contains valid, usable data. No garbage entries to clean up later.

**Empty-body submit** means the final submission is just a validation check, not a data transfer. Everything is already saved from auto-saves.

**Parallel page validation on submit** makes the final check fast even with many pages.

**Separation of draft and published** means organizers can freely edit their form while building the event, but once published and people start responding, the structure is stable.

---

## Summary

It's a form builder tailored for events. Organizers build custom questionnaires, attendees fill them page-by-page with auto-save and validation, and organizers get rich analytics to plan better events. The technical design prioritizes data integrity, user experience, and actionable insights.

## System Flow

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           ORGANIZER FLOW                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. Create Event Draft                                                      │
│     POST /api/v1/e-events/drafts                                           │
│                    ↓                                                        │
│  2. Enable Attendee Questions                                               │
│     PUT /api/v1/e-events/drafts/{draftId}/attendee-questions               │
│                    ↓                                                        │
│  3. Add Pages (optional - default page created automatically)               │
│     POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages        │
│                    ↓                                                        │
│  4. Add Fields to Pages                                                     │
│     POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/{pageId}/fields │
│                    ↓                                                        │
│  5. Add Options to Choice Fields (dropdown/radio/checkbox)                  │
│     POST /api/v1/e-events/drafts/{draftId}/attendee-questions/fields/{fieldId}/options │
│                    ↓                                                        │
│  6. Publish Event                                                           │
│     POST /api/v1/e-events/drafts/{draftId}/publish                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                           ATTENDEE FLOW                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. View Questionnaire (check if registration open)                         │
│     GET /api/v1/e-events/{eventId}/attendee-questions                      │
│                    ↓                                                        │
│  2. Get Each Page                                                           │
│     GET /api/v1/e-events/{eventId}/attendee-questions/pages/{pageNumber}   │
│                    ↓                                                        │
│  3. Save Page Answers (auto-save per page)                                  │
│     PATCH /api/v1/e-events/{eventId}/attendee-questions/responses/pages/{n}│
│                    ↓                                                        │
│  4. Submit Final Response                                                   │
│     POST /api/v1/e-events/{eventId}/attendee-questions/responses/submit    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                           ANALYTICS FLOW (Organizer Only)                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  GET /api/v1/e-events/{eventId}/attendee-questions/analytics/summary       │
│  GET /api/v1/e-events/{eventId}/attendee-questions/analytics/fields        │
│  GET /api/v1/e-events/{eventId}/attendee-questions/analytics/fields/{id}   │
│  GET /api/v1/e-events/{eventId}/attendee-questions/analytics/responses     │
│  GET /api/v1/e-events/{eventId}/attendee-questions/analytics/export/csv    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## Authentication

All endpoints require JWT authentication except public event viewing.

```
Header: Authorization: Bearer {jwt_token}
```

---

# PART 1: ORGANIZER ENDPOINTS

## Base URL
```
/api/v1/e-events/drafts/{draftId}/attendee-questions
```

---

### 1.1 Enable Attendee Questions

Creates a new questionnaire for the event draft. Automatically creates an empty form with one default page.

**Endpoint:**
```
PUT /api/v1/e-events/drafts/{draftId}/attendee-questions
```

**Request:**
```json
{
  "displayTime": "BEFORE_CHECKOUT",
  "isRequiredOnline": true,
  "applyToAtDoor": false
}
```

**Request Fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `displayTime` | enum | No | When to show form: `BEFORE_CHECKOUT` or `AFTER_CHECKOUT`. Default: `BEFORE_CHECKOUT` |
| `isRequiredOnline` | boolean | No | Must complete before checkout? Default: `false` |
| `applyToAtDoor` | boolean | No | Show to walk-in attendees? Default: `false` |

**Response (201 Created):**
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Attendee questions enabled",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "formId": "770e8400-e29b-41d4-a716-446655440002",
    "displayTime": "BEFORE_CHECKOUT",
    "isRequiredOnline": true,
    "applyToAtDoor": false,
    "createdAt": "2025-01-23T10:30:00Z",
    "updatedAt": "2025-01-23T10:30:00Z"
  }
}
```

**Error Responses:**

| Status | Message |
|--------|---------|
| 404 | Draft not found |
| 403 | You don't have access to this draft |
| 403 | Attendee questions already enabled. Use update instead. |

---

### 1.2 Update Attendee Questions Settings

**Endpoint:**
```
PATCH /api/v1/e-events/drafts/{draftId}/attendee-questions
```

**Request:**
```json
{
  "displayTime": "AFTER_CHECKOUT",
  "isRequiredOnline": false
}
```

*All fields optional - only updates provided fields.*

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Settings updated",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "formId": "770e8400-e29b-41d4-a716-446655440002",
    "displayTime": "AFTER_CHECKOUT",
    "isRequiredOnline": false,
    "applyToAtDoor": false,
    "createdAt": "2025-01-23T10:30:00Z",
    "updatedAt": "2025-01-23T10:35:00Z"
  }
}
```

---

### 1.3 Get Attendee Questions (with full form)

**Endpoint:**
```
GET /api/v1/e-events/drafts/{draftId}/attendee-questions
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendee questions retrieved",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "formId": "770e8400-e29b-41d4-a716-446655440002",
    "displayTime": "BEFORE_CHECKOUT",
    "isRequiredOnline": true,
    "applyToAtDoor": false,
    "createdAt": "2025-01-23T10:30:00Z",
    "updatedAt": "2025-01-23T10:30:00Z",
    "form": {
      "formId": "770e8400-e29b-41d4-a716-446655440002",
      "title": "Attendee Questions - Music Festival 2025",
      "description": "Custom questions for event attendees",
      "pages": [
        {
          "pageId": "880e8400-e29b-41d4-a716-446655440003",
          "title": "Personal Information",
          "description": "Basic attendee details",
          "displayOrder": 1,
          "fields": [
            {
              "fieldId": "990e8400-e29b-41d4-a716-446655440004",
              "type": "TEXT",
              "label": "Emergency Contact Name",
              "description": "Who should we contact in case of emergency?",
              "placeholder": "Full name",
              "displayOrder": 1,
              "required": true,
              "validation": {
                "minLength": 2,
                "maxLength": 100
              },
              "options": []
            },
            {
              "fieldId": "aa0e8400-e29b-41d4-a716-446655440005",
              "type": "PHONE",
              "label": "Emergency Contact Phone",
              "displayOrder": 2,
              "required": true,
              "options": []
            },
            {
              "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
              "type": "DROPDOWN",
              "label": "T-Shirt Size",
              "displayOrder": 3,
              "required": true,
              "options": [
                {"optionId": "opt1", "label": "Small", "displayOrder": 1},
                {"optionId": "opt2", "label": "Medium", "displayOrder": 2},
                {"optionId": "opt3", "label": "Large", "displayOrder": 3},
                {"optionId": "opt4", "label": "X-Large", "displayOrder": 4}
              ]
            }
          ]
        },
        {
          "pageId": "cc0e8400-e29b-41d4-a716-446655440007",
          "title": "Dietary Requirements",
          "displayOrder": 2,
          "fields": [
            {
              "fieldId": "dd0e8400-e29b-41d4-a716-446655440008",
              "type": "CHECKBOX",
              "label": "Dietary Restrictions",
              "description": "Select all that apply",
              "displayOrder": 1,
              "required": false,
              "options": [
                {"optionId": "opt5", "label": "Vegetarian", "displayOrder": 1},
                {"optionId": "opt6", "label": "Vegan", "displayOrder": 2},
                {"optionId": "opt7", "label": "Gluten-free", "displayOrder": 3},
                {"optionId": "opt8", "label": "Halal", "displayOrder": 4},
                {"optionId": "opt9", "label": "Kosher", "displayOrder": 5}
              ]
            },
            {
              "fieldId": "ee0e8400-e29b-41d4-a716-446655440009",
              "type": "TEXTAREA",
              "label": "Other Dietary Notes",
              "placeholder": "Any allergies or special requirements?",
              "displayOrder": 2,
              "required": false
            }
          ]
        }
      ]
    }
  }
}
```

**Response when not configured (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendee questions not configured",
  "data": null
}
```

---

### 1.4 Disable Attendee Questions

Permanently deletes the form and all configuration.

**Endpoint:**
```
DELETE /api/v1/e-events/drafts/{draftId}/attendee-questions
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Attendee questions disabled"
}
```

---

## Pages Management

### 1.5 Add Page

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages
```

**Request:**
```json
{
  "title": "Travel Information",
  "description": "Help us plan your arrival"
}
```

**Response (201 Created):**
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Page added",
  "data": {
    "pageId": "ff0e8400-e29b-41d4-a716-446655440010",
    "title": "Travel Information",
    "description": "Help us plan your arrival",
    "displayOrder": 3
  }
}
```

---

### 1.6 Update Page

**Endpoint:**
```
PATCH /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/{pageId}
```

**Request:**
```json
{
  "title": "Travel & Accommodation",
  "description": "Updated description"
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page updated",
  "data": {
    "pageId": "ff0e8400-e29b-41d4-a716-446655440010",
    "title": "Travel & Accommodation",
    "description": "Updated description",
    "displayOrder": 3
  }
}
```

---

### 1.7 Delete Page

**Endpoint:**
```
DELETE /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/{pageId}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page deleted"
}
```

---

### 1.8 Reorder Pages

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/reorder
```

**Request:**
```json
{
  "orderedIds": [
    "cc0e8400-e29b-41d4-a716-446655440007",
    "880e8400-e29b-41d4-a716-446655440003",
    "ff0e8400-e29b-41d4-a716-446655440010"
  ]
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Pages reordered"
}
```

---

## Fields Management

### 1.9 Add Field

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/{pageId}/fields
```

**Request (Text Field):**
```json
{
  "type": "TEXT",
  "label": "Company Name",
  "description": "Your current employer",
  "placeholder": "Enter company name",
  "required": false,
  "validation": {
    "minLength": 2,
    "maxLength": 100
  }
}
```

**Request (Dropdown Field):**
```json
{
  "type": "DROPDOWN",
  "label": "How did you hear about us?",
  "required": true
}
```

**Request (Rating Field):**
```json
{
  "type": "RATING",
  "label": "How excited are you for this event?",
  "required": true,
  "validation": {
    "min": 1,
    "max": 5
  }
}
```

**Request (File Upload Field):**
```json
{
  "type": "FILE",
  "label": "Upload ID Document",
  "description": "Government-issued ID for verification",
  "required": true,
  "validation": {
    "maxSizeMb": 5,
    "accept": "image/*,.pdf"
  }
}
```

**Available Field Types:**

| Type | Description | Has Options |
|------|-------------|-------------|
| `TEXT` | Single line text | No |
| `TEXTAREA` | Multi-line text | No |
| `EMAIL` | Email with validation | No |
| `PHONE` | Phone number | No |
| `NUMBER` | Numeric input | No |
| `DATE` | Date picker | No |
| `TIME` | Time picker | No |
| `DATETIME` | Date and time | No |
| `DROPDOWN` | Single select dropdown | Yes |
| `RADIO` | Single select radio buttons | Yes |
| `CHECKBOX` | Multi-select checkboxes | Yes |
| `RATING` | Star rating | No |
| `SCALE` | Linear scale (1-10) | No |
| `SLIDER` | Slider input | No |
| `FILE` | File upload | No |
| `URL` | URL with validation | No |
| `CURRENCY` | Money input | No |
| `SECTION_HEADER` | Display only - section title | No |
| `PARAGRAPH` | Display only - text block | No |

**Response (201 Created):**
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Field added",
  "data": {
    "fieldId": "110e8400-e29b-41d4-a716-446655440011",
    "type": "DROPDOWN",
    "label": "How did you hear about us?",
    "displayOrder": 1,
    "required": true,
    "options": []
  }
}
```

---

### 1.10 Update Field

**Endpoint:**
```
PATCH /api/v1/e-events/drafts/{draftId}/attendee-questions/fields/{fieldId}
```

**Request:**
```json
{
  "label": "How did you discover this event?",
  "required": false,
  "description": "Help us improve our marketing"
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Field updated",
  "data": {
    "fieldId": "110e8400-e29b-41d4-a716-446655440011",
    "type": "DROPDOWN",
    "label": "How did you discover this event?",
    "description": "Help us improve our marketing",
    "displayOrder": 1,
    "required": false,
    "options": []
  }
}
```

---

### 1.11 Delete Field

**Endpoint:**
```
DELETE /api/v1/e-events/drafts/{draftId}/attendee-questions/fields/{fieldId}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Field deleted"
}
```

---

### 1.12 Reorder Fields

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/pages/{pageId}/fields/reorder
```

**Request:**
```json
{
  "orderedIds": [
    "bb0e8400-e29b-41d4-a716-446655440006",
    "990e8400-e29b-41d4-a716-446655440004",
    "aa0e8400-e29b-41d4-a716-446655440005"
  ]
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fields reordered"
}
```

---

## Options Management (for DROPDOWN, RADIO, CHECKBOX)

### 1.13 Add Option

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/fields/{fieldId}/options
```

**Request:**
```json
{
  "label": "Social Media"
}
```

**Response (201 Created):**
```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Option added",
  "data": {
    "optionId": "220e8400-e29b-41d4-a716-446655440012",
    "label": "Social Media",
    "displayOrder": 1
  }
}
```

---

### 1.14 Update Option

**Endpoint:**
```
PATCH /api/v1/e-events/drafts/{draftId}/attendee-questions/options/{optionId}
```

**Request:**
```json
{
  "label": "Social Media (Facebook, Instagram, etc.)"
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Option updated",
  "data": {
    "optionId": "220e8400-e29b-41d4-a716-446655440012",
    "label": "Social Media (Facebook, Instagram, etc.)",
    "displayOrder": 1
  }
}
```

---

### 1.15 Delete Option

**Endpoint:**
```
DELETE /api/v1/e-events/drafts/{draftId}/attendee-questions/options/{optionId}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Option deleted"
}
```

---

### 1.16 Reorder Options

**Endpoint:**
```
POST /api/v1/e-events/drafts/{draftId}/attendee-questions/fields/{fieldId}/options/reorder
```

**Request:**
```json
{
  "orderedIds": [
    "220e8400-e29b-41d4-a716-446655440012",
    "330e8400-e29b-41d4-a716-446655440013",
    "440e8400-e29b-41d4-a716-446655440014"
  ]
}
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Options reordered"
}
```

---

# PART 2: ATTENDEE ENDPOINTS (Public)

## Base URL
```
/api/v1/e-events/{eventId}/attendee-questions
```

*Note: These endpoints require the event to be PUBLISHED.*

---

### 2.1 Get Questionnaire (First Page)

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions
```

**Response - No questionnaire configured:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "No questionnaire for this event",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "hasForm": false,
    "registrationOpen": false,
    "message": "No questionnaire for this event"
  }
}
```

**Response - Registration not yet open:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Registration opens on 2025-02-01",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "hasForm": true,
    "registrationOpen": false,
    "message": "Registration opens on 2025-02-01",
    "registrationOpensAt": "2025-02-01T09:00:00Z",
    "registrationClosesAt": "2025-02-15T18:00:00Z",
    "displayTime": "BEFORE_CHECKOUT",
    "isRequired": true
  }
}
```

**Response - Registration closed:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Registration closed on 2025-02-15",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "hasForm": true,
    "registrationOpen": false,
    "message": "Registration closed on 2025-02-15",
    "registrationOpensAt": "2025-02-01T09:00:00Z",
    "registrationClosesAt": "2025-02-15T18:00:00Z"
  }
}
```

**Response - Registration open (returns page 1):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Questionnaire retrieved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "hasForm": true,
    "registrationOpen": true,
    "registrationOpensAt": "2025-02-01T09:00:00Z",
    "registrationClosesAt": "2025-02-15T18:00:00Z",
    "displayTime": "BEFORE_CHECKOUT",
    "isRequired": true,
    "currentPage": 1,
    "totalPages": 2,
    "formTitle": "Attendee Questions - Music Festival 2025",
    "formDescription": "Please complete this questionnaire",
    "page": {
      "pageId": "880e8400-e29b-41d4-a716-446655440003",
      "title": "Personal Information",
      "description": "Basic attendee details",
      "displayOrder": 1,
      "fields": [
        {
          "fieldId": "990e8400-e29b-41d4-a716-446655440004",
          "type": "TEXT",
          "label": "Emergency Contact Name",
          "description": "Who should we contact in case of emergency?",
          "placeholder": "Full name",
          "displayOrder": 1,
          "required": true,
          "validation": {
            "minLength": 2,
            "maxLength": 100
          },
          "options": []
        },
        {
          "fieldId": "aa0e8400-e29b-41d4-a716-446655440005",
          "type": "PHONE",
          "label": "Emergency Contact Phone",
          "displayOrder": 2,
          "required": true,
          "options": []
        },
        {
          "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
          "type": "DROPDOWN",
          "label": "T-Shirt Size",
          "displayOrder": 3,
          "required": true,
          "options": [
            {"optionId": "opt1", "label": "Small", "displayOrder": 1},
            {"optionId": "opt2", "label": "Medium", "displayOrder": 2},
            {"optionId": "opt3", "label": "Large", "displayOrder": 3},
            {"optionId": "opt4", "label": "X-Large", "displayOrder": 4}
          ]
        }
      ]
    }
  }
}
```

---

### 2.2 Get Specific Page

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/pages/{pageNumber}
```

**Example:**
```
GET /api/v1/e-events/660e8400-e29b-41d4-a716-446655440001/attendee-questions/pages/2
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page 2 of 2",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "hasForm": true,
    "registrationOpen": true,
    "currentPage": 2,
    "totalPages": 2,
    "page": {
      "pageId": "cc0e8400-e29b-41d4-a716-446655440007",
      "title": "Dietary Requirements",
      "displayOrder": 2,
      "fields": [
        {
          "fieldId": "dd0e8400-e29b-41d4-a716-446655440008",
          "type": "CHECKBOX",
          "label": "Dietary Restrictions",
          "description": "Select all that apply",
          "displayOrder": 1,
          "required": false,
          "options": [
            {"optionId": "opt5", "label": "Vegetarian", "displayOrder": 1},
            {"optionId": "opt6", "label": "Vegan", "displayOrder": 2},
            {"optionId": "opt7", "label": "Gluten-free", "displayOrder": 3},
            {"optionId": "opt8", "label": "Halal", "displayOrder": 4},
            {"optionId": "opt9", "label": "Kosher", "displayOrder": 5}
          ]
        },
        {
          "fieldId": "ee0e8400-e29b-41d4-a716-446655440009",
          "type": "TEXTAREA",
          "label": "Other Dietary Notes",
          "placeholder": "Any allergies or special requirements?",
          "displayOrder": 2,
          "required": false
        }
      ]
    }
  }
}
```

**Error - Page not found:**
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Page not found. Valid pages: 1 to 2"
}
```

---

### 2.3 Save Page Answers (Auto-Save)

Saves answers for a single page. **Validates ALL fields on the page before saving.** If any field is invalid, nothing is saved.

**Endpoint:**
```
PATCH /api/v1/e-events/{eventId}/attendee-questions/responses/pages/{pageNumber}
```

**Request:**
```json
{
  "answers": {
    "990e8400-e29b-41d4-a716-446655440004": {
      "fieldId": "990e8400-e29b-41d4-a716-446655440004",
      "value": "John Doe"
    },
    "aa0e8400-e29b-41d4-a716-446655440005": {
      "fieldId": "aa0e8400-e29b-41d4-a716-446655440005",
      "value": "+255712345678"
    },
    "bb0e8400-e29b-41d4-a716-446655440006": {
      "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
      "value": "Large"
    }
  }
}
```

**Answer Format by Field Type:**

| Field Type | Value Format | Example |
|------------|--------------|---------|
| TEXT, TEXTAREA, EMAIL, PHONE, URL | string | `"John Doe"` |
| NUMBER, CURRENCY, RATING, SCALE, SLIDER | number | `42` or `4.5` |
| DATE | string (YYYY-MM-DD) | `"2025-02-15"` |
| TIME | string (HH:MM) | `"14:30"` |
| DATETIME | string (ISO) | `"2025-02-15T14:30:00"` |
| DROPDOWN, RADIO | string (selected label) | `"Large"` |
| CHECKBOX | array of strings | `["Vegetarian", "Gluten-free"]` |
| FILE | object | See below |

**File Upload Answer:**
```json
{
  "fieldId": "file-field-id",
  "value": null,
  "fileUrl": "https://storage.example.com/uploads/id-doc.pdf",
  "fileName": "id-doc.pdf",
  "fileSize": 1048576,
  "fileType": "application/pdf"
}
```

**Response - Success (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page 1 saved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "pageNumber": 1,
    "saved": true,
    "savedAt": "2025-01-23T10:45:00Z",
    "isValid": true,
    "errors": [],
    "totalFieldsOnPage": 3,
    "answeredFieldsOnPage": 3,
    "overallProgress": {
      "totalPages": 2,
      "completedPages": 1,
      "totalFields": 5,
      "answeredFields": 3,
      "completionPercentage": 60
    }
  }
}
```

**Response - Validation Failed (400 Bad Request):**
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Validation failed - page not saved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "pageNumber": 1,
    "saved": false,
    "isValid": false,
    "errors": [
      {
        "fieldId": "aa0e8400-e29b-41d4-a716-446655440005",
        "fieldLabel": "Emergency Contact Phone",
        "errorCode": "INVALID_PHONE",
        "message": "Please enter a valid phone number"
      },
      {
        "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
        "fieldLabel": "T-Shirt Size",
        "errorCode": "INVALID_OPTION",
        "message": "Selected option is not valid"
      }
    ],
    "totalFieldsOnPage": 3,
    "answeredFieldsOnPage": 3,
    "overallProgress": {
      "totalPages": 2,
      "completedPages": 0,
      "totalFields": 5,
      "answeredFields": 0,
      "completionPercentage": 0
    }
  }
}
```

**Validation Error Codes:**

| Code | Description |
|------|-------------|
| `REQUIRED` | Required field is missing |
| `INVALID_EMAIL` | Invalid email format |
| `INVALID_PHONE` | Invalid phone number |
| `INVALID_NUMBER` | Not a valid number |
| `INVALID_DATE` | Invalid date format |
| `INVALID_TIME` | Invalid time format |
| `INVALID_DATETIME` | Invalid datetime format |
| `INVALID_URL` | Invalid URL format |
| `INVALID_OPTION` | Selected option not in list |
| `INVALID_SELECTION` | Invalid selection format |
| `TOO_SHORT` | Text too short (minLength) |
| `TOO_LONG` | Text too long (maxLength) |
| `BELOW_MIN` | Number below minimum |
| `ABOVE_MAX` | Number above maximum |
| `DATE_TOO_EARLY` | Date before minDate |
| `DATE_TOO_LATE` | Date after maxDate |
| `TOO_FEW_SELECTIONS` | Not enough checkboxes |
| `TOO_MANY_SELECTIONS` | Too many checkboxes |
| `PATTERN_MISMATCH` | Doesn't match regex |
| `FILE_TOO_LARGE` | File exceeds maxSizeMb |
| `INVALID_FILE_TYPE` | File type not accepted |

---

### 2.4 Submit Final Response

Submits the form. **No request body needed** - validates all saved pages and submits if all valid.

**Endpoint:**
```
POST /api/v1/e-events/{eventId}/attendee-questions/responses/submit
```

**Response - Success (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Submission successful",
  "data": {
    "responseId": "550e8400-e29b-41d4-a716-446655440099",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "success": true,
    "status": "SUBMITTED",
    "submittedAt": "2025-01-23T10:50:00Z",
    "pageValidations": [
      {
        "pageNumber": 1,
        "pageTitle": "Personal Information",
        "isValid": true,
        "hasAnswers": true,
        "totalFields": 3,
        "answeredFields": 3,
        "requiredFields": 3,
        "requiredAnswered": 3,
        "errors": []
      },
      {
        "pageNumber": 2,
        "pageTitle": "Dietary Requirements",
        "isValid": true,
        "hasAnswers": true,
        "totalFields": 2,
        "answeredFields": 1,
        "requiredFields": 0,
        "requiredAnswered": 0,
        "errors": []
      }
    ],
    "totalPages": 2,
    "validPages": 2,
    "invalidPages": 0,
    "allPagesValid": true
  }
}
```

**Response - Validation Failed (400 Bad Request):**
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Validation failed - 1 page(s) have errors",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "success": false,
    "status": "DRAFT",
    "pageValidations": [
      {
        "pageNumber": 1,
        "pageTitle": "Personal Information",
        "isValid": false,
        "hasAnswers": true,
        "totalFields": 3,
        "answeredFields": 2,
        "requiredFields": 3,
        "requiredAnswered": 2,
        "errors": [
          {
            "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
            "fieldLabel": "T-Shirt Size",
            "errorCode": "REQUIRED",
            "message": "T-Shirt Size is required"
          }
        ]
      },
      {
        "pageNumber": 2,
        "pageTitle": "Dietary Requirements",
        "isValid": true,
        "hasAnswers": false,
        "totalFields": 2,
        "answeredFields": 0,
        "requiredFields": 0,
        "requiredAnswered": 0,
        "errors": []
      }
    ],
    "totalPages": 2,
    "validPages": 1,
    "invalidPages": 1,
    "allPagesValid": false
  }
}
```

**Error - No draft found:**
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "No draft found. Please save your answers first."
}
```

---

### 2.5 Get Submission Status

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/responses/status
```

**Response - No submission started:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "No submission started",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "success": false,
    "status": null,
    "totalPages": 2,
    "validPages": 0,
    "invalidPages": 0,
    "allPagesValid": false
  }
}
```

**Response - Draft in progress:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Status: DRAFT",
  "data": {
    "responseId": "550e8400-e29b-41d4-a716-446655440099",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "success": false,
    "status": "DRAFT",
    "submittedAt": null,
    "pageValidations": [
      {
        "pageNumber": 1,
        "pageTitle": "Personal Information",
        "isValid": true,
        "hasAnswers": true,
        "totalFields": 3,
        "answeredFields": 3,
        "requiredFields": 3,
        "requiredAnswered": 3,
        "errors": []
      },
      {
        "pageNumber": 2,
        "pageTitle": "Dietary Requirements",
        "isValid": true,
        "hasAnswers": false,
        "totalFields": 2,
        "answeredFields": 0,
        "requiredFields": 0,
        "requiredAnswered": 0,
        "errors": []
      }
    ],
    "totalPages": 2,
    "validPages": 2,
    "invalidPages": 0,
    "allPagesValid": true
  }
}
```

**Response - Already submitted:**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Status: SUBMITTED",
  "data": {
    "responseId": "550e8400-e29b-41d4-a716-446655440099",
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "success": true,
    "status": "SUBMITTED",
    "submittedAt": "2025-01-23T10:50:00Z",
    "totalPages": 2,
    "validPages": 2,
    "invalidPages": 0,
    "allPagesValid": true
  }
}
```

---

### 2.6 Withdraw Submission

**Endpoint:**
```
DELETE /api/v1/e-events/{eventId}/attendee-questions/responses
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Submission withdrawn"
}
```

---

# PART 3: ANALYTICS ENDPOINTS (Organizer Only)

## Base URL
```
/api/v1/e-events/{eventId}/attendee-questions/analytics
```

*Note: Only the event organizer can access analytics.*

---

### 3.1 Get Summary Statistics

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/summary
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Summary retrieved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "eventTitle": "Music Festival 2025",
    "totalStarted": 250,
    "totalDrafts": 45,
    "totalSubmitted": 180,
    "totalWithdrawn": 25,
    "completionRate": 72.0,
    "dropOffRate": 28.0,
    "averageCompletionTimeSeconds": 185.5,
    "fastestCompletionSeconds": 45,
    "slowestCompletionSeconds": 890,
    "submissionTrend": [
      {"date": "2025-01-01", "count": 0},
      {"date": "2025-01-02", "count": 5},
      {"date": "2025-01-03", "count": 12},
      {"date": "2025-01-04", "count": 8},
      {"date": "2025-01-05", "count": 15},
      {"date": "2025-01-06", "count": 22},
      {"date": "2025-01-07", "count": 18}
    ],
    "pageDropOff": [
      {
        "pageNumber": 1,
        "pageTitle": "Personal Information",
        "started": 250,
        "completed": 220,
        "dropOffRate": 12.0
      },
      {
        "pageNumber": 2,
        "pageTitle": "Dietary Requirements",
        "started": 220,
        "completed": 180,
        "dropOffRate": 18.2
      }
    ],
    "firstSubmissionAt": "2025-01-02T09:15:00",
    "lastSubmissionAt": "2025-01-23T10:50:00",
    "generatedAt": "2025-01-23T11:00:00"
  }
}
```

---

### 3.2 Get All Fields Analytics

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/fields
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Fields analytics retrieved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "totalResponses": 180,
    "fields": [
      {
        "fieldId": "990e8400-e29b-41d4-a716-446655440004",
        "label": "Emergency Contact Name",
        "type": "TEXT",
        "pageNumber": 1,
        "required": true,
        "responseCount": 180,
        "skippedCount": 0,
        "responseRate": 100.0,
        "textStats": {
          "averageLength": 15,
          "minLength": 5,
          "maxLength": 45,
          "commonWords": ["john", "jane", "mary", "james", "david"]
        }
      },
      {
        "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
        "label": "T-Shirt Size",
        "type": "DROPDOWN",
        "pageNumber": 1,
        "required": true,
        "responseCount": 180,
        "skippedCount": 0,
        "responseRate": 100.0,
        "optionBreakdown": [
          {"value": "Medium", "count": 65, "percentage": 36.1},
          {"value": "Large", "count": 58, "percentage": 32.2},
          {"value": "Small", "count": 35, "percentage": 19.4},
          {"value": "X-Large", "count": 22, "percentage": 12.2}
        ]
      },
      {
        "fieldId": "dd0e8400-e29b-41d4-a716-446655440008",
        "label": "Dietary Restrictions",
        "type": "CHECKBOX",
        "pageNumber": 2,
        "required": false,
        "responseCount": 95,
        "skippedCount": 85,
        "responseRate": 52.8,
        "optionBreakdown": [
          {"value": "Vegetarian", "count": 42, "percentage": 44.2},
          {"value": "Gluten-free", "count": 28, "percentage": 29.5},
          {"value": "Vegan", "count": 18, "percentage": 18.9},
          {"value": "Halal", "count": 12, "percentage": 12.6},
          {"value": "Kosher", "count": 5, "percentage": 5.3}
        ]
      }
    ]
  }
}
```

---

### 3.3 Get Single Field Analytics

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/fields/{fieldId}?page=0&size=20
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Field analytics retrieved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "fieldId": "bb0e8400-e29b-41d4-a716-446655440006",
    "label": "T-Shirt Size",
    "type": "DROPDOWN",
    "required": true,
    "totalResponses": 180,
    "responseCount": 180,
    "skippedCount": 0,
    "responseRate": 100.0,
    "optionBreakdown": [
      {"value": "Medium", "count": 65, "percentage": 36.1},
      {"value": "Large", "count": 58, "percentage": 32.2},
      {"value": "Small", "count": 35, "percentage": 19.4},
      {"value": "X-Large", "count": 22, "percentage": 12.2}
    ],
    "responses": [
      {
        "responseId": "resp-001",
        "value": "Large",
        "submittedAt": "2025-01-23T10:50:00"
      },
      {
        "responseId": "resp-002",
        "value": "Medium",
        "submittedAt": "2025-01-23T10:48:00"
      }
    ],
    "totalResponseCount": 180,
    "page": 0,
    "pageSize": 20
  }
}
```

---

### 3.4 Get All Responses (Spreadsheet View)

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/responses?page=0&size=20
```

**Response (200 OK):**
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Responses retrieved",
  "data": {
    "eventId": "660e8400-e29b-41d4-a716-446655440001",
    "totalResponses": 180,
    "page": 0,
    "pageSize": 20,
    "totalPages": 9,
    "columns": [
      {"fieldId": "990e8400-e29b-41d4-a716-446655440004", "label": "Emergency Contact Name", "type": "TEXT", "pageNumber": 1},
      {"fieldId": "aa0e8400-e29b-41d4-a716-446655440005", "label": "Emergency Contact Phone", "type": "PHONE", "pageNumber": 1},
      {"fieldId": "bb0e8400-e29b-41d4-a716-446655440006", "label": "T-Shirt Size", "type": "DROPDOWN", "pageNumber": 1},
      {"fieldId": "dd0e8400-e29b-41d4-a716-446655440008", "label": "Dietary Restrictions", "type": "CHECKBOX", "pageNumber": 2},
      {"fieldId": "ee0e8400-e29b-41d4-a716-446655440009", "label": "Other Dietary Notes", "type": "TEXTAREA", "pageNumber": 2}
    ],
    "responses": [
      {
        "responseId": "resp-001",
        "submittedById": "user-001",
        "submittedByName": "kibuti",
        "submittedByEmail": "kibuti@example.com",
        "status": "SUBMITTED",
        "startedAt": "2025-01-23T10:40:00",
        "submittedAt": "2025-01-23T10:50:00",
        "completionTimeSeconds": 600,
        "answers": {
          "990e8400-e29b-41d4-a716-446655440004": "John Doe",
          "aa0e8400-e29b-41d4-a716-446655440005": "+255712345678",
          "bb0e8400-e29b-41d4-a716-446655440006": "Large",
          "dd0e8400-e29b-41d4-a716-446655440008": ["Vegetarian", "Gluten-free"],
          "ee0e8400-e29b-41d4-a716-446655440009": "No nuts please"
        }
      },
      {
        "responseId": "resp-002",
        "submittedById": "user-002",
        "submittedByName": "john_doe",
        "submittedByEmail": "john@example.com",
        "status": "SUBMITTED",
        "startedAt": "2025-01-23T10:30:00",
        "submittedAt": "2025-01-23T10:45:00",
        "completionTimeSeconds": 900,
        "answers": {
          "990e8400-e29b-41d4-a716-446655440004": "Jane Smith",
          "aa0e8400-e29b-41d4-a716-446655440005": "+255798765432",
          "bb0e8400-e29b-41d4-a716-446655440006": "Medium",
          "dd0e8400-e29b-41d4-a716-446655440008": null,
          "ee0e8400-e29b-41d4-a716-446655440009": null
        }
      }
    ]
  }
}
```

---

### 3.5 Export to CSV

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/export/csv
```

**Response:** Downloads a CSV file

**CSV Content:**
```csv
Response ID,Submitted By,Email,Submitted At,Completion Time (seconds),Emergency Contact Name,Emergency Contact Phone,T-Shirt Size,Dietary Restrictions,Other Dietary Notes
resp-001,kibuti,kibuti@example.com,2025-01-23T10:50:00,600,John Doe,+255712345678,Large,Vegetarian; Gluten-free,No nuts please
resp-002,john_doe,john@example.com,2025-01-23T10:45:00,900,Jane Smith,+255798765432,Medium,,
```

---

### 3.6 Export to Excel

**Endpoint:**
```
GET /api/v1/e-events/{eventId}/attendee-questions/analytics/export/excel
```

**Response:** Downloads an Excel (.xlsx) file

---

# PART 4: COMPLETE ENDPOINTS REFERENCE

## Organizer Endpoints (Draft Events)

| Method | Endpoint | Description |
|--------|----------|-------------|
| PUT | `/drafts/{draftId}/attendee-questions` | Enable questionnaire |
| PATCH | `/drafts/{draftId}/attendee-questions` | Update settings |
| GET | `/drafts/{draftId}/attendee-questions` | Get with full form |
| DELETE | `/drafts/{draftId}/attendee-questions` | Disable (delete) |
| POST | `/drafts/{draftId}/attendee-questions/pages` | Add page |
| PATCH | `/drafts/{draftId}/attendee-questions/pages/{pageId}` | Update page |
| DELETE | `/drafts/{draftId}/attendee-questions/pages/{pageId}` | Delete page |
| POST | `/drafts/{draftId}/attendee-questions/pages/reorder` | Reorder pages |
| POST | `/drafts/{draftId}/attendee-questions/pages/{pageId}/fields` | Add field |
| PATCH | `/drafts/{draftId}/attendee-questions/fields/{fieldId}` | Update field |
| DELETE | `/drafts/{draftId}/attendee-questions/fields/{fieldId}` | Delete field |
| POST | `/drafts/{draftId}/attendee-questions/pages/{pageId}/fields/reorder` | Reorder fields |
| POST | `/drafts/{draftId}/attendee-questions/fields/{fieldId}/options` | Add option |
| PATCH | `/drafts/{draftId}/attendee-questions/options/{optionId}` | Update option |
| DELETE | `/drafts/{draftId}/attendee-questions/options/{optionId}` | Delete option |
| POST | `/drafts/{draftId}/attendee-questions/fields/{fieldId}/options/reorder` | Reorder options |

## Attendee Endpoints (Published Events)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/{eventId}/attendee-questions` | Get form (page 1) |
| GET | `/{eventId}/attendee-questions/pages/{n}` | Get specific page |
| PATCH | `/{eventId}/attendee-questions/responses/pages/{n}` | Save page (auto-save) |
| POST | `/{eventId}/attendee-questions/responses/submit` | Submit (no body) |
| GET | `/{eventId}/attendee-questions/responses/status` | Get my status |
| DELETE | `/{eventId}/attendee-questions/responses` | Withdraw |

## Analytics Endpoints (Organizer Only)

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/{eventId}/attendee-questions/analytics/summary` | Overall stats |
| GET | `/{eventId}/attendee-questions/analytics/fields` | All fields breakdown |
| GET | `/{eventId}/attendee-questions/analytics/fields/{fieldId}` | Single field detail |
| GET | `/{eventId}/attendee-questions/analytics/responses` | Spreadsheet view |
| GET | `/{eventId}/attendee-questions/analytics/export/csv` | Download CSV |
| GET | `/{eventId}/attendee-questions/analytics/export/excel` | Download Excel |

---

# PART 5: INSOMNIA TESTING GUIDE

## Setup Environment Variables

```
base_url: http://localhost:8080/api/v1
jwt_token: <your_token>
draft_id: <your_draft_id>
event_id: <your_published_event_id>
page_id: <page_uuid>
field_id: <field_uuid>
option_id: <option_uuid>
```

## Testing Flow

### Step 1: Create Event Draft
```
POST {{base_url}}/e-events/drafts
Authorization: Bearer {{jwt_token}}
Content-Type: application/json

{
  "title": "Test Music Festival 2025",
  "categoryId": "{{category_id}}",
  "eventFormat": "IN_PERSON",
  "description": "Test event for questionnaire"
}
```

### Step 2: Enable Attendee Questions
```
PUT {{base_url}}/e-events/drafts/{{draft_id}}/attendee-questions
Authorization: Bearer {{jwt_token}}
Content-Type: application/json

{
  "displayTime": "BEFORE_CHECKOUT",
  "isRequiredOnline": true,
  "applyToAtDoor": false
}
```

### Step 3: Get Questionnaire (see default page)
```
GET {{base_url}}/e-events/drafts/{{draft_id}}/attendee-questions
Authorization: Bearer {{jwt_token}}
```

### Step 4: Add Text Field
```
POST {{base_url}}/e-events/drafts/{{draft_id}}/attendee-questions/pages/{{page_id}}/fields
Authorization: Bearer {{jwt_token}}
Content-Type: application/json

{
  "type": "TEXT",
  "label": "Emergency Contact Name",
  "placeholder": "Full name",
  "required": true,
  "validation": {
    "minLength": 2,
    "maxLength": 100
  }
}
```

### Step 5: Add Dropdown Field
```
POST {{base_url}}/e-events/drafts/{{draft_id}}/attendee-questions/pages/{{page_id}}/fields
Authorization: Bearer {{jwt_token}}
Content-Type: application/json

{
  "type": "DROPDOWN",
  "label": "T-Shirt Size",
  "required": true
}
```

### Step 6: Add Options to Dropdown
```
POST {{base_url}}/e-events/drafts/{{draft_id}}/attendee-questions/fields/{{field_id}}/options
Authorization: Bearer {{jwt_token}}
Content-Type: application/json

{"label": "Small"}
```
Repeat for: Medium, Large, X-Large

### Step 7: Publish Event
```
POST {{base_url}}/e-events/drafts/{{draft_id}}/publish
Authorization: Bearer {{jwt_token}}
```

### Step 8: (As Attendee) Get Questionnaire
```
GET {{base_url}}/e-events/{{event_id}}/attendee-questions
Authorization: Bearer {{attendee_token}}
```

### Step 9: (As Attendee) Save Page 1
```
PATCH {{base_url}}/e-events/{{event_id}}/attendee-questions/responses/pages/1
Authorization: Bearer {{attendee_token}}
Content-Type: application/json

{
  "answers": {
    "{{text_field_id}}": {
      "fieldId": "{{text_field_id}}",
      "value": "John Doe"
    },
    "{{dropdown_field_id}}": {
      "fieldId": "{{dropdown_field_id}}",
      "value": "Large"
    }
  }
}
```

### Step 10: (As Attendee) Submit
```
POST {{base_url}}/e-events/{{event_id}}/attendee-questions/responses/submit
Authorization: Bearer {{attendee_token}}
```

### Step 11: (As Organizer) View Analytics
```
GET {{base_url}}/e-events/{{event_id}}/attendee-questions/analytics/summary
Authorization: Bearer {{jwt_token}}
```

### Step 12: (As Organizer) Export CSV
```
GET {{base_url}}/e-events/{{event_id}}/attendee-questions/analytics/export/csv
Authorization: Bearer {{jwt_token}}
```

---

This guide covers the complete Attendee Questions API. Ready to implement and test!