# Events  API

# Events  Management API

**Author**: Josh S. Sakweli, Backend Lead Team
**Last Updated**: 2026-05-25
**Version**: v1.2

**Base URL**: `https://your-api-domain.com/api/v1/e-events`

**Short Description**: The Event Core API provides full lifecycle management for events on the NextGate platform — from creating drafts and configuring schedules, to publishing, managing live events, and discovery. It is used by event organizers to build, publish, and manage events step-by-step, and by all authenticated users to browse, search, and filter published events.

**Hints**:
- All write operations (create, update, publish) require a valid Bearer token. Read operations on published events are public.
- Dates and times must always use ISO 8601 format with timezone offset (e.g., `2025-06-15T09:00:00+03:00`). The API stores and returns `ZonedDateTime`.
- Event creation follows a **staged workflow**: `BASIC_INFO → SCHEDULE → LOCATION_DETAILS → TICKETS`. All required stages must be completed before publishing.
- Slugs are auto-generated from the event title with a UUID suffix to guarantee uniqueness — do not pass a slug manually.
- Pagination uses 1-based page numbers (page=1 is the first page).
- Category seeding and management live in the **Event Categories API** (base: `/api/v1/e-events/categories`), documented separately.

---

### Event Creation User Journey

```
  [Organizer]
       │
       │  POST /draft
       ▼
  ┌─────────────┐
  │  BASIC INFO │  title, category, format, description, media, ctaLabel (optional)
  └──────┬──────┘
         │  PATCH /drafts/{id}/basic-info  (optional update)
         │
         │  PATCH /draft/{id}/schedule
         ▼
  ┌──────────────┐
  │   SCHEDULE   │  days[ date, startTime, endTime ], timezone
  └──────┬───────┘
         │
         │  PATCH /draft/{id}/location
         ▼
  ┌──────────────────┐
  │ LOCATION DETAILS │  venue (IN_PERSON/HYBRID) or virtualDetails (ONLINE/HYBRID)
  └──────┬───────────┘         or skip entirely (TBA format)
         │
         │  (Ticket creation via Tickets API — required before publish)
         ▼
  ┌──────────────┐
  │   TICKETS    │  at least one active ticket required
  └──────┬───────┘
         │
         │  (Optional enrichment)
         │  PATCH /drafts/{id}/highlights
         │  PATCH /drafts/{id}/faqs
         │  PATCH /drafts/{id}/lineup
         │  PATCH /drafts/{id}/agenda
         │  POST  /draft/{id}/products/{productId}
         │  POST  /draft/{id}/shops/{shopId}
         │
         │  PATCH /{eventId}/publish
         ▼
  ┌───────────────┐
  │   PUBLISHED   │  RSA keys generated, ctaLabel auto-derived if not set,
  │               │  category count incremented, event visible in public feed
  └───────────────┘
```

### Event Status Flow

```
  DRAFT ◄──────────────────────► PUBLISHED
    │           (unpublish           │
    │        if 0 tickets sold)      │  (unpublish — only if no tickets sold)
    │                                │
    │  (discardDraft)                │
    ▼                                ▼
  [deleted]                     CANCELLED ◄─── (cancel from any non-terminal status)
                                     │
                              [terminal state]

  PUBLISHED ──► (system / scheduled job) ──► HAPPENING ──► COMPLETED
```

> **Status Rules**:
> - `DRAFT ↔ PUBLISHED` — Free movement. Unpublish is only allowed if zero tickets have been sold.
> - `CANCELLED` — Terminal. Can be triggered from any non-terminal status. Triggers bulk refund if tickets were sold.
> - `HAPPENING` / `COMPLETED` — System-managed via scheduled jobs.

---

## Standard Response Format

All API responses follow a consistent structure using the Globe Response Builder pattern.

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": { }
}
```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, CREATED, BAD_REQUEST, etc.) |
| `message` | string | Human-readable description of the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object / string | Response payload on success; error detail on failure |

---

## Shared Response Object Definitions

The following objects are returned by multiple endpoints. They are defined once here and referenced throughout.

### A. EventResponse (Full Event Object)

Returned by all draft and event management endpoints.

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Unique event identifier |
| `title` | string | Event title |
| `slug` | string | URL-friendly identifier, auto-generated |
| `description` | string | Full event description |
| `category.categoryId` | UUID | Category identifier |
| `category.categoryName` | string | Category display name |
| `category.categorySlug` | string | Category slug |
| `eventFormat` | string | `IN_PERSON`, `ONLINE`, `HYBRID`, or `TBA` |
| `eventVisibility` | string | `PUBLIC`, `PRIVATE`, or `UNLISTED` |
| `status` | string | `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` |
| `schedule.startDateTime` | ZonedDateTime | Event start (ISO 8601 with offset) |
| `schedule.endDateTime` | ZonedDateTime | Event end (ISO 8601 with offset) |
| `schedule.timezone` | string | IANA timezone string (e.g., `Africa/Dar_es_Salaam`) |
| `schedule.days[]` | array | Day-level schedule entries (see EventDayInfo below) |
| `venue.name` | string | Venue name (IN_PERSON / HYBRID only) |
| `venue.address` | string | Venue address |
| `venue.coordinates.latitude` | string | Latitude decimal string |
| `venue.coordinates.longitude` | string | Longitude decimal string |
| `virtualDetails.meetingLink` | string | Meeting URL (ONLINE / HYBRID only) |
| `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 |
| `ctaLabel` | string | CTA button label (e.g., "Get Tickets", "Register for Free"). Auto-derived from ticket pricing at publish if not explicitly set |
| `hasApplicantForm` | boolean | Whether an applicant form is configured for this event |
| `applicantForm.displayTime` | string | When the form is shown: `BEFORE_CHECKOUT` or `AFTER_CHECKOUT` |
| `applicantForm.isRequired` | boolean | Whether form submission is required for online attendees |
| `applicantForm.applyToAtDoor` | boolean | Whether the form also applies to at-door check-in |
| `currentStage` | string | Current event creation stage |
| `completedStages[]` | array | List of completed stage names |
| `completionPercentage` | integer | 0–100 completion percentage |
| `canPublish` | boolean | Whether all required stages are completed |
| `createdAt` | ZonedDateTime | Creation timestamp |
| `updatedAt` | ZonedDateTime | Last update timestamp |
| `createdBy` | string | Username of creator |
| `updatedBy` | string | Username of last editor |

#### EventDayInfo

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Day entity ID |
| `date` | string | Date in `YYYY-MM-DD` format |
| `startTime` | string | Start time in `HH:mm:ss` |
| `endTime` | string | End time in `HH:mm:ss` |
| `description` | string | Optional day description |
| `dayOrder` | integer | Display order (1-based) |

#### HighlightEntry

| Field | Type | Description |
|-------|------|-------------|
| `type` | string | `AGE_RESTRICTION`, `CHECK_IN_TIME`, `PARKING`, `DRESS_CODE`, `FOOD_DRINKS`, `ACCESSIBILITY`, `REFUND_POLICY`, `WHAT_TO_BRING`, `PROHIBITED_ITEMS`, `WEATHER_INFO`, `CUSTOM` |
| `title` | string | Display title for this highlight |
| `value` | string | Short value (e.g., "18+") |
| `description` | string | Longer explanation |

#### FaqEntry

| Field | Type | Description |
|-------|------|-------------|
| `question` | string | The FAQ question |
| `answer` | string | The answer |
| `order` | integer | Display order |

#### LineupEntry

| Field | Type | Description |
|-------|------|-------------|
| `entryType` | string | `PLATFORM_USER` or `CUSTOM` |
| `userId` | UUID | Platform user ID (only when `entryType=PLATFORM_USER`) |
| `name` | string | Display name (auto-enriched from user profile when `PLATFORM_USER`) |
| `role` | string | `HEADLINER`, `PERFORMER`, `SPEAKER`, `DJ`, `HOST`, `PANELIST`, `MODERATOR`, `GUEST` |
| `title` | string | Professional title (e.g., "Lead Vocalist") |
| `bio` | string | Short biography |
| `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 within its sales window |
| `saleStatusMessage` | string | Human-readable explanation of the current sale status |

---

### 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 |
| `hasApplicantForm` | boolean | Whether an applicant form is configured |
| `ctaLabel` | string | CTA button label |
| `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": 25,
    "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"
    },
    "ctaLabel": null,
    "hasApplicantForm": false,
    "applicantForm": null,
    "createdAt": "2025-02-17T10:30:45+03:00",
    "updatedAt": null,
    "createdBy": "amina.hassan",
    "updatedBy": null
  }
}
```

**Success Response Fields**: See [Shared Response Object A — EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token — see [Shared Error: 401](#shared-error-response-examples) |
| `404` | Category ID not found |
| `422` | Missing required fields (title, categoryId, eventFormat) — see [Shared Error: 422](#shared-error-response-examples) |

---

## 2. Get My Drafts

**Purpose**: Returns a paginated list of all DRAFT events owned by the authenticated organizer.

**Endpoint**: `GET` `/api/v1/e-events/drafts`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Drafts retrieved",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [
      {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "title": "Dar es Salaam Jazz Festival 2025",
        "slug": "dar-es-salaam-jazz-festival-2025-a1b2c3d4",
        "shortDescription": "An annual celebration of jazz and live music...",
        "categoryId": "8e3a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        "categoryName": "Music & Concerts",
        "eventFormat": "IN_PERSON",
        "eventVisibility": "PUBLIC",
        "status": "DRAFT",
        "startDateTime": null,
        "endDateTime": null,
        "timezone": null,
        "locationSummary": null,
        "thumbnail": "https://cdn.example.com/thumbs/jazz-2025.jpg",
        "hasApplicantForm": false,
        "ctaLabel": null,
        "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",
  "ctaLabel": "Get Tickets",
  "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` |
| `ctaLabel` | string | No | CTA button label override | Max: 50 characters. If omitted, existing value is kept; auto-derived at publish if never set |
| `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. Advances `currentStage` to `TICKETS`.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/location`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample** (IN_PERSON):

```json
{
  "venue": {
    "name": "Mlimani City Arena",
    "address": "Sam Nujoma Road, Dar es Salaam",
    "coordinates": {
      "latitude": -6.7724,
      "longitude": 39.2083
    }
  }
}
```

**Request JSON Sample** (ONLINE):

```json
{
  "virtualDetails": {
    "meetingLink": "https://zoom.us/j/123456789",
    "meetingId": "123 456 789",
    "passcode": "jazz2025"
  }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `venue` | object | Conditional | Required when format is `IN_PERSON` or `HYBRID` | — |
| `venue.name` | string | Yes (if venue) | Venue name | Max: 200 characters |
| `venue.address` | string | No | Full address | Max: 500 characters |
| `venue.coordinates.latitude` | BigDecimal | No | GPS latitude | — |
| `venue.coordinates.longitude` | BigDecimal | No | GPS longitude | — |
| `virtualDetails` | object | Conditional | Required when format is `ONLINE` or `HYBRID` | — |
| `virtualDetails.meetingLink` | string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
| `virtualDetails.meetingId` | string | No | Platform meeting ID | Max: 100 characters |
| `virtualDetails.passcode` | string | No | Meeting passcode | Max: 100 characters |

> **Format-based rules**:
> - `IN_PERSON` → `venue.name` is required; `virtualDetails` is ignored
> - `ONLINE` → `virtualDetails.meetingLink` is required; `venue` is ignored
> - `HYBRID` → both `venue.name` and `virtualDetails.meetingLink` are required
> - `TBA` → no fields required; stage is immediately marked complete

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Location updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `completedStages` will include `"LOCATION_DETAILS"` when requirements are met and `currentStage` will advance to `"TICKETS"`.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found |
| `422` | Missing required venue/virtual fields for the event format |

---

## 8. Update Draft — Highlights

**Purpose**: Replaces the full list of event highlights (key attendee-facing info such as age restriction, dress code, parking, etc.). Sending an empty array clears all highlights.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/highlights`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "highlights": [
    {
      "type": "AGE_RESTRICTION",
      "title": "Age Limit",
      "value": "18+",
      "description": "This event is strictly for adults aged 18 and above."
    },
    {
      "type": "DRESS_CODE",
      "title": "Dress Code",
      "value": "Smart Casual",
      "description": "No sportswear or flip flops allowed."
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `highlights` | array | Yes | Full list of highlights | Empty array is valid (clears existing) |
| `highlights[].type` | string | Yes | Highlight category | Enum: `AGE_RESTRICTION`, `CHECK_IN_TIME`, `PARKING`, `DRESS_CODE`, `FOOD_DRINKS`, `ACCESSIBILITY`, `REFUND_POLICY`, `WHAT_TO_BRING`, `PROHIBITED_ITEMS`, `WEATHER_INFO`, `CUSTOM` |
| `highlights[].title` | string | Yes | Display title | — |
| `highlights[].value` | string | No | Short value summary | — |
| `highlights[].description` | string | No | Longer explanation | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Highlights updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.highlights` will reflect the new list.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the draft owner |
| `404` | Draft not found |

---

## 9. Update Draft — FAQs

**Purpose**: Replaces the full list of frequently asked questions for the event. Sending an empty array clears all FAQs.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/faqs`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "faqs": [
    {
      "question": "Is this event suitable for children?",
      "answer": "No. This event is strictly 18+ only.",
      "order": 1
    },
    {
      "question": "Is parking available?",
      "answer": "Yes, free parking is available on site.",
      "order": 2
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `faqs` | array | Yes | Full list of FAQs | Empty array is valid (clears existing) |
| `faqs[].question` | string | Yes | FAQ question text | — |
| `faqs[].answer` | string | Yes | FAQ answer text | — |
| `faqs[].order` | integer | No | Display order | — |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "FAQs updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object). `data.faqs` will reflect the new list.

**Possible Error Responses**: Same as Endpoint 8 (401, 403, 404).

---

## 10. Update Draft — Lineup

**Purpose**: Replaces the full event lineup. Supports both platform users (whose profile data is auto-enriched) and custom entries for external performers or speakers. Sending an empty array clears the lineup.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/lineup`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "lineup": [
    {
      "entryType": "PLATFORM_USER",
      "userId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "role": "HEADLINER",
      "performanceDay": 1,
      "performanceTime": "21:00",
      "order": 1
    },
    {
      "entryType": "CUSTOM",
      "name": "DJ Afrobeat",
      "role": "DJ",
      "title": "International Guest DJ",
      "bio": "Award-winning DJ from Lagos with 10 years of experience.",
      "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 |

---

## 11. Update Draft — Agenda

**Purpose**: Replaces the full event agenda organized by day and session. Supports both platform users and custom entries as session presenters. Sending an empty array clears the agenda.

**Endpoint**: `PATCH` `/api/v1/e-events/drafts/{draftId}/agenda`

**Access Level**: 🔒 Protected (Organizer — own drafts only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `draftId` | UUID | Yes | The draft to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "agenda": [
    {
      "dayNumber": 1,
      "date": "2025-07-18",
      "sessions": [
        {
          "startTime": "18:00",
          "endTime": "19:00",
          "title": "Welcome & Opening Ceremony",
          "type": "CEREMONY",
          "location": "Main Stage",
          "presenterType": "PLATFORM_USER",
          "presenterId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
        },
        {
          "startTime": "20:00",
          "endTime": "22:00",
          "title": "Headliner Performance",
          "type": "PERFORMANCE",
          "presenterType": "CUSTOM",
          "presenterName": "The Sauti Sol Band",
          "presenterTitle": "Headline Act",
          "presenterBio": "East Africa's most celebrated group."
        }
      ]
    }
  ]
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `agenda` | array | Yes | Full agenda list (by day) | Empty array is valid |
| `agenda[].dayNumber` | integer | Yes | Day number (1-based) | — |
| `agenda[].date` | string | No | Date string (`YYYY-MM-DD`) | — |
| `agenda[].sessions` | array | Yes | Sessions for this day | — |
| `sessions[].startTime` | string | Yes | Session start (`HH:mm`) | — |
| `sessions[].endTime` | string | Yes | Session end (`HH:mm`) | — |
| `sessions[].title` | string | Yes | Session name | — |
| `sessions[].description` | string | No | Session description | — |
| `sessions[].type` | string | No | Session type | Enum: `GENERAL`, `PERFORMANCE`, `CEREMONY`, `PANEL`, `WORKSHOP`, `NETWORKING`, `MEAL`, `BREAK` |
| `sessions[].location` | string | No | Sub-location within venue | — |
| `sessions[].presenterType` | string | No | Presenter source | Enum: `PLATFORM_USER`, `CUSTOM` |
| `sessions[].presenterId` | UUID | Conditional | Platform user ID | Required when `presenterType=PLATFORM_USER` |
| `sessions[].presenterName` | string | Conditional | Presenter name | Required when `presenterType=CUSTOM`; auto-set when `PLATFORM_USER` |
| `sessions[].presenterTitle` | string | No | Professional title | Auto-enriched for platform users |
| `sessions[].presenterBio` | string | No | Biography | Auto-enriched for platform users |
| `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 |

---

## 12. Attach Product to Event

**Purpose**: Links an existing active product from the PRODUCT domain to an event. Allowed on both `DRAFT` and `PUBLISHED` events. Marks the `LINKS` stage as completed.

**Endpoint**: `POST` `/api/v1/e-events/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 |

---

## 13. Remove Product from Event

**Purpose**: Detaches a previously linked product from the event. Allowed on both `DRAFT` and `PUBLISHED` events.

**Endpoint**: `DELETE` `/api/v1/e-events/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 |

---

## 14. Attach Shop to Event

**Purpose**: Links an existing active shop from the PRODUCT domain to an event. Allowed on both `DRAFT` and `PUBLISHED` events. Marks the `LINKS` stage as completed.

**Endpoint**: `POST` `/api/v1/e-events/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 |

---

## 15. Remove Shop from Event

**Purpose**: Detaches a previously linked shop from the event. Allowed on both `DRAFT` and `PUBLISHED` events.

**Endpoint**: `DELETE` `/api/v1/e-events/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 13 (401, 403, 404).

---

## 16. Publish Event

**Purpose**: Transitions a draft event to `PUBLISHED` status. This is the final step of the creation workflow. The system validates all required stages are complete, performs a duplicate event check against existing published events, generates an RSA key pair used for secure ticket QR code signing, and auto-derives the `ctaLabel` from ticket pricing if the organizer has not set one.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/publish`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to publish | Must be a valid UUID |

> **Pre-publish checklist** (all must pass or the request is rejected with `422`):
> - `BASIC_INFO` stage completed ✓
> - `SCHEDULE` stage completed (at least one day, not in the past) ✓
> - `LOCATION_DETAILS` stage completed ✓
> - At least one active ticket exists for the event ✓
> - Event start date is not in the past ✓
> - No duplicate event detected with ≥85% similarity score to another organizer's public event ✓

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event published successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "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 no active tickets exist |
| `500` | RSA key generation failed |

---

## 17. Unpublish Event

**Purpose**: Reverts a `PUBLISHED` event back to `DRAFT` status. Only allowed if zero tickets have been sold across all ticket types. If tickets have been sold, the organizer must cancel the event instead.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/unpublish`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to unpublish | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event unpublished successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "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 |

---

## 18. Cancel Event

**Purpose**: Cancels an event. Can be triggered from any non-terminal status (`DRAFT`, `PUBLISHED`, `HAPPENING`). This action is irreversible. If the event was published and tickets were sold, a bulk refund process is triggered.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/cancel`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to cancel | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event cancelled successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "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` |

---

## 19. Update Published Event — Basic Info

**Purpose**: Updates the description, media, and/or CTA label of a published event. Title, category, and format changes are blocked on published events as the slug has already been shared publicly.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/basic-info`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request JSON Sample**:

```json
{
  "description": "Updated: The biggest jazz event in East Africa. Now featuring 35 artists across 3 stages.",
  "ctaLabel": "Get Tickets Now",
  "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 |
| `ctaLabel` | string | No | CTA button label override | Max: 50 characters. If provided and non-blank → saved as-is. If omitted or null → re-derived from current ticket pricing |
| `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": "Event info updated",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not in PUBLISHED status |
| `422` | Validation failure on provided fields |

---

## 20. Update Published Event — Highlights

**Purpose**: Replaces the full list of highlights for a published event. Behavior is identical to the draft equivalent — sending an empty array clears all highlights.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/highlights`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 8 — Update Draft Highlights](#8-update-draft--highlights).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Highlights updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not PUBLISHED |

---

## 21. Update Published Event — FAQs

**Purpose**: Replaces the full list of FAQs for a published event. Sending an empty array clears all FAQs.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/faqs`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 9 — Update Draft FAQs](#9-update-draft--faqs).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "FAQs updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**: Same as Endpoint 20 (401, 403, 404, 400).

---

## 22. Update Published Event — Lineup

**Purpose**: Replaces the full event lineup on a published event. Supports the same platform user enrichment as the draft equivalent.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/lineup`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 10 — Update Draft Lineup](#10-update-draft--lineup).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Lineup updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or a `userId` in lineup does not exist |
| `400` | Event is not PUBLISHED |

---

## 23. Update Published Event — Agenda

**Purpose**: Replaces the full event agenda on a published event. Supports the same platform user enrichment as the draft equivalent.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/agenda`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published event to update | Must be a valid UUID |

**Request Body**: Same format as [Endpoint 11 — Update Draft Agenda](#11-update-draft--agenda).

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agenda updated successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found, or a `presenterId` does not exist |
| `400` | Event is not PUBLISHED |

---

## 24. Reveal Location (TBA → Actual)

**Purpose**: Reveals the actual location for an event that was originally published with `eventFormat=TBA`. The new format cannot be `TBA`. Required fields depend on the new format chosen.

**Endpoint**: `PATCH` `/api/v1/e-events/{eventId}/published/reveal-location`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |
| `Content-Type` | string | Yes | `application/json` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The published TBA event | Must be a valid UUID |

**Request JSON Sample** (revealing as IN_PERSON):

```json
{
  "eventFormat": "IN_PERSON",
  "venue": {
    "name": "Mlimani City Arena",
    "address": "Sam Nujoma Road, Dar es Salaam",
    "coordinates": {
      "latitude": -6.7724,
      "longitude": 39.2083
    }
  }
}
```

**Request JSON Sample** (revealing as ONLINE):

```json
{
  "eventFormat": "ONLINE",
  "virtualDetails": {
    "meetingLink": "https://zoom.us/j/123456789",
    "meetingId": "123 456 789",
    "passcode": "jazz2025"
  }
}
```

**Request Body Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventFormat` | string | Yes | The actual event format | Enum: `IN_PERSON`, `ONLINE`, `HYBRID` — **cannot be `TBA`** |
| `venue` | object | Conditional | Physical venue details | Required when new format is `IN_PERSON` or `HYBRID` |
| `venue.name` | string | Yes (if venue) | Venue name | Max: 200 characters |
| `venue.address` | string | No | Full address | Max: 500 characters |
| `venue.coordinates.latitude` | BigDecimal | No | GPS latitude | — |
| `venue.coordinates.longitude` | BigDecimal | No | GPS longitude | — |
| `virtualDetails` | object | Conditional | Virtual meeting details | Required when new format is `ONLINE` or `HYBRID` |
| `virtualDetails.meetingLink` | string | Yes (if virtualDetails) | Meeting URL | Max: 500 characters |
| `virtualDetails.meetingId` | string | No | Platform meeting ID | Max: 100 characters |
| `virtualDetails.passcode` | string | No | Meeting passcode | Max: 100 characters |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Location revealed successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `403` | Not the event organizer |
| `404` | Event not found |
| `400` | Event is not PUBLISHED, current format is not TBA, or new format is TBA |
| `422` | Missing required venue/virtual fields for the chosen format |

---

## 25. Get Event by ID

**Purpose**: Retrieves full event details. Published events are publicly accessible without authentication. Draft events can only be viewed by their organizer.

**Endpoint**: `GET` `/api/v1/e-events/{eventId}`

**Access Level**: 🌐 Public (for PUBLISHED events) | 🔒 Protected (for DRAFT events — organizer only)

**Authentication**: Bearer Token (required only for DRAFT access)

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Conditional | Required when accessing a DRAFT event |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `eventId` | UUID | Yes | The event to retrieve | Must be a valid UUID |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Event retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": { "...": "Full EventResponse object" }
}
```

**Success Response Fields**: `data` is a full [EventResponse](#a-eventresponse-full-event-object).

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `403` | Attempting to access a DRAFT event without being its organizer |
| `404` | Event not found or is soft-deleted |

---

## 26. Get My Events

**Purpose**: Returns a paginated list of all events (any status) created by the authenticated organizer.

**Endpoint**: `GET` `/api/v1/e-events/my-events`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 12,
    "totalPages": 2
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects. Pagination follows [Standard Paginated Response Wrapper](#c-standard-paginated-response-wrapper).

**Possible Error Responses**: 401 — see [Shared Error: 401](#shared-error-response-examples).

---

## 27. Get My Events by Status

**Purpose**: Returns a paginated list of the authenticated organizer's events filtered by a specific status.

**Endpoint**: `GET` `/api/v1/e-events/my-events/status/{status}`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:

| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `status` | string | Yes | Event status filter | Enum: `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 5,
    "totalPages": 1
  }
}
```

**Success Response Fields**: Same as Endpoint 26.

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | Invalid `status` enum value |

---

## 28. Get Events Feed

**Purpose**: Returns a paginated list of all published events for the public discovery feed. Results are ordered by creation date descending. This endpoint will eventually incorporate a recommendation algorithm.

**Endpoint**: `GET` `/api/v1/e-events/events-feed`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Events feed retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects — PUBLISHED only ]" ],
    "totalElements": 87,
    "totalPages": 9
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects (PUBLISHED status only).

**Possible Error Responses**: None expected (no authentication required).

---

## 29. Search Events

**Purpose**: Full-text search across published event titles. Results are ordered by start date ascending.

**Endpoint**: `GET` `/api/v1/e-events/search`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | Yes | Search keyword(s) | Non-empty string | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Search results retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 4,
    "totalPages": 1
  }
}
```

**Success Response Fields**: `data.content[]` contains [EventSummaryResponse](#b-eventsummaryresponse-lightweight-list-object) objects.

**Possible Error Responses**: None expected for normal queries (empty results return an empty `content` array, not a 404).

---

## 30. Filter Events by Date Range

**Purpose**: Returns published events whose schedule overlaps with the provided date range. An event is included if it starts before `endDate` AND ends after `startDate` (overlap logic, not exact range match).

**Endpoint**: `GET` `/api/v1/e-events/filter/date`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `startDate` | ZonedDateTime | Yes | Range start | ISO 8601 with offset (e.g., `2025-07-01T00:00:00+03:00`) | — |
| `endDate` | ZonedDateTime | Yes | Range end | ISO 8601 with offset; must be after `startDate` | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Filtered events retrieved successfully",
  "action_time": "2025-02-17T10:30:45",
  "data": {
    "content": [ "[ EventSummaryResponse objects ]" ],
    "totalElements": 7,
    "totalPages": 1
  }
}
```

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `400` | `startDate` is after `endDate` |

---

## 31. Search and Filter Events (Combined)

**Purpose**: Combines keyword search with optional date range filtering in a single call. All parameters are optional — calling with no parameters is equivalent to getting the full published feed.

**Endpoint**: `GET` `/api/v1/e-events/filter`

**Access Level**: 🌐 Public

**Authentication**: None required

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | No | Title keyword search | — | — |
| `startDate` | ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
| `endDate` | ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after `startDate` if both provided | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `400` | `startDate` is after `endDate` (when both are provided) |

---

## 32. Search My Events (Organizer)

**Purpose**: Allows an organizer to search and filter within their own events across all statuses. Supports keyword, status filter, and date range simultaneously.

**Endpoint**: `GET` `/api/v1/e-events/my-events/search`

**Access Level**: 🔒 Protected (Organizer — own events only)

**Authentication**: Bearer Token

**Request Headers**:

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:

| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `query` | string | No | Keyword search on title | — | — |
| `status` | string | No | Filter by status | Enum: `DRAFT`, `PUBLISHED`, `HAPPENING`, `CANCELLED`, `COMPLETED` | — |
| `startDate` | ZonedDateTime | No | Date range start | ISO 8601 with offset | — |
| `endDate` | ZonedDateTime | No | Date range end | ISO 8601 with offset; must be after `startDate` if both provided | — |
| `page` | integer | No | Page number (1-based) | Min: 1 | `1` |
| `size` | integer | No | Items per page | Min: 1 | `10` |

**Possible Error Responses**:

| Status | Scenario |
|--------|----------|
| `401` | No or expired token |
| `400` | `startDate` is after `endDate` |

---

## Data Format Standards

| Concern | Standard |
|---------|----------|
| Timestamps | ISO 8601 with timezone offset: `2025-07-18T18:00:00+03:00` |
| Dates | `YYYY-MM-DD` format: `2025-07-18` |
| Times | `HH:mm:ss` (24-hour): `18:00:00` |
| IDs | UUID v4: `3fa85f64-5717-4562-b3fc-2c963f66afa6` |
| Pagination | 1-based `page` query parameter, Spring `Page` wrapper in response |
| Prices | `BigDecimal` with 2 decimal places; `0.00` for free |
| Enums | Uppercase strings as defined (e.g., `IN_PERSON`, `PUBLISHED`) |

# Ticket Management API

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

# Event Checkout & Payment API

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

# Event Booking Orders API

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

# Event Check-In System API

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

# Organizer Analytics API

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

# Events  Applicant Form API

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

# Attendance Analytics API

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

# Event Feedback API

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

# Event Fund Claims

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