openapi: 3.0.3
info:
  title: PUP - CRM API v1
  description: >
    PUP is the universal player profile for padel. This API provides CRM contact management and player segmentation functionality.
  version: 1.0.0

servers:
  - url: https://api.gopup.app
    description: Development server

x-tagGroups:
  - name: CRM
    tags:
      - contacts
      - segments

tags:
  - name: contacts
    x-displayName: "Contacts"
    description: Operations related to CRM contact management
  - name: segments
    x-displayName: "Segments"
    description: Operations related to CRM player segmentation

security:
  - OAuth2:
      - urn:pup:crm:read
  - PAT: []

paths:
  /crm/v1/clubs/{club_id}/contacts:
    get:
      tags:
        - contacts
      operationId: searchContacts
      summary: Search club CRM contacts
      description: Returns a paginated list of CRM contacts for a specific club.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: search_term
          in: query
          required: false
          description: Search term matching contact name, email, or phone
          schema:
            type: string
        - name: consent_email_status
          in: query
          required: false
          description: Filter by email consent status
          schema:
            type: string
            enum:
              - granted
              - revoked
        - name: lesson_interest
          in: query
          required: false
          description: Filter by lesson interest
          schema:
            type: boolean
        - name: after_id
          in: query
          required: false
          description: Return contacts newer/after this contact_id (exclusive)
          schema:
            type: string
        - name: before_id
          in: query
          required: false
          description: Return contacts older/before this contact_id (exclusive)
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Number of records to return (max 100)
          schema:
            type: integer
            format: int32
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Contact search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchContactsResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/contacts/{contact_id}:
    get:
      tags:
        - contacts
      operationId: getContact
      summary: Get contact details
      description: Returns full details for a CRM contact.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: contact_id
          in: path
          required: true
          description: Contact ID
          schema:
            type: string
      responses:
        '200':
          description: Contact details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetContactResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    delete:
      tags:
        - contacts
      operationId: deleteContact
      summary: Delete contact (GDPR removal)
      description: Soft deletes a CRM contact and redacts personal identifiable information.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: contact_id
          in: path
          required: true
          description: Contact ID
          schema:
            type: string
      responses:
        '200':
          description: Contact deleted successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/contacts/{contact_id}/consent:
    put:
      tags:
        - contacts
      operationId: changeContactConsent
      summary: Update channel consent status
      description: Updates the consent status (granted or revoked) for a specific communication channel.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: contact_id
          in: path
          required: true
          description: Contact ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangeContactConsentRequest'
      responses:
        '200':
          description: Consent updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/contacts/{contact_id}/preferences:
    put:
      tags:
        - contacts
      operationId: updateContactPreferences
      summary: Update contact play preferences
      description: Updates lesson interest and preferred play time slots for a contact.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: contact_id
          in: path
          required: true
          description: Contact ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContactPreferencesRequest'
      responses:
        '200':
          description: Preferences updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/contacts/{contact_id}/auto-sync:
    put:
      tags:
        - contacts
      operationId: updateContactAutoSync
      summary: Update contact auto-sync status
      description: Enables or disables automatic CRM contact synchronization with third-party providers (e.g. Playtomic).
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: contact_id
          in: path
          required: true
          description: Contact ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContactAutoSyncRequest'
      responses:
        '200':
          description: Auto-sync status updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/clubs/{club_id}/segments:
    post:
      tags:
        - segments
      operationId: createSegment
      summary: Create a new segment
      description: Creates a player segment with filter criteria for a club.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSegmentRequest'
      responses:
        '200':
          description: Segment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSegmentResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      tags:
        - segments
      operationId: searchSegments
      summary: Search player segments
      description: Returns a paginated list of player segments for a club.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: search_term
          in: query
          required: false
          description: Filter segments by name
          schema:
            type: string
        - name: after_id
          in: query
          required: false
          description: Return segments after segment_id
          schema:
            type: string
        - name: before_id
          in: query
          required: false
          description: Return segments before segment_id
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Number of records to return
          schema:
            type: integer
            format: int32
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Segments search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchSegmentsResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/clubs/{club_id}/segments/{segment_id}:
    get:
      tags:
        - segments
      operationId: getSegment
      summary: Get segment details
      description: Returns details of a specific segment.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: segment_id
          in: path
          required: true
          description: Segment ID
          schema:
            type: string
      responses:
        '200':
          description: Segment details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetSegmentResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    put:
      tags:
        - segments
      operationId: renameSegment
      summary: Rename segment
      description: Updates the name and description of a segment.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: segment_id
          in: path
          required: true
          description: Segment ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RenameSegmentRequest'
      responses:
        '200':
          description: Segment renamed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    delete:
      tags:
        - segments
      operationId: deleteSegment
      summary: Delete segment
      description: Deletes a segment.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: segment_id
          in: path
          required: true
          description: Segment ID
          schema:
            type: string
      responses:
        '200':
          description: Segment deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/clubs/{club_id}/segments/{segment_id}/criteria:
    put:
      tags:
        - segments
      operationId: updateSegmentCriteria
      summary: Update segment filter criteria
      description: Updates the target filter criteria of a segment.
      security:
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
        - name: segment_id
          in: path
          required: true
          description: Segment ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateSegmentCriteriaRequest'
      responses:
        '200':
          description: Criteria updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Segment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/clubs/{club_id}/segments/eligible-contacts/count:
    post:
      tags:
        - segments
      operationId: countSegmentEligibleContacts
      summary: Count eligible contacts for segment criteria
      description: Calculates the total count of contacts matching a saved segment or raw criteria.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CountSegmentEligibleContactsRequest'
      responses:
        '200':
          description: Count result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountSegmentEligibleContactsResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /crm/v1/clubs/{club_id}/segments/eligible-contacts/preview:
    post:
      tags:
        - segments
      operationId: previewSegmentContacts
      summary: Preview contacts for segment criteria
      description: Returns a paginated list of contacts matching a saved segment or raw criteria.
      security:
        - OAuth2:
            - urn:pup:crm:read
        - OAuth2:
            - urn:pup:crm:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewSegmentContactsRequest'
      responses:
        '200':
          description: Preview contacts list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreviewSegmentContactsResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://oauth2.gopup.app/oauth2/auth
          tokenUrl: https://oauth2.gopup.app/oauth2/token
          scopes:
            urn:pup:crm:read: Read CRM contacts
            urn:pup:crm:manage: Manage CRM contacts
    PAT:
      type: http
      scheme: bearer
      bearerFormat: PAT
      description: Personal Access Token

  schemas:
    PlayTime:
      type: object
      required:
        - day_of_week
        - time_slot
      properties:
        day_of_week:
          type: string
          example: "monday"
          enum:
            - monday
            - tuesday
            - wednesday
            - thursday
            - friday
            - saturday
            - sunday
        time_slot:
          type: string
          example: "morning"
          enum:
            - morning
            - afternoon
            - evening

    PupScoreRange:
      type: object
      required:
        - min
        - max
      properties:
        min:
          type: number
          format: double
          minimum: 0
          maximum: 100
        max:
          type: number
          format: double
          minimum: 0
          maximum: 100

    Sport:
      type: string
      enum:
        - padel
        - tennis
        - pickleball
        - squash
        - badminton
        - table_tennis

    PupScoreItem:
      type: object
      required:
        - sport
        - score
      properties:
        sport:
          $ref: '#/components/schemas/Sport'
        score:
          type: number
          format: double

    AgeRange:
      type: object
      required:
        - min
        - max
      properties:
        min:
          type: integer
          format: int32
          minimum: 0
        max:
          type: integer
          format: int32
          minimum: 0

    SegmentCriteria:
      type: object
      properties:
        pup_score_range:
          $ref: '#/components/schemas/PupScoreRange'
        age_range:
          $ref: '#/components/schemas/AgeRange'
        gender:
          type: string
          enum:
            - male
            - female
        inactivity_days:
          type: integer
          format: int32
          minimum: 0
        preferred_play_times:
          type: array
          items:
            $ref: '#/components/schemas/PlayTime'
        lesson_interest:
          type: boolean

    Segment:
      type: object
      required:
        - id
        - club_id
        - name
        - description
        - criteria
        - created_at
        - updated_at
      properties:
        id:
          type: string
        club_id:
          type: string
        name:
          type: string
        description:
          type: string
        criteria:
          $ref: '#/components/schemas/SegmentCriteria'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreateSegmentRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string
        criteria:
          $ref: '#/components/schemas/SegmentCriteria'

    CreateSegmentResponse:
      type: object
      required:
        - id
      properties:
        id:
          type: string

    RenameSegmentRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        description:
          type: string

    UpdateSegmentCriteriaRequest:
      type: object
      required:
        - criteria
      properties:
        criteria:
          $ref: '#/components/schemas/SegmentCriteria'

    SearchSegmentsResponse:
      type: object
      required:
        - hits
        - meta
      properties:
        hits:
          type: array
          items:
            $ref: '#/components/schemas/Segment'
        meta:
          $ref: '#/components/schemas/SeekCollectionMeta'

    GetSegmentResponse:
      type: object
      required:
        - segment
      properties:
        segment:
          $ref: '#/components/schemas/Segment'

    CountSegmentEligibleContactsRequest:
      type: object
      properties:
        segment_id:
          type: string
        criteria:
          $ref: '#/components/schemas/SegmentCriteria'
        channel:
          type: string
          enum:
            - email
            - whatsapp
            - push

    CountSegmentEligibleContactsResponse:
      type: object
      required:
        - count
      properties:
        count:
          type: integer
          format: int64

    PreviewSegmentContactsRequest:
      type: object
      properties:
        segment_id:
          type: string
        criteria:
          $ref: '#/components/schemas/SegmentCriteria'
        channel:
          type: string
          enum:
            - email
            - whatsapp
            - push
        after_id:
          type: string
        before_id:
          type: string
        limit:
          type: integer
          format: int32
          minimum: 1
          maximum: 100

    ContactPreviewItem:
      type: object
      required:
        - id
        - name
        - email
        - phone
        - age
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        phone:
          type: string
        gender:
          type: string
          nullable: true
        pup_score_padel:
          type: number
          format: double
          nullable: true
        age:
          type: integer
          format: int32

    PreviewSegmentContactsResponse:
      type: object
      required:
        - contacts
        - pagination
      properties:
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/ContactPreviewItem'
        pagination:
          $ref: '#/components/schemas/SeekPagination'

    ConsentDetails:
      type: object
      required:
        - status
        - source
      properties:
        status:
          type: string
          enum:
            - granted
            - revoked
        given_at:
          type: string
          format: date-time
          nullable: true
        source:
          type: string
          example: "web_form"

    ContactListItem:
      type: object
      required:
        - id
        - name
        - email
        - phone
        - lesson_interest
        - auto_sync_enabled
        - created_at
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        phone:
          type: string
        gender:
          type: string
          nullable: true
          enum:
            - male
            - female
            - other
        date_of_birth:
          type: string
          nullable: true
          format: date
          example: "1990-01-01"
        lesson_interest:
          type: boolean
        auto_sync_enabled:
          type: boolean
        pup_scores:
          type: array
          items:
            $ref: '#/components/schemas/PupScoreItem'
        created_at:
          type: string
          format: date-time
        last_booking_activity_at:
          type: string
          format: date-time
          nullable: true

    Contact:
      type: object
      required:
        - id
        - club_id
        - name
        - email
        - phone
        - lesson_interest
        - preferred_play_times
        - auto_sync_enabled
        - created_at
        - segments
      properties:
        id:
          type: string
        club_id:
          type: string
        user_id:
          type: string
          nullable: true
        name:
          type: string
        email:
          type: string
        phone:
          type: string
        gender:
          type: string
          nullable: true
          enum:
            - male
            - female
            - other
        date_of_birth:
          type: string
          nullable: true
          format: date
          example: "1990-01-01"
        lesson_interest:
          type: boolean
        preferred_play_times:
          type: array
          items:
            $ref: '#/components/schemas/PlayTime'
        auto_sync_enabled:
          type: boolean
        consent_email:
          $ref: '#/components/schemas/ConsentDetails'
          nullable: true
        consent_whatsapp:
          $ref: '#/components/schemas/ConsentDetails'
          nullable: true
        consent_push:
          $ref: '#/components/schemas/ConsentDetails'
          nullable: true
        pup_scores:
          type: array
          items:
            $ref: '#/components/schemas/PupScoreItem'
        segments:
          type: array
          items:
            $ref: '#/components/schemas/Segment'
        created_at:
          type: string
          format: date-time
        last_booking_activity_at:
          type: string
          format: date-time
          nullable: true
        user_profile:
          $ref: '#/components/schemas/ContactUserProfile'
          nullable: true

    ContactUserProfile:
      type: object
      required:
        - username
      properties:
        username:
          type: string
          example: "johndoe"
        picture_url:
          type: string
          format: uri
          nullable: true
          example: "https://cdn.gopup.app/images/profile.png"
        card_image_url:
          type: string
          format: uri
          nullable: true
          example: "https://cdn.gopup.app/cards/card.png"
        profile_url:
          type: string
          format: uri
          nullable: true
          example: "https://gopup.app/u/johndoe"
        pup_score:
          type: integer
          nullable: true
          minimum: 0
          example: 65

    SearchContactsResponse:
      type: object
      required:
        - hits
        - meta
      properties:
        hits:
          type: array
          items:
            $ref: '#/components/schemas/ContactListItem'
        meta:
          $ref: '#/components/schemas/SeekCollectionMeta'

    GetContactResponse:
      type: object
      required:
        - contact
      properties:
        contact:
          $ref: '#/components/schemas/Contact'

    ChangeContactConsentRequest:
      type: object
      required:
        - channel
        - status
        - source
      properties:
        channel:
          type: string
          enum:
            - email
            - whatsapp
            - push
        status:
          type: string
          enum:
            - granted
            - revoked
        source:
          type: string
          minLength: 1

    UpdateContactPreferencesRequest:
      type: object
      required:
        - lesson_interest
        - preferred_play_times
      properties:
        lesson_interest:
          type: boolean
        preferred_play_times:
          type: array
          items:
            $ref: '#/components/schemas/PlayTime'

    UpdateContactAutoSyncRequest:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: Whether automatic sync is enabled for this contact

    SuccessResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          example: true

    SeekCollectionMeta:
      type: object
      required:
        - total
        - count
        - pagination
      properties:
        total:
          $ref: '#/components/schemas/CollectionTotal'
        count:
          type: integer
          format: int32
        pagination:
          $ref: '#/components/schemas/SeekPagination'

    SeekPagination:
      type: object
      properties:
        next_id:
          type: string
          nullable: true

    CollectionTotal:
      type: object
      required:
        - value
        - relation
      properties:
        value:
          type: integer
          format: int64
        relation:
          type: string
          enum:
            - exact
            - gte

    Error:
      type: object
      required:
        - error
        - code
      properties:
        error:
          type: string
          example: "An error message"
        code:
          type: string
          enum:
            - MALFORMED_REQUEST
            - INVALID_INPUT
            - UNAUTHORIZED
            - INSUFFICIENT_SCOPE
            - ACCESS_DENIED
            - RESOURCE_NOT_FOUND
            - RESOURCE_EXISTS
            - CONFLICT
            - ACTION_BLOCKED
            - INTERNAL_ERROR
        details:
          type: string
          nullable: true
          example: "Optional context or validation details"
      description: Generic error response
