openapi: 3.1.0
info:
  title: Eventro REST API
  version: 1.0.0
  description: |
    Production-grade REST API allowing third-party platforms (churches, businesses, ticketing apps) 
    to manage events, ticket inventory, attendee registrations, server-authoritative Paystack payments, 
    and signed webhooks under their own branding.
servers:
  - url: https://eventro.africa/api/v1
    description: Production API
  - url: http://localhost:3000/api/v1
    description: Local Development

security:
  - BearerAuth: []

paths:
  /me:
    get:
      summary: Integration profile and scopes
      operationId: getMe
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Active integration credentials and organization info.

  /events:
    get:
      summary: List events
      operationId: listEvents
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [all, published, draft, past, upcoming] }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
      responses:
        '200':
          description: Paginated collection of events.
    post:
      summary: Create an event
      operationId: createEvent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventInput'
      responses:
        '201':
          description: Event successfully created.

  /events/{id}:
    get:
      summary: Retrieve an event
      operationId: getEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Event details.
    patch:
      summary: Update an event
      operationId: updateEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EventInput'
      responses:
        '200':
          description: Updated event.
    delete:
      summary: Delete or archive an event
      operationId: deleteEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204':
          description: Deleted or archived.

  /events/{id}/publish:
    post:
      summary: Publish an event
      operationId: publishEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Event published.

  /events/{id}/cancel:
    post:
      summary: Cancel an event
      operationId: cancelEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        '200':
          description: Event cancelled.

  /events/{id}/duplicate:
    post:
      summary: Duplicate an event and ticket structure
      operationId: duplicateEvent
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '201':
          description: Duplicate draft event created.

  /events/{id}/stats:
    get:
      summary: Event ticket sales and revenue metrics
      operationId: getEventStats
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Aggregate statistics.

  /public/events/{slug_or_id}:
    get:
      summary: Cached public event read (unauthenticated)
      operationId: getPublicEvent
      security: []
      parameters:
        - name: slug_or_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Public event data with sanitized organizer profile. `url` is the public event page; `tickets` lists ticket types.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Event'

  /events/{event_id}/ticket_types:
    get:
      summary: List ticket types for an event
      operationId: listTicketTypes
      parameters:
        - name: event_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Paginated ticket types (`{object:list,data,meta}`). Prices are in the main currency unit (NGN).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TicketTypeList'
    post:
      summary: Create a ticket type
      operationId: createTicketType
      parameters:
        - name: event_id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TicketInput'
      responses:
        '201':
          description: Created ticket type.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TicketType'

  /events/{event_id}/registrations:
    get:
      summary: List registrations for an event
      operationId: listRegistrations
      parameters:
        - name: event_id
          in: path
          required: true
          schema: { type: string }
        - name: status
          in: query
          schema: { type: string, enum: [all, confirmed, pending_payment, cancelled] }
      responses:
        '200':
          description: Paginated registrations (`{object:list,data,meta}`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegistrationList'
    post:
      summary: Create an order / reservation
      operationId: createRegistration
      parameters:
        - name: event_id
          in: path
          required: true
          schema: { type: string }
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegistrationInput'
      responses:
        '201':
          description: Registration created and capacity reserved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Registration'

  /registrations/{id}:
    get:
      summary: Get registration details
      operationId: getRegistration
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Registration details with tickets.

  /registrations/{id}/check_in:
    post:
      summary: Check in attendee
      operationId: checkInAttendee
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Attendee checked in with timestamp.

  /registrations/{id}/undo_check_in:
    post:
      summary: Undo attendee check-in
      operationId: undoCheckIn
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Check-in reversed.

  /registrations/{id}/ticket_qr:
    get:
      summary: Retrieve standalone SVG QR code for admission
      operationId: getTicketQr
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Raw SVG barcode or JSON payload.
          content:
            image/svg+xml:
              schema: { type: string }

  /registrations/{id}/payment:
    post:
      summary: Initialize server-authoritative Paystack payment
      operationId: initializePayment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                callback_url: { type: string }
      responses:
        '201':
          description: Payment session with Paystack authorization URL.

  /registrations/{id}/payment/verify:
    post:
      summary: Verify Paystack transaction and confirm order
      operationId: verifyPayment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Payment verified and registration confirmed.

  /registrations/{id}/payment/simulate:
    post:
      summary: Instant test-mode payment simulation (sandbox events only)
      operationId: simulatePayment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                outcome: { type: string, enum: [success, failed] }
      responses:
        '200':
          description: Simulated payment status.

  /webhook_endpoints:
    get:
      summary: List configured webhook endpoints
      operationId: listWebhookEndpoints
      responses:
        '200':
          description: List of endpoints.
    post:
      summary: Register webhook endpoint
      operationId: createWebhookEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, enabled_events]
              properties:
                url: { type: string, format: uri }
                description: { type: string }
                enabled_events:
                  type: array
                  items: { type: string }
      responses:
        '201':
          description: Endpoint registered; reveals raw signing secret once.

  /webhook_endpoints/{id}/rotate_secret:
    post:
      summary: Rotate signing secret for webhook endpoint
      operationId: rotateWebhookSecret
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Returns new raw secret.

  /webhook_endpoints/{id}/test:
    post:
      summary: Send test ping event to webhook endpoint
      operationId: testWebhookEndpoint
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '202':
          description: Test delivery enqueued.

  /webhook_deliveries/{id}/redeliver:
    post:
      summary: Re-deliver a failed or past webhook event
      operationId: redeliverWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '202':
          description: Redelivery job enqueued with identical event ID.

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: evt_live_... or evt_test_...

  schemas:
    # Request bodies are wrapped under a root key: {"event": {...}}, {"ticket_type": {...}}, {"registration": {...}}.
    EventInput:
      type: object
      required: [event]
      properties:
        event:
          type: object
          required: [name, event_start_date, category_id]
          properties:
            name: { type: string }
            description: { type: string }
            category_id: { type: integer }
            event_type: { type: string }
            language: { type: string }
            event_start_date: { type: string, format: date }
            event_end_date: { type: string, format: date }
            event_start_time: { type: string, description: "HH:MM" }
            event_end_time: { type: string, description: "HH:MM" }
            event_location: { type: string }
            gps_location: { type: string }
            virtual_meeting_url: { type: string, format: uri }
            redirect_url: { type: string, format: uri }
            theme_color: { type: string }
            image_data_uri: { type: string, description: "Base64 data URI image (or send `image` as a multipart file upload)" }
            tags: { type: array, items: { type: string }, description: "Up to 20" }
            registration:
              type: object
              properties:
                enabled: { type: boolean }
                opens_at: { type: string, format: date-time }
            venues_attributes: { type: array, items: { type: object } }
            speakers_attributes: { type: array, items: { type: object } }
            sessions_attributes: { type: array, items: { type: object } }

    TicketInput:
      type: object
      required: [ticket_type]
      properties:
        ticket_type:
          type: object
          required: [type, name, quantity]
          properties:
            type: { type: string, enum: [paid, free, donation], description: "Immutable after creation" }
            name: { type: string, description: "Stored as the ticket's description" }
            price: { type: number, description: "Paid tickets; main currency unit (NGN), not kobo" }
            minimum_donation: { type: number, description: "Donation tickets; main currency unit" }
            quantity: { type: integer }
            discount_percentage: { type: number }
            early_bird_discount: { type: number }
            discount_end_date: { type: string, format: date }
            discount_end_time: { type: string }
            min_per_order: { type: integer, default: 1 }
            max_per_order: { type: integer, default: 10 }
            sale_starts_at: { type: string, format: date-time }
            sale_ends_at: { type: string, format: date-time }

    RegistrationInput:
      type: object
      required: [registration]
      properties:
        registration:
          type: object
          required: [ticket_type_id, attendee_first_name, attendee_last_name, attendee_email]
          properties:
            ticket_type_id: { type: string, description: "Ticket type `id` (legacy alias `ticket_id` also accepted)" }
            quantity: { type: integer, default: 1 }
            donation_amount: { type: number, description: "Donation tickets only; main currency unit" }
            attendee_first_name: { type: string }
            attendee_last_name: { type: string }
            attendee_email: { type: string, format: email }
            attendee_phone: { type: string }
            attendee_address: { type: string }
            attendee_gender: { type: string }
            date_of_birth: { type: string, format: date }
            attendee_country: { type: string }
            attendee_state: { type: string }
            attendee_city: { type: string }
            custom_field_responses: { type: object }

    # ---- Response schemas (mirror Api::V1::Serializer / Webhooks::Payloads) ----------------
    # Money: every amount is a JSON number in the MAIN currency unit (e.g. 2500.0 = NGN 2,500.00,
    # not kobo). The currency is always "NGN".

    ListMeta:
      type: object
      properties:
        page: { type: integer }
        per_page: { type: integer }
        total_count: { type: integer }
        total_pages: { type: integer }
        has_more: { type: boolean }

    TicketType:
      type: object
      properties:
        id: { type: string, example: tkt_abc123 }
        public_id: { type: string }
        object: { type: string, enum: [ticket_type] }
        type: { type: string, enum: [paid, free, donation] }
        name: { type: string, description: "The ticket's description" }
        price: { type: number, description: "Main currency unit; 0.0 for free/donation tickets" }
        currency: { type: string, enum: [NGN] }
        minimum_donation: { type: [number, "null"] }
        early_bird_active: { type: boolean }
        effective_price: { type: number, description: "Price a buyer pays now (early-bird aware); main currency unit" }
        quantity: { type: integer }
        remaining: { type: integer }
        sold_out: { type: boolean }
        on_sale: { type: boolean }
        sale_starts_at: { type: [string, "null"], format: date-time }
        sale_ends_at: { type: [string, "null"], format: date-time }
        min_per_order: { type: integer }
        max_per_order: { type: integer }
        enabled: { type: boolean, description: "Private view only" }
        archived: { type: boolean, description: "Private view only" }
        event_id: { type: string, description: "Private view only" }
        created_at: { type: string, format: date-time, description: "Private view only" }
        updated_at: { type: string, format: date-time, description: "Private view only" }
        inventory:
          type: object
          description: Included by the ticket_types endpoints
          properties:
            total: { type: integer }
            sold: { type: integer }
            reserved: { type: integer }
            remaining: { type: integer }

    TicketTypeList:
      type: object
      properties:
        object: { type: string, enum: [list] }
        data:
          type: array
          items: { $ref: '#/components/schemas/TicketType' }
        meta: { $ref: '#/components/schemas/ListMeta' }

    Event:
      type: object
      properties:
        id: { type: string }
        public_id: { type: string }
        object: { type: string, enum: [event] }
        name: { type: string }
        slug: { type: string }
        description: { type: string }
        event_type: { type: string }
        start_date: { type: string, format: date-time }
        end_date: { type: string, format: date-time }
        location: { type: string }
        image_url: { type: [string, "null"] }
        url:
          type: string
          description: "Public event page, shaped https://<host>/events/<numeric id>-<slug>. Use this field instead of building the URL."
        published: { type: boolean }
        cancelled: { type: boolean }
        livemode: { type: boolean }
        tickets:
          type: array
          items: { $ref: '#/components/schemas/TicketType' }

    Registration:
      type: object
      description: An order / registration. `amount` values are in the main currency unit.
      properties:
        id: { type: string }
        public_id: { type: string }
        object: { type: string, enum: [registration] }
        event_id: { type: string }
        ticket_type_id: { type: [string, "null"] }
        status: { type: string }
        payment_status: { type: string }
        quantity: { type: integer }
        amount:
          type: object
          properties:
            subtotal: { type: number }
            fees: { type: number }
            total: { type: number }
            currency: { type: string, enum: [NGN] }
        total_price: { type: number }
        payment_reference: { type: [string, "null"] }
        ticket_code: { type: [string, "null"] }
        checked_in: { type: boolean }
        checked_in_at: { type: [string, "null"], format: date-time }
        livemode: { type: boolean }
        expires_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        attendee:
          type: object
          description: Omitted when the key lacks PII access
          properties:
            first_name: { type: string }
            last_name: { type: string }
            email: { type: string }
            phone: { type: string }
        payment_required: { type: boolean, description: "Create response only" }
        next_action: { type: [string, "null"], description: "Create response only" }

    RegistrationList:
      type: object
      properties:
        object: { type: string, enum: [list] }
        data:
          type: array
          items: { $ref: '#/components/schemas/Registration' }
        meta: { $ref: '#/components/schemas/ListMeta' }

    WebhookEnvelope:
      type: object
      description: |
        POST body of every webhook. Headers: Eventro-Event-Id, Eventro-Event-Type, Eventro-Delivery-Id,
        Eventro-Delivery-Attempt, Eventro-Signature (t=<unix>,v1=<hmac-sha256>).
        Payloads are intentionally minimal (no attendee PII); fetch details via the API using the ids.
      properties:
        id: { type: string, description: "Event id; identical across retries and redeliveries (use for de-duplication)" }
        type: { type: string, example: payment.succeeded }
        api_version: { type: string }
        created_at: { type: string, format: date-time }
        livemode: { type: boolean }
        data:
          type: object
          properties:
            object:
              description: "Event, ticket_type, registration or refund object depending on `type`"
              oneOf:
                - $ref: '#/components/schemas/WebhookRegistrationObject'
                - $ref: '#/components/schemas/WebhookTicketTypeObject'
                - $ref: '#/components/schemas/WebhookEventObject'

    WebhookRegistrationObject:
      type: object
      description: "For registration.*, order.created, payment.*, ticket.*, attendee.updated. Null fields are omitted."
      properties:
        id: { type: string }
        object: { type: string, enum: [registration] }
        event_id: { type: string }
        ticket_type_id: { type: string }
        status: { type: string }
        payment_status: { type: string }
        quantity: { type: integer }
        amount: { type: number, description: "Total, main currency unit (flat number here, unlike the API object)" }
        currency: { type: string, enum: [NGN] }
        payment_reference: { type: string }
        ticket_code: { type: string, description: "Only on ticket.* and payment.succeeded" }
        checked_in: { type: boolean }

    WebhookTicketTypeObject:
      type: object
      description: "For ticket_type.*"
      properties:
        id: { type: string }
        object: { type: string, enum: [ticket_type] }
        event_id: { type: string }
        name: { type: string }
        quantity: { type: integer }
        remaining: { type: integer }
        enabled: { type: boolean }
        archived: { type: boolean }

    WebhookEventObject:
      type: object
      description: "For event.*"
      properties:
        id: { type: string }
        object: { type: string, enum: [event] }
        name: { type: string }
        published: { type: boolean }
        cancelled: { type: boolean }
        start_date: { type: string, format: date-time }
