# YuCon API (v1) — 機器連携 API の公開契約
#
# このファイルが API 仕様の単一ソース (docs/architecture/machine-api.md §10.1)。
# API の挙動を変更する PR は、同一 PR 内でこのファイルを更新すること。
# 公開 URL: https://help.yucon.info/openapi.yaml (apps/help のビルド時にコピー)
#
# /v1 は破壊的変更 (フィールド削除・意味変更・エラーコード変更) をしない契約。
# フィールド追加・エンドポイント追加は非破壊としてバージョン内で行う。
openapi: 3.1.0
info:
  title: YuCon API
  version: 1.0.0
  description: |
    施設の計測機器・自社システムから YuCon の混雑度を自動更新するための公開 API です。

    ## 認証

    管理画面の「外部連携 (API)」で発行した API トークン (`yucon_api_` で始まる文字列) を
    `Authorization: Bearer` ヘッダで送信します。トークンは発行時に一度だけ表示され、
    施設単位に紐づきます。クエリ文字列でのトークン送信は受け付けません。

    ## レートリミット

    - **更新間隔**: 同一の更新対象 (施設 / サブ設備) は 30 秒に 1 回まで更新できます。
      機器からの送信間隔は **60 秒以上を推奨**します (境界ちょうどの送信はクロック差で
      429 になりやすいため)。
    - **トークン単位**: 混雑度更新はトークンあたり 10 回/60 秒、状態取得は 30 回/60 秒まで。
      有効なトークンで認証できたリクエストは、成功・失敗を問わず消費されます。
    - 429 応答には `Retry-After` ヘッダ (秒) と `error.retryAfter` が含まれます。
      指定秒数待ってから再送してください。

    ## リトライ指針

    | 状況 | 推奨動作 |
    |---|---|
    | 429 | `Retry-After` 秒待って再送 |
    | 409 (`FACILITY_CLOSED`) | 正常系。営業時間外のため受理されません。次の営業時間まで送信を止めるか、無視して定期送信を継続 |
    | 401 / 403 / 400 / 404 | 再送しない (設定・実装の誤り。管理者に通知) |
    | 5xx / ネットワークエラー | 指数バックオフで再送 |

    ## セキュリティ

    - トークンは機器・サーバー側にのみ保管してください。ブラウザの JavaScript からの
      直接呼び出しは非サポートです (第三者オリジンに CORS を開けていません)。
    - ローテーション: 新トークン発行 → 機器の設定切替 → 旧トークン失効、の順で
      無停止交換できます (施設あたり有効トークン 20 本まで)。
servers:
  - url: https://api.yucon.info
    description: 本番環境
  - url: https://api.dev.yucon.info
    description: 開発環境
security:
  - bearerAuth: []
tags:
  - name: status
    description: 状態取得 (セットアップ・疎通確認)
  - name: congestion
    description: 混雑度更新

paths:
  /v1/facilities/{facilityId}:
    get:
      operationId: getFacilityStatus
      tags: [status]
      summary: 施設の現在状態を取得
      description: |
        機器のセットアップとセルフテスト用のエンドポイントです。有効なトークンであれば
        権限に関係なく呼び出せます。

        起動時にこのエンドポイントを呼ぶことで、混雑度の段階数 (`congestionScale`) と
        サブ設備の ID 一覧を機器側にハードコードせず取得できます。`openState` で
        「今 PUT すると 409 になるか」を事前に判定できます。

        `subFacilities` はトークンの `subScope` で絞られます (`primary` トークンには
        代表サブ設備のみ返ります)。
      parameters:
        - $ref: '#/components/parameters/FacilityId'
      responses:
        '200':
          description: 現在状態
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    const: true
                  data:
                    $ref: '#/components/schemas/FacilityStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/facilities/{facilityId}/congestion:
    put:
      operationId: updateCongestion
      tags: [congestion]
      summary: 施設の混雑度を更新
      description: |
        施設の代表混雑度を更新します。内部的には代表サブ設備への更新として扱われ、
        リアルタイム配信・履歴・混雑予測に反映されます。

        `level` は `1` (空いている) 〜 `congestionScale` (混雑) の整数です。
        段階数は施設ごとに 3 または 5 で、`GET /v1/facilities/{facilityId}` で取得できます。

        トークンに `update_congestion` 権限が必要です (API トークンは発行時に必ず持ちます)。
      parameters:
        - $ref: '#/components/parameters/FacilityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CongestionUpdate'
      responses:
        '200':
          description: 更新成功
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    const: true
                  data:
                    $ref: '#/components/schemas/CongestionUpdateResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FacilityClosed'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/facilities/{facilityId}/subfacilities/{subFacilityId}/congestion:
    put:
      operationId: updateSubFacilityCongestion
      tags: [congestion]
      summary: サブ設備の混雑度を更新
      description: |
        サブ設備 (男湯 / 女湯 / サウナ / 駐車場 など) 単位で混雑度を更新します。

        `level` の上限はサブ設備ごとの `congestionScale` です (施設本体と異なる場合が
        あります)。サブ設備 ID と段階数は `GET /v1/facilities/{facilityId}` で取得できます。

        トークンの `subScope` が対象サブ設備を許可している必要があります
        (`primary` = 代表サブ設備のみ / `all` = 全サブ設備)。
      parameters:
        - $ref: '#/components/parameters/FacilityId'
        - $ref: '#/components/parameters/SubFacilityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CongestionUpdate'
      responses:
        '200':
          description: 更新成功
          content:
            application/json:
              schema:
                type: object
                required: [success, data]
                properties:
                  success:
                    const: true
                  data:
                    $ref: '#/components/schemas/SubCongestionUpdateResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FacilityClosed'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        管理画面「外部連携 (API)」で発行した API トークン。
        `Authorization: Bearer yucon_api_...` の形で送信します。

  parameters:
    FacilityId:
      name: facilityId
      in: path
      required: true
      description: 施設 ID (トークン発行画面・管理画面の URL で確認できます)
      schema:
        type: string
    SubFacilityId:
      name: subFacilityId
      in: path
      required: true
      description: サブ設備 ID (`GET /v1/facilities/{facilityId}` で取得)
      schema:
        type: string

  schemas:
    CongestionUpdate:
      type: object
      required: [level]
      properties:
        level:
          type: integer
          minimum: 1
          description: 混雑度 (1 = 空いている 〜 congestionScale = 混雑)
      examples:
        - level: 2

    CongestionUpdateResult:
      type: object
      properties:
        facilityId:
          type: string
        currentLevel:
          type: integer
        currentLevelUpdatedAt:
          type: string
          format: date-time
        currentLevelUpdatedBy:
          type: string
          description: 更新者識別子 (`STAFF_TOKEN#{tokenId}`)
        congestionScale:
          type: integer
          enum: [3, 5]

    SubCongestionUpdateResult:
      type: object
      properties:
        facilityId:
          type: string
        subFacilityId:
          type: string
        currentLevel:
          type: integer
        currentLevelUpdatedAt:
          type: string
          format: date-time
        currentLevelUpdatedBy:
          type: string
          description: 更新者識別子 (`STAFF_TOKEN#{tokenId}`)
        isPrimary:
          type: boolean
          description: 代表サブ設備への更新だったかどうか

    FacilityStatus:
      type: object
      properties:
        facilityId:
          type: string
        name:
          type: string
        congestionScale:
          type: integer
          enum: [3, 5]
        currentLevel:
          type: [integer, 'null']
        currentLevelUpdatedAt:
          type: [string, 'null']
          format: date-time
        openState:
          type: string
          enum: [open, closed, pre_open, post_close, unknown]
          description: |
            現在の営業状態。`closed` のとき混雑度更新は 409 で拒否されます。
            `pre_open` / `post_close` (開店前・閉店後の猶予帯) は更新可能です。
            `unknown` は営業スケジュール未設定の施設で、更新は常に受理されます。
        token:
          type: object
          description: 認証に使ったトークン自身のメタ情報 (設定ミスの切り分け用)
          properties:
            label:
              type: string
            permissions:
              type: array
              items:
                type: string
            subScope:
              type: string
              enum: [primary, all]
        subFacilities:
          type: array
          description: トークンの subScope で絞られたサブ設備一覧
          items:
            type: object
            properties:
              subFacilityId:
                type: string
              name:
                type: string
              congestionScale:
                type: integer
                enum: [3, 5]
              currentLevel:
                type: [integer, 'null']
              currentLevelUpdatedAt:
                type: [string, 'null']
                format: date-time
              isPrimary:
                type: boolean

    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success:
          const: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            retryAfter:
              type: integer
              description: 429 のみ。再送まで待つ秒数 (`Retry-After` ヘッダと同値)

  responses:
    Unauthorized:
      description: |
        トークンが無効・期限切れ・失効済み (`UNAUTHORIZED`)。再送せず、管理画面で
        トークンの状態を確認してください。トークンの状態は意図的に細分して返しません。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: |
        トークンが対象施設・サブ設備・操作の権限を持たない (`FORBIDDEN`)。
        別施設のトークンを設定している場合もこの応答になります。再送せず設定を確認してください。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: 施設が存在しない (`FACILITY_NOT_FOUND`)。施設が削除された可能性があります。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    ValidationError:
      description: |
        リクエスト形式の誤り (`VALIDATION_ERROR`) または `level` が段階数の範囲外
        (`INVALID_LEVEL`)。再送せず実装を修正してください。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    FacilityClosed:
      description: |
        営業時間外のため更新を受理しない (`FACILITY_CLOSED`)。機器にとっては**正常系**です。
        閉店中のデータは混雑度表示と予測の品質を守るため受け付けません。
        `GET /v1/facilities/{facilityId}` の `openState` で事前判定できます。
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: |
        レートリミット超過 (`RATE_LIMITED`)。更新対象単位の 30 秒間隔、または
        トークン単位の回数上限に達しています。`Retry-After` 秒待って再送してください。
      headers:
        Retry-After:
          description: 再送まで待つ秒数
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
