# 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**: 2025-02-20
**Version**: v1.1

**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 → REGISTRATION_SETUPS → TICKETS → REVIEW`. 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.

---

### Event Creation User Journey

```
  [Organizer]
       │
       │  POST /draft
       ▼
  ┌─────────────┐
  │  BASIC INFO │  title, category, format, description, media
  └──────┬──────┘
         │  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)
         │
         │  PATCH /drafts/{id}/registration
         ▼
  ┌───────────────────────┐
  │  REGISTRATION SETUPS  │  registrationOpensAt, registrationClosesAt
  └──────┬────────────────┘
         │
         │  (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, 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) |
| `virtualDetails.meetingId` | string | Platform meeting ID |
| `virtualDetails.passcode` | string | Meeting passcode |
| `media.banner` | string | Banner image URL |
| `media.thumbnail` | string | Thumbnail image URL |
| `media.gallery[]` | array | List of gallery image URLs |
| `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.organizerId` | UUID | Organizer account ID |
| `organizer.organizerName` | string | Organizer full name |
| `organizer.organizerUsername` | string | Organizer system username |
| `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 |
| `image` | string | Profile/headshot image URL |
| `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 |
| `presenterImage` | string | Headshot image URL |

#### 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) |
| `totalTickets` | integer | Total slots allocated |
| `ticketsSold` | integer | Number sold |
| `ticketsAvailable` | integer | Remaining slots |
| `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 on sale |

---

### 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") |
| `thumbnail` | string | Thumbnail URL |
| `pricing.minPrice` | BigDecimal | Lowest available ticket price |
| `pricing.maxPrice` | BigDecimal | Highest available ticket price |
| `pricing.isFree` | boolean | True when all tickets are free |
| `pricing.hasPaidTickets` | boolean | True when at least one paid ticket exists |
| `organizerId` | UUID | Organizer ID |
| `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.

---

## 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.",
  "media": {
    "banner": "https://cdn.example.com/banners/jazz-2025.jpg",
    "thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
    "gallery": []
  }
}
```

**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 |
| `media` | object | No | Media URLs for the event | See MediaRequest below |
| `media.banner` | string | No | Banner image URL | Max: 500 characters |
| `media.thumbnail` | string | No | Thumbnail image URL | Max: 500 characters |
| `media.gallery` | array | No | List of gallery image URLs | — |

**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": 20,
    "canPublish": false,
    "schedule": null,
    "venue": null,
    "virtualDetails": null,
    "media": {
      "banner": "https://cdn.example.com/banners/jazz-2025.jpg",
      "thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
      "gallery": []
    },
    "highlights": null,
    "faqs": null,
    "lineup": null,
    "agenda": null,
    "linkedProducts": [],
    "linkedShops": [],
    "tickets": [],
    "organizer": {
      "organizerId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "organizerName": "Amina Hassan",
      "organizerUsername": "amina.hassan"
    },
    "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,
        "thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
        "pricing": { "isFree": true, "hasPaidTickets": false },
        "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",
  "media": {
    "banner": "https://cdn.example.com/banners/jazz-2025-v2.jpg",
    "thumbnail": "https://cdn.example.com/thumbs/jazz-2025-v2.jpg",
    "gallery": ["https://cdn.example.com/gallery/img1.jpg"]
  }
}
```

**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` |
| `media` | object | No | Updated media | See media fields in endpoint 1 |

**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.

**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.

**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 — Registration Config

**Purpose**: Sets the registration window (open and close dates) for the event. The window must be valid relative to itself and cannot close after the event ends. Advances `currentStage` to `TICKETS`.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/registration`

**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
{
  "registrationOpensAt": "2025-05-01T08:00:00+03:00",
  "registrationClosesAt": "2025-07-17T23:59:00+03:00",
  "ctaLabel":"Get ticket"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `registrationOpensAt` | ZonedDateTime | Yes | When registration opens | ISO 8601 with timezone offset; must be before `registrationClosesAt` |
| `registrationClosesAt` | ZonedDateTime | Yes | When registration closes | ISO 8601 with timezone offset; must not be after the event's `endDateTime` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Registration config 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 `"REGISTRATION_SETUPS"` 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` | `registrationOpensAt` is after `registrationClosesAt`, or `registrationClosesAt` is after event end |

---

## 9. 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 |

---

## 10. 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 9 (401, 403, 404).

---

## 11. 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.",
      "image": "https://cdn.example.com/artists/dj-afrobeat.jpg",
      "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[].image` | string | No | Headshot URL | 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 `image` from the user's profile. These are also refreshed on every `GET` of the event.

**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 |

---

## 12. 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 |
| `sessions[].presenterImage` | string | No | Image URL | Auto-enriched for platform users |

**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 |

---

## 13. 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/draft/{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 |

---

## 14. 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/draft/{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 |

---

## 15. 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/draft/{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 |

---

## 16. 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/draft/{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 14 (401, 403, 404).

---

## 17. 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, and generates an RSA key pair used for secure ticket QR code signing.

**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 ✓
> - `REGISTRATION_SETUPS` stage completed ✓
> - At least one active ticket exists for the event ✓
> - `registrationOpensAt` is before `registrationClosesAt` ✓
> - `registrationClosesAt` is not after event end ✓
> - 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": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.status` will be `"PUBLISHED"`.

**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 date/registration validation fails |
| `500` | RSA key generation failed |

---

## 18. 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": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.status` will be `"DRAFT"`.

**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 |

---

## 19. 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": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.status` will be `"CANCELLED"`.

**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` |

---

## 20. Update Published Event — Basic Info

**Purpose**: Updates the description and/or media of a published event. Title, category, and format changes are blocked on published events as the slug has already been shared publicly.

**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.",
  "media": {
    "banner": "https://cdn.example.com/banners/jazz-2025-v3.jpg",
    "thumbnail": "https://cdn.example.com/thumbs/jazz-2025-v3.jpg",
    "gallery": ["https://cdn.example.com/gallery/img1.jpg", "https://cdn.example.com/gallery/img2.jpg"]
  }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `description` | string | No | Updated event description | Min: 15, Max: 5000 characters |
| `media` | object | No | Updated media URLs | See media fields in Endpoint 1 |
| `media.banner` | string | No | Banner image URL | Max: 500 characters |
| `media.thumbnail` | string | No | Thumbnail image URL | Max: 500 characters |
| `media.gallery` | array | No | Gallery image URLs | — |

> **Blocked fields**: `title`, `categoryId`, `eventFormat`, and `eventVisibility` cannot be changed on a published event. Providing them will have no effect.

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Published event updated 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).

**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 |

---

## 21. Update Published Event — Registration Window

**Purpose**: Extends the registration close date for a published event. The new `registrationClosesAt` must be later than the current value — shortening the registration window is not allowed.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/registration`

**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
{
  "registrationClosesAt": "2025-07-20T23:59:00+03:00"
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `registrationClosesAt` | ZonedDateTime | Yes | New registration close date | Must be after the current `registrationClosesAt`; must not be after event end |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Registration window extended 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 |
| `422` | New close date is not after current close date, or is after event end |

---

## 22. 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 9 — Update Draft Highlights](#9-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 |

---

## 23. 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 10 — Update Draft FAQs](#10-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 22 (401, 403, 404, 400).

---

## 24. 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 11 — Update Draft Lineup](#11-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 |

---

## 25. 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 12 — Update Draft Agenda](#12-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 |

---

## 26. 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 |

---

## 27. 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).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `403` | Attempting to access a DRAFT event without being its organizer |
| `404` | Event not found or is soft-deleted |

---

## 28. 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).

---

## 29. 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 28.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | Invalid `status` enum value |

---

## 30. 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).

---

## 31. 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).

---

## 32. 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` |

---

## 33. 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) |

---

## 34. 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` |

---

## Quick Reference — Endpoint Summary

| # | Method | Path | Auth | Description |
|---|--------|------|------|-------------|
| 1 | POST | `/e-events/drafts` | 🔒 | Create event draft |
| 2 | GET | `/e-events/drafts` | 🔒 | List my drafts (paginated) |
| 3 | GET | `/e-events/drafts/{draftId}` | 🔒 | Get draft by ID |
| 4 | DELETE | `/e-events/drafts/{draftId}` | 🔒 | Discard a draft |
| 5 | PATCH | `/e-events/drafts/{draftId}/basic-info` | 🔒 | Update basic info |
| 6 | PATCH | `/e-events/drafts/{draftId}/schedule` | 🔒 | Update schedule / days |
| 7 | PATCH | `/e-events/drafts/{draftId}/location` | 🔒 | Update venue / virtual details |
| 8 | PATCH | `/e-events/drafts/{draftId}/registration` | 🔒 | Update registration window |
| 9 | PATCH | `/e-events/drafts/{draftId}/highlights` | 🔒 | Update highlights |
| 10 | PATCH | `/e-events/drafts/{draftId}/faqs` | 🔒 | Update FAQs |
| 11 | PATCH | `/e-events/drafts/{draftId}/lineup` | 🔒 | Update lineup |
| 12 | PATCH | `/e-events/drafts/{draftId}/agenda` | 🔒 | Update agenda |
| 13 | POST | `/e-events/draft/{eventId}/products/{productId}` | 🔒 | Attach product (DRAFT or PUBLISHED) |
| 14 | DELETE | `/e-events/draft/{eventId}/products/{productId}` | 🔒 | Remove product (DRAFT or PUBLISHED) |
| 15 | POST | `/e-events/draft/{eventId}/shops/{shopId}` | 🔒 | Attach shop (DRAFT or PUBLISHED) |
| 16 | DELETE | `/e-events/draft/{eventId}/shops/{shopId}` | 🔒 | Remove shop (DRAFT or PUBLISHED) |
| 17 | PATCH | `/e-events/{eventId}/publish` | 🔒 | Publish event |
| 18 | PATCH | `/e-events/{eventId}/unpublish` | 🔒 | Unpublish event (0 sales only) |
| 19 | PATCH | `/e-events/{eventId}/cancel` | 🔒 | Cancel event (terminal) |
| 20 | PATCH | `/e-events/{eventId}/published/basic-info` | 🔒 | Update description/media on published event |
| 21 | PATCH | `/e-events/{eventId}/published/registration` | 🔒 | Extend registration close date |
| 22 | PATCH | `/e-events/{eventId}/published/highlights` | 🔒 | Update highlights on published event |
| 23 | PATCH | `/e-events/{eventId}/published/faqs` | 🔒 | Update FAQs on published event |
| 24 | PATCH | `/e-events/{eventId}/published/lineup` | 🔒 | Update lineup on published event |
| 25 | PATCH | `/e-events/{eventId}/published/agenda` | 🔒 | Update agenda on published event |
| 26 | PATCH | `/e-events/{eventId}/published/reveal-location` | 🔒 | Reveal TBA location |
| 27 | GET | `/e-events/{eventId}` | 🌐/🔒 | Get event by ID |
| 28 | GET | `/e-events/my-events` | 🔒 | List my events |
| 29 | GET | `/e-events/my-events/status/{status}` | 🔒 | My events by status |
| 30 | GET | `/e-events/events-feed` | 🌐 | Public events feed |
| 31 | GET | `/e-events/search` | 🌐 | Search events by keyword |
| 32 | GET | `/e-events/filter/date` | 🌐 | Filter by date range |
| 33 | GET | `/e-events/filter` | 🌐 | Combined search + filter |
| 34 | GET | `/e-events/my-events/search` | 🔒 | Search within my events |

> **Note on path inconsistency**: Endpoints 5–12 use the path prefix `/drafts/{id}/...` (plural), while endpoints 6, 7, 13–16 use `/draft/{id}/...` (singular). This reflects the actual controller routing in the current codebase — use the exact paths shown in the table above.

---

## 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**: 2025-02-19
**Version**: v1.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
- 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"
}
```

### 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                       |
  [ Auto-processed immediately ]  |
          |                       v
          |           [ GET /checkout/{sessionId} ]
          |           [ — Review session details  ]
          |                       |
          |                       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, insufficient wallet balance, 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` | Validation failed on request fields (missing eventId, invalid email format, invalid phone format, etc.) |
| `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, session is expired, or attempting payment on a FREE ticket |
| `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` | Payment processing error, insufficient balance at time of processing |

---

## 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 ]
  [ Wallet balance validated upfront  ]
  [ 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 **twice**: once during session creation (upfront validation) and once at actual payment time — a window exists between the two checks where balance could change
- 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, Lead Backend Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Event Booking Orders API provides comprehensive booking management functionality for confirmed event ticket purchases in the Nexgate platform. This API enables users to view their booking details including JWT-signed QR codes for secure event entry, track multi-day event check-ins, retrieve complete booking history, and access event snapshots at time of booking. The system supports both single-day and multi-day events with per-day check-in tracking, automated ticket series generation, and comprehensive booking references.

**Hints**: 
- **Auto-Creation**: Bookings created automatically after successful checkout payment
- **JWT Security**: QR codes are RSA-signed JWTs containing ticket and event data
- **Multi-Day Support**: Full support for multi-day events with per-day check-in tracking
- **Ticket Series**: Auto-generated unique series (e.g., "VIP-0001", "GENER-0042")
- **Event Snapshots**: Event details captured at booking time (immutable)
- **Booking Reference**: Short readable codes (e.g., "EVT-2025-A3F4")
- **Check-In Tracking**: Complete history with location, staff, and day information
- **Access Control**: Customers see own bookings, organizers see event bookings, admins see all
- **Notification Integration**: Auto-emails tickets to buyer and optionally to attendees
- **Virtual Events**: Includes virtual meeting links for online/hybrid events
- **Attendee Management**: Track tickets for buyer and other attendees separately

---

## 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"
}
```

---

## 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 (Read operations)

---

## BookingOrderResponse Structure

This is the comprehensive response structure returned by booking endpoints:

```json
{
  "bookingId": "550e8400-e29b-41d4-a716-446655440000",
  "bookingReference": "EVT-A3F4B21C",
  "status": "CONFIRMED",
  "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",
    "virtualDetails": {
      "platform": "ZOOM",
      "meetingUrl": "https://zoom.us/j/123456789",
      "meetingId": "123 456 789",
      "passcode": "summit2025",
      "additionalInstructions": "Join 5 minutes early for networking"
    }
  },
  "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",
      "ticketTypeName": "VIP Pass",
      "ticketSeries": "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"
      },
      "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",
      "ticketTypeName": "General Admission",
      "ticketSeries": "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"
      },
      "checkIns": [],
      "hasBeenCheckedIn": false,
      "lastCheckedInAt": null,
      "lastCheckedInBy": null,
      "lastCheckInLocation": null,
      "lastCheckInDayName": null,
      "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
}
```

### Response Field Descriptions

#### Root Level Fields

| Field | Type | Description |
|-------|------|-------------|
| bookingId | string (UUID) | Unique booking identifier |
| bookingReference | string | Short readable code (e.g., "EVT-A3F4B21C") |
| status | enum | Booking status: CONFIRMED, CANCELLED |
| event | object | Event snapshot at time of booking |
| organizer | object | Organizer details snapshot |
| customer | object | Customer (buyer) information |
| tickets | array | List of all booked tickets with check-in history |
| totalTickets | integer | Total number of tickets in booking |
| checkedInTicketsCount | integer | Count of tickets with at least one check-in |
| subtotal | decimal | Subtotal amount |
| total | decimal | Total amount paid |
| bookedAt | string (LocalDateTime) | When booking was created |
| cancelledAt | string (LocalDateTime) | When booking was cancelled (null if active) |

#### Event Snapshot Object

| Field | Type | Description |
|-------|------|-------------|
| eventId | string (UUID) | Event identifier |
| title | string | Event title at time of booking |
| startDateTime | string (LocalDateTime) | Event start date/time |
| endDateTime | string (LocalDateTime) | Event end date/time |
| timezone | string | IANA timezone (e.g., "Africa/Nairobi") |
| location | string | Full venue location (name + address) |
| format | string | Event format: IN_PERSON, ONLINE, HYBRID |
| virtualDetails | object | Virtual meeting details (for ONLINE/HYBRID) |

#### Virtual Details Object

| Field | Type | Description |
|-------|------|-------------|
| platform | string | Platform: ZOOM, GOOGLE_MEET, MS_TEAMS, CUSTOM |
| meetingUrl | string | Full meeting URL |
| meetingId | string | Meeting ID (optional) |
| passcode | string | Meeting passcode (optional) |
| additionalInstructions | string | Extra instructions (optional) |

#### Organizer Snapshot Object

| Field | Type | Description |
|-------|------|-------------|
| name | string | Organizer name at time of booking |
| email | string | Organizer email |
| phone | string | Organizer phone |

#### Customer Info Object

| Field | Type | Description |
|-------|------|-------------|
| customerId | string (UUID) | Customer account ID |
| name | string | Customer username |
| email | string | Customer email |

#### Booked Ticket Response Object

| Field | Type | Description |
|-------|------|-------------|
| ticketInstanceId | string (UUID) | Unique ticket instance ID |
| ticketTypeName | string | Ticket type name (e.g., "VIP Pass") |
| ticketSeries | string | Auto-generated series (e.g., "VIP-0001") |
| price | decimal | Ticket price |
| qrCode | string | JWT-signed QR code (very long string) |
| attendanceMode | string | IN_PERSON or ONLINE (for hybrid events) |
| attendee | object | Attendee information |
| buyer | object | Buyer information (who paid) |
| checkIns | array | Complete check-in history (all days) |
| hasBeenCheckedIn | boolean | True if checked in at least once |
| lastCheckedInAt | string (ZonedDateTime) | Most recent check-in time |
| lastCheckedInBy | string | Who performed last check-in |
| lastCheckInLocation | string | Location of last check-in |
| lastCheckInDayName | string | Day name of last check-in |
| status | enum | ACTIVE, USED, CANCELLED |
| validFrom | string (ZonedDateTime) | Ticket valid from |
| validUntil | string (ZonedDateTime) | Ticket valid until |

#### Attendee/Buyer Info Object

| Field | Type | Description |
|-------|------|-------------|
| name | string | Person's name |
| email | string | Person's email |
| phone | string | Person's phone (attendee only) |

#### Check-In Record Object

| Field | Type | Description |
|-------|------|-------------|
| checkInTime | string (ZonedDateTime) | When check-in occurred |
| checkInLocation | string | Where (e.g., "Main Gate", "VIP Entrance") |
| checkedInBy | string | Staff/scanner operator name |
| dayName | string | Event day (e.g., "Day 1", "Day 2 - Saturday") |
| scannerId | string | Scanner device ID |
| checkInMethod | string | Method: QR_SCAN (default), MANUAL, NFC |

---

## BookingOrderSummaryResponse Structure

Lightweight response for listing bookings:

```json
{
  "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"
}
```

---

## Endpoints

## 1. Get Booking by ID
**Purpose**: Retrieve complete booking details including all tickets and check-in history

**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 |
|-----------|------|----------|-------------|
| bookingId | string (UUID) | Yes | Booking order ID |

**Success Response**: Returns complete BookingOrderResponse structure

**Success Response Message**: "Booking retrieved successfully"

**HTTP Status Code**: 200 OK

**Access Rules**:
- **Customers**: Can view their own bookings
- **Organizers**: Can view bookings for their events
- **Admins** (SUPER_ADMIN, STAFF_ADMIN): Can view any booking

**Behavior**:
- Returns complete booking details with all tickets
- Includes event snapshot (as it was at booking time)
- Shows full check-in history for multi-day events
- Includes JWT-signed QR codes for all tickets
- Virtual details included for ONLINE/HYBRID events

**Use Cases**:
- Customer viewing booking confirmation
- Customer retrieving QR codes for event entry
- Organizer checking booking details
- Admin reviewing booking for support

**Standard Error Types**:
- `401 UNAUTHORIZED`: Authentication issues
- `403 FORBIDDEN`: User doesn't have permission to view this booking
- `404 NOT_FOUND`: Booking not found

**Error Response Examples**:

*Booking Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Booking not found: 550e8400-e29b-41d4-a716-446655440000",
  "action_time": "2025-12-11T10:30:45",
  "data": "Booking not found: 550e8400-e29b-41d4-a716-446655440000"
}
```

*Access Denied (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You don't have permission to view this booking",
  "action_time": "2025-12-11T10:30:45",
  "data": "You don't have permission to view this booking"
}
```

---

## 2. Get My Bookings
**Purpose**: Retrieve all bookings for 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/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**: Returns array of BookingOrderSummaryResponse

**Success Response Message**: "Bookings retrieved successfully"

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Bookings retrieved successfully",
  "action_time": "2025-12-11T10: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"
    },
    {
      "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"
    }
  ]
}
```

**HTTP Status Code**: 200 OK

**Sorting**: Bookings sorted by `bookedAt` descending (newest first)

**Behavior**:
- Returns lightweight summary for all user's bookings
- Sorted newest first
- Includes check-in progress (checkedInTickets / totalTickets)
- Shows upcoming and past events

**Use Cases**:
- User viewing booking history
- Dashboard showing all purchases
- Finding booking for upcoming event
- Checking past event attendance

**Standard Error Types**:
- `401 UNAUTHORIZED`: Authentication issues
- `404 NOT_FOUND`: User not found/authenticated

---

## 3. Download Ticket PDF

**Purpose**: Download the ticket PDF for a specific ticket belonging to the authenticated user

**Endpoint**: <span style="background-color: #007bff; 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/{ticketId}/pdf`

**Access Level**: 🔒 Protected (Authenticated Users)

**Authentication**: Bearer Token

---

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token (format: `Bearer <token>`) |

**Path Parameters**:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| ticketId | UUID | Yes | The unique ID of the ticket to download |

**Query Parameters**:

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| mode | string | No | `download` | Controls how the PDF is served. `download` forces a file save dialog. `inline` opens the PDF directly in the browser. |

**Mode Values**:

| Value | Content-Disposition | Behaviour |
|-------|---------------------|-----------|
| `download` | `attachment; filename="ticket-{series}.pdf"` | Browser prompts user to save the file |
| `inline` | `inline; filename="ticket-{series}.pdf"` | PDF opens directly in the browser tab |

---

**Success Response**: Returns a binary PDF file

**Success Response Headers**:

| Header | Value |
|--------|-------|
| Content-Type | `application/pdf` |
| Content-Disposition | `attachment; filename="ticket-{series}.pdf"` *(or `inline` depending on mode)* |

**HTTP Status Code**: 200 OK

---

**Behavior**:
- Verifies the ticket belongs to the authenticated user before generating the PDF
- Generates the ticket PDF on-the-fly using the event's SVG template
- The PDF contains event details, attendee name, seat/series, QR code for check-in, and ticket number
- QR code encodes a signed JWT token used by gate staff to verify the ticket at entry
- File name in the `Content-Disposition` header uses the ticket's series (e.g. `ticket-VIP-0033.pdf`)

**Use Cases**:
- User downloading their ticket after booking
- Re-downloading a lost or deleted ticket
- Saving ticket to device for offline access at the event

---

**Standard Error Types**:

| Code | Type | Description |
|------|------|-------------|
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
| 403 | `FORBIDDEN` | Ticket does not belong to the authenticated user |
| 404 | `NOT_FOUND` | Ticket with the given ID not found |
| 500 | `INTERNAL_SERVER_ERROR` | PDF generation failed |

---

## Booking Status Lifecycle

### Status Descriptions

| Status | Description | Can Cancel? | Tickets Valid? |
|--------|-------------|-------------|----------------|
| **CONFIRMED** | Booking active and tickets valid | Future (not implemented) | Yes |
| **CANCELLED** | Booking cancelled (placeholder) | N/A | No |

**Current Implementation**:
- All bookings created as CONFIRMED
- Cancellation not yet implemented (status exists for future)
- Tickets remain ACTIVE after booking until checked in

---

## Ticket Instance Status

### Status Lifecycle

| Status | Description | Can Check In? |
|--------|-------------|---------------|
| **ACTIVE** | Ticket ready for use | Yes |
| **USED** | Ticket checked in | Yes (for multi-day) |
| **CANCELLED** | Ticket cancelled | No |

**Multi-Day Events**:
- Ticket status remains ACTIVE even after first check-in
- Can check in multiple times (once per day)
- Status changes to USED after all expected check-ins
- Each check-in tracked separately in checkIns array

---

## Ticket Series Generation

### How Ticket Series Work

**Format**: `{TICKET_CODE}-{COUNTER}`

**Examples**:
- VIP Pass → `VIP-0001`, `VIP-0002`, `VIP-0003`
- General Admission → `GENER-0001`, `GENER-0002`
- Early Bird → `EARLY-0001`, `EARLY-0002`

**Ticket Code Extraction**:
1. Take first 5 characters of ticket type name
2. Remove spaces
3. Convert to uppercase
4. Fallback to "TICK" if name empty

**Counter System**:
- Separate counter per ticket type (stored in DB)
- Pessimistic locking prevents duplicates
- Counter never resets (unique forever)
- Increments with each ticket created

**Example Flow**:
```
Ticket Type: "VIP Pass"
Counter: 0

First Booking (3 tickets):
- VIP-0001 (counter now 1)
- VIP-0002 (counter now 2)
- VIP-0003 (counter now 3)

Second Booking (2 tickets):
- VIP-0004 (counter now 4)
- VIP-0005 (counter now 5)
```

**Why Series Numbers?**
- Easy reference for staff
- Quick ticket identification
- Helps track capacity sold
- Professional appearance on PDF tickets

---

## JWT-Signed QR Codes

### Security Architecture

**What is the QR Code?**
- Not a random string
- Full JWT (JSON Web Token)
- Signed with event's RSA private key
- Contains complete ticket and event data
- Cannot be forged or tampered with

**JWT Structure**:
```
Header.Payload.Signature

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ0aWNrZXRJbnN0YW5jZUlkIjoiODgw...
[RSA signature]
```

**JWT Payload Contains**:
- ticketInstanceId (UUID)
- ticketTypeId (UUID)
- ticketTypeName (string)
- ticketSeries (string)
- eventId (UUID)
- eventName (string)
- eventStartDateTime (timestamp)
- attendeeName (string)
- attendeeEmail (string)
- attendeePhone (string)
- attendanceMode (IN_PERSON/ONLINE)
- bookingReference (string)
- **eventSchedules** (array of days for multi-day events)
- validFrom (timestamp)
- validUntil (timestamp)

**Multi-Day Event Schedules**:
```json
"eventSchedules": [
  {
    "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"
  },
  {
    "dayName": "Day 2 - Conference Day",
    "startDateTime": "2025-12-16T09:00:00+03:00",
    "endDateTime": "2025-12-16T18:00:00+03:00",
    "description": "Technical Sessions & Workshops"
  },
  {
    "dayName": "Day 3 - Closing Day",
    "startDateTime": "2025-12-17T09:00:00+03:00",
    "endDateTime": "2025-12-17T18:00:00+03:00",
    "description": "Finals & Networking"
  }
]
```

**RSA Key Pair**:
- Generated when event is published (2048-bit)
- Private key: Stored securely in database (for signing)
- Public key: Available to scanners (for verification)
- Cannot derive private key from public key

**Verification Process** (Scanner App):
1. Scanner reads QR code
2. Extracts JWT token
3. Fetches event's public key
4. Verifies JWT signature
5. Checks expiry and validity
6. Checks if already checked in for this day
7. Records check-in if valid

**Why JWT over Simple Codes?**
- **Cannot be forged**: Requires private key
- **Contains all data**: No database lookup needed
- **Offline capable**: Scanner can verify without internet
- **Tamper-proof**: Any modification breaks signature
- **Time-limited**: validFrom/validUntil built in
- **Day tracking**: Multi-day schedules embedded

---

## Multi-Day Event Check-Ins

### How Multi-Day Check-Ins Work

**Single-Day Event**:
- One check-in expected
- After check-in, status changes to USED
- Simple entry tracking

**Multi-Day Event** (e.g., 3-day festival):
- Multiple check-ins allowed (one per day)
- Each day tracked separately
- Ticket remains ACTIVE after first day
- Full history stored in `checkIns` array

**Check-In Record Fields**:
- **checkInTime**: When check-in happened
- **checkInLocation**: Where (e.g., "Main Gate", "VIP Entrance")
- **checkedInBy**: Staff/scanner operator name
- **dayName**: Which day (must match eventSchedules)
- **scannerId**: Scanner device ID
- **checkInMethod**: QR_SCAN, MANUAL, NFC (default: QR_SCAN)

**Example: 3-Day Festival**

Day 1 (Friday):
```json
{
  "checkInTime": "2025-12-15T18:30:00+03:00",
  "checkInLocation": "Main Gate",
  "checkedInBy": "Staff Member 1",
  "dayName": "Day 1 - Friday Night",
  "scannerId": "SCANNER-001",
  "checkInMethod": "QR_SCAN"
}
```

Day 2 (Saturday):
```json
{
  "checkInTime": "2025-12-16T14:15:00+03:00",
  "checkInLocation": "VIP Entrance",
  "checkedInBy": "Staff Member 2",
  "dayName": "Day 2 - Saturday",
  "scannerId": "SCANNER-003",
  "checkInMethod": "QR_SCAN"
}
```

Day 3 (Sunday):
```json
{
  "checkInTime": "2025-12-17T12:00:00+03:00",
  "checkInLocation": "Main Gate",
  "checkedInBy": "Staff Member 1",
  "dayName": "Day 3 - Sunday",
  "scannerId": "SCANNER-001",
  "checkInMethod": "QR_SCAN"
}
```

**Validation Rules**:
- `dayName` must match one from JWT's eventSchedules
- Cannot check in twice on same day
- Can check in on different days
- Check-in time must be within day's start/end

**Benefits**:
- Accurate attendance tracking per day
- Prevents duplicate same-day entries
- Allows organizers to see daily attendance
- Supports flexible entry (don't need all days)

---

## Event Snapshots

### Why Snapshots?

**Problem**: Event details can change after booking
- Event renamed
- Time changed
- Venue moved
- Organizer updated contact

**Solution**: Capture event state at booking time

**Snapshot Fields** (Immutable after booking):
- eventTitle
- eventStartDateTime
- eventEndDateTime
- eventTimezone
- eventLocation (full venue details)
- eventFormat
- virtualDetails (for online/hybrid)
- organizerName
- organizerEmail
- organizerPhone

**Benefits**:
- **PDF Tickets**: Generate correct ticket with original details
- **Legal Record**: What customer actually purchased
- **Email Confirmations**: Show accurate booking details
- **Refund Logic**: Reference original event details
- **Audit Trail**: Track what changed vs. what was booked

**Example Scenario**:
1. User books ticket for "Tech Summit" at KICC
2. Snapshot created: "Tech Summit", "KICC Nairobi"
3. Organizer renames event to "African Tech Summit"
4. Organizer moves venue to "Safari Park Hotel"
5. User's booking still shows original: "Tech Summit at KICC"
6. This is what they paid for (legal protection)

---

## Booking Creation Flow

### Automatic Creation After Payment

**Trigger**: EventPaymentCompletedListener fires after successful payment

**Steps**:
1. **Fetch Entities**:
   - Get EventCheckoutSession
   - Get Event details
   - Get TicketType details

2. **Generate Booking Reference**:
   - Format: `EVT-{8-char-UUID}`
   - Example: `EVT-A3F4B21C`
   - Unique and readable

3. **Create Ticket Instances**:
   - For buyer's tickets (ticketsForMe count)
   - For each other attendee (their quantity)
   - Each ticket gets unique series number
   - Each ticket assigned to correct attendee

4. **Generate JWT QR Codes**:
   - Create JWT payload with ticket data
   - Include event schedules for multi-day
   - Sign with event's RSA private key
   - Set as ticket's qrCode field

5. **Snapshot Event Details**:
   - Capture current event title, times, location
   - Capture organizer details
   - Store in booking (immutable)

6. **Save Booking**:
   - Create EventBookingOrderEntity
   - Save to database with all tickets
   - Link to checkout session

7. **Update Checkout Session**:
   - Set createdBookingOrderId
   - Mark as COMPLETED

8. **Publish Event**:
   - Fire BookingCreatedEvent
   - Triggers notifications

9. **Send Notifications**:
   - Email buyer confirmation with tickets
   - Optionally email other attendees their tickets
   - Notify event organizer of new booking

**Transaction Safety**:
- Entire flow in single transaction
- If any step fails, everything rolls back
- No partial bookings created

**Timing**:
- Happens immediately after payment success
- Usually completes in < 2 seconds
- User gets instant confirmation

---

## Notification System Integration

### Who Gets Notified?

**1. Buyer (ALWAYS)**:
- Receives booking confirmation
- Gets ALL tickets (if sendTicketsToAttendees = false)
- Gets only THEIR tickets (if sendTicketsToAttendees = true)
- Channels: EMAIL, SMS, PUSH, IN_APP
- Priority: HIGH

**2. Other Attendees (CONDITIONAL)**:
- Only if sendTicketsToAttendees = true
- Each attendee gets ONLY their tickets
- Channels: EMAIL, SMS
- Priority: HIGH
- Grouped by email (all tickets for same person)

**3. Event Organizer (ALWAYS)**:
- Notified of new booking
- Gets booking summary (not tickets)
- Channels: EMAIL, IN_APP
- Priority: NORMAL

### Notification Content

**Buyer Confirmation**:
- Booking reference
- Event title and date
- Number of tickets
- QR codes attached
- Event location/virtual link
- Instructions for entry

**Attendee Ticket Email**:
- "You've received tickets from [Buyer Name]"
- Event details
- Their specific QR codes
- Entry instructions

**Organizer Alert**:
- New booking notification
- Buyer name and email
- Number of tickets sold
- Total amount
- Booking reference for lookup

### QR Code Delivery

**sendTicketsToAttendees = false**:
```
Buyer receives:
- Ticket 1 QR (for self)
- Ticket 2 QR (for self)
- Ticket 3 QR (for Jane)
- Ticket 4 QR (for Jane)
- Ticket 5 QR (for Bob)

Buyer must forward to Jane and Bob manually
```

**sendTicketsToAttendees = true**:
```
Buyer receives:
- Ticket 1 QR (for self)
- Ticket 2 QR (for self)

Jane receives:
- Ticket 3 QR (for Jane)
- Ticket 4 QR (for Jane)

Bob receives:
- Ticket 5 QR (for Bob)

Everyone gets their own tickets directly
```

---

## Access Control Rules

### Who Can View Bookings?

**1. Booking Owner (Customer)**:
- Can view all their own bookings
- GET /booking-orders/my-bookings (all)
- GET /booking-orders/{bookingId} (specific)

**2. Event Organizer**:
- Can view bookings for their events
- Useful for checking attendance
- Verifying purchases
- Customer support

**3. Platform Admins**:
- Roles: SUPER_ADMIN, STAFF_ADMIN
- Can view any booking
- For support and dispute resolution

**4. Others**:
- Cannot view bookings they don't own
- Cannot view other users' bookings
- 403 Forbidden error
---

## Virtual Event Support

### Virtual Details Structure

**Included for**:
- ONLINE events (only virtual)
- HYBRID events (both physical + virtual)

**Fields**:
- **platform**: ZOOM, GOOGLE_MEET, MS_TEAMS, CUSTOM
- **meetingUrl**: Full meeting URL (required)
- **meetingId**: Meeting ID if applicable
- **passcode**: Meeting passcode if required
- **additionalInstructions**: Extra guidance

**Example (Zoom)**:
```json
{
  "platform": "ZOOM",
  "meetingUrl": "https://zoom.us/j/123456789?pwd=abc123",
  "meetingId": "123 456 789",
  "passcode": "summit2025",
  "additionalInstructions": "Please join 5 minutes early for a smooth start"
}
```

**Example (Custom)**:
```json
{
  "platform": "CUSTOM",
  "meetingUrl": "https://customplatform.com/event/12345",
  "meetingId": null,
  "passcode": null,
  "additionalInstructions": "Use your booking email to log in"
}
```

**Delivery**:
- Included in booking response
- Sent in confirmation email
- Sent in ticket emails
- Available before event starts

**Hybrid Events**:
- Virtual details + physical venue
- Attendees choose format via ticket type
- IN_PERSON tickets: Physical entry
- ONLINE tickets: Virtual access
- Both in same booking possible

---

## Date/Time Formats

### DateTime Standards

**LocalDateTime** (No timezone):
- Format: `YYYY-MM-DDTHH:mm:ss`
- Example: `2025-12-15T09:00:00`
- Used for: bookedAt, cancelledAt, event snapshots

**ZonedDateTime** (With timezone):
- Format: `YYYY-MM-DDTHH:mm:ss±HH:mm`
- Example: `2025-12-15T09:00:00+03:00`
- Used for: validFrom, validUntil, checkInTime

**Why Different Formats?**

**LocalDateTime**:
- Server-local time
- Audit timestamps
- No conversion needed
- Simple comparison

**ZonedDateTime**:
- User-aware time
- Ticket validity
- Check-in times
- Handles timezone correctly

**Example**:
```
Event in Nairobi (UTC+3):
startDateTime: 2025-12-15T09:00:00+03:00

Ticket validFrom: 2025-12-15T09:00:00+03:00
User in London sees: 2025-12-15T06:00:00+00:00 ✓
User in New York sees: 2025-12-15T01:00:00-05:00 ✓
```

---

## Validation Rules Summary

### Booking Retrieval Validation

**Access Validation**:
- ✅ User must be authenticated
- ✅ Booking must exist
- ✅ User must be: owner OR organizer OR admin
- ❌ Other users cannot access

**Data Validation**:
- ✅ bookingId must be valid UUID
- ✅ Booking must not be deleted (soft delete support)

---

## Quick Reference Guide

### Common HTTP Status Codes
- `200 OK`: Successful request
- `401 Unauthorized`: Authentication required/failed
- `403 Forbidden`: User doesn't have permission
- `404 Not Found`: Booking not found

### Booking Reference Format
- Pattern: `EVT-{8-char-UUID}`
- Examples: `EVT-A3F4B21C`, `EVT-F2D5E891`
- Unique and readable
- Easy for customer service

### Ticket Series Format
- Pattern: `{CODE}-{COUNTER}`
- Examples: `VIP-0001`, `GENER-0042`, `EARLY-0123`
- Code: First 5 chars of ticket name
- Counter: Unique incrementing number

### QR Code Format
- Type: RSA-signed JWT
- Length: ~1000-2000 characters
- Contains: Complete ticket + event data
- Cannot be: Forged, tampered, reused (tracked)

### Data Format Standards
- **Dates**: LocalDateTime for timestamps, ZonedDateTime for validity
- **IDs**: UUID format
- **Money**: Decimal with 2 decimals (150.00)
- **Currency**: TZS (Tanzanian Shilling)
- **Timezone**: IANA format (Africa/Nairobi)

### Booking Flow Checklist
1. ✅ User completes checkout payment
2. ✅ Payment listener triggers
3. ✅ System generates booking reference
4. ✅ System creates ticket instances
5. ✅ System generates JWT QR codes
6. ✅ System snapshots event details
7. ✅ System saves booking order
8. ✅ System publishes booking event
9. ✅ System sends email notifications
10. ✅ User receives confirmation + QR codes

### Best Practices

**For Users**:
- Save QR codes to phone immediately
- Screenshot confirmation email
- Arrive early for check-in
- One QR per person at gate

**For Developers**:
- Always show booking reference prominently
- Display QR codes large enough to scan
- Cache booking details locally
- Handle offline QR display
- Show check-in history clearly
- Support PDF ticket generation

**For Organizers**:
- Provide clear entry instructions
- Test scanner apps before event
- Have backup manual verification
- Track attendance by day
- Monitor check-in progress live

### Error Handling Tips
- **403 Forbidden**: Show "This booking belongs to another user"
- **404 Not Found**: Suggest "Check your email for correct reference"
- **Network Error**: Show cached booking if available
- **QR Load Failure**: Provide booking reference as backup

### Common Mistakes to Avoid
❌ Showing other users' bookings  
❌ Not displaying virtual details for ONLINE events  
❌ Forgetting to show multi-day check-in history  
❌ Not caching QR codes for offline use  
❌ Displaying mutable event data instead of snapshot  
❌ Not grouping attendees' tickets properly  
❌ Ignoring timezone in validity checks  

---

## Additional Notes

### Future Enhancements (Not Yet Implemented)

**Booking Cancellation**:
- Status: CANCELLED exists but not functional
- Future: Refund logic + ticket release
- Future: Cancellation deadline rules
- Future: Partial cancellations

**Ticket Transfers**:
- Currently: Tickets assigned at booking
- Future: Transfer ticket to another person
- Future: Resale marketplace
- Future: Name change requests

**Check-In API**:
- Currently: Manual check-ins not via API
- Future: Scanner app endpoints
- Future: QR verification endpoint
- Future: Manual check-in endpoint

**Booking Modifications**:
- Currently: Cannot modify after creation
- Future: Add more tickets
- Future: Change attendee names
- Future: Upgrade ticket types

### Integration Points

**Payment System**:
- Booking created after payment success
- Linked via checkoutSessionId
- Payment status determines creation

**Notification System**:
- BookingCreatedEvent triggers emails
- Async processing via listeners
- Multiple notification channels

**Escrow System**:
- Payments held until event completion
- Booking tracks payment via checkout session
- Organizer paid after event

**Scanner Apps**:
- Will consume booking data
- Verify JWT QR codes
- Record check-ins
- Update ticket status

---

## Conclusion

The Event Booking Orders API provides comprehensive booking management with:

✅ **Security**: JWT-signed QR codes prevent fraud  
✅ **Flexibility**: Multi-day events with per-day tracking  
✅ **Reliability**: Event snapshots preserve booking details  
✅ **Scalability**: Unique ticket series with DB counters  
✅ **User Experience**: Clear references and notifications  
✅ **Multi-Attendee**: Support for group bookings  
✅ **Virtual Events**: Full online/hybrid event support  
✅ **Access Control**: Proper permissions for all parties

# 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**: 2025-02-17
**Version**: v1.0

**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` automatically creates the underlying form and a default first page — you do not create them separately.
- `startSubmission` is idempotent — if the attendee already has a draft response it returns the existing one instead of creating a new one.
- `savePage` saves answers for a page without advancing the page index — it is a save-in-place operation. Call `submit` when all pages are done.
- 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                      │
  │   - Form auto-created internally    │
  │   - Default page auto-created       │
  │   - Config saved (displayTime etc.) │
  └──────────────┬──────────────────────┘
                 │
                 │  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    │
  └─────────────────────────────────────┘
```

### Attendee Submission Journey

```
  [Attendee — event must be PUBLISHED and within registration window]
         │
         │  POST /events/{eventId}/start
         ▼
  ┌──────────────────────────────────┐
  │  Response created (DRAFT)        │  Returns existing draft if one
  │  or existing draft returned      │  already exists (idempotent)
  └──────────────┬───────────────────┘
                 │
                 │  For each page:
                 │  PUT /events/{eventId}/pages/{pageId}/save
                 │  Body: { fieldId: { value, fileUrl, ... }, ... }
                 ▼
  ┌──────────────────────────────────┐
  │  Page answers saved              │  Save is in-place, does NOT
  │  (repeat per page)               │  advance page index automatically
  └──────────────┬───────────────────┘
                 │
                 │  POST /events/{eventId}/submit
                 ▼
  ┌──────────────────────────────────┐
  │  Response SUBMITTED              │  completionTimeSeconds recorded
  └──────────────────────────────────┘
```

### 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. 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, required field missing on save

### 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}"` and an initial default page titled `"Attendee Information"`. Only the event organizer can call this. Returns the full 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 with default page",
  "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": [
        {
          "pageId": "1a2b3c4d-...",
          "title": "Attendee Information",
          "description": "Please provide your details",
          "displayOrder": 1,
          "actionButtonText": "Next",
          "fields": []
        }
      ]
    }
  }
}
```

**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 |

---

## 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

---

## 22. Get Preview Metadata

**Purpose**: Returns a lightweight summary of the form structure — total pages, page numbers, and titles — without loading full field data. Used to render a progress indicator before the attendee starts filling.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/preview/metadata`

**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": "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 |

---

## 23. Preview Page by Number

**Purpose**: Returns a specific page by its 1-based position number, including all its active fields and options. Allows the organizer to preview what attendees will see before publishing.

**Endpoint**: `GET` `/api/v1/e-events/applicant-form/events/{eventId}/preview/pages/{pageNumber}`

**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 |
| `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 |

---

## 24. 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 (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 |
| `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

---

## 25. 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 |

---

## 26. Save Page Answers

**Purpose**: Saves the attendee's answers for a specific page. Can be called multiple times — each call replaces the previous answers for that page. This is a save-in-place operation; it does **not** advance the page index. Call submit (Endpoint 28) when all pages are complete.

**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 |

**Request JSON Sample**:

```json
{
  "c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f": {
    "value": "Amina Hassan"
  },
  "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a": {
    "value": "amina@example.com"
  },
  "e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b": {
    "value": "optionId-bycar"
  },
  "f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c": {
    "value": null,
    "fileUrl": "https://cdn.example.com/uploads/id.pdf",
    "fileName": "national-id.pdf",
    "fileSize": 204800,
    "fileType": "application/pdf"
  }
}
```

**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 |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Page saved",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full FormResponseObject with updated answers" }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission) reflecting the updated answer state.

**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 |
| `422` | A required field has no value |

---

## 27. 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 |

---

## 28. Submit

**Purpose**: Finalises and submits the attendee's response. The system validates all pages before accepting. Sets status to `SUBMITTED` and records `completionTimeSeconds`.

**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": "2025-02-17T10:30:45",
  "data": { "...": "Full FormResponseObject with status SUBMITTED" }
}
```

**Success Response Fields**: `data` is a [FormResponseObject](#g-formresponseobject-attendee-submission). `data.status` will be `"SUBMITTED"`.

**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` | Required fields on one or more pages are unanswered |

---

## Organizer Response Management

---

## 29. 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 |

---

## 30. 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 |

---

## 31. 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](#j-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 + create default page |
| 2 | PUT | `/events/{eventId}/settings` | 🔒 | Organizer | Update form display settings |
| 3 | DELETE | `/events/{eventId}/disable` | 🔒 | Organizer | Disable and remove form |
| **Page Management** |
| 4 | POST | `/events/{eventId}/pages` | 🔒 | Organizer | Add a page |
| 5 | PUT | `/events/{eventId}/pages/{pageId}` | 🔒 | Organizer | Update a page |
| 6 | DELETE | `/events/{eventId}/pages/{pageId}?hard=false` | 🔒 | Organizer | Delete a page (soft or hard) |
| 7 | POST | `/events/{eventId}/pages/bulk` | 🔒 | Organizer | Bulk add pages with fields |
| 8 | DELETE | `/events/{eventId}/pages/bulk?hard=false` | 🔒 | Organizer | Bulk delete pages |
| 9 | POST | `/events/{eventId}/pages/{pageId}/clone` | 🔒 | Organizer | Clone a page |
| **Field Management** |
| 10 | POST | `/events/{eventId}/pages/{pageId}/fields` | 🔒 | Organizer | Add a field to a page |
| 11 | PUT | `/events/{eventId}/fields/{fieldId}` | 🔒 | Organizer | Update a field |
| 12 | DELETE | `/events/{eventId}/fields/{fieldId}?hard=false` | 🔒 | Organizer | Delete a field (soft or hard) |
| 13 | POST | `/events/{eventId}/pages/{pageId}/fields/bulk` | 🔒 | Organizer | Bulk add fields |
| 14 | DELETE | `/events/{eventId}/pages/{pageId}/fields/bulk?hard=false` | 🔒 | Organizer | Bulk delete fields |
| 15 | PATCH | `/events/{eventId}/fields/bulk` | 🔒 | Organizer | Bulk update fields |
| 16 | POST | `/events/{eventId}/fields/{fieldId}/clone` | 🔒 | Organizer | Clone a field |
| **Option Management** |
| 17 | POST | `/events/{eventId}/fields/{fieldId}/options` | 🔒 | Organizer | Add option to a field |
| 18 | PUT | `/events/{eventId}/options/{optionId}` | 🔒 | Organizer | Update an option |
| 19 | DELETE | `/events/{eventId}/options/{optionId}?hard=false` | 🔒 | Organizer | Delete an option (soft or hard) |
| 20 | DELETE | `/events/{eventId}/fields/{fieldId}/options/bulk?hard=false` | 🔒 | Organizer | Bulk delete options |
| 21 | PATCH | `/events/{eventId}/options/bulk` | 🔒 | Organizer | Bulk update option labels |
| **Preview** |
| 22 | GET | `/events/{eventId}/preview/metadata` | 🔒 | Organizer | Get form page metadata |
| 23 | GET | `/events/{eventId}/preview/pages/{pageNumber}` | 🔒 | Organizer | Preview a page by number |
| 24 | POST | `/events/{eventId}/preview/pages/{pageNumber}/validate` | 🔒 | Organizer | Validate answers without saving |
| **Attendee Submission** |
| 25 | POST | `/events/{eventId}/start` | 🔒 | Attendee | Start form / get existing draft |
| 26 | PUT | `/events/{eventId}/pages/{pageId}/save` | 🔒 | Attendee | Save page answers |
| 27 | GET | `/events/{eventId}/my-response` | 🔒 | Attendee | Get my response |
| 28 | POST | `/events/{eventId}/submit` | 🔒 | Attendee | Submit form |
| **Organizer — Responses & Analytics** |
| 29 | GET | `/events/{eventId}/responses/{responseId}` | 🔒 | Organizer | Get response by ID |
| 30 | GET | `/events/{eventId}/responses` | 🔒 | Organizer | Get all responses (paginated) |
| 31 | GET | `/events/{eventId}/analytics` | 🔒 | Organizer | Get form analytics |

> 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, Lead Backend Team  
**Last Updated**: 2025-12-11  
**Version**: v1.0

**Base URL**: `https://api.nexgate.com/api/v1`

**Short Description**: The Attendance Analytics API provides comprehensive attendee tracking and analytics for event organizers. This API enables organizers to view attendance statistics with per-day breakdowns, list all checked-in attendees with filters, track absentees by category (full no-shows vs specific-day absences), and view detailed check-in history for individual attendees. The system supports multi-day events with separate tracking per day and provides real-time attendance metrics.

**Hints**: 
- **Organizer Only**: All endpoints restricted to event organizers
- **Multi-Day Support**: Per-day statistics and filtering
- **Real-Time**: Updates instantly after check-ins
- **Absentee Categories**: FULL_NO_SHOW, SPECIFIC_DAY_ONLY, ALL
- **Ticket Type Filter**: Filter by specific ticket types
- **Search**: Search attendees by name/email
- **Pagination**: Lists paginated (default 50 per page)
- **Check-In History**: Complete per-day check-in records
- **Attendance Patterns**: Tracks which days attended/absent

---

## Response Structures

### AttendanceStatsResponse
```json
{
  "eventId": "uuid",
  "eventTitle": "East African Tech Summit 2025",
  "totalDays": 3,
  "eventSchedule": [
    {"dayNumber": 1, "dayName": "Day 1 - Opening", "date": "2025-12-15"},
    {"dayNumber": 2, "dayName": "Day 2 - Conference", "date": "2025-12-16"},
    {"dayNumber": 3, "dayName": "Day 3 - Closing", "date": "2025-12-17"}
  ],
  "overallStats": {
    "totalTickets": 500,
    "totalCheckedIn": 462,
    "totalAbsent": 38,
    "attendanceRate": 92.4,
    "byDay": [
      {
        "dayNumber": 1,
        "dayName": "Day 1 - Opening",
        "date": "2025-12-15",
        "totalTickets": 500,
        "checkedIn": 475,
        "absent": 25,
        "attendanceRate": 95.0,
        "status": "COMPLETED"
      }
    ]
  },
  "byTicketType": [
    {
      "ticketTypeId": "uuid",
      "ticketTypeName": "VIP Pass",
      "totalSold": 100,
      "totalCheckedIn": 98,
      "totalAbsent": 2,
      "attendanceRate": 98.0,
      "byDay": [
        {
          "dayNumber": 1,
          "dayName": "Day 1 - Opening",
          "checkedIn": 99,
          "absent": 1,
          "attendanceRate": 99.0
        }
      ]
    }
  ]
}
```

### AttendeeListResponse
```json
{
  "eventId": "uuid",
  "eventTitle": "East African Tech Summit 2025",
  "dayNumber": 1,
  "dayName": "Day 1 - Opening",
  "dayDate": "2025-12-15",
  "ticketTypeId": "uuid",
  "ticketTypeName": "VIP Pass",
  "summary": {
    "totalTicketsForType": 100,
    "checkedInThisDay": 99
  },
  "attendees": [
    {
      "ticketInstanceId": "uuid",
      "attendeeName": "John Doe",
      "attendeeEmail": "john@example.com",
      "attendeePhone": "+255712345678",
      "ticketType": "VIP Pass",
      "ticketSeries": "VIP-0001",
      "bookingReference": "EVT-A3F4B21C",
      "pricePaid": 150.00,
      "checkInTime": "2025-12-15T09:15:00+03:00",
      "checkInLocation": "Main Gate",
      "checkedInBy": "Scanner Operator 1",
      "scannerId": "uuid-string"
    }
  ],
  "pagination": {
    "currentPage": 0,
    "pageSize": 50,
    "totalPages": 2,
    "totalElements": 99,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

### AbsenteeListResponse
```json
{
  "eventId": "uuid",
  "eventTitle": "East African Tech Summit 2025",
  "dayNumber": 1,
  "dayName": "Day 1 - Opening",
  "dayDate": "2025-12-15",
  "ticketTypeId": null,
  "ticketTypeName": null,
  "summary": {
    "totalTicketsForType": 500,
    "absentThisDay": 25,
    "absenteeRate": 5.0,
    "breakdown": {
      "fullNoShow": 10,
      "specificDayOnly": 15
    }
  },
  "absentees": [
    {
      "ticketInstanceId": "uuid",
      "attendeeName": "Jane Smith",
      "attendeeEmail": "jane@example.com",
      "attendeePhone": "+255723456789",
      "ticketType": "General Admission",
      "ticketSeries": "GENER-0042",
      "bookingReference": "EVT-B5D2E12F",
      "pricePaid": 50.00,
      "statusForThisDay": "NOT_CHECKED_IN",
      "attendancePattern": {
        "totalEventDays": 3,
        "daysAttended": 2,
        "daysAbsent": 1,
        "attendedDayNumbers": [2, 3],
        "absentDayNumbers": [1],
        "category": "SPECIFIC_DAY_ONLY"
      }
    }
  ],
  "pagination": {
    "currentPage": 0,
    "pageSize": 50,
    "totalPages": 1,
    "totalElements": 25,
    "hasNext": false,
    "hasPrevious": false
  }
}
```

### AttendeeDetailResponse
```json
{
  "ticketInstanceId": "uuid",
  "attendeeName": "John Doe",
  "attendeeEmail": "john@example.com",
  "attendeePhone": "+255712345678",
  "ticketType": "VIP Pass",
  "ticketSeries": "VIP-0001",
  "bookingReference": "EVT-A3F4B21C",
  "pricePaid": 150.00,
  "overallStatus": "FULLY_ATTENDED",
  "daysAttended": 3,
  "daysTotal": 3,
  "checkInsByDay": [
    {
      "dayNumber": 1,
      "dayName": "Day 1 - Opening",
      "dayDate": "2025-12-15",
      "status": "CHECKED_IN",
      "checkInTime": "2025-12-15T09:15:00+03:00",
      "checkInLocation": "Main Gate",
      "checkedInBy": "Scanner Operator 1",
      "scannerId": "uuid-string"
    },
    {
      "dayNumber": 2,
      "dayName": "Day 2 - Conference",
      "dayDate": "2025-12-16",
      "status": "CHECKED_IN",
      "checkInTime": "2025-12-16T08:45:00+03:00",
      "checkInLocation": "VIP Entrance",
      "checkedInBy": "Scanner Operator 2",
      "scannerId": "uuid-string"
    },
    {
      "dayNumber": 3,
      "dayName": "Day 3 - Closing",
      "dayDate": "2025-12-17",
      "status": "CHECKED_IN",
      "checkInTime": "2025-12-17T09:00:00+03:00",
      "checkInLocation": "Main Gate",
      "checkedInBy": "Scanner Operator 1",
      "scannerId": "uuid-string"
    }
  ]
}
```

---

## Endpoints

## 1. Get Attendance Stats
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/attendance/{eventId}/stats`

**Access**: 🔒 Event Organizer Only

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Success Response**: Returns AttendanceStatsResponse

**Success Response Message**: "Attendance stats retrieved"

**Behavior**:
- Validates organizer owns event
- Calculates overall attendance metrics
- Breaks down by day (multi-day events)
- Breaks down by ticket type
- Real-time from check-in data

**Errors**:
- `403 FORBIDDEN`: Not event organizer
- `404 NOT_FOUND`: Event not found

---

## 2. Get Attendees
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/attendance/{eventId}/attendees?dayNumber=1&ticketTypeId=uuid&search=john&page=0&size=50`

**Access**: 🔒 Event Organizer Only

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| dayNumber | integer | Multi-day: Yes | Day number (1, 2, 3...) |
| ticketTypeId | string (UUID) | No | Filter by ticket type |
| search | string | No | Search by name/email |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 50) |

**Success Response**: Returns AttendeeListResponse with pagination

**Success Response Message**: "Attendees retrieved"

**Behavior**:
- Lists checked-in attendees for specified day
- Filters by ticket type if provided
- Searches by name/email if provided
- Paginated results
- Shows check-in details

**Validation**:
- Multi-day events require dayNumber
- Ticket type must belong to event
- Day number must be valid (1 to totalDays)

---

## 3. Get Absentees
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/attendance/{eventId}/absentees?dayNumber=1&ticketTypeId=uuid&category=FULL_NO_SHOW&search=jane&page=0&size=50`

**Access**: 🔒 Event Organizer Only

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| dayNumber | integer | Multi-day: Yes | Day number (1, 2, 3...) |
| ticketTypeId | string (UUID) | No | Filter by ticket type |
| category | enum | No | ALL, FULL_NO_SHOW, SPECIFIC_DAY_ONLY (default: ALL) |
| search | string | No | Search by name/email |
| page | integer | No | Page number (0-indexed, default: 0) |
| size | integer | No | Items per page (default: 50) |

**Success Response**: Returns AbsenteeListResponse with pagination

**Success Response Message**: "Absentees retrieved"

**Behavior**:
- Lists NOT checked-in attendees for specified day
- Categories:
  - **ALL**: Everyone not checked-in for this day
  - **FULL_NO_SHOW**: Never checked-in any day
  - **SPECIFIC_DAY_ONLY**: Attended other days but not this one
- Shows attendance patterns
- Paginated results

**Absentee Breakdown**:
- Full no-shows: Tickets never used
- Specific day only: Partial attendance

---

## 4. Get Attendee Detail
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px;">GET</span> `/attendance/{eventId}/attendees/{ticketInstanceId}`

**Access**: 🔒 Event Organizer Only

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| eventId | string (UUID) | Yes | Event identifier |
| ticketInstanceId | string (UUID) | Yes | Ticket instance identifier |

**Success Response**: Returns AttendeeDetailResponse

**Success Response Message**: "Attendee detail retrieved"

**Behavior**:
- Shows complete check-in history
- Per-day status (CHECKED_IN, NOT_CHECKED_IN, UPCOMING)
- Overall attendance status
- Check-in details (time, location, scanner)

**Overall Status**:
- **FULLY_ATTENDED**: Checked-in all days
- **PARTIALLY_ATTENDED**: Checked-in some days
- **NOT_ATTENDED**: Never checked-in

---

## Absentee Categories Explained

### ALL (Default)
**Who**: Everyone who didn't check in for the specified day

**Includes**:
- Full no-shows (never attended any day)
- Specific-day absentees (attended other days)

**Use Case**: Complete list of who's missing today

### FULL_NO_SHOW
**Who**: Tickets never checked-in for ANY event day

**Characteristics**:
- Purchased ticket but never attended
- 0% attendance rate
- Wasted tickets

**Use Case**: Identify completely unused tickets for follow-up

### SPECIFIC_DAY_ONLY
**Who**: Attended some days but NOT this specific day

**Characteristics**:
- Checked-in on other event days
- Partial attendance
- Shows attendance pattern

**Use Case**: Identify who skipped specific days (e.g., missed keynote)

**Example**:
```
3-Day Festival:
- Ticket A: Attended Day 1, 2, 3 → FULLY_ATTENDED
- Ticket B: Attended Day 1, 3 (missed Day 2) → SPECIFIC_DAY_ONLY for Day 2
- Ticket C: Never attended → FULL_NO_SHOW for all days
```

---

## Multi-Day Event Logic

### Day Number Requirements

**Single-Day Events**:
- `dayNumber` optional (defaults to 1)
- Only one day to track

**Multi-Day Events**:
- `dayNumber` REQUIRED
- Must specify which day (1, 2, 3...)
- System returns error if missing

**Validation**:
```
Event has 3 days → dayNumber must be 1, 2, or 3
Request dayNumber=5 → Error: "Invalid day 5. Valid: 1-3"
```

### Per-Day Tracking

**Statistics**:
- Total tickets (same for all days)
- Checked-in (varies per day)
- Absent (varies per day)
- Attendance rate (varies per day)

**Day Status**:
- **COMPLETED**: Day has passed
- **ONGOING**: Day is today
- **UPCOMING**: Day hasn't started

**Example**:
```
3-Day Event (Dec 15-17), Today: Dec 16

Day 1: status=COMPLETED, checkedIn=475
Day 2: status=ONGOING, checkedIn=450
Day 3: status=UPCOMING, checkedIn=0
```

---

## Attendance Rate Calculations

### Overall Attendance Rate
```
Rate = (Unique Tickets Checked-In / Total Tickets) × 100
```

**Example**:
- Total tickets: 500
- Tickets with at least one check-in: 462
- Rate: 92.4%

### Per-Day Attendance Rate
```
Rate = (Checked-In This Day / Total Tickets) × 100
```

**Example**:
- Total tickets: 500
- Checked-in Day 1: 475
- Rate: 95.0%

### Per-Ticket-Type Rate
```
Rate = (Checked-In for Type / Total Sold for Type) × 100
```

**Example**:
- VIP tickets sold: 100
- VIP checked-in: 98
- Rate: 98.0%

---

## Search Functionality

**Searches**:
- Attendee name (case-insensitive)
- Attendee email (case-insensitive)

**Match**: Partial match (contains)

**Examples**:
- Search "john" → Matches "John Doe", "Johnny Smith", "john@example.com"
- Search "@gmail" → Matches all Gmail addresses
- Search "255712" → Matches phone numbers containing this

---

## Use Cases

### Dashboard Overview
```
GET /attendance/{eventId}/stats

Shows:
- Overall attendance rate
- Per-day breakdown
- Per-ticket-type breakdown
- Real-time metrics
```

### Check Who Attended Today
```
GET /attendance/{eventId}/attendees?dayNumber=1

Shows:
- All checked-in attendees for Day 1
- Check-in times and locations
- Searchable and filterable
```

### Find No-Shows
```
GET /attendance/{eventId}/absentees?dayNumber=1&category=FULL_NO_SHOW

Shows:
- Tickets that were never used
- For follow-up or refund processing
```

### Track Partial Attendance
```
GET /attendance/{eventId}/absentees?dayNumber=2&category=SPECIFIC_DAY_ONLY

Shows:
- Who attended Day 1 but missed Day 2
- Useful for understanding engagement patterns
```

### Individual Ticket History
```
GET /attendance/{eventId}/attendees/{ticketInstanceId}

Shows:
- Complete check-in history
- Which days attended/missed
- Check-in details per day
```

---

## Best Practices

### For Organizers
✅ Monitor attendance in real-time during event  
✅ Follow up with full no-shows post-event  
✅ Track partial attendance patterns  
✅ Export attendee lists for post-event communications  
✅ Use absentee data to improve future events  

### For Developers
✅ Require dayNumber for multi-day events  
✅ Show attendance rate with 1 decimal (92.4%)  
✅ Implement export to CSV functionality  
✅ Cache stats (refresh every 5 minutes)  
✅ Display check-in times in event timezone  

---

## Quick Reference

### HTTP Status Codes
- `200 OK`: Successful request
- `401 UNAUTHORIZED`: Authentication required
- `403 FORBIDDEN`: Not event organizer
- `404 NOT_FOUND`: Event/ticket not found

### Attendance Status
- **FULLY_ATTENDED**: All days checked-in
- **PARTIALLY_ATTENDED**: Some days checked-in
- **NOT_ATTENDED**: Never checked-in

### Day Status
- **COMPLETED**: Day has passed
- **ONGOING**: Day is today
- **UPCOMING**: Day hasn't started

### Absentee Categories
- **ALL**: All absentees for this day
- **FULL_NO_SHOW**: Never attended any day
- **SPECIFIC_DAY_ONLY**: Missed this day only

---

## Conclusion

The Attendance Analytics API provides comprehensive tracking with:

✅ **Real-Time Stats**: Instant attendance metrics  
✅ **Multi-Day Support**: Per-day breakdowns  
✅ **Absentee Tracking**: Full no-shows vs partial attendance  
✅ **Search & Filter**: By ticket type, day, name/email  
✅ **Individual History**: Complete check-in records  
✅ **Attendance Patterns**: Understand engagement

# 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!