openapi: 3.0.3
info:
  title: HNS Ticketing System API
  description: |
    REST API specification for the Croatian Football Federation (HNS) Ticketing System.

    ## Authentication
    All authenticated endpoints require a Bearer token in the Authorization header.
    Tokens are obtained via the Drupal SSO integration.

    ## Base URLs
    - Local: `http://localhost:8000/api/v1`
    - Production: `https://api.ulaznice.hns.family/api/v1`
    - Staging: `https://api.staging.ulaznice.hns.family/api/v1`

    ## Rate Limiting
    - Standard endpoints: 100 requests/minute per user
    - Queue endpoints: 10 requests/minute per user
    - Admin endpoints: 1000 requests/minute per admin

    ## Error Responses
    All errors follow RFC 7807 Problem Details format.
  version: 1.0.0
  contact:
    name: HNS Development Team
    email: hns@websolutions.hr
  license:
    name: Proprietary

servers:
  - url: http://localhost:8000/api/v1
    description: Local Development
  - url: https://api.ulaznice.hns.family/api/v1
    description: Production
  - url: https://api.staging.ulaznice.hns.family/api/v1
    description: Staging

tags:
  - name: Authentication
    description: User authentication and session management (E1)
  - name: Profile
    description: User profile and saved profiles management (E1)
  - name: Matches
    description: Match listing and details (E2, E4)
  - name: Purchase
    description: Ticket purchase flow (E4)
  - name: Queue
    description: Waiting queue management (E5)
  - name: Loyalty
    description: Loyalty points and early access (E6)
  - name: Tickets
    description: Ticket management and delivery (E9)
  - name: Quota Portal
    description: Quota holder operations (E7)
  - name: Admin - Matches
    description: Admin match management (E2)
  - name: Admin - Stadiums
    description: Stadium template and seat map configuration (E2-F3, E2-F10)
  - name: Admin - Inventory
    description: Admin seat inventory operations (E3)
  - name: Admin - Quotas
    description: Admin quota management (E7)
  - name: Admin - Blacklist
    description: Admin blacklist operations (E11)
  - name: Admin - Support
    description: Customer support operations (E10)
  - name: Admin - Reports
    description: Reporting and analytics (E14)
  - name: Petrol
    description: Petrol station sales (E12)
  - name: Webhooks
    description: Inbound webhooks from external services (E8)

paths:
  # ============================================
  # E1: USER & PROFILE MANAGEMENT
  # ============================================

  /auth/login:
    post:
      tags: [Authentication]
      summary: Authenticate user via Drupal SSO
      description: |
        Exchanges Drupal SSO token for ticketing API access token.
        The Drupal token is obtained from the mobile app authentication flow.
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [drupal_token]
              properties:
                drupal_token:
                  type: string
                  description: SSO token from Drupal authentication
                device_id:
                  type: string
                  description: Unique device identifier for push notifications
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'

  /auth/refresh:
    post:
      tags: [Authentication]
      summary: Refresh access token
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token:
                  type: string
      responses:
        '200':
          description: Token refreshed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/logout:
    post:
      tags: [Authentication]
      summary: Logout and invalidate session
      operationId: logout
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Logged out successfully
        '401':
          $ref: '#/components/responses/Unauthorized'

  /users/me/profile:
    get:
      tags: [Profile]
      summary: Get current user's ticketing profile
      description: |
        Returns the user's ticketing profile if it exists.
        Returns 404 if the user hasn't created their ticketing profile yet.

        Some fields may be pre-populated from Drupal profile data, but
        the ticketing profile must be explicitly created via POST.
      operationId: getProfile
      security:
        - bearerAuth: []
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Profile not set up yet - user must create profile via POST

    post:
      tags: [Profile]
      summary: Create initial ticketing profile
      description: |
        Creates the user's initial ticketing profile. This is required before
        the user can purchase tickets.

        **Pre-populated data from Drupal:**
        The mobile app may pre-fill some fields from the Drupal profile, but
        the user must confirm/complete the data and submit via this endpoint.

        **OIB Validation:**
        - OIB is validated using ISO 7064 MOD 11-10 checksum
        - OIB must be globally unique across all users
        - Once set, OIB cannot be changed (requires support intervention)

        **Age Requirement:**
        - Primary buyer must be 18+ years old (calculated from date_of_birth)

        **Identity Requirements:**
        - Croatian citizens must provide OIB
        - Non-Croatian citizens must provide passport_number
      operationId: createProfile
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserProfileCreate'
      responses:
        '201':
          description: Profile created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: |
            Conflict - possible reasons:
            - Profile already exists (use PUT to update)
            - OIB already registered to another user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

    put:
      tags: [Profile]
      summary: Update user's ticketing profile
      description: |
        Updates the user's existing ticketing profile.

        **Restrictions:**
        - OIB cannot be changed once set (requires support intervention)
        - Profile must exist (use POST to create initially)

        **OIB Validation:**
        OIB is validated using ISO 7064 MOD 11-10 checksum.
      operationId: updateProfile
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserProfileUpdate'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Profile not found - use POST to create initially
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /users/me/saved-profiles:
    get:
      tags: [Profile]
      summary: List saved ticket holder profiles
      operationId: listSavedProfiles
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of saved profiles
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SavedProfile'
                    maxItems: 10
                  count:
                    type: integer
                    maximum: 10
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Profile]
      summary: Create a saved profile
      description: |
        Creates a new saved profile for faster checkout.
        Maximum 10 profiles per user. OIB must be unique within user's profiles.
      operationId: createSavedProfile
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SavedProfileCreate'
      responses:
        '201':
          description: Profile created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedProfile'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'

  /users/me/saved-profiles/{id}:
    parameters:
      - $ref: '#/components/parameters/ProfileId'
    get:
      tags: [Profile]
      summary: Get a saved profile
      operationId: getSavedProfile
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Saved profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Profile]
      summary: Update a saved profile
      operationId: updateSavedProfile
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SavedProfileCreate'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedProfile'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [Profile]
      summary: Delete a saved profile
      operationId: deleteSavedProfile
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Profile deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # E2/E4: MATCHES (Public)
  # ============================================

  /matches:
    get:
      tags: [Matches]
      summary: List published matches
      description: |
        Returns list of matches for the home screen / match listing.

        **Included in response:**
        - Basic match info (teams, logos, competition, venue, kickoff time)
        - Sales status (not_started, in_progress, sold_out, ended)

        **NOT included (use match details for these):**
        - Pricing information
        - Queue status
        - Sector availability
      operationId: listMatches
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [upcoming, past]
            default: upcoming
        - name: from_date
          in: query
          schema:
            type: string
            format: date
        - name: to_date
          in: query
          schema:
            type: string
            format: date
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: List of matches
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MatchSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /matches/{matchId}:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Matches]
      summary: Get match details
      description: |
        Returns match details for the match information page.

        **What this endpoint returns:**
        - Match details (teams, logos, venue, kickoff time)
        - Current sales phase info
        - Whether user can purchase (`can_purchase` boolean)

        **What this endpoint does NOT return:**
        - Sectors/pricing (use `/matches/{matchId}/purchase` when user clicks "Buy Tickets")
        - Queue status (handled by `/matches/{matchId}/purchase`)

        **Mobile app flow:**
        1. Show match details from this endpoint
        2. If `can_purchase` is true, show "Buy Tickets" button
        3. When user clicks "Buy Tickets", call `/matches/{matchId}/purchase`
      operationId: getMatch
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /matches/{matchId}/purchase:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Purchase]
      summary: Get purchase page data (sectors, prices, queue status)
      description: |
        Called when user clicks "Buy Tickets" on the match details page.

        **Server decides the flow:**
        - If queue is required → returns `action: "queue"` with queue info
        - If direct purchase → returns `action: "purchase"` with sectors/pricing

        **Response scenarios:**

        1. **Queue required** (`action: "queue"`):
           - User not in queue: Show "Join Queue" button
           - User already in queue: Show position and wait time
           - User's turn: Proceed to sector selection

        2. **Direct purchase** (`action: "purchase"`):
           - Returns available sectors with pricing
           - User selects sector and quantity
           - Proceed to create order

        3. **Cannot purchase** (`action: "blocked"`):
           - Sales not active, sold out, or user not eligible
           - Returns message explaining why
      operationId: getPurchasePage
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Purchase page data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurchasePageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: User cannot purchase (not eligible, sales closed, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # E4: TICKET PURCHASE FLOW
  # ============================================

  /cart:
    get:
      tags: [Purchase]
      summary: Get current cart
      description: |
        Returns the user's current active cart from Redis.
        User can only have one active cart at a time.
      operationId: getCart
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current cart
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cart'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No active cart

    delete:
      tags: [Purchase]
      summary: Clear cart
      description: |
        Clears the user's active cart and releases reserved seats.
        Seats become available for other users immediately.
      operationId: clearCart
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Cart cleared
        '401':
          $ref: '#/components/responses/Unauthorized'

  /cart/add:
    post:
      tags: [Purchase]
      summary: Add seats to cart (reserve seats)
      description: |
        Adds seats to the user's cart and reserves them:
        1. Validates user eligibility for current sales phase
        2. Assigns best available seats using snake algorithm
        3. Creates reservation with TTL in Redis

        **Business rules:**
        - User can only have one active cart at a time
        - Maximum 4 tickets per cart
      operationId: addToCart
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [match_id, sector_id, quantity]
              properties:
                match_id:
                  type: string
                  format: uuid
                  description: Match to purchase tickets for
                sector_id:
                  type: string
                  format: uuid
                  description: Selected sector
                quantity:
                  type: integer
                  minimum: 1
                  maximum: 4
                  description: Number of tickets (max 4)
                price_category_id:
                  type: string
                  format: uuid
                  description: Price category for the tickets
      responses:
        '200':
          description: Seats added to cart
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cart'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Match or sector not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Insufficient available seats or cart conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /checkout/ticket-holders:
    get:
      tags: [Purchase]
      summary: Get checkout ticket holder info
      description: |
        Returns the current ticket holder data for the checkout step.
        Shows saved holders from user's profile for quick selection.
      operationId: getCheckoutHolders
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Checkout holder info
          content:
            application/json:
              schema:
                type: object
                properties:
                  holders:
                    type: array
                    items:
                      $ref: '#/components/schemas/TicketHolderInput'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Purchase]
      summary: Set ticket holder information and initiate payment
      description: |
        Assigns ticket holders to reserved seats and creates Stripe PaymentIntent.

        **Validations performed:**
        - OIB checksum validation (ISO 7064 MOD 11-10)
        - Blacklist check for ALL holders
        - Age verification (primary buyer must be 18+)

        **All-or-nothing:** If ANY holder is blacklisted, the entire request fails.

        **Payment flow:**
        Returns Stripe client_secret for the mobile app to complete payment.
        Payment confirmation is handled via Stripe webhook (POST /webhooks/stripe).
      operationId: setCheckoutHolders
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [holders]
              properties:
                holders:
                  type: array
                  items:
                    $ref: '#/components/schemas/TicketHolderInput'
                  minItems: 1
                  maxItems: 4
      responses:
        '200':
          description: Holders set, payment intent created
          content:
            application/json:
              schema:
                type: object
                properties:
                  stripe_client_secret:
                    type: string
                    description: Stripe PaymentIntent client secret for mobile SDK
                  order:
                    $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: One or more holders are blacklisted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  # ============================================
  # ORDERS (completed purchases)
  # ============================================

  /orders:
    get:
      tags: [Tickets]
      summary: List my orders
      description: Returns all completed orders for the authenticated user
      operationId: listOrders
      security:
        - bearerAuth: []
      parameters:
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: List of orders
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /orders/{orderId}:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Tickets]
      summary: Get order details
      description: Returns details of a completed order including tickets
      operationId: getOrder
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Order details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # E5: WAITING QUEUE SYSTEM
  # ============================================

  /queue/{matchId}/join:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Queue]
      summary: Join the waiting queue
      description: |
        Joins the waiting queue for a high-demand match.
        Returns initial position and estimated wait time.

        WebSocket connection should be established after joining
        for real-time position updates.
      operationId: joinQueue
      security:
        - bearerAuth: []
      responses:
        '201':
          description: Joined queue
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueEntry'
        '400':
          description: Queue not enabled for this match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Already in queue or has active purchase
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /queue/{matchId}/position:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Queue]
      summary: Get current queue position
      description: |
        Returns current queue position and status.
        Use WebSocket for real-time updates instead of polling.
      operationId: getQueuePosition
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Queue position
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not in queue
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /queue/{matchId}/leave:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    delete:
      tags: [Queue]
      summary: Leave the queue
      operationId: leaveQueue
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Left queue
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Not in queue
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # E6: LOYALTY PROGRAM
  # ============================================

  /users/me/loyalty:
    get:
      tags: [Loyalty]
      summary: Get loyalty points balance
      description: |
        Returns the user's loyalty point balance.
        Points expire after 5 years (rolling window).
      operationId: getLoyaltyBalance
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Loyalty balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoyaltyBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /users/me/loyalty/history:
    get:
      tags: [Loyalty]
      summary: Get loyalty points history
      operationId: getLoyaltyHistory
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: Points history
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LoyaltyPoint'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================
  # E9: TICKET MANAGEMENT & DELIVERY
  # ============================================

  /tickets:
    get:
      tags: [Tickets]
      summary: List my tickets
      description: Returns all tickets owned by or assigned to the user
      operationId: listTickets
      security:
        - bearerAuth: []
      parameters:
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum: [active, used, cancelled]
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: List of tickets
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Ticket'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /tickets/{ticketId}:
    parameters:
      - $ref: '#/components/parameters/TicketId'
    get:
      tags: [Tickets]
      summary: Get ticket details
      description: |
        Returns ticket details with tiered visibility:
        - >4 days before match: Order info only
        - 4 days to 5 hours: Full ticket without QR
        - <5 hours: Full ticket with QR code
      operationId: getTicket
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Ticket details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TicketDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /tickets/{ticketId}/transfer:
    parameters:
      - $ref: '#/components/parameters/TicketId'
    post:
      tags: [Tickets]
      summary: Transfer ticket to another person
      description: |
        Self-service ticket transfer. Available until 48 hours before match.
        New holder is validated against blacklist.
      operationId: transferTicket
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferRequest'
      responses:
        '200':
          description: Ticket transferred
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Ticket'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            Transfer not allowed:
            - New holder is blacklisted
            - Ticket marked as non-transferable
            - Transfer window closed (<48 hours)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /orders/{orderId}/cancel-tickets:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    post:
      tags: [Tickets]
      summary: Cancel tickets from an order (self-service refund)
      description: |
        Self-service ticket cancellation with refund at the order level.
        - >48 hours before match: Full refund (ticket + fee)
        - <48 hours: Cancellation may be rejected

        Cancels specified tickets (or all tickets if none specified) from the order.
      operationId: cancelOrderTickets
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                ticket_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Specific ticket IDs to cancel (omit for all)
                reason:
                  type: string
                  maxLength: 255
      responses:
        '200':
          description: Tickets cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  cancelled_count:
                    type: integer
                  refund:
                    $ref: '#/components/schemas/RefundSummary'
        '400':
          description: Cannot cancel (within 48h of match or already cancelled)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /tickets/{ticketId}/wallet/{walletType}:
    parameters:
      - $ref: '#/components/parameters/TicketId'
      - name: walletType
        in: path
        required: true
        schema:
          type: string
          enum: [apple, google]
        description: Wallet type (apple or google)
    get:
      tags: [Tickets]
      summary: Get wallet pass for ticket
      description: |
        Generates a signed wallet pass for the ticket.

        **Why server-side is required:**
        - Passes must be cryptographically signed
        - Signing keys must remain on server (security)

        **Response by wallet type:**
        - `apple`: Returns URL to `.pkpass` file for `PKAddPassesViewController`
        - `google`: Returns "Save to Google Wallet" URL to open in browser/app
      operationId: getWalletPass
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Wallet pass URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  wallet_type:
                    type: string
                    enum: [apple, google]
                  pass_url:
                    type: string
                    format: uri
                    description: |
                      - Apple: URL to .pkpass file
                      - Google: "Save to Google Wallet" deep link
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # E7: QUOTA PORTAL
  # ============================================

  /quota-portal/dashboard:
    get:
      tags: [Quota Portal]
      summary: Get quota holder dashboard
      description: Returns all quotas assigned to the authenticated user
      operationId: getQuotaDashboard
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Quota dashboard
          content:
            application/json:
              schema:
                type: object
                properties:
                  quotas:
                    type: array
                    items:
                      $ref: '#/components/schemas/QuotaSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /quota-portal/quotas/{quotaId}/select-seats:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Quota Portal]
      summary: Select seats from quota (Step 1 of 3)
      description: |
        Selects seats from the quota allocation.

        **For numbered (seated) quotas:** Provide `seat_ids` array with specific seat UUIDs.
        **For unnumbered (standing) quotas:** Provide `quantity` of tickets.
      operationId: quotaSelectSeats
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                seat_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: For numbered quotas - specific seat IDs
                quantity:
                  type: integer
                  minimum: 1
                  description: For unnumbered quotas - number of tickets
      responses:
        '200':
          description: Seats selected
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  selected_seats:
                    type: array
                    items:
                      type: object
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Not authorized to claim this quota
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  /quota-portal/quotas/{quotaId}/ticket-holders:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Quota Portal]
      summary: Set ticket holder information (Step 2 of 3)
      description: |
        Assigns ticket holder details to each selected seat.

        **Validation:**
        - OIB required for Croatian nationals (checksum validated)
        - Passport required for non-Croatian nationals
        - All holders checked against blacklist
      operationId: quotaSetTicketHolders
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [holders]
              properties:
                holders:
                  type: array
                  items:
                    $ref: '#/components/schemas/TicketHolderInput'
      responses:
        '200':
          description: Ticket holders set
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: One or more holders are blacklisted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /quota-portal/quotas/{quotaId}/payment:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Quota Portal]
      summary: Initiate payment for quota claim (Step 3 of 3)
      description: |
        Creates a Stripe PaymentIntent for the quota claim.
        Payment confirmation is handled via Stripe webhook.
      operationId: quotaPayment
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Payment initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  stripe_client_secret:
                    type: string
                  amount:
                    type: string
                  currency:
                    type: string
                    default: EUR
        '400':
          description: Missing seat selection or ticket holders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /quota-portal/quotas/{quotaId}/subquotas:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    get:
      tags: [Quota Portal]
      summary: List subquotas
      description: Returns all subquotas created from this quota
      operationId: listSubquotas
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of subquotas
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Subquota'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Not authorized to view subquotas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags: [Quota Portal]
      summary: Create subquota (delegate tickets)
      description: |
        Delegates a portion of the quota to another recipient.
        Only available if quota has can_create_subquotas=true.
      operationId: createSubquota
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubquotaCreate'
      responses:
        '201':
          description: Subquota created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subquota'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Quota does not allow subquota creation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Insufficient available seats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /quota-portal/quotas/{quotaId}/subquotas/{subquotaId}/retract:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
      - name: subquotaId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Quota Portal]
      summary: Retract unclaimed subquota
      description: Returns unclaimed seats back to the parent quota
      operationId: retractSubquota
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Subquota retracted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subquota'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Not authorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Subquota already claimed or cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # E12: PETROL STATION SALES
  # ============================================

  /petrol-sales/{matchId}/generate-pin:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Petrol]
      summary: Generate purchase PIN for Petrol station sale
      description: |
        Generates a 5-digit PIN for use at Petrol station.
        PIN is valid until match kick-off.
        Ticket holders are validated against blacklist.
      operationId: generatePetrolPin
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [holders]
              properties:
                quantity:
                  type: integer
                  minimum: 1
                  maximum: 4
                holders:
                  type: array
                  items:
                    $ref: '#/components/schemas/TicketHolderInput'
                  minItems: 1
                  maxItems: 4
      responses:
        '201':
          description: PIN generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  pin:
                    type: string
                    pattern: '^\d{5}$'
                  expires_at:
                    type: string
                    format: date-time
                  quantity:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: One or more holders are blacklisted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /petrol-portal/pins/{pin}:
    parameters:
      - name: pin
        in: path
        required: true
        schema:
          type: string
          pattern: '^\d{5}$'
    get:
      tags: [Petrol]
      summary: Look up PIN for Petrol portal staff
      description: |
        Validates PIN and returns reservation details.
        Used by Petrol station staff to verify customer PINs.
        Creates a reservation if PIN is valid and not already reserved.
      operationId: lookupPetrolPin
      security:
        - bearerAuth: []
      responses:
        '200':
          description: PIN details with reservation
          content:
            application/json:
              schema:
                type: object
                properties:
                  pin:
                    type: string
                  match:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      home_team:
                        type: string
                      away_team:
                        type: string
                  quantity:
                    type: integer
                  reservation_id:
                    type: string
                    format: uuid
                  holders:
                    type: array
                    items:
                      type: object
        '400':
          description: PIN expired or already used
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: PIN not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /petrol-portal/pins/{pin}/confirm:
    parameters:
      - name: pin
        in: path
        required: true
        schema:
          type: string
          pattern: '^\d{5}$'
    post:
      tags: [Petrol]
      summary: Confirm Petrol sale
      description: |
        Confirms the sale for a PIN, creates PetrolSale record,
        and marks reservation as completed.
      operationId: confirmPetrolSale
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payment_method, total_amount]
              properties:
                payment_method:
                  type: string
                  enum: [CASH, CARD]
                receipt_number:
                  type: string
                total_amount:
                  type: string
                  description: Total amount in EUR (decimal string)
      responses:
        '200':
          description: Sale confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  sale_id:
                    type: string
                    format: uuid
        '400':
          description: No active reservation for this PIN
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /petrol-portal/reservations/{id}/cancel:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Petrol]
      summary: Cancel Petrol reservation
      description: Cancels an active reservation, releasing the PIN for reuse.
      operationId: cancelPetrolReservation
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
      responses:
        '200':
          description: Reservation cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          description: Reservation cannot be cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # ADMIN: MATCH MANAGEMENT (E2)
  # ============================================

  /admin/matches:
    get:
      tags: [Admin - Matches]
      summary: List all matches
      operationId: adminListMatches
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, published, active, completed, cancelled]
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: List of matches
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdminMatchSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Admin - Matches]
      summary: Create new match
      operationId: adminCreateMatch
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchCreate'
      responses:
        '201':
          description: Match created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminMatchDetail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/matches/{matchId}:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Matches]
      summary: Get match details
      operationId: adminGetMatch
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminMatchDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Admin - Matches]
      summary: Update match
      description: |
        Updates match details. For active matches with sold tickets,
        system detects changes and offers notification options.

        **Notification options:**
        - `notify`: Send push notification and email to ticket holders
        - `silent`: Update without notification (for minor corrections)

        All changes logged in audit trail regardless of notification setting.
      operationId: adminUpdateMatch
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/MatchUpdate'
                - type: object
                  properties:
                    notify_ticket_holders:
                      type: boolean
                      default: true
                      description: |
                        If true, notify ticket holders of changes.
                        If false, silent update (no notification).
      responses:
        '200':
          description: Match updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  match:
                    $ref: '#/components/schemas/AdminMatchDetail'
                  notifications_sent:
                    type: integer
                    description: Number of ticket holders notified (0 if silent)
                  changes_detected:
                    type: array
                    items:
                      type: string
                    description: List of changed fields
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/matches/{matchId}/publish:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Matches]
      summary: Publish match
      description: Makes match visible to users and enables ticket sales
      operationId: adminPublishMatch
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminMatchDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Match cannot be published (incomplete configuration)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/matches/{matchId}/cancel:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Matches]
      summary: Cancel match
      description: |
        Cancels match and triggers refunds for all sold tickets.
        Requires Super Admin role.
      operationId: adminCancelMatch
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  minLength: 10
                notify_customers:
                  type: boolean
                  default: true
      responses:
        '200':
          description: Match cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  match:
                    $ref: '#/components/schemas/AdminMatchDetail'
                  refunds_initiated:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/close:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Matches]
      summary: Close match
      description: |
        Closes match after completion. Two modes:

        **With attendance data:**
        - First import attendance via `/import/attendance` endpoint
        - Then call close to finalize

        **Without attendance data:**
        - Provide reason why data is unavailable
        - All sold tickets marked as ATTENDED (assumes everyone attended)

        **After closing:**
        - Loyalty points calculated automatically
        - Match status changed to CLOSED
        - No further ticket changes allowed
      operationId: adminCloseMatch
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [attendance_mode]
              properties:
                attendance_mode:
                  type: string
                  enum: [with_data, without_data]
                  description: |
                    - with_data: Attendance already imported
                    - without_data: No attendance data available
                no_data_reason:
                  type: string
                  enum: [scanner_malfunction, partial_data_loss, external_venue_no_data, other]
                  description: Required if attendance_mode=without_data
                no_data_notes:
                  type: string
                  description: Additional explanation (required if reason=other)
      responses:
        '200':
          description: Match closed
          content:
            application/json:
              schema:
                type: object
                properties:
                  match:
                    $ref: '#/components/schemas/AdminMatchDetail'
                  attendance_summary:
                    type: object
                    properties:
                      total_tickets:
                        type: integer
                      attended:
                        type: integer
                      not_attended:
                        type: integer
                  loyalty_points_awarded:
                    type: integer
                    description: Number of fans who received loyalty points
        '400':
          description: |
            Bad request:
            - Missing no_data_reason when attendance_mode=without_data
            - Attendance data not imported when attendance_mode=with_data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Match already closed or cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/matches/{matchId}/media:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Matches]
      summary: Upload match media (banner image)
      description: |
        Uploads a banner image for the match. Accepts JPEG, PNG, or WebP.
        Maximum file size: 5MB. Recommended dimensions: 1200x630px.
        Returns the URL of the uploaded image.
      operationId: adminUploadMatchMedia
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, type]
              properties:
                file:
                  type: string
                  format: binary
                  description: Image file (JPEG, PNG, WebP, max 5MB)
                type:
                  type: string
                  enum: [banner]
                  description: Media type
      responses:
        '200':
          description: Media uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    description: URL of the uploaded image
                  type:
                    type: string
        '400':
          description: Invalid file type or size
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Admin - Matches]
      summary: Delete match media
      description: Removes the banner image for the match
      operationId: adminDeleteMatchMedia
      security:
        - bearerAuth: []
      parameters:
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum: [banner]
      responses:
        '204':
          description: Media deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/sales-phases:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Matches]
      summary: List sales phases
      operationId: adminListSalesPhases
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Sales phases
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SalesPhase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags: [Admin - Matches]
      summary: Create sales phase
      operationId: adminCreateSalesPhase
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SalesPhaseCreate'
      responses:
        '201':
          description: Sales phase created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesPhase'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Phase overlaps with existing phase
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  # ============================================
  # ADMIN: STADIUM TEMPLATES (E2)
  # ============================================

  /admin/stadiums:
    get:
      tags: [Admin - Stadiums]
      summary: List stadium templates
      description: |
        Returns base stadium templates (pre-configured Croatian stadiums).
        These templates are copied when configuring a match.
      operationId: adminListStadiums
      security:
        - bearerAuth: []
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [home, away]
            description: Filter by stadium type
      responses:
        '200':
          description: List of stadium templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/StadiumTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Admin - Stadiums]
      summary: Create stadium template
      description: |
        Creates a new stadium template. Used for:
        - Adding new Croatian venues (HOME)
        - Creating away venue templates for reuse
      operationId: adminCreateStadium
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StadiumTemplateCreate'
      responses:
        '201':
          description: Stadium created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StadiumTemplate'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/stadiums/{stadiumId}:
    parameters:
      - name: stadiumId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Admin - Stadiums]
      summary: Get stadium template details
      operationId: adminGetStadium
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Stadium details with sectors
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StadiumTemplateDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: MATCH STADIUM CONFIGURATION (E2)
  # ============================================

  /admin/matches/{matchId}/stadium:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Match Stadium]
      summary: Get match stadium configuration
      description: Returns the stadium configuration specific to this match
      operationId: adminGetMatchStadium
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match stadium configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchStadiumConfig'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Match not found or stadium not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    post:
      tags: [Admin - Match Stadium]
      summary: Configure match stadium
      description: |
        Copies a stadium template to create match-specific configuration.

        **For HOME matches:** Select from pre-configured Croatian stadiums.
        **For AWAY matches:** Create new stadium or select existing away stadium.
      operationId: adminConfigureMatchStadium
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                stadium_template_id:
                  type: string
                  format: uuid
                  description: Copy configuration from this stadium template
                import_from_match_id:
                  type: string
                  format: uuid
                  description: Import configuration from a previous match (alternative to template)
                create_new:
                  type: object
                  description: Create new away stadium inline (alternative to template)
                  properties:
                    name:
                      type: string
                    city:
                      type: string
                    country:
                      type: string
                    address:
                      type: string
      responses:
        '201':
          description: Stadium configured for match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchStadiumConfig'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Stadium already configured for this match
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/matches/{matchId}/stadium/validate:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Match Stadium]
      summary: Validate stadium configuration
      description: |
        Validates the stadium configuration before publishing.
        Checks for: unassigned sectors, pricing gaps, capacity issues.
      operationId: adminValidateMatchStadium
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Validation results
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                        message:
                          type: string
                        sector_id:
                          type: string
                          format: uuid
                  warnings:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                        message:
                          type: string
                  summary:
                    type: object
                    properties:
                      total_capacity:
                        type: integer
                      sellable_capacity:
                        type: integer
                      sectors_configured:
                        type: integer
                      sectors_with_pricing:
                        type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/sectors:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Match Stadium]
      summary: List match sectors
      description: Returns all sectors configured for this match
      operationId: adminListMatchSectors
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match sectors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/MatchSector'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags: [Admin - Match Stadium]
      summary: Add sector to match
      description: |
        Adds a new sector to the match. Used primarily for AWAY matches
        where sectors are created based on federation allocation.
      operationId: adminAddMatchSector
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchSectorCreate'
      responses:
        '201':
          description: Sector added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchSector'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/matches/{matchId}/sectors/{sectorId}:
    parameters:
      - $ref: '#/components/parameters/MatchId'
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      tags: [Admin - Match Stadium]
      summary: Update sector configuration
      description: |
        Updates sector settings including:
        - Availability (active, closed, maintenance)
        - Purpose (away_fans, vip, press, general, free)
        - Price category assignment
      operationId: adminUpdateMatchSector
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchSectorUpdate'
      responses:
        '200':
          description: Sector updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchSector'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Cannot modify sector with sold tickets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/matches/{matchId}/sectors/bulk:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    put:
      tags: [Admin - Match Stadium]
      summary: Bulk update sectors
      description: Apply configuration to multiple sectors at once
      operationId: adminBulkUpdateSectors
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sector_ids]
              properties:
                sector_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                availability:
                  type: string
                  enum: [active, closed, maintenance]
                purpose:
                  type: string
                  enum: [general, away_fans, vip, press, free]
                price_category_id:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Sectors updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated_count:
                    type: integer
                  sectors:
                    type: array
                    items:
                      $ref: '#/components/schemas/MatchSector'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/price-categories:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Match Stadium]
      summary: List price categories
      operationId: adminListPriceCategories
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Price categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PriceCategory'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags: [Admin - Match Stadium]
      summary: Create price category
      operationId: adminCreatePriceCategory
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceCategoryCreate'
      responses:
        '201':
          description: Price category created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceCategory'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/matches/{matchId}/price-categories/{categoryId}:
    parameters:
      - $ref: '#/components/parameters/MatchId'
      - name: categoryId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      tags: [Admin - Match Stadium]
      summary: Update price category
      operationId: adminUpdatePriceCategory
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceCategoryCreate'
      responses:
        '200':
          description: Price category updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceCategory'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Admin - Match Stadium]
      summary: Delete price category
      operationId: adminDeletePriceCategory
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Price category deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Category in use by sectors
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # ADMIN: AWAY MATCH SEAT MAP (E2)
  # ============================================

  /admin/matches/{matchId}/seat-map:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Match Stadium]
      summary: Create seat map for away match
      description: |
        Creates a seat map for an away match sector.
        HNS defines the seat numbering (rows, seats per row).

        **Workflow:**
        1. Create seat map with row/seat configuration
        2. System validates capacity >= sold tickets
        3. Run seat assignment (snake algorithm)
      operationId: adminCreateSeatMap
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sector_id, rows]
              properties:
                sector_id:
                  type: string
                  format: uuid
                rows:
                  type: array
                  items:
                    type: object
                    required: [row_number, seat_count]
                    properties:
                      row_number:
                        type: string
                        description: Row label (e.g., "1", "A", "AA")
                      seat_count:
                        type: integer
                        minimum: 1
                      first_seat_number:
                        type: integer
                        default: 1
                      technical_seats:
                        type: array
                        items:
                          type: integer
                        description: Seat numbers that are technical (not sellable)
      responses:
        '201':
          description: Seat map created
          content:
            application/json:
              schema:
                type: object
                properties:
                  sector_id:
                    type: string
                    format: uuid
                  total_seats:
                    type: integer
                  sellable_seats:
                    type: integer
                  technical_seats:
                    type: integer
                  can_assign:
                    type: boolean
                    description: True if sellable_seats >= sold tickets
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Sector already has seat map
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Seat map capacity less than sold tickets
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - type: object
                    properties:
                      sold_tickets:
                        type: integer
                      seat_map_capacity:
                        type: integer

  /admin/matches/{matchId}/assign-seats:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Match Stadium]
      summary: Assign seats using snake algorithm
      description: |
        Runs the snake algorithm to assign specific seats to sold tickets.

        **Assignment order options:**
        - `quota_first`: Process quota tickets first, then chronological
        - `chronological`: Process all tickets in order of purchase

        **Algorithm:**
        - Tickets from same order get adjacent seats
        - Fills rows in snaking pattern (left-to-right, then right-to-left)
      operationId: adminAssignSeats
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sector_id:
                  type: string
                  format: uuid
                  description: Assign seats for specific sector (optional, default all)
                assignment_order:
                  type: string
                  enum: [quota_first, chronological]
                  default: quota_first
                preview:
                  type: boolean
                  default: false
                  description: If true, returns preview without saving
      responses:
        '200':
          description: Seats assigned (or preview)
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview:
                    type: boolean
                  assignments:
                    type: array
                    items:
                      type: object
                      properties:
                        ticket_id:
                          type: string
                          format: uuid
                        order_id:
                          type: string
                        holder_name:
                          type: string
                        sector:
                          type: string
                        row:
                          type: string
                        seat_number:
                          type: string
                  summary:
                    type: object
                    properties:
                      total_assigned:
                        type: integer
                      quota_tickets:
                        type: integer
                      public_tickets:
                        type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Seats already assigned or no seat map available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # ADMIN: STADIUM TEMPLATE SECTORS (E2-F3, E2-F10)
  # ============================================

  /admin/stadiums/{stadiumId}/sectors:
    parameters:
      - name: stadiumId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Admin - Stadiums]
      summary: List stadium template sectors
      description: Returns all sectors defined for a stadium template
      operationId: adminListStadiumSectors
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of sectors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TemplateSector'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    post:
      tags: [Admin - Stadiums]
      summary: Create sector in stadium template
      description: Adds a new sector to a stadium template
      operationId: adminCreateStadiumSector
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateSectorCreate'
      responses:
        '201':
          description: Sector created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateSector'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Sector code already exists in this stadium
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/stadiums/{stadiumId}/sectors/{sectorId}:
    parameters:
      - name: stadiumId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Admin - Stadiums]
      summary: Get sector details
      operationId: adminGetStadiumSector
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Sector details with seat map info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateSectorDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Admin - Stadiums]
      summary: Update sector in stadium template
      operationId: adminUpdateStadiumSector
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateSectorUpdate'
      responses:
        '200':
          description: Sector updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateSector'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [Admin - Stadiums]
      summary: Delete sector from stadium template
      operationId: adminDeleteStadiumSector
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Sector deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Sector has seat map or is used by matches
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # ADMIN: SECTOR SEAT MAP CONFIGURATION (E2-F10)
  # ============================================

  /admin/sectors/{sectorId}/seat-map:
    parameters:
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Admin - Stadiums]
      summary: Get sector seat map
      description: |
        Returns the seat map configuration for a sector.
        The seat map is stored as a 2D JSON structure where gaps (staircases, aisles)
        are represented by missing seats.
      operationId: adminGetSectorSeatMap
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Seat map configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SectorSeatMap'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Sector not found or no seat map configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    post:
      tags: [Admin - Stadiums]
      summary: Create sector seat map
      description: |
        Creates a new seat map for a sector using the visual editor data.

        **Workflow:**
        1. Admin draws seats on canvas (Konva.js editor)
        2. Canvas exports JSON seat map
        3. This endpoint saves the seat map
        4. Call POST /admin/sectors/{id}/seats/generate to create individual Seat records

        **JSON Structure:**
        The seat map is a 2D grid where seats are placed by row/column coordinates.
        Gaps (staircases, aisles, walls) are created by simply not having seats at those positions.
      operationId: adminCreateSectorSeatMap
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SectorSeatMapCreate'
      responses:
        '201':
          description: Seat map created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SectorSeatMap'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Sector already has a seat map
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

    put:
      tags: [Admin - Stadiums]
      summary: Update sector seat map
      description: |
        Updates an existing seat map configuration.

        **Warning:** If seats have already been generated, updating the seat map
        will require regenerating seats. This may fail if tickets have been sold.
      operationId: adminUpdateSectorSeatMap
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SectorSeatMapCreate'
      responses:
        '200':
          description: Seat map updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SectorSeatMap'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Cannot update - seats already generated with sold tickets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [Admin - Stadiums]
      summary: Delete sector seat map
      operationId: adminDeleteSectorSeatMap
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Seat map deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Cannot delete - seats generated or tickets sold
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/sectors/{sectorId}/seats/generate:
    parameters:
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Admin - Stadiums]
      summary: Generate seats from seat map
      description: |
        Generates individual Seat records from the sector's seat map configuration.

        **Process:**
        1. Parse seat_map_json from sector
        2. For each seat entry, create a Seat record
        3. Apply row labels and numbering direction
        4. Auto-generate seat numbers (skipping gaps)

        **Options:**
        - `preview`: If true, returns what would be created without saving
        - `force_regenerate`: If true, deletes existing seats first (fails if tickets sold)
      operationId: adminGenerateSeats
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                preview:
                  type: boolean
                  default: false
                  description: If true, returns preview without creating seats
                force_regenerate:
                  type: boolean
                  default: false
                  description: If true, deletes existing seats first
      responses:
        '200':
          description: Seats generated (or preview)
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      total_seats:
                        type: integer
                      standard_seats:
                        type: integer
                      technical_seats:
                        type: integer
                      accessibility_seats:
                        type: integer
                      companion_seats:
                        type: integer
                      rows:
                        type: integer
                  seats:
                    type: array
                    description: Only returned if preview=true
                    items:
                      type: object
                      properties:
                        row:
                          type: string
                        seat_number:
                          type: string
                        seat_type:
                          type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: Sector not found or no seat map configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Seats already exist (use force_regenerate) or tickets sold
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/sectors/{sectorId}/seats/classify:
    parameters:
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags: [Admin - Stadiums]
      summary: Bulk classify seats
      description: |
        Applies seat type classification to multiple seats.
        Used for marking seats as technical, accessibility, or companion.
      operationId: adminClassifySeats
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [seat_type]
              properties:
                seat_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Specific seats to classify (if omitted, uses selection)
                selection:
                  type: object
                  description: Alternative to seat_ids - select by range
                  properties:
                    from_row:
                      type: string
                    to_row:
                      type: string
                    from_seat:
                      type: integer
                    to_seat:
                      type: integer
                seat_type:
                  type: string
                  enum: [standard, technical, accessibility, companion]
      responses:
        '200':
          description: Seats classified
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated_count:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/seats/{seatId}/type:
    parameters:
      - name: seatId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      tags: [Admin - Stadiums]
      summary: Update individual seat type
      description: Changes the type classification of a single seat
      operationId: adminUpdateSeatType
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [seat_type]
              properties:
                seat_type:
                  type: string
                  enum: [standard, technical, accessibility, companion]
      responses:
        '200':
          description: Seat type updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Seat'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: SNAKE ALGORITHM CONFIGURATION (E4-F3)
  # ============================================

  /admin/sectors/{sectorId}/snake-config:
    parameters:
      - name: sectorId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Admin - Stadiums]
      summary: Get snake algorithm configuration
      description: Returns the snake algorithm settings for seat assignment in this sector
      operationId: adminGetSnakeConfig
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Snake configuration
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SectorSnakeConfig'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Admin - Stadiums]
      summary: Update snake algorithm configuration
      description: |
        Configures how the snake algorithm assigns seats in this sector.

        **Configuration options:**
        - `best_row_index`: Starting row (row closest to field = 1)
        - `traversal_direction`: BOTTOM_TO_TOP or TOP_TO_BOTTOM
        - `within_row_strategy`: LEFT_TO_RIGHT or RIGHT_TO_LEFT
      operationId: adminUpdateSnakeConfig
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SectorSnakeConfigUpdate'
      responses:
        '200':
          description: Configuration updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SectorSnakeConfig'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  # ============================================
  # ADMIN: MATCH SEAT STATUS VISUALIZATION (E3-F5)
  # ============================================

  /admin/matches/{matchId}/sectors/status-summary:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Inventory]
      summary: Get sector status summary for visualization
      description: |
        Returns aggregated seat status counts per sector for the stadium visualization.
        Used by the admin seat map viewer to color-code sectors.
      operationId: adminGetSectorStatusSummary
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Sector status summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  match_id:
                    type: string
                    format: uuid
                  sectors:
                    type: array
                    items:
                      type: object
                      properties:
                        sector_id:
                          type: string
                          format: uuid
                        sector_name:
                          type: string
                        sector_code:
                          type: string
                        total_capacity:
                          type: integer
                        status_counts:
                          type: object
                          properties:
                            available:
                              type: integer
                            sold:
                              type: integer
                            reserved:
                              type: integer
                            technical:
                              type: integer
                            official:
                              type: integer
                            blocked:
                              type: integer
                            maintenance:
                              type: integer
                            quarantined:
                              type: integer
                            allocated:
                              type: integer
                            inactive:
                              type: integer
                        dominant_status:
                          type: string
                          description: Most common status (for sector coloring)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: MATCH EXPORTS (E2, E13)
  # ============================================

  /admin/matches/{matchId}/export/barcodes:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Matches]
      summary: Export barcodes for access control
      description: |
        Generates Excel export for access control systems.

        **Contents (barcodes only, no PII):**
        - Barcode value for scanning
        - Ticket status (valid only)

        **Includes extra codes for post-export changes:**
        - Codes for unsold seats (last-minute sales)
        - ~1000 reserve codes for transfers/changes
      operationId: adminExportBarcodes
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Excel file
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="MatchID_BarcodeExport_20260915_1400.xlsx"'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/export/attendees:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Matches]
      summary: Export attendee list with seat assignments
      description: |
        Generates Excel export with attendee names and seat numbers.
        Used for away matches to send to away federation.

        **Contents:**
        - Full name
        - Sector, Row, Seat number
        - Nationality
      operationId: adminExportAttendees
      security:
        - bearerAuth: []
      parameters:
        - name: sector_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by sector
      responses:
        '200':
          description: Excel file
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="MatchID_Attendees_20260915.xlsx"'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/import/attendance:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Matches]
      summary: Import attendance data
      description: |
        Imports attendance data from access control systems.
        Maps scanned barcodes to tickets and marks as ATTENDED.

        **Workflow:**
        1. Upload Excel with scanned barcodes
        2. System validates and shows preview
        3. Confirm to apply changes and close match
      operationId: adminImportAttendance
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Excel file with scanned barcodes
                preview_only:
                  type: boolean
                  default: true
                  description: If true, returns preview without applying
      responses:
        '200':
          description: Import preview or result
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview:
                    type: boolean
                  summary:
                    type: object
                    properties:
                      total_scanned:
                        type: integer
                      matched:
                        type: integer
                      unmatched:
                        type: integer
                      already_attended:
                        type: integer
                  unmatched_barcodes:
                    type: array
                    items:
                      type: string
                    description: Barcodes that couldn't be matched
                  applied:
                    type: boolean
                    description: True if changes were applied (preview_only=false)
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Invalid file format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================
  # ADMIN: SEAT INVENTORY (E3)
  # ============================================

  /admin/matches/{matchId}/seats:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    get:
      tags: [Admin - Inventory]
      summary: Get seat inventory
      description: Returns seat inventory with filtering options
      operationId: adminGetSeats
      security:
        - bearerAuth: []
      parameters:
        - name: sector_id
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: array
            items:
              type: string
              enum: [available, reserved, sold, allocated, blocked, technical, official, maintenance, quarantined, inactive]
        - name: row
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: Seat inventory
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/SeatInventory'
                  summary:
                    $ref: '#/components/schemas/InventorySummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/matches/{matchId}/seats/{seatId}/status:
    parameters:
      - $ref: '#/components/parameters/MatchId'
      - name: seatId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      tags: [Admin - Inventory]
      summary: Change seat status
      operationId: adminChangeSeatStatus
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [available, blocked, technical, official, maintenance, quarantined, inactive]
                reason:
                  type: string
                  maxLength: 255
      responses:
        '200':
          description: Status changed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeatInventory'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/matches/{matchId}/seats/bulk-operation:
    parameters:
      - $ref: '#/components/parameters/MatchId'
    post:
      tags: [Admin - Inventory]
      summary: Bulk seat status change
      description: Changes status for multiple seats at once
      operationId: adminBulkSeatOperation
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [target_status]
              properties:
                seat_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: Specific seats (mutually exclusive with selection_criteria)
                selection_criteria:
                  type: object
                  properties:
                    sector_id:
                      type: string
                      format: uuid
                    current_status:
                      type: string
                    rows:
                      type: array
                      items:
                        type: string
                target_status:
                  type: string
                  enum: [available, blocked, technical, official, maintenance, quarantined, inactive]
                reason:
                  type: string
      responses:
        '202':
          description: Bulk operation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  operation_id:
                    type: string
                    format: uuid
                  total_seats:
                    type: integer
                  status:
                    type: string
                    enum: [pending, processing]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: QUOTA MANAGEMENT (E7)
  # ============================================

  /admin/quotas:
    get:
      tags: [Admin - Quotas]
      summary: List quotas
      operationId: adminListQuotas
      security:
        - bearerAuth: []
      parameters:
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, active, past_deadline, cancelled, fully_claimed]
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: List of quotas
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdminQuotaSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Admin - Quotas]
      summary: Create quota
      description: |
        Creates a new quota allocation. Seats are immediately allocated
        using the specified algorithm (NM or REDOM).
      operationId: adminCreateQuota
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuotaCreate'
      responses:
        '201':
          description: Quota created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminQuotaDetail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Insufficient available seats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/quotas/import:
    post:
      tags: [Admin - Quotas]
      summary: Bulk import quotas from CSV
      operationId: adminImportQuotas
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [match_id, file]
              properties:
                match_id:
                  type: string
                  format: uuid
                file:
                  type: string
                  format: binary
                  description: CSV file with quota definitions
      responses:
        '202':
          description: Import started
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id:
                    type: string
                    format: uuid
                  total_rows:
                    type: integer
                  status:
                    type: string
                    enum: [processing]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: CSV validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/quotas/{quotaId}:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    get:
      tags: [Admin - Quotas]
      summary: Get quota details
      operationId: adminGetQuota
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Quota details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminQuotaDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/quotas/{quotaId}/cancel:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Admin - Quotas]
      summary: Cancel quota
      description: |
        Cancels a quota and returns tickets to inventory.

        **Cancellation Types:**
        - `all_unused`: Cancel ALL tickets that are not SOLD (both ALLOCATED and RESERVED)
        - `unfulfilled_only`: Cancel only ALLOCATED tickets, preserve RESERVED (allows in-progress claims to complete)

        Already SOLD tickets remain valid and are not affected.
      operationId: adminCancelQuota
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cancellation_type, reason]
              properties:
                cancellation_type:
                  type: string
                  enum: [all_unused, unfulfilled_only]
                  description: |
                    - all_unused: Cancel ALLOCATED + RESERVED tickets
                    - unfulfilled_only: Cancel only ALLOCATED tickets
                reason:
                  type: string
                  maxLength: 500
                  description: Reason for cancellation (logged in audit trail)
      responses:
        '200':
          description: Quota cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  quota:
                    $ref: '#/components/schemas/AdminQuotaDetail'
                  tickets_cancelled:
                    type: integer
                    description: Number of tickets returned to inventory
                  tickets_preserved:
                    type: integer
                    description: Number of RESERVED tickets preserved (if unfulfilled_only)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/quotas/{quotaId}/resend-email:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Admin - Quotas]
      summary: Resend quota email invitation
      description: |
        Resends the email invitation to the quota recipient.
        Use when original email failed delivery or recipient requests resend.
      operationId: adminResendQuotaEmail
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Email sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Email invitation sent to recipient@example.com"
                  sent_at:
                    type: string
                    format: date-time
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Quota is cancelled or fully claimed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /admin/quotas/{quotaId}/masquerade:
    parameters:
      - $ref: '#/components/parameters/QuotaId'
    post:
      tags: [Admin - Quotas]
      summary: Masquerade as quota owner
      description: |
        Generates a temporary session token to view the quota portal as the quota owner.
        Used for support and troubleshooting.

        - All actions logged with "Performed by Admin [name] on behalf of [quota owner]"
        - Session expires after 30 minutes
        - Cannot perform payment actions while masquerading
      operationId: adminMasqueradeQuota
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Masquerade session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  masquerade_token:
                    type: string
                    description: Temporary token for quota portal access
                  expires_at:
                    type: string
                    format: date-time
                  quota_portal_url:
                    type: string
                    format: uri
                    description: URL to access quota portal with masquerade token
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: BLACKLIST (E11)
  # ============================================

  /admin/blacklist:
    get:
      tags: [Admin - Blacklist]
      summary: List blacklist entries
      operationId: adminListBlacklist
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [active, removed]
            default: active
        - name: search
          in: query
          schema:
            type: string
          description: Search by OIB, name
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: Blacklist entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/BlacklistEntry'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Admin - Blacklist]
      summary: Add to blacklist
      description: |
        Adds a person to the blacklist. Optionally triggers
        auto-cancellation of their existing tickets.
      operationId: adminAddToBlacklist
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlacklistCreate'
      responses:
        '201':
          description: Added to blacklist
          content:
            application/json:
              schema:
                type: object
                properties:
                  entry:
                    $ref: '#/components/schemas/BlacklistEntry'
                  tickets_cancelled:
                    type: integer
                  orders_cancelled:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: OIB already in blacklist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          $ref: '#/components/responses/ValidationError'

  /admin/blacklist/import:
    post:
      tags: [Admin - Blacklist]
      summary: Import blacklist from CSV
      operationId: adminImportBlacklist
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                auto_cancel_tickets:
                  type: boolean
                  default: true
      responses:
        '202':
          description: Import started
          content:
            application/json:
              schema:
                type: object
                properties:
                  batch_id:
                    type: string
                    format: uuid
                  total_rows:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /admin/blacklist/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    put:
      tags: [Admin - Blacklist]
      summary: Update blacklist entry
      operationId: adminUpdateBlacklist
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                first_name:
                  type: string
                last_name:
                  type: string
                date_of_birth:
                  type: string
                  format: date
                notes:
                  type: string
      responses:
        '200':
          description: Entry updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlacklistEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Admin - Blacklist]
      summary: Remove from blacklist
      description: Soft-deletes the blacklist entry (marks as removed)
      operationId: adminRemoveFromBlacklist
      security:
        - bearerAuth: []
      parameters:
        - name: reason
          in: query
          required: true
          description: Reason for removing from blacklist (min 5 characters)
          schema:
            type: string
            minLength: 5
      responses:
        '200':
          description: Removed from blacklist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlacklistEntry'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/blacklist/violations:
    get:
      tags: [Admin - Blacklist]
      summary: Get violation report
      description: Lists blocked purchase/transfer attempts
      operationId: adminGetViolations
      security:
        - bearerAuth: []
      parameters:
        - name: from_date
          in: query
          schema:
            type: string
            format: date
        - name: to_date
          in: query
          schema:
            type: string
            format: date
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: Violation log
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ViolationLog'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================
  # ADMIN: CUSTOMER SUPPORT (E10)
  # ============================================

  /admin/support/tickets/search:
    get:
      tags: [Admin - Support]
      summary: Search tickets
      description: Search tickets by various criteria for support operations
      operationId: adminSearchTickets
      security:
        - bearerAuth: []
      parameters:
        - name: barcode
          in: query
          schema:
            type: string
        - name: order_number
          in: query
          schema:
            type: string
        - name: holder_oib
          in: query
          schema:
            type: string
        - name: holder_email
          in: query
          schema:
            type: string
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/PageNumber'
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdminTicketDetail'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /admin/support/tickets/{ticketId}/transfer:
    parameters:
      - $ref: '#/components/parameters/TicketId'
    post:
      tags: [Admin - Support]
      summary: Process support ticket transfer
      description: |
        Transfers ticket to new holder via customer support.
        Available until 48 hours before match.
      operationId: adminTransferTicket
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [requester_name, requester_contact, new_holder, identity_verified]
              properties:
                requester_name:
                  type: string
                requester_contact:
                  type: string
                new_holder:
                  $ref: '#/components/schemas/TicketHolderInput'
                identity_verified:
                  type: boolean
                  description: Agent confirms requester identity was verified
      responses:
        '200':
          description: Ticket transferred
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminTicketDetail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            Transfer not allowed:
            - New holder is blacklisted
            - Transfer window closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/support/tickets/{ticketId}/refund:
    parameters:
      - $ref: '#/components/parameters/TicketId'
    post:
      tags: [Admin - Support]
      summary: Process support refund
      description: |
        Processes refund via customer support.
        - Service fee is retained (ticket only refunded)
        - May require supervisor approval within 48 hours of match
      operationId: adminRefundTicket
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identity_verified]
              properties:
                identity_verified:
                  type: boolean
                reason:
                  type: string
      responses:
        '200':
          description: Refund processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/AdminTicketDetail'
                  refund:
                    $ref: '#/components/schemas/RefundSummary'
        '202':
          description: Refund requires supervisor approval
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [pending_approval]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/support/tickets/{ticketId}/print:
    parameters:
      - $ref: '#/components/parameters/TicketId'
    post:
      tags: [Admin - Support]
      summary: Emergency ticket print
      description: |
        Generates printable ticket for emergency situations.
        Ticket is watermarked with "EMERGENCY REPRINT" and audit trail is created.
      operationId: adminPrintTicket
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason, identity_verified, verification_document_type]
              properties:
                reason:
                  type: string
                identity_verified:
                  type: boolean
                verification_document_type:
                  type: string
                  description: e.g., "ID Card", "Passport"
                print_location:
                  type: string
      responses:
        '200':
          description: Printable ticket generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  pdf_url:
                    type: string
                    format: uri
                  print_log_id:
                    type: string
                    format: uuid
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # ADMIN: REPORTS (E14)
  # ============================================

  /admin/reports/sales:
    get:
      tags: [Admin - Reports]
      summary: Generate sales report
      operationId: adminSalesReport
      security:
        - bearerAuth: []
      parameters:
        - name: match_id
          in: query
          schema:
            type: string
            format: uuid
        - name: from_date
          in: query
          schema:
            type: string
            format: date
        - name: to_date
          in: query
          schema:
            type: string
            format: date
        - name: format
          in: query
          schema:
            type: string
            enum: [json, csv, xlsx]
            default: json
      responses:
        '200':
          description: Sales report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalesReport'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /admin/reports/dashboard:
    get:
      tags: [Admin - Reports]
      summary: Real-time match dashboard
      operationId: adminMatchDashboard
      security:
        - bearerAuth: []
      parameters:
        - name: match_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Dashboard data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchDashboard'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================
  # WEBHOOKS (E8)
  # ============================================

  /webhooks/stripe:
    post:
      tags: [Webhooks]
      summary: Stripe webhook handler
      description: |
        Handles Stripe payment events:
        - payment_intent.succeeded
        - payment_intent.payment_failed
        - charge.refunded
      operationId: stripeWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Webhook processed
        '400':
          description: Invalid webhook signature

# ============================================
# COMPONENTS
# ============================================

# NOTE: e-racuni.com Integration
# ------------------------------
# e-racuni is an OUTBOUND integration (we push to their API).
# Invoice generation happens server-side after successful payment:
# 1. Order confirmed → Backend calls e-racuni API to create invoice
# 2. e-racuni returns fiscalized invoice with JIR number
# 3. Invoice PDF stored and sent to customer via email
# No webhook endpoint needed - this is a synchronous API call.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT access token from authentication

  parameters:
    MatchId:
      name: matchId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Match UUID

    CartId:
      name: cartId
      in: path
      required: true
      schema:
        type: string
      description: |
        Cart ID (human-readable format, e.g., "26000001").
        This ID becomes the order_number after successful purchase.
      example: "26000001"

    OrderId:
      name: orderId
      in: path
      required: true
      schema:
        type: string
      description: |
        Order number (same as cart ID used during purchase).
        Human-readable format, e.g., "26000001".
      example: "26000001"

    TicketId:
      name: ticketId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Ticket UUID

    QuotaId:
      name: quotaId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Quota UUID

    ProfileId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Saved profile UUID

    PageSize:
      name: page_size
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

    PageNumber:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1

  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Error'
              - type: object
                properties:
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        field:
                          type: string
                        message:
                          type: string
                        code:
                          type: string

  schemas:
    # ----------------------------------------
    # Common
    # ----------------------------------------

    Error:
      type: object
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: Error type URI
        title:
          type: string
          description: Human-readable error title
        status:
          type: integer
          description: HTTP status code
        detail:
          type: string
          description: Detailed error description
        instance:
          type: string
          description: URI reference for this occurrence
        code:
          type: string
          description: Application-specific error code

    Pagination:
      type: object
      properties:
        page:
          type: integer
        page_size:
          type: integer
        total_items:
          type: integer
        total_pages:
          type: integer

    # ----------------------------------------
    # Authentication
    # ----------------------------------------

    AuthResponse:
      type: object
      properties:
        access_token:
          type: string
          description: JWT access token (15 min expiry)
        refresh_token:
          type: string
          description: Refresh token (30 day expiry)
        expires_in:
          type: integer
          description: Access token TTL in seconds
        user:
          type: object
          properties:
            id:
              type: string
              format: uuid
            email:
              type: string
              format: email
            has_profile:
              type: boolean

    # ----------------------------------------
    # User Profile
    # ----------------------------------------

    UserProfile:
      type: object
      properties:
        user_id:
          type: string
          format: uuid
        full_name:
          type: string
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
        oib:
          type: string
          pattern: '^\d{11}$'
          description: Croatian personal ID (masked for display)
        phone:
          type: string
        has_oib:
          type: boolean
        loyalty_points:
          type: integer
        updated_at:
          type: string
          format: date-time

    UserProfileCreate:
      type: object
      required: [full_name, date_of_birth, nationality]
      description: |
        Schema for creating the initial ticketing profile.
        Either OIB (for Croatian citizens) or passport_number (for non-Croatian) is required.
      properties:
        full_name:
          type: string
          maxLength: 255
          description: Full legal name as it appears on ID document
          example: "Ivan Horvat"
        date_of_birth:
          type: string
          format: date
          description: Date of birth (user must be 18+ to purchase tickets)
          example: "1990-05-15"
        nationality:
          type: string
          maxLength: 100
          description: Country of citizenship (ISO 3166-1 alpha-2 code recommended)
          example: "HR"
        oib:
          type: string
          pattern: '^\d{11}$'
          description: |
            Croatian Personal Identification Number (OIB).
            Required for Croatian citizens. Must be globally unique.
            Validated using ISO 7064 MOD 11-10 checksum.
            Cannot be changed once set.
          example: "12345678903"
        passport_number:
          type: string
          maxLength: 50
          description: |
            Passport number for non-Croatian citizens.
            Required if OIB is not provided.
          example: "AB1234567"
        phone:
          type: string
          maxLength: 50
          description: Phone number with country code
          example: "+385 91 234 5678"

    UserProfileUpdate:
      type: object
      required: [full_name, date_of_birth, nationality]
      description: |
        Schema for updating an existing ticketing profile.
        Note: OIB cannot be changed once set.
      properties:
        full_name:
          type: string
          maxLength: 255
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
          maxLength: 100
        oib:
          type: string
          pattern: '^\d{11}$'
          description: |
            Can only be set if not already present.
            Once set, cannot be changed (requires support intervention).
        passport_number:
          type: string
          description: For non-Croatian citizens
        phone:
          type: string
          maxLength: 50

    SavedProfile:
      type: object
      properties:
        id:
          type: string
          format: uuid
        full_name:
          type: string
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
        has_oib:
          type: boolean
        email:
          type: string
          format: email
        phone:
          type: string
        relationship:
          type: string
        created_at:
          type: string
          format: date-time

    SavedProfileCreate:
      type: object
      required: [full_name, date_of_birth, nationality]
      properties:
        full_name:
          type: string
          maxLength: 255
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
        oib:
          type: string
          pattern: '^\d{11}$'
        passport_number:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        relationship:
          type: string
          maxLength: 100

    # ----------------------------------------
    # Matches
    # ----------------------------------------

    MatchSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        home_team:
          type: string
          example: "Hrvatska"
        away_team:
          type: string
          example: "Italija"
        competition:
          type: string
          example: "UEFA Nations League"
        home_team_logo:
          type: string
          format: uri
          description: URL to home team logo image
          example: "https://cdn.hns.family/logos/croatia.png"
        away_team_logo:
          type: string
          format: uri
          description: URL to away team logo image
          example: "https://cdn.hns.family/logos/italy.png"
        competition_logo:
          type: string
          format: uri
          description: URL to competition logo image
          example: "https://cdn.hns.family/logos/nations-league.png"
        kick_off_time:
          type: string
          format: date-time
        venue:
          type: string
          description: Venue name with city and country
          example: "Maksimir, Zagreb, Hrvatska"
        status:
          type: string
          enum: [published, active, completed, cancelled]
        sales_status:
          type: string
          enum: [not_started, in_progress, sold_out, ended]

    MatchDetail:
      allOf:
        - $ref: '#/components/schemas/MatchSummary'
        - type: object
          description: |
            Extended match information for the match details page.
            Does NOT include ticket/sector information - use /matches/{matchId}/purchase
            endpoint when user clicks "Buy Tickets".
          properties:
            description:
              type: string
              description: Match description or additional info
            current_sales_phase:
              $ref: '#/components/schemas/SalesPhase'
            can_purchase:
              type: boolean
              description: |
                Whether the current user can purchase tickets.
                True if: sales are active AND user is eligible (loyalty/public phase).
                Mobile app should show "Buy Tickets" button when true.
            purchase_message:
              type: string
              description: |
                Message to display if can_purchase is false.
                Examples: "Sales not started", "Sales ended", "Sold out",
                "Loyalty phase - you need X points to purchase"

    SectorAvailability:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        code:
          type: string
        price:
          type: number
          format: decimal
        currency:
          type: string
          default: EUR
        availability:
          type: string
          enum: [available, limited, sold_out]
        available_count:
          type: integer
          description: Approximate available seats

    PurchasePageResponse:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [queue, purchase, blocked]
          description: |
            - queue: User must join/wait in queue before purchasing
            - purchase: User can proceed directly to sector selection
            - blocked: User cannot purchase (see message for reason)
        message:
          type: string
          description: |
            Human-readable message explaining the current state.
            For "blocked" action, explains why user cannot purchase.
        queue:
          type: object
          description: Present when action is "queue"
          properties:
            status:
              type: string
              enum: [not_joined, waiting, your_turn]
            position:
              type: integer
              description: Current position in queue (null if not joined)
            estimated_wait_minutes:
              type: integer
            queue_token:
              type: string
              description: Token to use when creating order (present when your_turn)
            window_expires_at:
              type: string
              format: date-time
              description: When purchase window expires (present when your_turn)
        sectors:
          type: array
          description: Present when action is "purchase" or queue status is "your_turn"
          items:
            $ref: '#/components/schemas/SectorAvailability'
        max_tickets:
          type: integer
          description: Maximum tickets user can purchase (default 4)
          default: 4
        tickets_remaining:
          type: integer
          description: Tickets user can still purchase for this match (considers previous purchases)

    # ----------------------------------------
    # Orders & Purchase
    # ----------------------------------------

    Cart:
      type: object
      description: Active shopping cart with reserved seats (20-minute TTL)
      properties:
        id:
          type: string
          description: |
            Cart ID in human-readable format (e.g., "26000001").
            This becomes the order_number after successful purchase.
          example: "26000001"
        match:
          $ref: '#/components/schemas/MatchSummary'
        sector:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            code:
              type: string
        seats:
          type: array
          items:
            $ref: '#/components/schemas/ReservedSeat'
        ticket_holders:
          type: array
          description: Ticket holders (empty until set via PUT /cart/ticket-holders)
          items:
            type: object
            properties:
              seat_id:
                type: string
                format: uuid
              full_name:
                type: string
              is_set:
                type: boolean
        ticket_amount:
          type: number
          format: decimal
          description: Subtotal for tickets
        fee_amount:
          type: number
          format: decimal
          description: Service fee
        total_amount:
          type: number
          format: decimal
          description: Total to pay
        currency:
          type: string
          default: EUR
        expires_at:
          type: string
          format: date-time
          description: Cart expires at this time (seats released)
        time_remaining_seconds:
          type: integer
          description: Seconds until cart expires
        is_ready_for_checkout:
          type: boolean
          description: True when all ticket holders are set
        created_at:
          type: string
          format: date-time

    Order:
      type: object
      description: Completed purchase order
      properties:
        order_number:
          type: string
          description: |
            Order number (same ID used during cart/checkout flow).
            Human-readable format, e.g., "26000001".
          example: "26000001"
        match:
          $ref: '#/components/schemas/MatchSummary'
        status:
          type: string
          enum: [completed, cancelled, refunded]
          description: Order status (only completed orders are returned via API)
        ticket_count:
          type: integer
          description: Number of tickets in this order
        ticket_amount:
          type: number
          format: decimal
        fee_amount:
          type: number
          format: decimal
        total_amount:
          type: number
          format: decimal
        currency:
          type: string
        purchased_at:
          type: string
          format: date-time
          description: When the purchase was completed

    OrderDetail:
      allOf:
        - $ref: '#/components/schemas/Order'
        - type: object
          properties:
            tickets:
              type: array
              items:
                $ref: '#/components/schemas/Ticket'
            payment:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                status:
                  type: string
                completed_at:
                  type: string
                  format: date-time

    ReservedSeat:
      type: object
      properties:
        seat_id:
          type: string
          format: uuid
        sector_name:
          type: string
        row:
          type: string
        seat_number:
          type: string
        price:
          type: number
          format: decimal

    TicketHolderInput:
      type: object
      required: [full_name, date_of_birth, nationality]
      properties:
        seat_id:
          type: string
          format: uuid
          description: Specific seat (optional, assigned in order if not specified)
        full_name:
          type: string
          maxLength: 255
        date_of_birth:
          type: string
          format: date
        nationality:
          type: string
        oib:
          type: string
          pattern: '^\d{11}$'
        passport_number:
          type: string
        email:
          type: string
          format: email
          description: Ticket delivery email (optional)
        saved_profile_id:
          type: string
          format: uuid
          description: Use saved profile instead of manual entry

    TicketHoldersInput:
      type: object
      description: |
        Ticket holder information for quota claim.
        Maps each ticket (by ID) to its holder details.
      required: [holders]
      properties:
        holders:
          type: array
          description: |
            Array of ticket-to-holder mappings.
            Must provide one entry for each ticket in the claim cart.
          items:
            type: object
            required: [ticket_id, full_name, date_of_birth, nationality]
            properties:
              ticket_id:
                type: string
                format: uuid
                description: ID of the ticket from the claim cart
              full_name:
                type: string
                maxLength: 255
              date_of_birth:
                type: string
                format: date
              nationality:
                type: string
                description: ISO country code (e.g., "HR", "DE", "IT")
              oib:
                type: string
                pattern: '^\d{11}$'
                description: Required for Croatian nationals (validated with checksum)
              passport_number:
                type: string
                description: Required for non-Croatian nationals
              email:
                type: string
                format: email
                description: |
                  Email for ticket delivery. Ticket will be sent to this email's HNS app.
                  - If blank/null: ticket stays on quota holder's device
                  - If same email for multiple tickets: those tickets enter together
                  - Disabled for minors (under 18) - must be null
              phone:
                type: string
                description: Contact phone number (optional)
              saved_profile_id:
                type: string
                format: uuid
                description: Use saved profile instead of manual entry (overrides other fields)
      example:
        holders:
          - ticket_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
            full_name: "Ivan Horvat"
            date_of_birth: "1985-03-15"
            nationality: "HR"
            oib: "12345678903"
            email: "ivan@example.com"
          - ticket_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
            full_name: "Ana Horvat"
            date_of_birth: "2010-06-20"
            nationality: "HR"
            oib: "98765432101"
            email: null

    # ----------------------------------------
    # Queue
    # ----------------------------------------

    QueueEntry:
      type: object
      properties:
        match_id:
          type: string
          format: uuid
        position:
          type: integer
          description: Current position in queue (1 = front)
        status:
          type: string
          enum: [waiting, active, expired, completed]
        estimated_wait_minutes:
          type: integer
        joined_at:
          type: string
          format: date-time
        window_started_at:
          type: string
          format: date-time
          description: When purchase window started (if active)
        window_expires_at:
          type: string
          format: date-time
          description: When purchase window expires (if active)
        queue_token:
          type: string
          description: Token to use when creating order

    # ----------------------------------------
    # Loyalty
    # ----------------------------------------

    LoyaltyBalance:
      type: object
      properties:
        total_points:
          type: integer
          description: All-time points earned
        qualifying_points:
          type: integer
          description: Points within 5-year window
        oldest_point_date:
          type: string
          format: date
        newest_point_date:
          type: string
          format: date

    LoyaltyPoint:
      type: object
      properties:
        id:
          type: string
          format: uuid
        match:
          type: object
          properties:
            id:
              type: string
              format: uuid
            home_team:
              type: string
            away_team:
              type: string
            kick_off_time:
              type: string
              format: date-time
        points:
          type: integer
        earned_at:
          type: string
          format: date-time

    # ----------------------------------------
    # Tickets
    # ----------------------------------------

    Ticket:
      type: object
      properties:
        id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        match:
          $ref: '#/components/schemas/MatchSummary'
        seat:
          type: object
          properties:
            sector_name:
              type: string
            row:
              type: string
            seat_number:
              type: string
        holder:
          type: object
          properties:
            full_name:
              type: string
            is_self:
              type: boolean
        status:
          type: string
          enum: [sold, attended, cancelled, transferred, refunded]
        transfer_allowed:
          type: boolean
        visibility_tier:
          type: string
          enum: [order_only, details_only, full]
          description: What information is currently visible

    TicketDetail:
      allOf:
        - $ref: '#/components/schemas/Ticket'
        - type: object
          properties:
            barcode:
              type: string
              description: |
                Barcode/QR code value for stadium entry.
                Only visible in 'full' tier (< 5 hours before match).
                Mobile app generates QR image from this value.
              example: "HNS26000001001"
            holder_details:
              type: object
              description: Only visible in 'details_only' and 'full' tiers
              properties:
                full_name:
                  type: string
                date_of_birth:
                  type: string
                  format: date
                nationality:
                  type: string
            external_pdf_url:
              type: string
              format: uri
              description: For away matches with PDF tickets
            wallet_added:
              type: boolean
            wallet_type:
              type: string
              enum: [apple, google]

    TransferRequest:
      type: object
      required: [new_holder]
      properties:
        new_holder:
          $ref: '#/components/schemas/TicketHolderInput'
        reason:
          type: string
          maxLength: 255

    RefundSummary:
      type: object
      properties:
        refund_id:
          type: string
          format: uuid
        ticket_refund:
          type: number
          format: decimal
        fee_refund:
          type: number
          format: decimal
        total_refund:
          type: number
          format: decimal
        status:
          type: string
          enum: [pending, processing, completed]

    # ----------------------------------------
    # Quotas
    # ----------------------------------------

    QuotaSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        match:
          $ref: '#/components/schemas/MatchSummary'
        total_quantity:
          type: integer
          description: Total tickets allocated to this quota
        allocated_quantity:
          type: integer
          description: Tickets available for claiming (not yet claimed or delegated)
        reserved_quantity:
          type: integer
          description: Tickets with holder info filled but payment pending
        sold_quantity:
          type: integer
          description: Tickets fully claimed and paid
        delegated_quantity:
          type: integer
          description: Tickets assigned to subquotas
        status:
          type: string
          enum: [pending, active, past_deadline, cancelled, fully_claimed]
          description: |
            - pending: Quota created but not yet accessed
            - active: Quota holder has accessed/started claiming
            - past_deadline: Expiration date passed (can still claim until admin cancels)
            - cancelled: Admin cancelled the quota
            - fully_claimed: All tickets claimed/sold
        can_create_subquotas:
          type: boolean
          description: If true, quota holder can delegate tickets to others
        transfer_allowed:
          type: boolean
          description: Whether tickets from this quota can be transferred after claiming
        expiration_date:
          type: string
          format: date
          description: Visual reminder date (no auto-expiry)

    QuotaDetail:
      allOf:
        - $ref: '#/components/schemas/QuotaSummary'
        - type: object
          properties:
            is_numbered:
              type: boolean
              description: |
                True for seated sectors (specific seat selection required).
                False for standing/unnumbered sectors (quantity selection only).
            tickets:
              type: array
              description: |
                List of tickets in this quota.
                For numbered quotas: each ticket has specific seat assignment.
                For unnumbered quotas: tickets represent capacity without seat numbers.
              items:
                $ref: '#/components/schemas/QuotaTicket'
            discount_percent:
              type: number
              format: decimal
              description: Discount applied to ticket prices (0-100, use 100 for free/GR)
            deferred_payment:
              type: boolean
              description: If true, payment can be deferred (invoice later)
            subquotas:
              type: array
              description: List of subquotas created from this quota (if can_create_subquotas=true)
              items:
                $ref: '#/components/schemas/Subquota'

    QuotaTicket:
      type: object
      description: An individual ticket in a quota allocation
      properties:
        id:
          type: string
          format: uuid
          description: Unique ticket identifier (use this for claim/subquota request)
        sector:
          type: string
          description: Sector name
          example: "Zapad Dolje"
        row:
          type: string
          description: Row number/label (null for standing)
          example: "12"
          nullable: true
        seat_number:
          type: string
          description: Seat number (null for standing)
          example: "24"
          nullable: true
        price:
          type: number
          format: decimal
          description: Original ticket price before discount
          example: 50.00
        discounted_price:
          type: number
          format: decimal
          description: Price after quota discount applied
          example: 40.00
        status:
          type: string
          enum: [allocated, reserved, sold, delegated, cancelled]
          description: |
            - allocated: Available for claiming (not yet started)
            - reserved: Temporarily held in active claim cart (pending payment)
            - sold: Claimed and paid, ticket issued
            - delegated: Assigned to a subquota
            - cancelled: Cancelled by admin or refunded
        delegated_to:
          type: string
          nullable: true
          description: Subquota recipient email (only if status=delegated)

    QuotaClaimCart:
      type: object
      description: A claim cart for reserving and purchasing quota tickets
      properties:
        id:
          type: string
          description: Claim cart ID (human-readable format)
          example: "26000001"
        quota_id:
          type: string
          format: uuid
        quota_name:
          type: string
          description: Name/description of the quota
        match:
          $ref: '#/components/schemas/MatchSummary'
        is_numbered:
          type: boolean
          description: Whether this is a seated (numbered) quota
        tickets:
          type: array
          description: Tickets reserved in this claim cart
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              sector:
                type: string
              row:
                type: string
                nullable: true
              seat_number:
                type: string
                nullable: true
              price:
                type: number
                format: decimal
              discounted_price:
                type: number
                format: decimal
              ticket_holder:
                $ref: '#/components/schemas/TicketHolderInput'
        amounts:
          type: object
          properties:
            subtotal:
              type: number
              format: decimal
              description: Sum of discounted ticket prices
            service_fee:
              type: number
              format: decimal
            total:
              type: number
              format: decimal
        deferred_payment:
          type: boolean
          description: If true, payment can be deferred
        expires_at:
          type: string
          format: date-time
          description: Cart expires and tickets released after this time
        status:
          type: string
          enum: [active, expired, completed, abandoned]

    QuotaClaimCheckout:
      type: object
      description: Checkout response for quota claim
      properties:
        claim_id:
          type: string
          example: "26000001"
        payment_required:
          type: boolean
          description: False if quota is free (100% discount) or deferred payment
        client_secret:
          type: string
          description: Stripe PaymentIntent client_secret (only if payment_required=true)
        total_amount:
          type: number
          format: decimal
        deferred_payment:
          type: boolean
          description: If true, claim is confirmed with pending payment (invoice later)
        order_id:
          type: string
          description: Order ID if claim completed immediately (free or deferred)

    Subquota:
      type: object
      properties:
        id:
          type: string
          format: uuid
        recipient_email:
          type: string
          format: email
        recipient_name:
          type: string
        quantity:
          type: integer
          description: Total tickets delegated
        tickets:
          type: array
          description: Specific tickets delegated (for numbered quotas)
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              sector:
                type: string
              row:
                type: string
                nullable: true
              seat_number:
                type: string
                nullable: true
              status:
                type: string
                enum: [allocated, reserved, sold, cancelled]
        status:
          type: string
          enum: [allocated, reserved, sold, expired, cancelled, retracted]
          description: |
            - allocated: Tickets assigned, not yet claimed by recipient
            - reserved: Recipient started claiming (info filled, pending payment)
            - sold: Recipient completed claiming (paid)
            - expired: Passed expiration without claiming (admin must cancel)
            - cancelled: Cancelled by admin
            - retracted: Recalled by parent quota holder
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        claimed_at:
          type: string
          format: date-time
          nullable: true
        retracted_at:
          type: string
          format: date-time
          nullable: true

    SubquotaCreate:
      type: object
      required: [recipient_email, recipient_name]
      properties:
        recipient_email:
          type: string
          format: email
        recipient_name:
          type: string
          maxLength: 255
        ticket_ids:
          type: array
          items:
            type: string
            format: uuid
          description: |
            For numbered (seated) quotas: specific ticket IDs to delegate.
            Required for seated quotas.
        quantity:
          type: integer
          minimum: 1
          description: |
            For unnumbered (standing) quotas: number of tickets to delegate.
            Required for standing quotas, ignored for seated.
        expiration_days:
          type: integer
          minimum: 1
          maximum: 30
          default: 7

    # ----------------------------------------
    # Admin Schemas
    # ----------------------------------------

    AdminMatchSummary:
      allOf:
        - $ref: '#/components/schemas/MatchSummary'
        - type: object
          properties:
            tickets_sold:
              type: integer
            tickets_available:
              type: integer
            revenue:
              type: number
              format: decimal
            created_at:
              type: string
              format: date-time

    AdminMatchDetail:
      allOf:
        - $ref: '#/components/schemas/AdminMatchSummary'
        - type: object
          properties:
            description:
              type: string
              description: Match description or additional info displayed to fans
            important_info:
              type: string
              description: Important information text displayed on the mobile app match page
            banner_image_url:
              type: string
              format: uri
              description: URL to uploaded match banner image
            sales_phases:
              type: array
              items:
                $ref: '#/components/schemas/SalesPhase'
            inventory_summary:
              $ref: '#/components/schemas/InventorySummary'
            quota_summary:
              type: object
              properties:
                total_quotas:
                  type: integer
                total_allocated:
                  type: integer
                total_claimed:
                  type: integer

    MatchCreate:
      type: object
      required: [home_team, away_team, competition, kick_off_time, venue_id]
      properties:
        home_team:
          type: string
          maxLength: 255
        away_team:
          type: string
          maxLength: 255
        competition:
          type: string
          maxLength: 255
        kick_off_time:
          type: string
          format: date-time
        venue_id:
          type: string
          format: uuid
        description:
          type: string
          maxLength: 2000
          description: Match description or additional info
        important_info:
          type: string
          maxLength: 2000
          description: Important information text for the mobile app match page

    MatchUpdate:
      type: object
      properties:
        home_team:
          type: string
        away_team:
          type: string
        competition:
          type: string
        kick_off_time:
          type: string
          format: date-time
        description:
          type: string
          maxLength: 2000
          description: Match description or additional info
        important_info:
          type: string
          maxLength: 2000
          description: Important information text for the mobile app match page

    SalesPhase:
      type: object
      properties:
        id:
          type: string
          format: uuid
        phase_type:
          type: string
          enum: [loyalty, general_public, quota_only]
        name:
          type: string
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
        ticket_limit:
          type: integer
        is_active:
          type: boolean
        eligibility_rules:
          type: object
          properties:
            min_loyalty_points:
              type: integer

    SalesPhaseCreate:
      type: object
      required: [phase_type, name, start_at, end_at]
      properties:
        phase_type:
          type: string
          enum: [loyalty, general_public, quota_only]
        name:
          type: string
          maxLength: 100
        start_at:
          type: string
          format: date-time
        end_at:
          type: string
          format: date-time
        ticket_limit:
          type: integer
          minimum: 1
          maximum: 4
          default: 4
        eligibility_rules:
          type: object
          properties:
            min_loyalty_points:
              type: integer

    SeatInventory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        seat_id:
          type: string
          format: uuid
        sector:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        row:
          type: string
        seat_number:
          type: string
        status:
          type: string
          enum: [available, reserved, sold, allocated, blocked, technical, official, maintenance, quarantined, inactive]
        ticket_id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        quota_id:
          type: string
          format: uuid
        last_modified_at:
          type: string
          format: date-time
        last_modified_by:
          type: string

    InventorySummary:
      type: object
      properties:
        total_capacity:
          type: integer
        available:
          type: integer
        reserved:
          type: integer
        sold:
          type: integer
        allocated:
          type: integer
        blocked:
          type: integer
        technical:
          type: integer
        by_sector:
          type: array
          items:
            type: object
            properties:
              sector_id:
                type: string
                format: uuid
              sector_name:
                type: string
              total:
                type: integer
              available:
                type: integer
              sold:
                type: integer

    # ----------------------------------------
    # Stadium & Match Configuration Schemas
    # ----------------------------------------

    StadiumTemplate:
      type: object
      description: Base stadium template for reuse across matches
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: "Maksimir - Standard"
        city:
          type: string
          example: "Zagreb"
        country:
          type: string
          example: "HR"
        type:
          type: string
          enum: [home, away]
        total_capacity:
          type: integer
          readOnly: true
          description: Auto-calculated from sector seat counts
        sectors_count:
          type: integer
        has_seat_map:
          type: boolean
        created_at:
          type: string
          format: date-time

    StadiumTemplateDetail:
      allOf:
        - $ref: '#/components/schemas/StadiumTemplate'
        - type: object
          properties:
            address:
              type: string
            sectors:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                  code:
                    type: string
                  capacity:
                    type: integer
                    readOnly: true
                    description: Calculated from seat map when present
                  has_seat_map:
                    type: boolean

    StadiumTemplateCreate:
      type: object
      required: [name, city, country, type]
      properties:
        name:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        country:
          type: string
          maxLength: 2
          description: ISO country code
        type:
          type: string
          enum: [home, away]
        address:
          type: string
          maxLength: 500

    # ----------------------------------------
    # Template Sector Schemas (E2-F3, E2-F10)
    # ----------------------------------------

    TemplateSector:
      type: object
      description: Sector within a stadium template
      properties:
        id:
          type: string
          format: uuid
        stadium_id:
          type: string
          format: uuid
        name:
          type: string
          example: "Zapad Dolje"
        code:
          type: string
          example: "ZD"
        capacity:
          type: integer
        sector_type:
          type: string
          enum: [standard, vip, accessibility, standing]
        has_seat_map:
          type: boolean
        seat_count:
          type: integer
          description: Actual seat count from seat map (may differ from capacity)
        created_at:
          type: string
          format: date-time

    TemplateSectorDetail:
      allOf:
        - $ref: '#/components/schemas/TemplateSector'
        - type: object
          properties:
            seat_map:
              $ref: '#/components/schemas/SectorSeatMap'
            snake_config:
              $ref: '#/components/schemas/SectorSnakeConfig'

    TemplateSectorCreate:
      type: object
      required: [name, code]
      properties:
        name:
          type: string
          maxLength: 100
        code:
          type: string
          maxLength: 10
        capacity:
          type: integer
          minimum: 1
          description: Optional estimated capacity for reference. Actual capacity is calculated from seat map when present.
        sector_type:
          type: string
          enum: [standard, vip, accessibility, standing]
          default: standard

    TemplateSectorUpdate:
      type: object
      properties:
        name:
          type: string
          maxLength: 100
        code:
          type: string
          maxLength: 10
        capacity:
          type: integer
          minimum: 1
          description: Optional estimated capacity for reference. Actual capacity is calculated from seat map when present.
        sector_type:
          type: string
          enum: [standard, vip, accessibility, standing]

    # ----------------------------------------
    # Seat Map Configuration Schemas (E2-F10)
    # ----------------------------------------

    SectorSeatMap:
      type: object
      description: |
        Visual seat map configuration stored as 2D JSON.
        Admin draws seats on canvas, gaps are represented by missing entries.
      properties:
        id:
          type: string
          format: uuid
        sector_id:
          type: string
          format: uuid
        dimensions:
          type: object
          properties:
            rows:
              type: integer
              description: Maximum number of rows
            cols:
              type: integer
              description: Maximum seats per row
        seats:
          type: array
          description: Array of seat positions (gaps are missing entries)
          items:
            $ref: '#/components/schemas/SeatMapEntry'
        row_labels:
          type: array
          description: Custom row labels (e.g., ["A", "B", "C"] or ["1", "2", "3"])
          items:
            type: string
        numbering_direction:
          type: string
          enum: [LTR, RTL]
          description: Left-to-Right or Right-to-Left seat numbering
        total_seats:
          type: integer
        configuration_status:
          type: string
          enum: [draft, active]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SeatMapEntry:
      type: object
      description: Individual seat in the 2D seat map grid
      properties:
        row:
          type: integer
          description: Row index (0-based grid position)
        col:
          type: integer
          description: Column index (0-based grid position)
        type:
          type: string
          enum: [standard, technical, accessibility, companion]
          default: standard
        number:
          type: string
          description: Display seat number (auto-generated or custom)

    SectorSeatMapCreate:
      type: object
      description: Create seat map from visual canvas editor
      required: [dimensions, seats]
      properties:
        dimensions:
          type: object
          required: [rows, cols]
          properties:
            rows:
              type: integer
              minimum: 1
              maximum: 100
            cols:
              type: integer
              minimum: 1
              maximum: 100
        seats:
          type: array
          items:
            type: object
            required: [row, col]
            properties:
              row:
                type: integer
                minimum: 0
              col:
                type: integer
                minimum: 0
              type:
                type: string
                enum: [standard, technical, accessibility, companion]
                default: standard
        row_labels:
          type: array
          items:
            type: string
          description: Optional custom row labels
        numbering_direction:
          type: string
          enum: [LTR, RTL]
          default: LTR

    Seat:
      type: object
      description: Individual seat record
      properties:
        id:
          type: string
          format: uuid
        sector_id:
          type: string
          format: uuid
        row_identifier:
          type: string
          example: "A"
        seat_number:
          type: string
          example: "12"
        seat_type:
          type: string
          enum: [standard, technical, accessibility, companion]
        created_at:
          type: string
          format: date-time

    # ----------------------------------------
    # Snake Algorithm Configuration (E4-F3)
    # ----------------------------------------

    SectorSnakeConfig:
      type: object
      description: Configuration for snake algorithm seat assignment
      properties:
        sector_id:
          type: string
          format: uuid
        best_row_index:
          type: integer
          description: Starting row for assignment (1 = closest to field)
          default: 1
        traversal_direction:
          type: string
          enum: [TOP_TO_BOTTOM, BOTTOM_TO_TOP]
          description: Direction to fill rows
          default: BOTTOM_TO_TOP
        within_row_strategy:
          type: string
          enum: [LEFT_TO_RIGHT, RIGHT_TO_LEFT]
          description: How to fill seats within a row
          default: LEFT_TO_RIGHT
        subsector_grouping_json:
          type: object
          description: Optional subsector groupings for complex layouts

    SectorSnakeConfigUpdate:
      type: object
      properties:
        best_row_index:
          type: integer
          minimum: 1
        traversal_direction:
          type: string
          enum: [TOP_TO_BOTTOM, BOTTOM_TO_TOP]
        within_row_strategy:
          type: string
          enum: [LEFT_TO_RIGHT, RIGHT_TO_LEFT]
        subsector_grouping_json:
          type: object

    MatchStadiumConfig:
      type: object
      description: Stadium configuration specific to a match
      properties:
        match_id:
          type: string
          format: uuid
        stadium_name:
          type: string
        city:
          type: string
        country:
          type: string
        match_type:
          type: string
          enum: [home, away]
        is_numbered:
          type: boolean
          description: True if match has seat maps, false for non-numbered
        total_capacity:
          type: integer
        sellable_capacity:
          type: integer
        sectors:
          type: array
          items:
            $ref: '#/components/schemas/MatchSector'
        price_categories:
          type: array
          items:
            $ref: '#/components/schemas/PriceCategory'
        configuration_status:
          type: string
          enum: [draft, validated, locked]
        validated_at:
          type: string
          format: date-time
        locked_at:
          type: string
          format: date-time

    MatchSector:
      type: object
      description: Sector configuration for a specific match
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: "Zapad Dolje"
        code:
          type: string
          example: "ZD"
        total_capacity:
          type: integer
        technical_seats:
          type: integer
          description: Non-sellable seats (cameras, security, etc.)
        sellable_capacity:
          type: integer
        availability:
          type: string
          enum: [active, closed, maintenance]
        purpose:
          type: string
          enum: [general, away_fans, vip, press, free]
          description: |
            - general: Standard public sale
            - away_fans: Reserved for away team supporters
            - vip: VIP/hospitality area
            - press: Media/press area
            - free: Free entry (VAT exempt)
        price_category_id:
          type: string
          format: uuid
        price_category_name:
          type: string
        price:
          type: number
          format: decimal
        has_seat_map:
          type: boolean
        is_numbered:
          type: boolean
        inventory_summary:
          type: object
          properties:
            available:
              type: integer
            reserved:
              type: integer
            sold:
              type: integer
            blocked:
              type: integer
            quota_allocated:
              type: integer

    MatchSectorCreate:
      type: object
      description: Create sector for away match
      required: [name, total_capacity]
      properties:
        name:
          type: string
          maxLength: 100
          example: "Away Stand Block C"
        code:
          type: string
          maxLength: 10
        total_capacity:
          type: integer
          minimum: 1
        technical_seats:
          type: integer
          default: 0
          description: Non-sellable seats
        is_numbered:
          type: boolean
          default: false
          description: False for non-numbered/standing sectors
        price_category_id:
          type: string
          format: uuid

    MatchSectorUpdate:
      type: object
      properties:
        availability:
          type: string
          enum: [active, closed, maintenance]
        purpose:
          type: string
          enum: [general, away_fans, vip, press, free]
        price_category_id:
          type: string
          format: uuid
        technical_seats:
          type: integer
          description: Update non-sellable seat count

    PriceCategory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: "Category 1"
        price:
          type: number
          format: decimal
          example: 50.00
        color:
          type: string
          description: Hex color for map visualization
          example: "#FF5733"
        sectors_count:
          type: integer
          description: Number of sectors using this category

    PriceCategoryCreate:
      type: object
      required: [name, price]
      properties:
        name:
          type: string
          maxLength: 50
        price:
          type: number
          format: decimal
          minimum: 0
        color:
          type: string
          pattern: '^#[0-9A-Fa-f]{6}$'

    AdminQuotaSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        match:
          type: object
          properties:
            id:
              type: string
              format: uuid
            home_team:
              type: string
            away_team:
              type: string
            match_date:
              type: string
              format: date-time
        recipient_name:
          type: string
        recipient_email:
          type: string
        internal_note:
          type: string
          description: Admin-only note (not visible to quota holder)
        total_quantity:
          type: integer
        allocated_quantity:
          type: integer
          description: Tickets available (not yet claimed or delegated)
        reserved_quantity:
          type: integer
          description: Tickets with holder info but pending payment
        sold_quantity:
          type: integer
          description: Tickets fully claimed and paid
        delegated_quantity:
          type: integer
          description: Tickets assigned to subquotas
        can_create_subquotas:
          type: boolean
        transfer_allowed:
          type: boolean
          description: Whether tickets can be transferred after claiming
        deferred_payment:
          type: boolean
        auto_send_email:
          type: boolean
        allocation_algorithm:
          type: string
          enum: [NM, REDOM]
        status:
          type: string
          enum: [pending, active, past_deadline, cancelled, fully_claimed]
        expiration_date:
          type: string
          format: date
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
          description: Admin user who created the quota

    AdminQuotaDetail:
      allOf:
        - $ref: '#/components/schemas/AdminQuotaSummary'
        - type: object
          properties:
            is_numbered:
              type: boolean
              description: True for seated sectors, false for standing
            tickets:
              type: array
              description: All tickets allocated to this quota with their current status
              items:
                $ref: '#/components/schemas/AdminQuotaTicket'
            sector_summary:
              type: array
              description: Summary counts by sector (for overview)
              items:
                type: object
                properties:
                  sector_id:
                    type: string
                    format: uuid
                  sector_name:
                    type: string
                  allocated_count:
                    type: integer
                  available_count:
                    type: integer
                  claimed_count:
                    type: integer
            discount_percent:
              type: number
            allocation_algorithm:
              type: string
              enum: [NM, REDOM]
            can_create_subquotas:
              type: boolean
            deferred_payment:
              type: boolean
            subquotas:
              type: array
              items:
                $ref: '#/components/schemas/Subquota'
            created_by:
              type: string
            batch_id:
              type: string
              format: uuid

    AdminQuotaTicket:
      type: object
      description: Ticket details for admin quota view
      properties:
        id:
          type: string
          format: uuid
        sector:
          type: string
        row:
          type: string
          nullable: true
        seat_number:
          type: string
          nullable: true
        price:
          type: number
          format: decimal
        discounted_price:
          type: number
          format: decimal
        status:
          type: string
          enum: [available, reserved, claimed, transferred, cancelled]
        claimed_by:
          type: object
          nullable: true
          description: Ticket holder info if claimed
          properties:
            name:
              type: string
            oib_last4:
              type: string
            email:
              type: string
        claimed_at:
          type: string
          format: date-time
          nullable: true
        order_id:
          type: string
          nullable: true
          description: Associated order ID if claimed

    QuotaCreate:
      type: object
      required: [match_id, recipient_email, recipient_name, quantity, sector_ids, expiration_date]
      properties:
        match_id:
          type: string
          format: uuid
        recipient_email:
          type: string
          format: email
        recipient_name:
          type: string
          maxLength: 255
        internal_note:
          type: string
          maxLength: 500
          description: Admin-only note (e.g., "VIP Sponsor Allocation") - not visible to quota holder
        quantity:
          type: integer
          minimum: 1
        sector_ids:
          type: array
          items:
            type: string
            format: uuid
          minItems: 1
        discount_percent:
          type: number
          minimum: 0
          maximum: 100
          default: 0
          description: Discount percentage (0=full price, 100=free/GR)
        allocation_algorithm:
          type: string
          enum: [NM, REDOM]
          default: NM
          description: |
            - NM (Najbolja Mjesta): Best available seats
            - REDOM: Distributed across the sector
        expiration_date:
          type: string
          format: date
          description: Visual reminder date (no auto-expiry - admin must cancel manually)
        can_create_subquotas:
          type: boolean
          default: false
          description: If true, quota holder can delegate tickets to others
        transfer_allowed:
          type: boolean
          default: true
          description: |
            Whether tickets from this quota can be transferred after claiming.
            YES = unlimited reassignment (for players/VIPs)
            NO = standard rules (support only, 48h deadline)
        deferred_payment:
          type: boolean
          default: false
          description: If true, payment can be deferred (invoice later)
        auto_send_email:
          type: boolean
          default: true
          description: If true, email invitation sent immediately upon creation

    BlacklistEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        oib:
          type: string
          description: Required - 11-digit Croatian personal ID
        first_name:
          type: string
          description: Optional
        last_name:
          type: string
          description: Optional
        status:
          type: string
          enum: [active, removed]
        source:
          type: string
        created_at:
          type: string
          format: date-time
        created_by:
          type: string
        removed_at:
          type: string
          format: date-time

    BlacklistCreate:
      type: object
      required: [oib]
      properties:
        oib:
          type: string
          pattern: '^\d{11}$'
          description: Required - 11-digit Croatian personal ID
        first_name:
          type: string
          maxLength: 255
          description: Optional
        last_name:
          type: string
          maxLength: 255
          description: Optional
        source:
          type: string
        auto_cancel_tickets:
          type: boolean
          default: true

    ViolationLog:
      type: object
      properties:
        id:
          type: string
          format: uuid
        oib:
          type: string
        blacklist_entry:
          type: object
          properties:
            first_name:
              type: string
            last_name:
              type: string
        action_type:
          type: string
          enum: [purchase_attempt, transfer_attempt]
        match:
          type: object
          properties:
            id:
              type: string
              format: uuid
            home_team:
              type: string
            away_team:
              type: string
        user_id:
          type: string
          format: uuid
        ip_address:
          type: string
        blocked_at:
          type: string
          format: date-time

    AdminTicketDetail:
      allOf:
        - $ref: '#/components/schemas/TicketDetail'
        - type: object
          properties:
            order:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                order_number:
                  type: string
                buyer_email:
                  type: string
            holder:
              type: object
              properties:
                full_name:
                  type: string
                date_of_birth:
                  type: string
                  format: date
                oib_masked:
                  type: string
                email:
                  type: string
            transfer_history:
              type: array
              items:
                type: object
                properties:
                  from_holder:
                    type: string
                  to_holder:
                    type: string
                  transferred_at:
                    type: string
                    format: date-time
                  transfer_type:
                    type: string

    SalesReport:
      type: object
      properties:
        period:
          type: object
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        summary:
          type: object
          properties:
            total_orders:
              type: integer
            total_tickets:
              type: integer
            total_revenue:
              type: number
              format: decimal
            total_fees:
              type: number
              format: decimal
            total_refunds:
              type: number
              format: decimal
        by_match:
          type: array
          items:
            type: object
            properties:
              match_id:
                type: string
                format: uuid
              match_name:
                type: string
              tickets_sold:
                type: integer
              revenue:
                type: number
                format: decimal
        by_sector:
          type: array
          items:
            type: object
            properties:
              sector_name:
                type: string
              tickets_sold:
                type: integer
              revenue:
                type: number
                format: decimal

    MatchDashboard:
      type: object
      properties:
        match:
          $ref: '#/components/schemas/MatchSummary'
        live_stats:
          type: object
          properties:
            tickets_sold:
              type: integer
            tickets_available:
              type: integer
            revenue:
              type: number
              format: decimal
            active_carts:
              type: integer
            queue_size:
              type: integer
            sales_rate_per_minute:
              type: number
        by_sector:
          type: array
          items:
            type: object
            properties:
              sector_name:
                type: string
              capacity:
                type: integer
              sold:
                type: integer
              available:
                type: integer
              fill_percentage:
                type: number
        recent_sales:
          type: array
          items:
            type: object
            properties:
              order_number:
                type: string
              tickets:
                type: integer
              amount:
                type: number
              timestamp:
                type: string
                format: date-time
