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

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

x-tagGroups:
  - name: Club
    tags:
      - clubs
      - integrations
      - wallet

tags:
  - name: clubs
    x-displayName: "Clubs"
    description: Operations related to club management
  - name: integrations
    x-displayName: "Integrations"
    description: Operations related to 3rd party integration management
  - name: wallet
    x-displayName: "Wallet"
    description: Operations related to club wallet and transactions

paths:
  /club/v1/new:
    post:
      tags:
        - clubs
      summary: Create a new club
      description: Creates a new club with the provided information
      operationId: createClub
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateClubRequest'
      responses:
        '201':
          description: Club created successfully
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                properties:
                  id:
                    type: string
                    example: "550e8400-e29b-41d4-a716-446655440000"
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '422':
          description: Unprocessable entity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/search:
    get:
      tags:
        - clubs
      summary: Search clubs
      description: Search for clubs with optional filters
      operationId: searchClubs
      parameters:
        - in: query
          name: term
          example: Home padel
          schema:
            type: string
        - in: query
          name: city
          example: Amsterdam
          schema:
            type: string
        - in: query
          name: country
          example: NL
          description: ISO 3166-1 alpha-2 country code
          schema:
            type: string
            minLength: 2
            maxLength: 2
        - in: query
          name: geoloc_lat
          example: 52.3534646
          description: User geolocation latitude for searching nearby clubs
          schema:
            type: number
            format: float
            minimum: -90
            maximum: 90
        - in: query
          name: geoloc_lng
          example: 4.9037305
          description: User geolocation longitude for searching nearby clubs
          schema:
            type: number
            format: float
            minimum: -180
            maximum: 180
        - in: query
          name: geoloc_radius
          example: 20
          description: >
            Radius (in km) for searching nearby clubs. Only works when geoloc_lat and geoloc_lng are also set
          schema:
            type: integer
            minimum: 1
        - in: query
          name: open_at
          example: "14:30"
          description: >
            Filter clubs open at the specified time. Use the player's current local time in HH:MM format (24-hour).
            Example: "14:30"
          schema:
            type: string
            pattern: '^([01]?[0-9]|2[0-3]):[0-5][0-9]$'
        - in: query
          name: offset
          example: 0
          description: Results offset (useful for pagination)
          schema:
            type: integer
            minimum: 0
            default: 0
        - in: query
          name: limit
          example: 20
          description: Results limit (useful for pagination)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      security:
        - OAuth2:
            - urn:pup:club:read
        - PAT: []
      responses:
        '200':
          description: The search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchClubsResponse'
        '401':
          description: Unauthorized
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/club/{id}:
    get:
      tags:
        - clubs
      summary: Get club by ID
      description: Returns the club information for the specified ID
      operationId: getClubById
      parameters:
        - in: path
          name: id
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:read
        - PAT: []
      responses:
        '200':
          description: The club information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClubResponse'
        '404':
          description: Club not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: "Club not found"
        '401':
          description: Unauthorized
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    patch:
      tags:
        - clubs
      summary: Update club
      description: Updates club information
      operationId: updateClub
      parameters:
        - in: path
          name: id
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateClubRequest'
      responses:
        '204':
          description: Club updated successfully
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '404':
          description: Club not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: Unprocessable entity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/logo:
    put:
      tags:
        - clubs
      summary: Upload club logo
      description: Upload a new logo for the club
      operationId: uploadClubLogo
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                club_id:
                  type: string
                  description: The club ID
                logo:
                  type: string
                  format: binary
                  description: The logo file to upload (JPG, PNG, WebP)
              required:
                - club_id
                - logo
      responses:
        '204':
          description: Logo uploaded successfully
        '400':
          description: Invalid input or file too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '404':
          description: Club not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Uploading logo failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/clubs/{club_id}/integrations/playtomic/validate:
    post:
      tags:
        - integrations
      summary: Validate Playtomic credentials without persisting (dry run)
      description: Tests Playtomic credentials without creating an integration
      operationId: validatePlaytomicIntegration
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidatePlaytomicRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidatePlaytomicResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - user is not a manager of this club
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/clubs/{club_id}/integrations/playtomic:
    post:
      tags:
        - integrations
      summary: Connect Playtomic integration
      description: Creates a new Playtomic integration for the club
      operationId: connectPlaytomicIntegration
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectPlaytomicRequest'
      responses:
        '201':
          description: Integration connected successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectPlaytomicResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '422':
          description: Unprocessable entity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    put:
      tags:
        - integrations
      summary: Update Playtomic integration credentials
      description: Updates the Playtomic integration credentials
      operationId: updatePlaytomicIntegration
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePlaytomicRequest'
      responses:
        '200':
          description: Integration credentials updated
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '404':
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      tags:
        - integrations
      summary: Disconnect Playtomic integration
      description: Disconnects and revokes the Playtomic integration
      operationId: disconnectPlaytomicIntegration
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club identifier
          schema:
            type: string
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisconnectPlaytomicRequest'
      responses:
        '204':
          description: Integration disconnected successfully
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
        '404':
          description: Integration not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/clubs/{club_id}/wallet:
    get:
      tags:
        - wallet
      summary: Get club wallet
      description: Returns the club's wallet balance and details. Requires user to be a club manager.
      operationId: getClubWallet
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
            minLength: 1
            maxLength: 64
          example: "550e8400-e29b-41d4-a716-446655440000"
      responses:
        '200':
          description: Wallet details retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WalletResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - User is not a manager of this club
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Club wallet not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /club/v1/clubs/{club_id}/wallet/transactions:
    get:
      tags:
        - wallet
      summary: Get club wallet transactions
      description: Returns the club's wallet transaction history with pagination and filtering. Requires user to be a club manager.
      operationId: getClubWalletTransactions
      security:
        - OAuth2:
            - urn:pup:club:manage
        - PAT: []
      parameters:
        - name: club_id
          in: path
          required: true
          description: Club ID
          schema:
            type: string
            minLength: 1
            maxLength: 64
          example: "550e8400-e29b-41d4-a716-446655440000"
        - name: transaction_type
          in: query
          required: false
          description: Filter by transaction type
          schema:
            type: string
            enum: [spend_credits, purchase_credits, refund, transfer, challenge_escrow_lock, challenge_escrow_release]
          example: "spend_credits"
        - name: offset
          in: query
          required: false
          description: Offset for pagination (default 0)
          schema:
            type: integer
            minimum: 0
            default: 0
          example: 0
        - name: limit
          in: query
          required: false
          description: Limit for pagination (default 20, max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          example: 20
      responses:
        '200':
          description: Transactions retrieved successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionsResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - User is not a manager of this club
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        default:
          description: Generic error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          example: "An error message"
      description: Generic error response

    Address:
      type: object
      required:
        - street
        - city
        - postal_code
        - country
      properties:
        street:
          type: string
          minLength: 3
          maxLength: 200
          example: "Main Street 123"
        city:
          type: string
          minLength: 2
          maxLength: 100
          example: "Amsterdam"
        postal_code:
          type: string
          minLength: 2
          maxLength: 20
          example: "1012 AB"
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code
          example: "NL"
          enum: [ AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AU, AT, AZ, BS, BH, BD, BB, BY, BE, BZ, BJ, BM, BT, BO, BQ, BA, BW, BV, BR, IO, BN, BG, BF, BI, CV, KH, CM, CA, KY, CF, TD, CL, CN, CX, CC, CO, KM, CG, CD, CK, CR, CI, HR, CU, CW, CY, CZ, DK, DJ, DM, DO, EC, EG, SV, GQ, ER, EE, SZ, ET, FK, FO, FJ, FI, FR, GF, PF, TF, GA, GM, GE, DE, GH, GI, GR, GL, GD, GP, GU, GT, GG, GN, GW, GY, HT, HM, VA, HN, HK, HU, IS, IN, ID, IR, IQ, IE, IM, IL, IT, JM, JP, JE, JO, KZ, KE, KI, KP, KR, KW, KG, LA, LV, LB, LS, LR, LY, LI, LT, LU, MO, MG, MW, MY, MV, ML, MT, MH, MQ, MR, MU, YT, MX, FM, MD, MC, MN, ME, MS, MA, MZ, MM, NA, NR, NP, NL, NC, NZ, NI, NE, NG, NU, NF, MK, MP, "NO", OM, PK, PW, PS, PA, PG, PY, PE, PH, PN, PL, PT, PR, QA, RE, RO, RU, RW, BL, SH, KN, LC, MF, PM, VC, WS, SM, ST, SA, SN, RS, SC, SL, SG, SX, SK, SI, SB, SO, ZA, GS, SS, ES, LK, SD, SR, SJ, SE, CH, SY, TW, TJ, TZ, TH, TL, TG, TK, TO, TT, TN, TR, TM, TC, TV, UG, UA, AE, GB, US, UM, UY, UZ, VU, VE, VN, VG, VI, WF, EH, YE, ZM, ZW ]
        geolocation:
          type: object
          nullable: true
          description: Optional geolocation coordinates
          required:
            - latitude
            - longitude
          properties:
            latitude:
              type: number
              format: float
              minimum: -90
              maximum: 90
              description: Latitude coordinate
              example: 52.3534646
            longitude:
              type: number
              format: float
              minimum: -180
              maximum: 180
              description: Longitude coordinate
              example: 4.9037305

    Contact:
      type: object
      required:
        - name
        - email
        - phone
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
          example: "John Doe"
        email:
          type: string
          format: email
          example: "contact@example.com"
        phone:
          type: string
          minLength: 7
          maxLength: 20
          pattern: '^[0-9\s\-()+]+$'
          example: "+31 20 1234567"
        website:
          type: string
          pattern: '^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$'
          nullable: true
          example: "https://www.example.com"

    DaySchedule:
      type: object
      required:
        - opening_time
        - closing_time
      properties:
        opening_time:
          type: string
          pattern: '^([01]?[0-9]|2[0-3]):[0-5][0-9]$'
          example: "08:00"
          description: "Opening time in HH:MM format (24-hour)"
        closing_time:
          type: string
          pattern: '^([01]?[0-9]|2[0-3]):[0-5][0-9]$'
          example: "22:00"
          description: "Closing time in HH:MM format (24-hour)"

    WeeklySchedule:
      type: object
      description: "Weekly opening hours schedule. Days not specified are considered closed."
      properties:
        monday:
          $ref: '#/components/schemas/DaySchedule'
        tuesday:
          $ref: '#/components/schemas/DaySchedule'
        wednesday:
          $ref: '#/components/schemas/DaySchedule'
        thursday:
          $ref: '#/components/schemas/DaySchedule'
        friday:
          $ref: '#/components/schemas/DaySchedule'
        saturday:
          $ref: '#/components/schemas/DaySchedule'
        sunday:
          $ref: '#/components/schemas/DaySchedule'
      example:
        monday:
          opening_time: "08:00"
          closing_time: "22:00"
        tuesday:
          opening_time: "08:00"
          closing_time: "22:00"
        friday:
          opening_time: "08:00"
          closing_time: "23:00"

    CreateClubRequest:
      type: object
      required:
        - name
        - address
        - contact
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
          example: "Home Padel Club"
        description:
          type: string
          nullable: true
          example: "The best padel club in the city"
        address:
          $ref: '#/components/schemas/Address'
        contact:
          $ref: '#/components/schemas/Contact'
        number_of_courts:
          type: integer
          minimum: 0
          maximum: 100
          nullable: true
          example: 4
          description: "Number of courts available"
        weekly_schedule:
          $ref: '#/components/schemas/WeeklySchedule'

    UpdateClubRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
          example: "Home Padel Club"
        description:
          type: string
          nullable: true
          example: "The best padel club in the city"
        address:
          $ref: '#/components/schemas/Address'
        contact:
          $ref: '#/components/schemas/Contact'
        number_of_courts:
          type: integer
          minimum: 0
          maximum: 100
          nullable: true
          example: 4
          description: "Number of courts available"
        weekly_schedule:
          $ref: '#/components/schemas/WeeklySchedule'

    Club:
      type: object
      required:
        - id
        - name
        - address
        - contact
      properties:
        id:
          type: string
          example: "550e8400-e29b-41d4-a716-446655440000"
        name:
          type: string
          example: "Home Padel Club"
        description:
          type: string
          nullable: true
          example: "The best padel club in the city"
        logo_url:
          type: string
          format: uri
          nullable: true
          example: "https://cdn.example.com/club/550e8400-e29b-41d4-a716-446655440000.png"
        address:
          $ref: '#/components/schemas/Address'
        contact:
          $ref: '#/components/schemas/Contact'
        number_of_courts:
          type: integer
          minimum: 0
          maximum: 100
          nullable: true
          example: 4
          description: "Number of courts available"
        weekly_schedule:
          $ref: '#/components/schemas/WeeklySchedule'

    ResponseMetaMany:
      type: object
      required:
        - pagination
      properties:
        pagination:
          type: object
          required:
            - total
            - returned
            - offset
            - limit
          properties:
            total:
              type: integer
              description: Total number of matching records
              example: 145
            returned:
              type: integer
              description: Number of records actually returned in this response
              example: 20
            offset:
              type: integer
              description: Offset used in the request
              example: 0
            limit:
              type: integer
              description: Limit used in the request
              example: 20
            next:
              type: string
              description: Relative path to next page of results
              example: /club/v1/search?term=something&offset=20&limit=20
            previous:
              type: string
              description: Relative path to previous page of results
              example: /club/v1/search?term=something&offset=0&limit=20

    ClubResponse:
      type: object
      required:
        - club
      properties:
        club:
          $ref: '#/components/schemas/Club'

    SearchClubsResponse:
      type: object
      required:
        - hits
        - meta
      properties:
        hits:
          type: array
          items:
            $ref: '#/components/schemas/Club'
        meta:
          $ref: '#/components/schemas/ResponseMetaMany'

    ValidatePlaytomicRequest:
      type: object
      required:
        - client_id
        - secret
        - venue_id
      properties:
        client_id:
          type: string
          description: Playtomic client ID
        secret:
          type: string
          description: Playtomic secret
        venue_id:
          type: string
          description: Playtomic venue ID

    ValidatePlaytomicResponse:
      type: object
      required:
        - valid
      properties:
        valid:
          type: boolean
          description: Whether credentials are valid
        venue_name:
          type: string
          nullable: true
          description: Venue name from Playtomic (if valid)
        venue_id:
          type: string
          nullable: true
          description: Venue ID from Playtomic (if valid)
        error_message:
          type: string
          nullable: true
          description: Error message (if invalid)

    ConnectPlaytomicRequest:
      type: object
      required:
        - client_id
        - secret
        - venue_id
      properties:
        client_id:
          type: string
          description: Playtomic client ID
        secret:
          type: string
          description: Playtomic secret
        venue_id:
          type: string
          description: Playtomic venue ID

    ConnectPlaytomicResponse:
      type: object
      required:
        - integration_id
      properties:
        integration_id:
          type: string
          description: ID of the created integration (format club:integration:{type}:{club_id})

    UpdatePlaytomicRequest:
      type: object
      required:
        - client_id
        - secret
      properties:
        client_id:
          type: string
          description: New Playtomic client ID
        secret:
          type: string
          description: New Playtomic secret
        venue_id:
          type: string
          nullable: true
          description: Optional - update venue ID

    DisconnectPlaytomicRequest:
      type: object
      properties: {}

    WalletResponse:
      type: object
      required:
        - wallet
      properties:
        wallet:
          $ref: '#/components/schemas/Wallet'

    Wallet:
      type: object
      required:
        - account_id
        - balance
        - currency
        - status
        - opened_at
      properties:
        account_id:
          type: string
          description: Unique identifier for the wallet account
          example: "club:550e8400-e29b-41d4-a716-446655440000"
        balance:
          type: integer
          description: Current balance in credits
          example: 5000
        currency:
          type: string
          description: Currency code
          example: "PUP"
        status:
          type: string
          description: Account status
          example: "open"
        opened_at:
          type: string
          format: date-time
          description: Timestamp when the wallet was opened
          example: "2024-01-01T00:00:00Z"
        closed_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the wallet was closed (if applicable)
          example: null

    TransactionsResponse:
      type: object
      required:
        - hits
        - meta
      properties:
        hits:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'
          description: Array of transactions
        meta:
          $ref: '#/components/schemas/ResponseMetaMany'

    Transaction:
      type: object
      required:
        - transaction_id
        - type
        - amount
        - counterparty_account_id
        - description
        - created_at
      properties:
        transaction_id:
          type: string
          description: Unique transaction identifier
          example: "tx-abc123"
        type:
          type: string
          description: Type of transaction
          example: "spend_credits"
        amount:
          type: integer
          description: Transaction amount (positive for credit, negative for debit)
          example: -100
        counterparty_account_id:
          type: string
          description: The account ID of the other party in the transaction (where funds came from or went to)
          example: "user:550e8400-e29b-41d4-a716-446655440000"
        description:
          type: string
          description: Human-readable transaction description
          example: "Sent 100 credits to Player"
        created_at:
          type: string
          format: date-time
          description: Timestamp when the transaction was recorded
          example: "2024-01-15T10:30:00Z"

  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: "https://oauth2.gopup.app/oauth2/auth"
          tokenUrl: "https://oauth2.gopup.app/oauth2/token"
          scopes:
            urn:pup:club:read: "Read club information"
            urn:pup:club:manage: "Manage clubs"
      description: OAuth2

    PAT:
      type: http
      scheme: bearer
      bearerFormat: PAT
      description: Personal Access Token
