openapi: 3.0.3
info:
  title: BATbern Event Speakers API
  version: 1.0.0
  description: 'Event-scoped speaker coordination: speaker pool, self-nomination, promotion to READY, invitations, and outreach history.


    Carved from the consolidated events-api spec (API consolidation Phase 6). Paths preserved verbatim.'
  contact:
    name: BATbern Platform Team
    email: platform@batbern.ch
  license:
    name: Proprietary
servers:
- url: https://api.batbern.ch/api/v1
  description: Production
- url: https://api.batbern.ch/api/v1
  description: Staging
- url: http://localhost:8080/api/v1
  description: Development
security:
- BearerAuth: []
tags:
- name: Event Actions
  description: Event publishing and workflow operations
- name: Speaker Status
  description: Speaker status management, content submission + quality review (Stories 5.4, 5.5, 11.B-D)
- name: Speaker Self-Nomination
  description: Attendee speaker self-nomination — "I Could Speak on That" (Story 7.2)
- name: Speaker Outreach
  description: Speaker outreach tracking and history (Story 5.3)
paths:
  /events/{eventCode}/speakers/pool:
    post:
      tags:
      - Event Actions
      summary: Add speaker to event speaker pool
      description: "Add a potential speaker to the event speaker pool during brainstorming phase.\n\n**Story**: 5.2 - Topic Selection & Speaker Brainstorming\n**Acceptance Criteria**: AC9-13\n**Authorization**: Requires ORGANIZER role\n**Rate Limiting**: Applied at API Gateway level\n\n**Business Rules**:\n- Event must exist\n- Speaker name is required\n- Company, expertise, and notes are optional\n- Organizer assignment is optional (can be assigned later)\n- Initial status is automatically set to 'identified'\n- **Tightened by ADR-009 (Story 11.D.1):** `email` is not accepted on this endpoint;\n  speakers are only promoted to a real identity via\n  `POST /speakers/{speakerId}/promote` once they reach the READY state per ADR-009 §0.2.\n  Any client-supplied `email` (or other unknown) field returns 400 Bad Request.\n\n**Performance**: <150ms (P95)\n"
      operationId: addSpeakerToPool
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddSpeakerToPoolRequest'
      responses:
        '201':
          description: Speaker added to pool successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerPoolResponse'
        '400':
          description: 'Validation error. Two paths to this status:

            (a) speakerName missing/empty, or

            (b) unknown field present in the body — notably `email`, which is rejected

            per Story 11.D.1 (AR23): use `POST /speakers/{speakerId}/promote` to

            attach an email when promoting a CONTACTED speaker to READY.

            '
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              examples:
                missingSpeakerName:
                  summary: speakerName missing
                  value:
                    message: Speaker name is required
                    timestamp: '2025-12-13T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/pool
                    status: 400
                    error: Bad Request
                unknownField:
                  summary: Unknown field 'email' rejected (AR23)
                  value:
                    message: Unknown field 'email' is not permitted on this endpoint; use POST /speakers/{speakerId}/promote to provision a speaker with an email
                    timestamp: '2025-12-13T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/pool
                    status: 400
                    error: Bad Request
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              example:
                message: 'Event not found: BATbern56'
                timestamp: '2025-12-13T10:00:00Z'
                path: /api/v1/events/BATbern56/speakers/pool
                status: 404
                error: Not Found
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
    get:
      tags:
      - Event Actions
      summary: Get speaker pool for event
      description: 'Retrieve all speakers in the event''s speaker pool.


        **Story**: 5.2 - Topic Selection & Speaker Brainstorming

        **Authorization**: Requires ORGANIZER role

        **Rate Limiting**: Applied at API Gateway level


        **Performance**: <150ms (P95)

        '
      operationId: getSpeakerPool
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      responses:
        '200':
          description: Speaker pool retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SpeakerPoolResponse'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/self-nominate:
    post:
      tags:
      - Speaker Self-Nomination
      summary: Attendee self-nominates as a speaker ("I Could Speak on That")
      description: "A logged-in attendee raises their hand with a proposed talk once the event's topic\nis set + published, feeding the speaker pipeline from the floor.\n\n**Story**: 7.2 - \"I Could Speak on That\"\n**Authorization**: Requires ATTENDEE role (login-gated end-to-end)\n**Rate Limiting**: Applied at API Gateway level\n\n**Business Rules**:\n- The event's topic must be set (`topicCode != null`) AND the event must be\n  published (`publishedAt != null` or `currentPublishedPhase != 'none'`) — else 409.\n- The nomination lands as a `speaker_pool` row at the `IDENTIFIED` default, tagged\n  `source = 'self_nomination'` with `proposedByUsername` = the caller's username.\n  **No** Cognito user, SPEAKER role, or session is created here — provisioning stays\n  organizer-only at promote-to-READY.\n- Identity (speaker name + company) is auto-filled from the attendee's profile; the\n  body carries only the proposed talk. Unknown body fields are rejected (400).\n- At most **one** self-nomination per attendee per event — a second attempt returns 409.\n"
      operationId: selfNominateSpeaker
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern57
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SelfNominateSpeakerRequest'
      responses:
        '201':
          description: Self-nomination created (status IDENTIFIED, source self_nomination)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerPoolResponse'
        '400':
          description: Validation error (missing sessionTitle/abstract, or an unknown field)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '401':
          description: Not authenticated (anonymous caller — rejected at the API gateway)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '409':
          description: 'Conflict. Two paths:

            (a) `SELF_NOMINATION_NOT_ALLOWED` — the event''s topic is unset or it is unpublished, or

            (b) `DUPLICATE_SELF_NOMINATION` — this attendee already self-nominated for the event.

            '
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              examples:
                notAllowed:
                  summary: Topic unset / not published
                  value:
                    message: Self-nomination is not open for BATbern57 — the event topic must be set and published first.
                    status: 409
                    error: Conflict
                    details:
                      code: SELF_NOMINATION_NOT_ALLOWED
                duplicate:
                  summary: Already self-nominated
                  value:
                    message: You have already self-nominated for BATbern57.
                    status: 409
                    error: Conflict
                    details:
                      code: DUPLICATE_SELF_NOMINATION
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/pool/{speakerId}:
    patch:
      tags:
      - Event Actions
      summary: Patch a speaker pool entry (organizer assignment / notes)
      description: 'Partial update of a speaker pool entry. Only fields named in

        `PatchSpeakerPoolRequest` may be sent; any other field (notably `email`)

        is rejected with HTTP 400 (`additionalProperties: false` +

        `@JsonIgnoreProperties(ignoreUnknown = false)`).


        **Story**: 11.D.1 (AR23) — `email` is captured exclusively by

        `POST /speakers/{speakerId}/promote`. Sending `email` here is a

        contract error.

        **Authorization**: Requires ORGANIZER role.

        '
      operationId: patchSpeakerPoolEntry
      parameters:
      - name: eventCode
        in: path
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        required: true
        schema:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchSpeakerPoolRequest'
      responses:
        '200':
          description: Speaker pool entry updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerPoolResponse'
        '400':
          description: 'Validation error — typically because the request body contains a

            field outside `PatchSpeakerPoolRequest` (e.g. `email`).

            '
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              examples:
                emailRejected:
                  summary: Email field rejected — use POST /promote instead
                  value:
                    message: Unknown field 'email' is not permitted on this endpoint; use POST /speakers/{speakerId}/promote to attach an email
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/pool/550e8400-e29b-41d4-a716-446655440000
                    status: 400
                    error: Bad Request
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Event or speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
    delete:
      tags:
      - Event Actions
      summary: Delete speaker from event pool
      description: 'Remove a speaker from the event''s speaker pool.


        **Authorization**: Requires ORGANIZER role

        **Rate Limiting**: Applied at API Gateway level


        **Business Rules**:

        - Event must exist

        - Speaker must exist in the pool

        - Can only delete speakers in IDENTIFIED or CONTACTED status


        **Performance**: <150ms (P95)

        '
      operationId: deleteSpeakerFromPool
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '204':
          description: Speaker deleted successfully
        '404':
          description: Event or speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/{speakerId}/promote:
    post:
      tags:
      - Speaker Status
      summary: Promote a CONTACTED speaker to READY (provisions User + SPEAKER role)
      description: "Drives the `CONTACTED → READY` workflow transition for a speaker in the event pool.\n\n**Story**: 11.D.1 — Unified Speaker Workflow Refactor / Phase D\n**ADR**: ADR-009 §0.2 — `CONTACTED → READY` is the User-provisioning gate\n**Authorization**: Requires ORGANIZER role\n**Same-state behaviour**: Re-promoting an already-READY speaker returns\n  `409 Conflict` with `details.code = INVALID_PROMOTION_STATE` and\n  `details.currentState = READY`. Promote is only valid from CONTACTED;\n  subsequent identity changes go through a dedicated organizer action\n  (not this endpoint).\n\n**Side effects on success**:\n  - Workflow state transitions `CONTACTED → READY` via\n    `SpeakerWorkflowService.transition(...)` (the sole status writer).\n  - `UserApiClient.provisionUserWithRole(...)` is called: a User row is created if\n    none exists for the email; SPEAKER role is granted (idempotent).\n  - A `Session` + `PRIMARY_SPEAKER` `session_users` row is provisioned with the\n    canonical username (Story 11.E.8). The legacy `speaker_pool.username` and\n    `speaker_pool.email` columns were dropped in Story 11.E.9 (Flyway V103); the\n    response's `username` / `email` are now overlay-derived via\n    `PrimarySpeakerResolver.applyOverlay(...)`.\n  - `speaker_status_history` row is inserted (previous_status=contacted,\n    new_status=ready, changed_by_username=organizer).\n  - `SpeakerWorkflowStateChangeEvent` and `SpeakerPromotedToReadyEvent` are published.\n\n**Why a dedicated endpoint?** `POST /speakers/pool` rejects `email` (AR23); the\nonly path that attaches an email + provisions an identity is this endpoint.\n`PUT /speakers/{speakerId}/status` with `newStatus=READY` also returns 400 with\ncode `READY_REQUIRES_PROMOTE_ENDPOINT` — promotion MUST go through this path.\n"
      operationId: promoteSpeakerToReady
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PromoteSpeakerRequest'
      responses:
        '200':
          description: 'Speaker promoted to READY. Response is the updated speaker pool entry

            with status=ready, username populated, and email populated.

            '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerPoolResponse'
        '400':
          description: 'Validation error. Triggered by: missing/blank/malformed email, body fields

            beyond the schema (`additionalProperties: false`), or invalid path parameters.

            '
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              examples:
                missingEmail:
                  summary: Email missing
                  value:
                    message: 'email: must not be blank'
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/promote
                    status: 400
                    error: Bad Request
                unknownField:
                  summary: Unknown field rejected
                  value:
                    message: Invalid request format
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/promote
                    status: 400
                    error: Bad Request
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Event or speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '409':
          description: 'Speaker is in a state where promotion is invalid (any state other than

            CONTACTED). Body contains `details.code = "INVALID_PROMOTION_STATE"` and

            `details.currentState = "<state>"` so callers can render a tailored

            message. Includes already-READY (re-promote is not idempotent — see

            Same-state behaviour above).

            '
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              examples:
                alreadyInvited:
                  summary: Speaker already INVITED
                  value:
                    message: speaker is already in INVITED; promote is only valid from CONTACTED
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/promote
                    status: 409
                    error: Conflict
                    details:
                      code: INVALID_PROMOTION_STATE
                      currentState: INVITED
                alreadyReady:
                  summary: Speaker already READY (re-promote rejected)
                  value:
                    message: speaker is already READY; promote is only valid from CONTACTED
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/promote
                    status: 409
                    error: Conflict
                    details:
                      code: INVALID_PROMOTION_STATE
                      currentState: READY
                stillIdentified:
                  summary: Speaker still in IDENTIFIED — log outreach first
                  value:
                    message: speaker must be in CONTACTED before promoting; log outreach first
                    timestamp: '2026-05-16T10:00:00Z'
                    path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/promote
                    status: 409
                    error: Conflict
                    details:
                      code: INVALID_PROMOTION_STATE
                      currentState: IDENTIFIED
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/{speakerId}/status:
    put:
      tags:
      - Speaker Status
      summary: Update speaker status (Story 5.4 / 11.B.3 8-state model)
      description: "Update the workflow status of a speaker. Per ADR-009 §0.7 the legal
        transitions are IDENTIFIED→CONTACTED/DECLINED, CONTACTED→DECLINED (READY needs
        POST /promote), INVITED→ACCEPTED/DECLINED, ACCEPTED→CONTENT_SUBMITTED/DECLINED,
        CONTENT_SUBMITTED→QUALITY_REVIEWED/DECLINED, QUALITY_REVIEWED→DECLINED; DECLINED is
        terminal. newStatus=READY → 400 READY_REQUIRES_PROMOTE_ENDPOINT; a removed legacy
        value → 400 INVALID_SPEAKER_WORKFLOW_STATE."
      operationId: updateSpeakerStatus
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateStatusRequest'
      responses:
        '200':
          description: Status updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerStatusResponse'
        '400':
          description: Invalid status (removed legacy value or READY) or reason too long
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '422':
          description: Illegal state-machine transition or workflow validation failure
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/{speakerId}/status/history:
    get:
      tags:
      - Speaker Status
      summary: Get speaker status history (Story 5.4 AC3-4, AC15)
      description: Retrieve the complete status-change + content-review timeline for a speaker.
      operationId: getSpeakerStatusHistory
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Status history retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/StatusHistoryItem'
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/status-summary:
    get:
      tags:
      - Speaker Status
      summary: Get status dashboard summary (Story 5.4 AC5-6, AC13)
      description: Aggregated speaker status counts, acceptance rate, slot thresholds + overflow.
      operationId: getStatusSummary
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      responses:
        '200':
          description: Status summary retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusSummaryResponse'
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/{speakerId}/content:
    get:
      tags:
      - Speaker Status
      summary: Get speaker content (Story 5.5 AC34)
      description: Retrieve existing presentation content + material info for a speaker.
      operationId: getSpeakerContent
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
      responses:
        '200':
          description: Speaker content retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerContentResponse'
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
    post:
      tags:
      - Speaker Status
      summary: Submit speaker content on behalf (Story 5.5 AC6-10, Story 11.C.2)
      description: "Organizer-on-behalf content submission. Creates/updates the session and
        transitions the speaker to CONTENT_SUBMITTED via the consolidated
        ContentSubmissionService. Unknown body fields are rejected (400)."
      operationId: submitSpeakerContent
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitContentRequest'
      responses:
        '201':
          description: Content submitted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentSubmitResponse'
        '400':
          description: Validation error (e.g. unknown field, title/abstract too long)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '422':
          description: Invalid state — speaker must be in ACCEPTED
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/review-queue:
    get:
      tags:
      - Speaker Status
      summary: Get quality-review queue (Story 5.5 AC11)
      description: "Speakers with CONTENT_SUBMITTED status, oldest submission first. Returns the
        same SpeakerPoolResponse shape as GET /speakers/pool (API-consolidation Phase 7:
        replaced the prior raw-entity return)."
      operationId: getReviewQueue
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      responses:
        '200':
          description: Review queue retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SpeakerPoolResponse'
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/{speakerId}/review:
    post:
      tags:
      - Speaker Status
      summary: Approve or reject speaker content (Story 5.5 AC13-14, AC17)
      description: "APPROVE → status QUALITY_REVIEWED. REJECT → requires feedback, notifies the
        speaker with a link to the Cognito-secured portal for revisions. Content rejection is
        NOT a state transition (ADR-009)."
      operationId: reviewSpeakerContent
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReviewRequest'
      responses:
        '204':
          description: Review action completed successfully
        '400':
          description: Validation error (e.g. feedback required for rejection)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '401':
          description: No authentication token
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '422':
          description: Invalid state — speaker must be in CONTENT_SUBMITTED
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
  /events/{eventCode}/speakers/invite:
    post:
      tags:
      - Speaker Invitation
      summary: Invite a speaker to an event
      description: 'Create a SpeakerPool entry for a speaker and optionally auto-create

        a User account if one doesn''t exist.


        **Story**: 6.1b - Speaker Invitation System

        **Acceptance Criteria**: AC1 (invite), AC2 (auto-create user), AC7 (idempotency)

        **Authorization**: Requires ORGANIZER role


        **Business Rules**:

        - If the speaker email already exists in the pool for this event, returns existing entry (idempotent)

        - Auto-creates a User via UserApiClient if no user exists for the email

        - Speaker starts in IDENTIFIED status

        - Cognito account is NOT created for speakers initially


        **Performance**: <300ms (P95)

        '
      operationId: inviteSpeaker
      parameters:
      - name: eventCode
        in: path
        description: Event code
        required: true
        schema:
          type: string
          example: batbern-2026-spring
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteSpeakerRequest'
      responses:
        '201':
          description: Speaker invited successfully (new entry created)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteSpeakerResponse'
        '200':
          description: Speaker already exists in pool (idempotent return)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InviteSpeakerResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/invite-batch:
    post:
      tags:
      - Speaker Invitation
      summary: Batch invite speakers to an event
      description: 'Invite multiple speakers in a single request with partial failure support.


        **Story**: 6.1b - Speaker Invitation System

        **Acceptance Criteria**: AC5 (batch)

        **Authorization**: Requires ORGANIZER role


        **Business Rules**:

        - Maximum 50 speakers per batch

        - Processing continues even if individual invitations fail

        - Returns 207 Multi-Status when some succeed and some fail


        **Performance**: <2000ms (P95) for 50 speakers

        '
      operationId: inviteSpeakerBatch
      parameters:
      - name: eventCode
        in: path
        description: Event code
        required: true
        schema:
          type: string
          example: batbern-2026-spring
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchInviteRequest'
      responses:
        '200':
          description: All invitations processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchInviteResponse'
        '207':
          description: Partial success - some invitations failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchInviteResponse'
        '400':
          description: Validation error (e.g., empty speakers list)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '404':
          description: Event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/{username}/send-invitation:
    post:
      tags:
      - Speaker Invitation
      summary: Send invitation email to a speaker
      description: "Send or resend a personalized Cognito-flow invitation email containing a login URL\nand (for first-time speakers) a temporary password. Story 11.F.1 retired magic-link\ntokens; speakers now sign in via Cognito and the portal is gated by\n@PreAuthorize(\"hasRole('SPEAKER')\").\n\n**Story**: 6.1b - Speaker Invitation System (updated by 11.E.2 / 11.F.1)\n**Acceptance Criteria**: AC3 (email), AC4 (i18n), AC6 (domain events)\n**Authorization**: Requires ORGANIZER role\n\n**Business Rules**:\n- Speaker must exist in the pool for this event\n- Transitions speaker status to INVITED\n- CUMS sibling endpoint `POST /users/{username}/issue-invitation-credentials`\n  mints a Cognito temporary password when needed (FRESH_TEMP_PASSWORD action),\n  otherwise the email signals the speaker to reuse their existing password.\n- Publishes SpeakerInvitationSentEvent domain event\n- Records outreach history and status transition history\n- Email sent asynchronously; failure does not block the response\n\n**Performance**: <300ms (P95)\n"
      operationId: sendSpeakerInvitation
      parameters:
      - name: eventCode
        in: path
        description: Event code
        required: true
        schema:
          type: string
          example: batbern-2026-spring
      - name: username
        in: path
        description: Speaker's username or UUID (for brainstormed speakers)
        required: true
        schema:
          type: string
          example: john.doe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendInvitationRequest'
      responses:
        '200':
          description: Invitation sent successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendInvitationResponse'
        '400':
          description: Validation error (e.g., missing email, invalid deadlines)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '404':
          description: Event or speaker not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []
  /events/{eventCode}/speakers/{speakerId}/outreach:
    post:
      tags:
      - Speaker Outreach
      summary: Record speaker outreach attempt
      description: 'Record a contact attempt with a potential speaker during outreach phase.


        **Story**: 5.3 - Speaker Outreach Tracking

        **Acceptance Criteria**: AC1-4

        **Authorization**: Requires ORGANIZER role

        **Rate Limiting**: Applied at API Gateway level


        **Business Rules**:

        - Speaker must exist in speaker pool

        - Speaker must be in IDENTIFIED, CONTACTED, or READY state

        - Contact method must be email, phone, or in_person

        - Automatically transitions speaker from IDENTIFIED → CONTACTED on first outreach

        - Subsequent outreach attempts are logged without status change

        - Organizer username is captured from authentication context


        **State Transitions**:

        - IDENTIFIED → CONTACTED (first outreach)

        - CONTACTED → CONTACTED (additional outreach)

        - READY → READY (follow-up outreach)


        **Performance**: <200ms (P95)

        '
      operationId: recordSpeakerOutreach
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordOutreachRequest'
      responses:
        '201':
          description: Outreach attempt recorded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutreachHistoryResponse'
        '400':
          description: Validation error (e.g., invalid contact method)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              example:
                message: 'Invalid contact method: sms. Must be one of: email, phone, in_person'
                timestamp: '2025-12-14T10:00:00Z'
                path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/outreach
                status: 400
                error: Bad Request
        '404':
          description: Speaker or event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              example:
                message: 'Speaker not found: 550e8400-e29b-41d4-a716-446655440000'
                timestamp: '2025-12-14T10:00:00Z'
                path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/outreach
                status: 404
                error: Not Found
        '409':
          description: Invalid state transition (speaker not in valid state for outreach)
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              example:
                message: Cannot record outreach for speaker 550e8400-e29b-41d4-a716-446655440000 in state DECLINED. Speaker must be in IDENTIFIED, CONTACTED, or READY state.
                timestamp: '2025-12-14T10:00:00Z'
                path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/outreach
                status: 409
                error: Conflict
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []
    get:
      tags:
      - Speaker Outreach
      summary: Get speaker outreach history
      description: 'Retrieve all outreach attempts for a speaker, ordered by most recent first.


        **Story**: 5.3 - Speaker Outreach Tracking

        **Acceptance Criteria**: AC6

        **Authorization**: Requires ORGANIZER role

        **Rate Limiting**: Applied at API Gateway level


        **Response**:

        - Returns chronological history of all contact attempts

        - Includes contact method, date, notes, and organizer

        - Ordered by contact date descending (most recent first)


        **Performance**: <150ms (P95)

        '
      operationId: getSpeakerOutreachHistory
      parameters:
      - name: eventCode
        in: path
        description: Event code in format BATbern{number}
        required: true
        schema:
          type: string
          pattern: ^BATbern[0-9]+$
          example: BATbern56
      - name: speakerId
        in: path
        description: UUID of the speaker in the speaker pool
        required: true
        schema:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '200':
          description: Outreach history retrieved successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OutreachHistoryResponse'
        '404':
          description: Speaker or event not found
          content:
            application/json:
              schema:
                $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
              example:
                message: 'Speaker not found: 550e8400-e29b-41d4-a716-446655440000'
                timestamp: '2025-12-14T10:00:00Z'
                path: /api/v1/events/BATbern56/speakers/550e8400-e29b-41d4-a716-446655440000/outreach
                status: 404
                error: Not Found
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
      - BearerAuth: []

  /speaker-portal/dashboard:
    get:
      tags:
        - Speaker Portal Dashboard
      summary: Get the authenticated speaker's dashboard
      description: |
        Aggregate dashboard across every event the authenticated speaker has a pool row in
        (Story 6.4). The one speaker-portal endpoint that does not take an eventCode.

        **Authorization**: SPEAKER role required.
      operationId: getDashboard
      responses:
        '200':
          description: Speaker dashboard summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerDashboardDto'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []

  /speaker-portal/events/{eventCode}/respond:
    post:
      tags:
        - Speaker Portal Response
      summary: Submit a speaker's response to an invitation
      description: |
        The authenticated speaker accepts or declines an invitation for the given event
        (Story 6.2a). DECLINE requires a reason. ACCEPT may carry optional preferences.

        **Authorization**: SPEAKER role required (pool ownership enforced server-side).
      operationId: respond
      parameters:
        - name: eventCode
          in: path
          required: true
          description: Event the speaker is responding to
          schema:
            type: string
            example: 'BATbern58'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpeakerResponseRequest'
      responses:
        '200':
          description: Response processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerResponseResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
      security:
      - BearerAuth: []

  /speaker-portal/events/{eventCode}/content:
    get:
      tags:
        - Speaker Portal Content
      summary: Get the speaker's content-submission state for an event
      description: 'Story 6.3 speaker content self-submission. SPEAKER role (pool ownership enforced).'
      operationId: getContentInfo
      parameters:
        - $ref: '#/components/parameters/SpeakerPortalEventCode'
      responses:
        '200':
          description: Content info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerContentInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
      - BearerAuth: []

  /speaker-portal/events/{eventCode}/content/submit:
    post:
      tags:
        - Speaker Portal Content
      summary: Submit title/abstract/bio (and optional presentation upload id)
      description: 'Story 6.3 / 11.C.2. SPEAKER role (pool ownership enforced).'
      operationId: submitContent
      parameters:
        - $ref: '#/components/parameters/SpeakerPortalEventCode'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContentSubmitRequest'
      responses:
        '201':
          description: Content submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentSubmitResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
      - BearerAuth: []

  /speaker-portal/events/{eventCode}/materials/presigned-url:
    post:
      tags:
        - Speaker Portal Content
      summary: Request a presigned S3 URL for a material upload
      description: 'Story 6.3 material upload. SPEAKER role (pool ownership enforced).'
      operationId: generatePresignedUrl
      parameters:
        - $ref: '#/components/parameters/SpeakerPortalEventCode'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpeakerMaterialUploadRequest'
      responses:
        '200':
          description: Presigned upload URL + metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerMaterialUploadResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
      - BearerAuth: []

  /speaker-portal/events/{eventCode}/materials/confirm:
    post:
      tags:
        - Speaker Portal Content
      summary: Confirm a completed material upload
      description: 'Story 6.3 material confirm (moves the object into place). SPEAKER role.'
      operationId: confirmUpload
      parameters:
        - $ref: '#/components/parameters/SpeakerPortalEventCode'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpeakerMaterialConfirmRequest'
      responses:
        '201':
          description: Material confirmed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeakerMaterialConfirmResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
      - BearerAuth: []

  /events/{eventCode}/speaker-pool/{speakerPoolId}/send-reminder:
    post:
      tags:
        - Speaker Reminders
      summary: Manually send a deadline reminder to a speaker (Story 6.5 AC8)
      description: 'Bypasses dedup but respects the reminders-disabled flag. ORGANIZER role.'
      operationId: sendSpeakerReminder
      parameters:
        - name: eventCode
          in: path
          required: true
          schema:
            type: string
        - name: speakerPoolId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendReminderRequest'
      responses:
        '200':
          description: Reminder sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendReminderResponse'
        '400':
          description: Invalid reminder type or speaker state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendReminderResponse'
        '409':
          description: Reminders disabled for this speaker
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendReminderResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []

  /events/{eventCode}/speaker-pool/{speakerPoolId}/reminders:
    patch:
      tags:
        - Speaker Reminders
      summary: Toggle the reminders-disabled flag for a speaker (Story 6.5 AC6)
      description: 'ORGANIZER role.'
      operationId: updateRemindersDisabled
      parameters:
        - name: eventCode
          in: path
          required: true
          schema:
            type: string
        - name: speakerPoolId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRemindersDisabledRequest'
      responses:
        '200':
          description: Flag updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemindersDisabledResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
      - BearerAuth: []

components:
  parameters:
    SpeakerPortalEventCode:
      name: eventCode
      in: path
      required: true
      description: Event code the speaker-portal action targets
      schema:
        type: string
        example: 'BATbern58'
  schemas:
    ContentSubmitResponse:
      type: object
      description: 'Response after a successful speaker-portal content submission.
        (Copied from the dormant speakers-api per ADR-014 so EMS — which serves the
        speaker-portal content submit — generates the DTO; the speaker-portal submit
        operations are not yet documented as paths here.)'
      required:
        - submissionId
        - version
        - status
        - sessionTitle
      properties:
        submissionId:
          type: string
          format: uuid
          description: The content submission record ID.
        version:
          type: integer
          format: int32
          description: Submission version (1 for first, increments on resubmission).
          example: 1
        status:
          type: string
          description: Submission status — always "SUBMITTED" after a successful submit.
          example: SUBMITTED
        sessionTitle:
          type: string
          description: Session title (for confirmation display).
          example: Zero Trust Security in Enterprise Environments
    SelfNominateSpeakerRequest:
      type: object
      description: 'Story 7.2 "I Could Speak on That": an attendee''s speaker self-nomination body. The

        attendee supplies only the proposed talk — their name + company are auto-filled from

        their user profile server-side, so identity is never re-typed and cannot be spoofed.

        `additionalProperties: false`: an unexpected field (e.g. `speakerName`, `status`) is

        rejected with HTTP 400.

        '
      additionalProperties: false
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = false)'
      required:
      - sessionTitle
      - abstract
      properties:
        sessionTitle:
          type: string
          minLength: 5
          maxLength: 200
          description: The proposed talk title. Max 200 to match the canonical session content title — the pitch is carried verbatim into the session at promote (ADR-012).
          example: Event-driven architecture in practice
        abstract:
          type: string
          minLength: 10
          maxLength: 5000
          description: The proposed talk abstract, stored raw (no agent pre-screen).
          example: A field report on migrating a monolith to an event-driven core, including the dead ends.
    AddSpeakerToPoolRequest:
      type: object
      description: 'Request to add a potential speaker to the event speaker pool during brainstorming phase.

        Story 5.2 - AC9-12: Speaker Pool Management.

        Story 11.D.1 (AR23): `additionalProperties: false` — any client-supplied `email` (or

        other unknown field) is rejected with HTTP 400. Use `POST /speakers/{speakerId}/promote`

        to attach an email when promoting a CONTACTED speaker to READY.

        '
      additionalProperties: false
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = false)'
      required:
      - speakerName
      properties:
        speakerName:
          type: string
          maxLength: 255
          description: Name of the potential speaker (required)
          example: Jane Smith
        company:
          type: string
          maxLength: 255
          description: Company or organization of the speaker (optional)
          example: Tech Corp AG
        expertise:
          type: string
          description: Areas of expertise (optional)
          example: Cloud Architecture, Kubernetes, DevOps
        assignedOrganizerId:
          type: string
          maxLength: 255
          description: Username of organizer assigned for outreach (optional)
          example: alice.mueller
        notes:
          type: string
          description: Free-text notes about the speaker (optional)
          example: Met at KubeCon 2024. Very enthusiastic about BATbern. Follow up next week.
    PatchSpeakerPoolRequest:
      type: object
      description: 'Partial-update body for `PATCH /events/{eventCode}/speakers/pool/{speakerId}`

        (Story 11.D.1 AR23; extended by Epic 11 bug fix 2026-05-18 to support inline

        edits of the brainstorm-level metadata `speakerName` / `company` / `expertise`).

        All fields are optional — only non-null fields are applied to the entry.

        `additionalProperties: false` rejects stale fields, notably `email`, which is

        captured exclusively by `POST /speakers/{speakerId}/promote`.

        '
      additionalProperties: false
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = false)'
      properties:
        speakerName:
          type: string
          minLength: 1
          maxLength: 255
          description: 'Display name on the brainstorm/kanban (nullable not allowed — pass an existing

            value to keep, omit to leave unchanged). For READY+ speakers the canonical name

            lives on the linked User; this field updates the pool entry only.

            '
          example: Jane Smith
        company:
          type: string
          maxLength: 255
          description: Speaker's company affiliation as known at brainstorm time (free text).
          example: ACME GmbH
        expertise:
          type: string
          maxLength: 1000
          description: Comma-separated or free-text expertise tags shown on the kanban card.
          example: Cloud Architecture, Kubernetes
        assignedOrganizerId:
          type: string
          maxLength: 255
          description: Username of organizer assigned for outreach (nullable to clear).
          example: alice.mueller
        notes:
          type: string
          description: Free-text notes about the speaker (nullable to clear).
          example: Followed up via LinkedIn 2026-05-12 — interested but needs date confirmation.
    PromoteSpeakerRequest:
      type: object
      description: 'Request body for `POST /speakers/{speakerId}/promote` (Story 11.D.1; tightened by

        Story 11.E.4 AC4). Drives the `CONTACTED → READY` workflow transition and provisions

        a User + SPEAKER role via `UserApiClient.provisionUserWithRole(...)`.


        `firstName` and `lastName` are **required** (tightened from optional by Story 11.E.4

        AC4, PM decision 2026-05-18). They populate the Cognito user''s `given_name` /

        `family_name` attributes; the previous fallback to splitting `speaker_pool.speakerName`

        (with literal placeholders `"Speaker"` / `"Unknown"` when the name was blank) is

        removed. Callers MUST collect these fields from the organizer.


        `additionalProperties: false`: stale fields from pre-refactor frontends are rejected

        with HTTP 400 rather than silently ignored.

        '
      additionalProperties: false
      required:
      - email
      - firstName
      - lastName
      properties:
        email:
          type: string
          format: email
          maxLength: 320
          description: 'Required. Speaker email — becomes the User lookup key (case-insensitive) in

            CUMS and the Cognito username on first provisioning. Post Story 11.E.9 (V103)

            the email is not duplicated on `speaker_pool`; it lives only in CUMS and is

            resolved via `PrimarySpeakerResolver.resolveEmail(pool)` at read time.

            '
          example: jane.smith@example.com
        firstName:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^.*\S.*$
          description: 'Required (Story 11.E.4 AC4). Populates the Cognito user''s `given_name` attribute

            and the User''s `first_name` column. Must contain at least one non-whitespace

            character — blank / whitespace-only values are rejected by both the spec

            `pattern` and server-side `@NotBlank` validation → HTTP 400.

            '
          example: Jane
        lastName:
          type: string
          minLength: 1
          maxLength: 100
          pattern: ^.*\S.*$
          description: 'Required (Story 11.E.4 AC4). Populates the Cognito user''s `family_name` attribute

            and the User''s `last_name` column. Must contain at least one non-whitespace

            character — blank / whitespace-only values are rejected by both the spec

            `pattern` and server-side `@NotBlank` validation → HTTP 400.

            '
          example: Smith
    SpeakerWorkflowState:
      type: string
      description: 'The 8 ADR-009 §0.1 speaker workflow states, serialized as the UPPER_CASE
        enum name (the legacy SLOT_ASSIGNED/CONFIRMED/WITHDREW/OVERFLOW were removed per §0.7).'
      enum:
      - IDENTIFIED
      - CONTACTED
      - READY
      - INVITED
      - ACCEPTED
      - CONTENT_SUBMITTED
      - QUALITY_REVIEWED
      - DECLINED
      example: IDENTIFIED
    UpdateStatusRequest:
      type: object
      description: Request to update speaker status (Story 5.4 AC1-4).
      required:
      - newStatus
      properties:
        newStatus:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        reason:
          type: string
          maxLength: 2000
          description: Optional reason for the status change (max 2000 chars).
          example: Speaker confirmed availability via email
    SpeakerStatusResponse:
      type: object
      description: Response after a speaker status update (Story 5.4).
      properties:
        speakerId:
          type: string
          format: uuid
          description: Speaker pool ID
        eventCode:
          type: string
          description: Event code (ADR-003)
          example: BATbern56
        currentStatus:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        previousStatus:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        changedByUsername:
          type: string
          description: Organizer who made the change
          example: john.doe
        changeReason:
          type: string
          description: Reason for the change
        changedAt:
          type: string
          format: date-time
          description: When the change occurred
        isSlotAssigned:
          type: boolean
          description: 'Derived (ADR-009 §0.1): true when the speaker has a sessionId assigned.'
        isPublishable:
          type: boolean
          description: 'Derived: currentStatus == QUALITY_REVIEWED AND isSlotAssigned.'
    StatusHistoryItem:
      type: object
      description: 'Single timeline entry — a workflow state change (speaker_status_history) or a
        synthesised content-rejection event (session_content_history with reviewer feedback).'
      properties:
        id:
          type: string
          format: uuid
          description: History record ID
        kind:
          type: string
          enum:
          - STATUS_CHANGE
          - CONTENT_REJECTED
          default: STATUS_CHANGE
          description: 'STATUS_CHANGE = speaker_status_history row; CONTENT_REJECTED = a
            content-rejection event (not a state transition, surfaced for the History tab).'
        previousStatus:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        newStatus:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        changedByUsername:
          type: string
          example: john.doe
        changeReason:
          type: string
        changedAt:
          type: string
          format: date-time
    StatusSummaryResponse:
      type: object
      description: Status dashboard summary (Story 5.4 AC5-6, AC13).
      properties:
        eventCode:
          type: string
          example: BATbern56
        statusCounts:
          type: object
          description: Count of speakers per workflow state (keys are SpeakerWorkflowState names).
          additionalProperties:
            type: integer
            format: int64
          example:
            IDENTIFIED: 3
            CONTACTED: 5
            ACCEPTED: 6
        totalSpeakers:
          type: integer
          format: int64
        acceptedCount:
          type: integer
          format: int64
        declinedCount:
          type: integer
          format: int64
        pendingCount:
          type: integer
          format: int64
        acceptanceRate:
          type: number
          format: double
        minSlotsRequired:
          type: integer
        maxSlotsAllowed:
          type: integer
        thresholdMet:
          type: boolean
        overflowDetected:
          type: boolean
    SubmitContentRequest:
      type: object
      description: 'Organizer-on-behalf content submission (Story 5.5 / 11.C.2). Unknown body
        fields are rejected (400).'
      additionalProperties: false
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = false)'
      required:
      - presentationTitle
      - presentationAbstract
      properties:
        presentationTitle:
          type: string
          maxLength: 200
          description: Presentation title (becomes session.title).
          example: Zero Trust Security in Enterprise Environments
        presentationAbstract:
          type: string
          maxLength: 1000
          description: Presentation abstract (becomes session.description).
          example: A field report on implementing Zero Trust in large enterprises.
        bio:
          type: string
          maxLength: 5000
          description: Optional speaker bio, patched onto the User profile.
        profilePictureUrl:
          type: string
          maxLength: 2048
          description: Optional speaker portrait URL, patched onto the User profile.
          example: https://cdn.batbern.ch/users/jane.smith.jpg
        presentationUploadId:
          type: string
          description: Optional upload ID from a presigned-URL upload, linked to the session.
          example: upload-abc-123
    SpeakerContentResponse:
      type: object
      description: Response for speaker content get/submit operations (Story 5.5).
      properties:
        speakerPoolId:
          type: string
          format: uuid
        eventId:
          type: string
          format: uuid
        sessionId:
          type: string
          format: uuid
        presentationTitle:
          type: string
        presentationAbstract:
          type: string
        username:
          type: string
          example: john.doe
        speakerName:
          type: string
          example: John Doe
        company:
          type: string
        status:
          $ref: '#/components/schemas/SpeakerWorkflowState'
        hasContent:
          type: boolean
          description: False if the session was deleted (AC34).
        warning:
          type: string
          description: Warning message if issues detected (AC34).
        submittedAt:
          type: string
          format: date-time
        hasMaterial:
          type: boolean
        materialUrl:
          type: string
          description: Material download URL (CloudFront CDN).
          example: https://cdn.batbern.ch/materials/presentation.pdf
        materialFileName:
          type: string
          example: zero-trust-presentation.pdf
    ReviewAction:
      type: string
      enum:
      - APPROVE
      - REJECT
      description: 'Quality-review action: APPROVE → QUALITY_REVIEWED; REJECT → requires feedback.'
    ReviewRequest:
      type: object
      description: Request for a quality-review action (Story 5.5 AC13-14).
      required:
      - action
      properties:
        action:
          $ref: '#/components/schemas/ReviewAction'
        feedback:
          type: string
          maxLength: 2000
          description: Feedback for rejection (required when action is REJECT).
    SpeakerPoolResponse:
      type: object
      description: 'Response representing a speaker pool entry.

        Story 5.2 - AC9-13: Speaker Pool Management

        '
      required:
      - id
      - eventId
      - speakerName
      - status
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the speaker pool entry
          example: 123e4567-e89b-12d3-a456-426614174000
        eventId:
          type: string
          format: uuid
          description: UUID of the event this speaker is associated with
          example: 789e4567-e89b-12d3-a456-426614174111
        speakerName:
          type: string
          description: Name of the potential speaker
          example: Jane Smith
        company:
          type: string
          nullable: true
          description: Company identifier/slug of the speaker (from User.companyId). Stable key — NOT for display.
          example: TechCorpAG
        companyDisplayName:
          type: string
          nullable: true
          description: 'Human-readable company name (companies.display_name, falling back to

            companies.name, then the slug). Prefer this over `company` for display.

            '
          example: Tech Corp AG
        expertise:
          type: string
          nullable: true
          description: Areas of expertise
          example: Cloud Architecture, Kubernetes, DevOps
        assignedOrganizerId:
          type: string
          nullable: true
          description: Username of organizer assigned for outreach
          example: alice.mueller
        status:
          type: string
          enum:
          - IDENTIFIED
          - CONTACTED
          - READY
          - INVITED
          - ACCEPTED
          - CONTENT_SUBMITTED
          - QUALITY_REVIEWED
          - DECLINED
          description: 'Current status of the speaker in the pool workflow — the 8 ADR-009 §0.1
            states, serialized as the UPPER_CASE SpeakerWorkflowState enum name (legacy
            SLOT_ASSIGNED/CONFIRMED/WITHDREW/OVERFLOW were removed per ADR-009 §0.7; slot
            assignment is now the derived isSlotAssigned flag).

            Story 5.2 - AC13: Initial status = ''IDENTIFIED''

            '
          example: IDENTIFIED
        notes:
          type: string
          nullable: true
          description: Free-text notes about the speaker
          example: Met at KubeCon 2024. Very enthusiastic about BATbern.
        source:
          type: string
          enum:
          - organizer_added
          - self_nomination
          description: 'Story 7.2 — provenance of the pool row. `organizer_added` for organizer-sourced

            candidates (the default), `self_nomination` for attendee "I Could Speak on That"

            entries. Lets the organizer pool/brainstorming UI flag self-nominations.

            '
          example: self_nomination
        proposedByUsername:
          type: string
          nullable: true
          description: Story 7.2 — username of the self-nominating attendee (null for organizer-added rows).
          example: jane.attendee
        proposedSessionTitle:
          type: string
          nullable: true
          description: Story 7.2 — the talk title the attendee proposed (null for organizer-added rows).
          example: Event-driven architecture in practice
        proposedAbstract:
          type: string
          nullable: true
          description: Story 7.2 — the talk abstract the attendee proposed, stored raw (null for organizer-added rows).
          example: A field report on migrating a monolith to an event-driven core.
        createdAt:
          type: string
          format: date-time
          description: Timestamp when speaker was added to pool
          example: 2025-12-13 10:00:00+00:00
        updatedAt:
          type: string
          format: date-time
          description: Timestamp of last update
          example: 2025-12-13 10:15:00+00:00
        sessionId:
          type: string
          format: uuid
          nullable: true
          description: Session UUID — set once a session is assigned to the speaker (Story 5.5).
          example: 456e4567-e89b-12d3-a456-426614174222
        sessionSlug:
          type: string
          nullable: true
          description: 'Addressable session slug, surfaced alongside sessionId so the organizer
            drawer''s content tab can PATCH /events/{code}/sessions/{slug} directly (2026-05-20).'
          example: event-driven-architecture
        username:
          type: string
          nullable: true
          description: 'Canonical primary-speaker username, resolved from session_users →
            CUMS at read time (Story 11.E.9). Null for pre-READY pool rows (no User yet).'
          example: jane.smith
        email:
          type: string
          nullable: true
          description: 'Primary-speaker email, resolved live from CUMS (Story 6.1c / 11.E.9).
            Null for pre-READY rows or when CUMS is degraded.'
          example: jane.smith@example.com
        invitedAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp the invitation was sent (Story 6.1b).
          example: 2025-12-14 09:00:00+00:00
        responseDeadline:
          type: string
          format: date
          nullable: true
          description: Date by which the speaker must respond to the invitation (Story 6.1b).
          example: 2025-12-21
        contentDeadline:
          type: string
          format: date
          nullable: true
          description: Date by which the speaker must submit content (Story 6.1b).
          example: 2026-01-15
        acceptedAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp the speaker accepted the invitation (Story 6.2a).
          example: 2025-12-16 11:30:00+00:00
        declinedAt:
          type: string
          format: date-time
          nullable: true
          description: Timestamp the speaker declined (Story 6.2a).
          example: 2025-12-16 11:30:00+00:00
        declineReason:
          type: string
          nullable: true
          description: Free-text reason given when the speaker declined (Story 6.2a).
          example: Scheduling conflict with another conference.
        preferredTimeSlot:
          type: string
          nullable: true
          description: Speaker-stated time-slot preference (Story 6.2a).
          example: Morning
        travelRequirements:
          type: string
          nullable: true
          description: Speaker-stated travel requirements (Story 6.2a).
          example: Needs hotel booking for the night before.
        technicalRequirements:
          type: string
          nullable: true
          description: Speaker-stated technical/AV requirements (Story 6.2a).
          example: HDMI adapter for MacBook; clicker.
        initialPresentationTitle:
          type: string
          nullable: true
          description: 'Initial working title captured at response time (Story 6.2a).
            sessions.title is canonical post-content; this stays null after Story 11.E.8.'
          example: Scaling event-driven systems
        preferenceComments:
          type: string
          nullable: true
          description: Free-text comments accompanying the speaker''s preferences (Story 6.2a).
          example: Happy to do a workshop instead of a talk if useful.
        contentSubmittedAt:
          type: string
          format: date-time
          nullable: true
          description: 'Timestamp of the latest content submission, derived from
            session_content_history at read time (Story 6.3 / 11.E.8).'
          example: 2026-01-10 14:00:00+00:00
        submittedTitle:
          type: string
          nullable: true
          description: 'Current presentation title (mirrors sessions.title, the canonical
            source per Story 11.E.8 §2.9).'
          example: Event-driven architecture in practice
        submittedAbstract:
          type: string
          nullable: true
          description: 'Current presentation abstract (mirrors sessions.description, canonical
            per Story 11.E.8 §2.9).'
          example: A field report on migrating a monolith to an event-driven core.
        isSlotAssigned:
          type: boolean
          nullable: true
          description: 'Derived flag (ADR-009 §0.1, computed at read time — NOT persisted):
            true when the speaker''s session has a start_time set.'
          example: true
    RecordOutreachRequest:
      type: object
      description: 'Request to record a speaker outreach attempt.

        Story 5.3 - AC1-4: Speaker Outreach Tracking

        '
      required:
      - contactDate
      - contactMethod
      properties:
        contactDate:
          type: string
          format: date-time
          description: Date and time when contact was made
          example: 2025-12-14 14:30:00+00:00
        contactMethod:
          type: string
          enum:
          - email
          - phone
          - in_person
          description: 'Method of contact used to reach the speaker.

            Valid values: email, phone, in_person

            '
          example: email
        notes:
          type: string
          maxLength: 5000
          description: 'Free-text notes about the conversation.

            Organizers record speaker response, next steps, availability, etc.

            '
          example: 'Spoke with Jane about presenting on Kubernetes security.

            She''s interested but needs to check her calendar.

            Follow up next Tuesday.

            '
    OutreachHistoryResponse:
      type: object
      description: 'Response representing a speaker outreach attempt.

        Story 5.3 - AC5-6: Speaker Outreach History

        '
      required:
      - id
      - speakerPoolId
      - contactDate
      - contactMethod
      - organizerUsername
      - createdAt
      - updatedAt
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this outreach record
          example: 550e8400-e29b-41d4-a716-446655440000
        speakerPoolId:
          type: string
          format: uuid
          description: UUID of the speaker in the speaker pool
          example: 123e4567-e89b-12d3-a456-426614174000
        contactDate:
          type: string
          format: date-time
          description: Date and time when contact was made
          example: 2025-12-14 14:30:00+00:00
        contactMethod:
          type: string
          enum:
          - email
          - phone
          - in_person
          description: Method of contact used
          example: email
        notes:
          type: string
          nullable: true
          description: Notes about the conversation
          example: 'Spoke with Jane about presenting on Kubernetes security.

            She''s interested but needs to check her calendar.

            Follow up next Tuesday.

            '
        organizerUsername:
          type: string
          description: Username of the organizer who made the contact
          example: alice.mueller
        createdAt:
          type: string
          format: date-time
          description: Timestamp when outreach record was created
          example: 2025-12-14 14:35:00+00:00
        updatedAt:
          type: string
          format: date-time
          description: Timestamp of last update
          example: 2025-12-14 14:35:00+00:00
    InviteSpeakerRequest:
      type: object
      description: 'Request to invite a speaker to an event.

        Story 6.1b - AC1: Speaker Invitation

        '
      required:
      - email
      properties:
        email:
          type: string
          format: email
          description: Speaker's email address
          example: jane.smith@example.com
        firstName:
          type: string
          nullable: true
          description: Speaker's first name (used for user creation if not found)
          example: Jane
        lastName:
          type: string
          nullable: true
          description: Speaker's last name (used for user creation if not found)
          example: Smith
        company:
          type: string
          nullable: true
          description: Speaker's company name
          example: Tech Corp AG
        sessionId:
          type: string
          format: uuid
          nullable: true
          description: Session to assign speaker to (optional)
          example: 550e8400-e29b-41d4-a716-446655440000
        notes:
          type: string
          nullable: true
          description: Notes about the speaker
          example: Expert in cloud architecture
    InviteSpeakerResponse:
      type: object
      description: 'Response for a speaker invitation.

        Story 6.1b - AC1, AC7: Speaker Invitation with idempotency

        '
      required:
      - speakerPoolId
      - email
      - speakerName
      - status
      - created
      - createdAt
      properties:
        speakerPoolId:
          type: string
          format: uuid
          description: UUID of the SpeakerPool entry
          example: 123e4567-e89b-12d3-a456-426614174000
        username:
          type: string
          nullable: true
          description: Speaker's username
          example: jane.smith
        email:
          type: string
          format: email
          description: Speaker's email address
          example: jane.smith@example.com
        speakerName:
          type: string
          description: Display name of the speaker
          example: Jane Smith
        status:
          type: string
          enum:
          - IDENTIFIED
          - CONTACTED
          - READY
          - INVITED
          - ACCEPTED
          - CONTENT_SUBMITTED
          - QUALITY_REVIEWED
          - DECLINED
          description: 'Current workflow status — the 8 ADR-009 §0.1 states, serialized as the
            UPPER_CASE SpeakerWorkflowState enum name.'
          example: IDENTIFIED
        created:
          type: boolean
          description: True if newly created, false if existing (idempotent return)
          example: true
        userCreated:
          type: boolean
          description: True if a new User account was created
          example: true
        createdAt:
          type: string
          format: date-time
          description: When the SpeakerPool entry was created
          example: 2026-01-15 10:00:00+00:00
    BatchInviteRequest:
      type: object
      description: 'Request to invite multiple speakers in a batch.

        Story 6.1b - AC5: Batch Invitation

        '
      required:
      - speakers
      properties:
        speakers:
          type: array
          minItems: 1
          maxItems: 50
          description: List of speakers to invite (1-50 per batch)
          items:
            $ref: '#/components/schemas/InviteSpeakerRequest'
    BatchInviteResponse:
      type: object
      description: 'Response for a batch speaker invitation with partial failure support.

        Story 6.1b - AC5: Batch Invitation

        '
      required:
      - totalRequested
      - successCount
      - failedCount
      - results
      - errors
      properties:
        totalRequested:
          type: integer
          description: Total number of speakers requested
          example: 5
        successCount:
          type: integer
          description: Number of speakers successfully invited
          example: 4
        failedCount:
          type: integer
          description: Number of failed invitations
          example: 1
        results:
          type: array
          description: Individual results for successful invitations
          items:
            $ref: '#/components/schemas/InviteSpeakerResponse'
        errors:
          type: array
          description: Details of failed invitations
          items:
            type: object
            required:
            - email
            - errorCode
            - errorMessage
            properties:
              email:
                type: string
                format: email
                description: Email of the speaker that failed
                example: invalid@broken.test
              errorCode:
                type: string
                description: Error classification code
                enum:
                - INVALID_REQUEST
                - EVENT_NOT_FOUND
                - USER_SERVICE_ERROR
                - INTERNAL_ERROR
                example: USER_SERVICE_ERROR
              errorMessage:
                type: string
                description: Human-readable error message
                example: Failed to create user account
    SendInvitationRequest:
      type: object
      description: "Request to send/resend an invitation email to a speaker.\nStory 6.1b - AC3 (updated by 11.E.2 / 11.F.1): Invitation Email\n\nThe email includes:\n- Cognito login URL + temporary password (FRESH_TEMP_PASSWORD)\n  OR existing-password reminder (USE_EXISTING_PASSWORD)\n- Event details, deadlines, and session info\n"
      required:
      - responseDeadline
      properties:
        responseDeadline:
          type: string
          format: date
          description: Deadline for speaker to respond (must be in the future)
          example: '2026-02-28'
        contentDeadline:
          type: string
          format: date
          nullable: true
          description: Deadline for speaker to submit content (must be after responseDeadline)
          example: '2026-03-10'
        locale:
          type: string
          nullable: true
          description: 'Preferred language for the email (defaults to ''de''). Email templates
            exist for de + en; any other value falls back to English at render time (free
            string, not enum-constrained, to preserve the deployed pass-through behaviour).'
          example: de
        email:
          type: string
          format: email
          nullable: true
          description: Override email address (for speakers without email in database)
          example: jane.smith@example.com
    SendInvitationResponse:
      type: object
      description: 'Response after sending an invitation email.

        Story 6.1b - AC3: Invitation Email

        '
      required:
      - speakerPoolId
      - status
      - invitedAt
      - responseDeadline
      properties:
        speakerPoolId:
          type: string
          format: uuid
          description: UUID of the SpeakerPool entry
          example: 123e4567-e89b-12d3-a456-426614174000
        username:
          type: string
          nullable: true
          description: Speaker's username
          example: jane.smith
        email:
          type: string
          format: email
          description: Speaker's email address
          example: jane.smith@example.com
        status:
          type: string
          enum:
          - INVITED
          description: Updated status — always INVITED after sending (UPPER_CASE
            SpeakerWorkflowState enum name).
          example: INVITED
        invitedAt:
          type: string
          format: date-time
          description: When the invitation was sent
          example: 2026-01-15 10:00:00+00:00
        responseDeadline:
          type: string
          format: date
          description: Deadline for speaker to respond
          example: '2026-02-28'
        contentDeadline:
          type: string
          format: date
          nullable: true
          description: Deadline for content submission
          example: '2026-03-10'
    SpeakerDashboardDto:
      type: object
      description: Speaker portal dashboard summary across all of the speaker's events (Story 6.4)
      properties:
        speakerName:
          type: string
        profilePictureUrl:
          type: string
        profileCompleteness:
          type: integer
          description: Profile completeness percentage (0–100)
        upcomingEvents:
          type: array
          items:
            $ref: '#/components/schemas/DashboardUpcomingEventDto'
        pastEvents:
          type: array
          items:
            $ref: '#/components/schemas/DashboardPastEventDto'
    DashboardUpcomingEventDto:
      type: object
      description: An upcoming event the speaker is involved in
      properties:
        eventCode:
          type: string
        eventTitle:
          type: string
        eventDate:
          type: string
        eventLocation:
          type: string
        sessionTitle:
          type: string
        workflowState:
          type: string
        workflowStateLabel:
          type: string
        hasTitle:
          type: boolean
        hasAbstract:
          type: boolean
        hasMaterial:
          type: boolean
        materialFileName:
          type: string
        responseDeadline:
          type: string
        contentDeadline:
          type: string
        reviewerFeedback:
          type: string
        organizerName:
          type: string
        organizerEmail:
          type: string
        respondUrl:
          type: string
        contentUrl:
          type: string
    DashboardPastEventDto:
      type: object
      description: A past event the speaker presented at
      properties:
        eventCode:
          type: string
        eventTitle:
          type: string
        eventDate:
          type: string
        sessionTitle:
          type: string
        hasMaterial:
          type: boolean
        materialFileName:
          type: string
    SpeakerResponseType:
      type: string
      description: Speaker's response to an invitation
      enum:
        - ACCEPT
        - DECLINE
    SpeakerResponsePreferences:
      type: object
      description: Optional logistics preferences captured on ACCEPT (Story 6.2a)
      properties:
        timeSlot:
          type: string
          description: 'Preferred presentation time slot: morning, afternoon, no_preference'
        travelRequirements:
          type: string
          description: 'Travel requirements: local, accommodation, virtual'
        technicalRequirements:
          type: array
          items:
            type: string
          description: 'Technical requirements: mac_adapter, remote_option, special_av'
        initialTitle:
          type: string
          description: Preliminary presentation title
        comments:
          type: string
          description: Additional comments for the organizer
    SpeakerResponseRequest:
      type: object
      description: A speaker's accept/decline response to an invitation (Story 6.2a)
      required:
        - response
      properties:
        response:
          $ref: '#/components/schemas/SpeakerResponseType'
        reason:
          type: string
          description: Reason for decline (required for DECLINE)
        constraints:
          type: string
          description: Optional constraints or notes (e.g. travel, schedule)
        preferences:
          $ref: '#/components/schemas/SpeakerResponsePreferences'
    SpeakerResponseResult:
      type: object
      description: Outcome of processing a speaker's invitation response (Story 6.2a)
      properties:
        success:
          type: boolean
        speakerName:
          type: string
        eventName:
          type: string
        eventDate:
          type: string
          description: Formatted event date (e.g. "20. November 2025")
        sessionTitle:
          type: string
        nextSteps:
          type: array
          items:
            type: string
          description: Next steps for the speaker after responding
        contentDeadline:
          type: string
          format: date
          description: Content submission deadline (if accepted)
        dashboardUrl:
          type: string
        errorMessage:
          type: string
    SpeakerContentInfo:
      type: object
      description: The speaker portal's content-submission state for one event (Story 6.3)
      properties:
        speakerName:
          type: string
        eventCode:
          type: string
        eventTitle:
          type: string
        hasSessionAssigned:
          type: boolean
        sessionTitle:
          type: string
        canSubmitContent:
          type: boolean
        hasDraft:
          type: boolean
        draftTitle:
          type: string
        draftAbstract:
          type: string
        draftVersion:
          type: integer
        lastSavedAt:
          type: string
          format: date-time
        needsRevision:
          type: boolean
        reviewerFeedback:
          type: string
        reviewedAt:
          type: string
          format: date-time
        reviewedBy:
          type: string
        hasMaterial:
          type: boolean
        materialUrl:
          type: string
        materialFileName:
          type: string
    ContentSubmitRequest:
      type: object
      description: Speaker content submission (title/abstract/bio + optional presentation upload)
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = false)'
      required:
        - title
        - contentAbstract
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        contentAbstract:
          type: string
          minLength: 1
          maxLength: 1000
        bio:
          type: string
          maxLength: 5000
        profilePictureUrl:
          type: string
          maxLength: 2048
        presentationUploadId:
          type: string
          maxLength: 200
    SpeakerMaterialUploadRequest:
      type: object
      description: Request a presigned URL for a material upload
      required:
        - fileName
        - fileSize
        - mimeType
      properties:
        fileName:
          type: string
        fileSize:
          type: integer
          format: int64
        mimeType:
          type: string
    SpeakerMaterialUploadResponse:
      type: object
      description: Presigned upload URL + metadata
      properties:
        uploadUrl:
          type: string
        uploadId:
          type: string
        s3Key:
          type: string
        fileExtension:
          type: string
        expiresInMinutes:
          type: integer
        requiredHeaders:
          type: object
          additionalProperties:
            type: string
          description: Headers the client must send on the presigned PUT
    SpeakerMaterialConfirmRequest:
      type: object
      description: Confirm a completed material upload
      required:
        - uploadId
        - fileName
        - fileExtension
        - fileSize
        - mimeType
      properties:
        uploadId:
          type: string
        fileName:
          type: string
        fileExtension:
          type: string
        fileSize:
          type: integer
          format: int64
        mimeType:
          type: string
        materialType:
          type: string
          description: Defaults to PRESENTATION when omitted
    SpeakerMaterialConfirmResponse:
      type: object
      description: Result of confirming a material upload
      properties:
        materialId:
          type: string
          format: uuid
        uploadId:
          type: string
        fileName:
          type: string
        cloudFrontUrl:
          type: string
        materialType:
          type: string
        uploadedAt:
          type: string
          format: date-time
    SendReminderRequest:
      type: object
      description: Manual speaker reminder request (Story 6.5)
      properties:
        reminderType:
          type: string
          description: 'RESPONSE or CONTENT'
        tier:
          type: string
          description: 'Optional: TIER_1/TIER_2/TIER_3 (auto-detected if omitted)'
    SendReminderResponse:
      type: object
      description: Manual reminder result; error fields populated on the 400/409 paths
      x-class-extra-annotation: '@com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)'
      properties:
        message:
          type: string
        tier:
          type: string
        emailAddress:
          type: string
        error:
          type: string
    UpdateRemindersDisabledRequest:
      type: object
      required:
        - remindersDisabled
      properties:
        remindersDisabled:
          type: boolean
    RemindersDisabledResponse:
      type: object
      properties:
        speakerPoolId:
          type: string
        remindersDisabled:
          type: boolean
  responses:
    Conflict:
      description: Conflict - the speaker has already responded
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
    BadRequest:
      description: Bad request - validation error
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            validationError:
              value:
                error: VALIDATION_ERROR
                errorCode: BAD_REQUEST
                message: Invalid filter syntax
                timestamp: 2025-01-15 10:30:00+00:00
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            eventNotFound:
              value:
                error: RESOURCE_NOT_FOUND
                errorCode: NOT_FOUND
                message: Event not found with id evt-999
                timestamp: 2025-01-15 10:30:00+00:00
    UnprocessableEntity:
      description: Request cannot be processed due to business logic
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            workflowError:
              value:
                error: WORKFLOW_ERROR
                errorCode: UNPROCESSABLE_ENTITY
                message: Cannot advance workflow for archived event
                timestamp: 2025-01-15 10:30:00+00:00
            validationError:
              value:
                error: VALIDATION_ERROR
                errorCode: UNPROCESSABLE_ENTITY
                message: Event must have a date before publishing
                timestamp: 2025-01-15 10:30:00+00:00
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            serverError:
              value:
                error: INTERNAL_SERVER_ERROR
                errorCode: INTERNAL_ERROR
                message: An unexpected error occurred
                timestamp: 2025-01-15 10:30:00+00:00
    Forbidden:
      description: Forbidden - insufficient permissions
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            forbiddenError:
              value:
                error: FORBIDDEN
                errorCode: FORBIDDEN
                message: Insufficient permissions to perform this action
                timestamp: '2025-01-15T10:30:00Z'
    Unauthorized:
      description: Unauthorized - missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: ./_shared.openapi.yml#/components/schemas/ErrorResponse
          examples:
            missingToken:
              value:
                error: UNAUTHORIZED
                errorCode: UNAUTHORIZED
                message: Authentication token is missing or invalid
                timestamp: '2025-01-15T10:30:00Z'
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: AWS Cognito JWT token
