openapi: 3.1.0
info:
  title: 口コミ広場 外部連携 API（OAuth 2.0 + OpenID Connect）
  version: 0.3.0-draft
  description: |
    口コミ広場（report.clinic）が提供する外部サービス連携用 API です。
    W/Beauty などの提携アプリが、ユーザーの同意のもとで予約・施術・口コミデータを取得するために利用します。

    認証は OAuth 2.0 Authorization Code Grant + PKCE (S256) + OpenID Connect (OIDC) Core 1.0 に基づきます。
    一方向（口コミ広場 → 連携先）の Pull 型 API を提供します。

    ## OIDC 準拠

    - `scope` に `openid` を含めると、`/api/v1/oauth/token` のレスポンスに `id_token`（JWT, RS256）が含まれます
    - `id_token` の署名鍵は AWS KMS Asymmetric Key (RSA_2048) で管理し、公開鍵は `/.well-known/jwks.json` で配布します
    - メタデータは `/.well-known/openid-configuration` から取得できます（OIDC Discovery 1.0）

    ## URL 設計

    - 公開 API はすべて `/api/v1/` プレフィックス配下に配置（v1 バージョニング）
    - `/.well-known/openid-configuration` および `/.well-known/jwks.json` は OIDC 仕様によりバージョンなしの固定パス

    ## データの提供単位

    - ユーザー基本情報（`/api/v1/user/me`）：OIDC 標準 claim 準拠
    - 予約データ（`/api/v1/reservations`）：未来予約 / 過去予約 / キャンセル含む
    - 施術データ（`/api/v1/treatments`）：確定済みの実績支払い（`activityPayments` テーブル起点）
    - 口コミデータ（`/api/v1/reviews`）：公開済み実績レポート

    クリニック情報・施術メニュー情報は予約 / 施術データに埋め込みで返します。

    ## 本仕様書はドラフトです
    先方との協議で随時更新します。データ項目の必須／任意は今後変動する可能性があります。

  contact:
    name: 株式会社口コミ広場
    url: https://report.clinic

servers:
  - url: https://api.report.clinic
    description: 本番環境（仮）
  - url: https://test-b.api.report.clinic
    description: ステージング環境（test-b）

tags:
  - name: OIDC Discovery
    description: OpenID Connect Discovery / JWKS
  - name: OAuth
    description: OAuth 2.0 + OIDC 認証・認可エンドポイント
  - name: User
    description: 認可済みユーザー情報（OIDC userinfo）
  - name: Reservations
    description: 予約データ（appointment テーブル起点）
  - name: Treatments
    description: 施術データ（activityPayments テーブル起点）
  - name: Reviews
    description: 口コミデータ（report_real テーブル起点）

security:
  - OAuth2: []

paths:
  /.well-known/openid-configuration:
    get:
      tags: [OIDC Discovery]
      summary: OIDC Discovery メタデータ
      description: |
        OpenID Connect Discovery 1.0 準拠のメタデータを返します。
        クライアントはこの URL を読むだけで、authorization / token / userinfo / jwks の各エンドポイントを発見できます。
      security: []
      responses:
        "200":
          description: 成功
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=3600
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OidcDiscoveryMetadata"

  /.well-known/jwks.json:
    get:
      tags: [OIDC Discovery]
      summary: JSON Web Key Set
      description: |
        id_token の署名検証用公開鍵を返します。
        active な鍵と直近 retired の鍵を並列で含め、key rotation 中の旧 id_token も検証可能にします。
      security: []
      responses:
        "200":
          description: 成功
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=3600
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JwksResponse"

  /api/v1/oauth/authorize:
    get:
      tags: [OAuth]
      summary: 認可エンドポイント（同意画面）
      description: |
        ユーザーをこのエンドポイントにリダイレクトすることで、ログイン → 同意 → 認可コードを取得します。
        本エンドポイントは UI（HTML）を返すため、OpenAPI のスキーマで完全には表現できません。
        OAuth 2.0 (RFC 6749) Section 4.1.1 および OIDC Core 1.0 Section 3.1.2 に準拠します。
      security: []
      parameters:
        - name: response_type
          in: query
          required: true
          schema:
            type: string
            enum: [code]
        - name: client_id
          in: query
          required: true
          schema:
            type: string
          example: wbeauty_client
        - name: redirect_uri
          in: query
          required: true
          schema:
            type: string
            format: uri
          example: https://app.wbeauty.example.com/oauth/callback
        - name: scope
          in: query
          required: true
          description: |
            スペース区切りのスコープ。`openid` を含めると id_token が発行されます。
            OIDC 標準: openid / profile / email / phone / address
            独自: reservations:read / treatments:read / reviews:read
          schema:
            type: string
          example: openid profile email treatments:read reviews:read
        - name: state
          in: query
          required: true
          description: CSRF 対策用のランダム文字列
          schema:
            type: string
        - name: code_challenge
          in: query
          required: true
          description: PKCE の code_challenge（Base64URL(SHA256(code_verifier))）
          schema:
            type: string
        - name: code_challenge_method
          in: query
          required: true
          schema:
            type: string
            enum: [S256]
        - name: nonce
          in: query
          required: false
          description: OIDC nonce。id_token に埋め込まれ、リプレイ攻撃対策に使用します
          schema:
            type: string
        - name: prompt
          in: query
          required: false
          description: |
            UI の挙動制御：
            - `consent`: 同意画面を強制表示
            - `login`: 再認証を強制
            - `none`: UI 表示なし（既存セッションが使えなければエラー）
          schema:
            type: string
            enum: [consent, login, none]
        - name: max_age
          in: query
          required: false
          description: ユーザーが認証されてから N 秒以上経過していたら再認証を強制
          schema:
            type: integer
            minimum: 0
        - name: ui_locales
          in: query
          required: false
          description: UI 表示言語（BCP 47）。例：`ja-JP`
          schema:
            type: string
          example: ja-JP
        - name: response_mode
          in: query
          required: false
          description: レスポンス形式。初期実装は `query` のみ
          schema:
            type: string
            enum: [query]
            default: query
      responses:
        "302":
          description: |
            認可成功時は `redirect_uri` に code と state をクエリパラメータとして付与してリダイレクトします。
            実際のレスポンスは HTTP 302 リダイレクトで、以下のプロパティは `redirect_uri?code=<code>&state=<state>` の形式で URL に設定されます。
          content:
            application/x-www-form-urlencoded:
              schema:
                type: object
                required: [code, state]
                properties:
                  code:
                    type: string
                    description: 認可コード（ワンタイム、10 分有効）。`/api/v1/oauth/token` でアクセストークンと交換する
                    example: AUTHORIZATION_CODE
                  state:
                    type: string
                    description: リクエスト時に送信した state がそのまま返されます。CSRF 対策のため必ず一致を検証してください
                    example: STATE
        "400":
          description: パラメータ不正
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"

  /api/v1/oauth/token:
    post:
      tags: [OAuth]
      summary: トークンエンドポイント
      description: |
        認可コードまたはリフレッシュトークンを access_token に交換します。
        `scope` に `openid` を含む場合、`id_token`（JWT, RS256）も同時に返却されます。
        OAuth 2.0 (RFC 6749) Section 4.1.3 / Section 6 および OIDC Core 1.0 Section 3.1.3 に準拠。
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - $ref: "#/components/schemas/AuthorizationCodeGrantRequest"
                - $ref: "#/components/schemas/RefreshTokenGrantRequest"
      responses:
        "200":
          description: トークン発行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenResponse"
        "400":
          description: パラメータ不正 / 認証失敗
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"
        "401":
          description: client_id / client_secret 不一致
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuthError"

  /api/v1/oauth/revoke:
    post:
      tags: [OAuth]
      summary: トークン失効エンドポイント
      description: |
        access_token または refresh_token を即時無効化します。
        OAuth 2.0 (RFC 7009) に準拠。
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [token, client_id, client_secret]
              properties:
                token:
                  type: string
                token_type_hint:
                  type: string
                  enum: [access_token, refresh_token]
                client_id:
                  type: string
                client_secret:
                  type: string
      responses:
        "200":
          description: 失効成功（既に失効している場合も 200 を返す）

  /api/v1/oauth/userinfo:
    get:
      tags: [User]
      summary: OIDC userinfo エンドポイント
      description: |
        access_token に紐づくユーザーの基本情報を OIDC 標準 claim 形式で返します。
        付与された scope に応じて含まれる claim が変わります。
        OIDC Core 1.0 Section 5.3 に準拠。
      security:
        - OAuth2: [openid]
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserInfo"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/user/me:
    get:
      tags: [User]
      summary: 認可済みユーザー情報（OIDC userinfo の別名）
      description: |
        `/api/v1/oauth/userinfo` と同じレスポンスを返します。直感的な API パスとして提供。
      security:
        - OAuth2: [openid]
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserInfo"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/reservations:
    get:
      tags: [Reservations]
      summary: 予約一覧の取得
      description: |
        認可ユーザーの予約データを取得します。`appointment` テーブルが起点です。
        `updated_since` を指定することで差分取得が可能です。
        論理削除済み（valid=0）のレコードは含みません。
      security:
        - OAuth2: [reservations:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          required: false
          description: ステータスでフィルタ（複数指定可、カンマ区切り）
          schema:
            type: string
          example: confirmed,completed
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedReservations"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/reservations/{id}:
    get:
      tags: [Reservations]
      summary: 予約詳細の取得
      security:
        - OAuth2: [reservations:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: apt_abc123
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Reservation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/treatments:
    get:
      tags: [Treatments]
      summary: 施術（実績）一覧の取得
      description: |
        認可ユーザーの確定済み施術データを取得します。`activityPayments` テーブルが起点です。
        `updated_since` を指定することで差分取得が可能です。
        論理削除済み（valid=0）および取り消し済み（status=2）のレコードは含みません。
      security:
        - OAuth2: [treatments:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Limit"
        - name: accept_status
          in: query
          required: false
          description: ユーザー承認ステータスでフィルタ
          schema:
            type: string
            enum: [pending, accepted, amend_requested]
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedTreatments"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/treatments/{id}:
    get:
      tags: [Treatments]
      summary: 施術詳細の取得
      security:
        - OAuth2: [treatments:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: trt_def456
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Treatment"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/reviews:
    get:
      tags: [Reviews]
      summary: 口コミ一覧の取得
      description: |
        認可ユーザーが投稿した公開済み口コミ（実績レポート）を取得します。
      security:
        - OAuth2: [reviews:read]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedReviews"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/reviews/{id}:
    get:
      tags: [Reviews]
      summary: 口コミ詳細の取得
      security:
        - OAuth2: [reviews:read]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: rev_xyz789
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Review"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      description: OAuth 2.0 Authorization Code Grant + PKCE (S256) + OIDC Core 1.0
      flows:
        authorizationCode:
          authorizationUrl: https://api.report.clinic/api/v1/oauth/authorize
          tokenUrl: https://api.report.clinic/api/v1/oauth/token
          refreshUrl: https://api.report.clinic/api/v1/oauth/token
          scopes:
            openid: OIDC 認証（id_token に sub を含める。OIDC を有効化するために必須）
            profile: id_token に name / picture / given_name / family_name を含める
            email: id_token に email / email_verified を含める
            phone: id_token に phone_number / phone_number_verified を含める
            address: id_token に address を含める（都道府県・市区町村レベル）
            reservations:read: 予約データの読み取り
            treatments:read: 施術データの読み取り
            reviews:read: 口コミデータの読み取り

  parameters:
    Cursor:
      name: cursor
      in: query
      required: false
      description: 次ページ取得用カーソル（前回レスポンスの `next_cursor` を指定）
      schema:
        type: string
    UpdatedSince:
      name: updated_since
      in: query
      required: false
      description: この日時以降に更新されたレコードのみ取得（差分連携用、ISO 8601 形式）
      schema:
        type: string
        format: date-time
      example: "2026-05-22T00:00:00Z"
    Limit:
      name: limit
      in: query
      required: false
      description: 1 ページあたりの取得件数
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 200

  responses:
    Unauthorized:
      description: 認証エラー（access_token が無効・期限切れ・取り消し済み）
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: 認可エラー（必要なスコープが付与されていない、あるいは他ユーザーのリソースへのアクセス）
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: リソースが見つからない
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: レート制限超過
      headers:
        Retry-After:
          schema:
            type: integer
          description: 再試行までの待機秒数
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    OidcDiscoveryMetadata:
      type: object
      description: OIDC Discovery 1.0 準拠のメタデータ
      required:
        - issuer
        - authorization_endpoint
        - token_endpoint
        - userinfo_endpoint
        - jwks_uri
        - response_types_supported
        - subject_types_supported
        - id_token_signing_alg_values_supported
      properties:
        issuer:
          type: string
          example: https://api.report.clinic
        authorization_endpoint:
          type: string
          example: https://api.report.clinic/api/v1/oauth/authorize
        token_endpoint:
          type: string
          example: https://api.report.clinic/api/v1/oauth/token
        userinfo_endpoint:
          type: string
          example: https://api.report.clinic/api/v1/oauth/userinfo
        revocation_endpoint:
          type: string
          example: https://api.report.clinic/api/v1/oauth/revoke
        jwks_uri:
          type: string
          example: https://api.report.clinic/.well-known/jwks.json
        response_types_supported:
          type: array
          items:
            type: string
          example: ["code"]
        subject_types_supported:
          type: array
          items:
            type: string
          example: ["public"]
        id_token_signing_alg_values_supported:
          type: array
          items:
            type: string
          example: ["RS256"]
        scopes_supported:
          type: array
          items:
            type: string
          example: ["openid", "profile", "email", "phone", "address", "reservations:read", "treatments:read", "reviews:read"]
        token_endpoint_auth_methods_supported:
          type: array
          items:
            type: string
          example: ["client_secret_basic", "client_secret_post"]
        claims_supported:
          type: array
          items:
            type: string
          example: ["sub", "iss", "aud", "exp", "iat", "auth_time", "nonce", "amr", "name", "given_name", "family_name", "picture", "email", "email_verified", "phone_number", "phone_number_verified", "address"]
        code_challenge_methods_supported:
          type: array
          items:
            type: string
          example: ["S256"]
        grant_types_supported:
          type: array
          items:
            type: string
          example: ["authorization_code", "refresh_token"]

    JwksResponse:
      type: object
      description: JSON Web Key Set (RFC 7517)
      required: [keys]
      properties:
        keys:
          type: array
          items:
            $ref: "#/components/schemas/Jwk"

    Jwk:
      type: object
      description: JSON Web Key（RSA 公開鍵、署名検証用）
      required: [kty, use, kid, alg, n, e]
      properties:
        kty:
          type: string
          enum: [RSA]
          example: RSA
        use:
          type: string
          enum: [sig]
          example: sig
        kid:
          type: string
          description: 鍵 ID。id_token の header.kid と一致する鍵が正
          example: rk-2026-05-26
        alg:
          type: string
          enum: [RS256]
          example: RS256
        n:
          type: string
          description: RSA modulus（Base64URL エンコード）
        e:
          type: string
          description: RSA exponent（通常 AQAB）
          example: AQAB

    AuthorizationCodeGrantRequest:
      type: object
      required: [grant_type, code, redirect_uri, client_id]
      properties:
        grant_type:
          type: string
          enum: [authorization_code]
        code:
          type: string
        redirect_uri:
          type: string
          format: uri
        client_id:
          type: string
        client_secret:
          type: string
          description: confidential client の場合は必須
        code_verifier:
          type: string
          description: PKCE の code_verifier（43〜128 文字）

    RefreshTokenGrantRequest:
      type: object
      required: [grant_type, refresh_token, client_id]
      properties:
        grant_type:
          type: string
          enum: [refresh_token]
        refresh_token:
          type: string
        client_id:
          type: string
        client_secret:
          type: string
        scope:
          type: string
          description: 取得時より絞り込みたい場合のみ指定（追加は不可）

    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
          description: opaque なアクセストークン
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          description: access_token の有効期限（秒、デフォルト 3600）
          example: 3600
        refresh_token:
          type: string
          description: 新しいリフレッシュトークン（rotation 方式）
        scope:
          type: string
          description: 付与されたスコープ（スペース区切り）
          example: openid profile email treatments:read
        id_token:
          type: string
          description: |
            OIDC id_token（JWT, RS256）。scope に `openid` を含む場合のみ返却。
            ヘッダに `kid` を含み、検証は `/.well-known/jwks.json` の公開鍵で行います。
          example: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJrLTIwMjYtMDUtMjYifQ...

    UserInfo:
      type: object
      description: |
        OIDC standard claims に準拠したユーザー情報。
        付与された scope に応じて含まれる claim が変わります。
      required: [sub]
      properties:
        sub:
          type: string
          description: ユーザーの安定な識別子（external_id）
          example: usr_a3kf2lkj98sndjf02kfla9
        name:
          type: string
          description: 表示名（profile scope 時）
          example: 山田 花子
        given_name:
          type: string
          description: 名（profile scope 時）
          example: 花子
        family_name:
          type: string
          description: 姓（profile scope 時）
          example: 山田
        picture:
          type: string
          format: uri
          description: プロフィール画像 URL（profile scope 時、保持していれば）
        email:
          type: string
          format: email
          description: メールアドレス（email scope 時）
          example: hanako@example.com
        email_verified:
          type: boolean
          description: メールアドレスの検証済みフラグ（email scope 時）
        phone_number:
          type: string
          description: 電話番号（phone scope 時、E.164 形式）
          example: "+819012345678"
        phone_number_verified:
          type: boolean
          description: 電話番号の検証済みフラグ（phone scope 時）
        address:
          type: object
          description: 住所（address scope 時、都道府県・市区町村レベル）
          properties:
            country:
              type: string
              example: JP
            region:
              type: string
              example: 東京都
            locality:
              type: string
              example: 渋谷区

    OAuthError:
      type: object
      description: OAuth 2.0 (RFC 6749) Section 5.2 形式
      required: [error]
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - unauthorized_client
            - unsupported_grant_type
            - invalid_scope
            - access_denied
            - login_required
            - interaction_required
        error_description:
          type: string
        error_uri:
          type: string
          format: uri

    Error:
      type: object
      description: RFC 7807 Problem Details 形式
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string

    Clinic:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          example: cln_111222
        name:
          type: string
          example: 〇〇美容クリニック 渋谷院
        prefecture:
          type: [string, "null"]
          example: 東京都
        group_id:
          type: [string, "null"]

    Menu:
      type: object
      required: [title]
      properties:
        title:
          type: string
          example: ボトックス注射（額・眉間セット）
        course_name:
          type: [string, "null"]
        category:
          type: [string, "null"]
          example: 注射・点滴 > ボトックス注射

    Reservation:
      type: object
      required: [id, status, reserved_at, clinic, category, created_at, updated_at]
      properties:
        id:
          type: string
          example: apt_abc123
        status:
          type: string
          enum: [pending, confirmed, completed, cancelled, no_show]
        reserved_at:
          type: string
          format: date-time
        operation_at:
          type: [string, "null"]
          format: date-time
        clinic:
          $ref: "#/components/schemas/Clinic"
        category:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
        treatment_id:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Treatment:
      type: object
      required:
        - id
        - reference_code
        - performed_at
        - accept_status
        - price
        - clinic
        - menu
        - created_at
        - updated_at
      properties:
        id:
          type: string
          example: trt_def456
        reference_code:
          type: string
          example: AP12345
        performed_at:
          type: string
          format: date
        confirmed_at:
          type: [string, "null"]
          format: date-time
        user_accepted_at:
          type: [string, "null"]
          format: date-time
        accept_status:
          type: string
          enum: [pending, accepted, amend_requested]
        price:
          type: integer
          example: 33000
        tax_percentage:
          type: [integer, "null"]
        insurance_medical:
          type: boolean
        payment_type:
          type: [string, "null"]
          enum: [first, additional, null]
        clinic:
          $ref: "#/components/schemas/Clinic"
        menu:
          $ref: "#/components/schemas/Menu"
        doctor_name:
          type: [string, "null"]
        related_appointment_id:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Review:
      type: object
      required: [id, reference_code, type, opened_at, clinic, created_at, updated_at]
      properties:
        id:
          type: string
          example: rev_xyz789
        reference_code:
          type: string
          example: RR98765
        type:
          type: string
          enum: [real]
        report_type:
          type: string
          enum: [experience, result]
        experience_date:
          type: [string, "null"]
          format: date
        submitted_at:
          type: [string, "null"]
          format: date-time
        opened_at:
          type: string
          format: date-time
        score:
          type: [integer, "null"]
        clinic:
          $ref: "#/components/schemas/Clinic"
        category:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
        related_treatment_id:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PaginatedReservations:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Reservation"
        next_cursor:
          type: [string, "null"]

    PaginatedTreatments:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Treatment"
        next_cursor:
          type: [string, "null"]

    PaginatedReviews:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Review"
        next_cursor:
          type: [string, "null"]
