openapi: 3.0.3

info:
  title: Danjiri API Reference
  version: 2.0.0
  description: |
    祭礼アプリの API リファレンスです。
    routes/api.php と include 先の auth.php / iap.php / webhook.php を起点に、
    controller / service のレスポンス形式を優先して記述しています。

servers:
  - url: /
    description: Current Laravel host

tags:
  - name: Auth
    description: 認証、トークン再発行、ログアウト
  - name: Device Tokens
    description: Push 通知用デバイストークン
  - name: GPS
    description: GPS 位置・デバイス取得
  - name: AWS API Gateway
    description: Laravel の GPS service が内部的に呼び出す API Gateway
  - name: Area Tree
    description: 地域、自治体、連合、町のツリー
  - name: Towns
    description: 町情報
  - name: Unions
    description: 連合情報
  - name: News
    description: お知らせの取得、作成、更新、削除、既読
  - name: Festival Types
    description: 祭礼種別
  - name: Sponsors
    description: スポンサー
  - name: Schedules
    description: 日程
  - name: Favorites
    description: お気に入り
  - name: Inquiries
    description: 問い合わせ
  - name: In-App Purchase
    description: アプリ内課金
  - name: Webhooks
    description: 外部ストア webhook
  - name: Not Implemented
    description: routes/api.php に定義はあるが、controller / service が未完成の API

paths:
  /api/auth/register:
    post:
      tags:
        - Auth
      summary: アカウント作成
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "201":
          description: アカウント作成成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
              example:
                data: {}
                message: created.
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/login:
    post:
      tags:
        - Auth
      summary: ログイン
      description: |
        email / password / device_ref_id を照合し、API 利用用の auth_token と再発行用の refresh_token を発行します。
        同じユーザーで既に有効な auth_token は無効化されますが、他端末の refresh_token は無効化されません。
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: ログイン成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthTokenPairResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/refresh:
    post:
      tags:
        - Auth
      summary: トークン再発行
      description: |
        device_ref_id と refresh_token を照合し、新しい auth_token と refresh_token を発行します。
        同じユーザーで既に有効な auth_token は無効化されます。
      operationId: refreshAuthToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshRequest"
      responses:
        "200":
          description: 再発行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthTokenPairResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/password-reset:
    post:
      tags:
        - Auth
      summary: パスワードリセットメール送信
      operationId: requestPasswordReset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordResetRequest"
      responses:
        "200":
          description: パスワードリセットメール送信成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/password-reset/confirm:
    post:
      tags:
        - Auth
      summary: パスワードリセット確定
      description: パスワードを更新し、対象ユーザーの全端末の auth_token と refresh_token を無効化します。
      operationId: confirmPasswordReset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordResetConfirmRequest"
      responses:
        "200":
          description: パスワードリセット成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/me:
    get:
      tags:
        - Auth
      summary: 認証済みユーザー情報取得
      operationId: getAuthenticatedUser
      security:
        - bearerAuth: []
      responses:
        "200":
          description: 認証済みユーザー情報取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthMeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/me/newsletter:
    patch:
      tags:
        - Auth
      summary: お知らせメール受信設定変更
      operationId: updateNewsletterEnabled
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NewsletterRequest"
      responses:
        "200":
          description: お知らせメール受信設定変更成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/me/password:
    patch:
      tags:
        - Auth
      summary: パスワード変更
      description: 現在のパスワードを照合して新しいパスワードへ変更します。既存の auth_token と refresh_token は維持されます。
      operationId: updatePassword
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordUpdateRequest"
      responses:
        "200":
          description: パスワード変更成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/auth/logout:
    post:
      tags:
        - Auth
      summary: ログアウト
      description: |
        Authorization ヘッダの auth_token を無効化し、指定した device_ref_id に紐づく refresh_token も無効化します。
      operationId: logout
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LogoutRequest"
      responses:
        "200":
          description: ログアウト成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/device-tokens:
    post:
      tags:
        - Device Tokens
      summary: デバイストークン登録
      operationId: storeDeviceToken
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DeviceTokenStoreRequest"
      responses:
        "200":
          description: 登録成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/gps-locations/towns/{townId}:
    get:
      tags:
        - GPS
      summary: 町の GPS 位置取得
      operationId: getGpsLocationsByTown
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GpsLocationListResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/gps-locations/unions/{unionId}:
    get:
      tags:
        - GPS
      summary: 連合の GPS 位置取得
      operationId: getGpsLocationsByUnion
      parameters:
        - $ref: "#/components/parameters/UnionIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GpsLocationListResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/gps-devices/towns/{townId}:
    get:
      tags:
        - GPS
      summary: 町の GPS デバイス取得
      operationId: getGpsDevicesByTown
      description: 町に紐づく現在有効な GPS デバイス ID 群を取得します。フロントから API Gateway を直接呼び出す旧式互換用の API です。
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GpsDeviceListResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/gps-devices/unions/{unionId}:
    get:
      tags:
        - GPS
      summary: 連合の GPS デバイス取得
      operationId: getGpsDevicesByUnion
      description: 連合または大連合に所属する町に紐づく、現在有効な GPS デバイス ID 群を取得します。フロントから API Gateway を直接呼び出す旧式互換用の API です。
      parameters:
        - $ref: "#/components/parameters/UnionIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GpsDeviceListResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /:
    post:
      tags:
        - AWS API Gateway
      summary: GPS デバイス位置取得
      operationId: getGpsLocationsFromApiGateway
      description: GpsLocationService が内部的に呼び出す API Gateway です。routes/api.php の Laravel route ではありません。
      servers:
        - url: https://l0kaz9gf2f.execute-api.ap-northeast-1.amazonaws.com/public
          description: AWS API Gateway
      security:
        - apiGatewayKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/ApiGatewayGpsLocationRequestItem"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                oneOf:
                  - type: array
                    items:
                      $ref: "#/components/schemas/ApiGatewayGpsLocation"
                  - type: object
                    properties:
                      body:
                        oneOf:
                          - type: array
                            items:
                              $ref: "#/components/schemas/ApiGatewayGpsLocation"
                          - type: string
                            description: JSON 文字列化された位置情報配列
        "500":
          description: API Gateway または Lambda 側のエラー

  /api/area-tree/{festivalTypeId}:
    get:
      tags:
        - Area Tree
      summary: 地域ツリー取得
      operationId: getAreaTree
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/FestivalTypeIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AreaTreeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/area-tree/{festivalTypeId}/active:
    get:
      tags:
        - Area Tree
      summary: 開催中の地域ツリー取得
      operationId: getActiveAreaTree
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/FestivalTypeIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AreaTreeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/towns/{townId}:
    get:
      tags:
        - Towns
      summary: 町情報取得
      operationId: getTown
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TownDetailResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/festival-types/{festivalTypeId}:
    get:
      tags:
        - News
      summary: 祭礼種別ごとの町別最新お知らせ取得
      operationId: getNewsByFestivalType
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/FestivalTypeIdPath"
        - $ref: "#/components/parameters/LimitQuery"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsFestivalTypeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/towns/{townId}:
    get:
      tags:
        - News
      summary: 町ごとのお知らせ一覧取得
      operationId: getNewsByTown
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsTownResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/announcements:
    get:
      tags:
        - News
      summary: 公式お知らせ取得
      operationId: getNewsAnnouncements
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/LimitQuery"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnnouncementListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/create-token:
    post:
      tags:
        - News
      summary: お知らせ作成トークン発行
      operationId: createNewsCreateToken
      security:
        - bearerAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NewsOperationPasswordRequest"
      responses:
        "201":
          description: 発行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsCreateTokenResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/{newsId}/operation-token:
    post:
      tags:
        - News
      summary: お知らせ編集/削除トークン発行
      operationId: createNewsOperationToken
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/NewsIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NewsOperationPasswordRequest"
      responses:
        "201":
          description: 発行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsOperationTokenResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news:
    post:
      tags:
        - News
      summary: お知らせ作成
      operationId: createNews
      security:
        - bearerAuth: []
        - {}
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/NewsStoreRequest"
      responses:
        "201":
          description: 作成成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsMutationResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/{newsId}:
    post:
      tags:
        - News
      summary: お知らせ更新
      operationId: updateNews
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/NewsIdPath"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/NewsStoreRequest"
      responses:
        "200":
          description: 更新成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NewsMutationResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - News
      summary: お知らせ削除
      operationId: deleteNews
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/NewsIdPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NewsDeleteRequest"
      responses:
        "204":
          description: 削除成功
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/news/towns/{townId}/read:
    post:
      tags:
        - News
      summary: 町のお知らせ既読化
      operationId: markTownNewsAsRead
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 既読化成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/unions/{unionId}:
    get:
      tags:
        - Unions
      summary: 連合詳細取得
      operationId: getUnion
      parameters:
        - $ref: "#/components/parameters/UnionIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnionDetailResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/unions/{unionId}/facilities:
    get:
      tags:
        - Unions
      summary: 連合に紐づく施設一覧取得
      operationId: getUnionFacilities
      parameters:
        - $ref: "#/components/parameters/UnionIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FacilityListResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/festival-types:
    get:
      tags:
        - Festival Types
      summary: 祭礼種別一覧取得
      operationId: getFestivalTypes
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FestivalTypeListResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/sponsors:
    get:
      tags:
        - Sponsors
      summary: スポンサー一覧取得
      operationId: getSponsors
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SponsorListResponse"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/sponsors/{sponsorId}:
    get:
      tags:
        - Sponsors
      summary: スポンサー詳細取得
      operationId: getSponsor
      parameters:
        - $ref: "#/components/parameters/SponsorIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SponsorDetailResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/schedules:
    get:
      tags:
        - Schedules
      summary: 日程取得
      operationId: getSchedules
      parameters:
        - name: festival_type_id
          in: query
          required: true
          description: 祭礼種別 ID
          schema:
            type: integer
            minimum: 1
        - name: schedule_type
          in: query
          required: true
          description: スケジュール種別
          schema:
            type: integer
            enum:
              - 1
              - 2
        - name: year
          in: query
          required: true
          description: 対象年。schedules.date の年で絞り込みます。
          schema:
            type: string
            pattern: "^[0-9]{4}$"
            example: "2026"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScheduleResponse"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/inquiries:
    post:
      tags:
        - Inquiries
      summary: 問い合わせ送信
      operationId: createInquiry
      security:
        - bearerAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InquiryStoreRequest"
      responses:
        "204":
          description: 送信成功
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/favorites:
    get:
      tags:
        - Favorites
      summary: お気に入り一覧取得
      operationId: getFavorites
      security:
        - bearerAuth: []
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FavoriteListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/favorites/towns/{townId}:
    get:
      tags:
        - Favorites
      summary: 町のお気に入り状態取得
      operationId: getFavoriteTownStatus
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FavoriteTownStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"
    post:
      tags:
        - Favorites
      summary: お気に入り追加
      operationId: createFavorite
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "201":
          description: 新規追加
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "200":
          description: 既に登録済み
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"
    delete:
      tags:
        - Favorites
      summary: お気に入り削除
      operationId: deleteFavoriteTown
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/TownIdPath"
      responses:
        "200":
          description: 削除成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/iap/subscriptions/session-token:
    post:
      tags:
        - In-App Purchase
      summary: アプリ内課金セッショントークン発行
      operationId: createIapSessionToken
      description: フロントがネイティブ課金処理に渡すための 10 分有効な session_token を発行します。
      security:
        - bearerAuth: []
      responses:
        "201":
          description: 発行成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IapSessionTokenResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/iap/subscriptions/google-play/acknowledge:
    post:
      tags:
        - In-App Purchase
      summary: Google Play サブスクリプション承認/同期
      operationId: acknowledgeGooglePlaySubscription
      description: session_token でユーザーを復元し、Google Play の purchase token を検証します。必要に応じて acknowledge した上で user_subscriptions を更新します。画面上の最終的な購読状態は、この API 後に auth/me を呼び直して取得します。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SubscriptionAcknowledgeRequest"
      responses:
        "200":
          description: 同期成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionAcknowledgeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/iap/subscriptions/app-store/verify:
    post:
      tags:
        - In-App Purchase
        - Not Implemented
      summary: App Store サブスクリプション検証
      operationId: verifyAppStoreSubscription
      description: route はありますが、現在は明示的に 501 を返します。
      security:
        - bearerAuth: []
      responses:
        "501":
          $ref: "#/components/responses/NotImplemented"

  /api/webhooks/google-play/subscriptions:
    post:
      tags:
        - Webhooks
      summary: Google Play サブスクリプション webhook
      operationId: receiveGooglePlaySubscriptionWebhook
      description: Google Play RTDN の Pub/Sub push payload を受信し、既存の purchase token に紐づく user_subscriptions を同期します。未知の purchase token は購読付与せずイベントログのみ記録します。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GooglePlayWebhookRequest"
      responses:
        "200":
          description: 受信成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmptyDataResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /api/webhooks/app-store/subscriptions:
    post:
      tags:
        - Webhooks
        - Not Implemented
      summary: App Store サブスクリプション webhook
      operationId: receiveAppStoreSubscriptionWebhook
      description: route はありますが、現在は明示的に 501 を返します。
      responses:
        "501":
          $ref: "#/components/responses/NotImplemented"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: /api/auth/login または /api/auth/refresh で発行される API 利用用の auth_token
    apiGatewayKey:
      type: apiKey
      in: header
      name: x-api-key

  parameters:
    TownIdPath:
      name: townId
      in: path
      required: true
      description: 町 ID
      schema:
        type: integer
        minimum: 1
    UnionIdPath:
      name: unionId
      in: path
      required: true
      description: 連合 ID
      schema:
        type: integer
        minimum: 1
    FestivalTypeIdPath:
      name: festivalTypeId
      in: path
      required: true
      description: 祭礼種別 ID
      schema:
        type: integer
        minimum: 1
    NewsIdPath:
      name: newsId
      in: path
      required: true
      description: お知らせ ID
      schema:
        type: integer
        minimum: 1
    SponsorIdPath:
      name: sponsorId
      in: path
      required: true
      description: スポンサー ID
      schema:
        type: integer
        minimum: 1
    LimitQuery:
      name: limit
      in: query
      required: false
      description: 取得件数上限
      schema:
        type: integer
        minimum: 1

  responses:
    BadRequest:
      description: リクエスト不正
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: 未認証、認証トークン不正、または操作トークン不正
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: 対象データなし
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: 競合
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: バリデーションエラー
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrorResponse"
    TooManyRequests:
      description: リクエスト回数の上限超過
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalServerError:
      description: サーバーエラー
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotImplemented:
      description: 未実装
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            data: {}
            message: Not Implemented.

  schemas:
    EmptyDataResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: object
          additionalProperties: true
          example: {}
        message:
          type: string
          example: ok.

    ErrorResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: object
          additionalProperties: true
          example: {}
        message:
          type: string
          example: Error message.

    ValidationErrorResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: object
          required:
            - fields
          properties:
            fields:
              type: object
              additionalProperties:
                type: array
                items:
                  type: string
              example:
                email:
                  - The email field is required.
        message:
          type: string
          example: Validation failed.

    RegisterRequest:
      type: object
      required:
        - email
        - password
        - newsletter_enabled
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          example: user@example.com
        password:
          type: string
          format: password
          minLength: 8
          example: password123
        newsletter_enabled:
          type: boolean
          description: お知らせメールの受信可否
          example: false

    NewsletterRequest:
      type: object
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          description: 変更後のお知らせメール受信可否
          example: true

    PasswordResetRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          example: user@example.com

    PasswordResetConfirmRequest:
      type: object
      required:
        - token
        - password
      properties:
        token:
          type: string
          description: パスワードリセットメールで発行された短命トークン
          example: 0123456789abcdef
        password:
          type: string
          format: password
          minLength: 8
          maxLength: 64
          example: new-password123

    PasswordUpdateRequest:
      type: object
      required:
        - current_password
        - password
      properties:
        current_password:
          type: string
          format: password
          example: current-password123
        password:
          type: string
          format: password
          minLength: 8
          maxLength: 64
          example: new-password123

    LoginRequest:
      type: object
      required:
        - email
        - password
        - device_ref_id
      properties:
        email:
          type: string
          format: email
          example: user@example.com
        password:
          type: string
          format: password
          example: password123
        device_ref_id:
          type: string
          maxLength: 128
          example: device-uuid-or-native-id

    RefreshRequest:
      type: object
      required:
        - device_ref_id
        - refresh_token
      properties:
        device_ref_id:
          type: string
          maxLength: 128
          example: device-uuid-or-native-id
        refresh_token:
          type: string
          description: device_ref_id に紐づく再発行トークン
          example: 0123456789abcdef

    LogoutRequest:
      type: object
      required:
        - device_ref_id
      properties:
        device_ref_id:
          type: string
          maxLength: 128
          description: ログアウト対象端末の識別 ID
          example: device-uuid-or-native-id

    AuthTokenPairResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/AuthTokenPair"
        message:
          type: string
          example: ok.

    AuthTokenPair:
      type: object
      required:
        - auth_token
        - refresh_token
      properties:
        auth_token:
          type: string
          description: API 利用用の認証トークン。Authorization Bearer に指定します。
        refresh_token:
          type: string
          description: auth_token 再発行用のトークン。端末ごとに管理します。

    AuthMeResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/AuthMeData"
        message:
          type: string
          example: ok.

    AuthMeData:
      type: object
      required:
        - authenticated
        - user
        - subscription
      properties:
        authenticated:
          type: boolean
          example: true
        user:
          $ref: "#/components/schemas/AuthUser"
        subscription:
          $ref: "#/components/schemas/AuthSubscription"

    AuthUser:
      type: object
      required:
        - id
        - public_id
        - email
        - role
        - newsletter_enabled
        - is_data_restored
      properties:
        id:
          type: integer
          example: 1
        public_id:
          type: string
          example: 0123456789
        email:
          type: string
          format: email
          example: user@example.com
        role:
          type: string
          enum:
            - user
            - town_member
            - operator
            - admin
          example: user
        newsletter_enabled:
          type: boolean
          description: お知らせメールの受信可否
          example: false
        is_data_restored:
          type: boolean
          description: お気に入りなどのユーザー関連データが復元済みかどうか
          example: false

    AuthSubscription:
      type: object
      required:
        - is_subscribed
        - status
        - provider
        - auto_renew_enabled
        - current_period_start_at
        - current_period_end_at
        - grace_period_end_at
        - canceled_at
      properties:
        is_subscribed:
          type: boolean
          example: true
        status:
          type: string
          nullable: true
          enum:
            - pending
            - active
            - grace_period
            - on_hold
            - canceled
            - expired
            - revoked
            - failed
            - unknown
          example: active
        provider:
          type: string
          nullable: true
          enum:
            - google_play
            - app_store
          example: google_play
        auto_renew_enabled:
          type: boolean
          example: true
        current_period_start_at:
          type: string
          format: date-time
          nullable: true
          example: 2026-06-01T00:00:00+09:00
        current_period_end_at:
          type: string
          format: date-time
          nullable: true
          example: 2026-07-01T00:00:00+09:00
        grace_period_end_at:
          type: string
          format: date-time
          nullable: true
          example: null
        canceled_at:
          type: string
          format: date-time
          nullable: true
          example: null

    DeviceTokenStoreRequest:
      type: object
      required:
        - device_token
        - device_ref_id
        - platform
      properties:
        device_token:
          type: string
          maxLength: 1024
        device_ref_id:
          type: string
          maxLength: 128
          example: device-uuid-or-native-id
        platform:
          type: string
          enum:
            - android
            - iphone

    GpsLocationListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/GpsLocation"
        message:
          type: string
          example: ok.

    GpsLocation:
      type: object
      required:
        - town_id
        - device_id
        - status
        - latitude
        - longitude
      properties:
        town_id:
          type: integer
          example: 1
        device_id:
          type: integer
          example: 10
        status:
          type: string
          enum:
            - ok
            - unavailable
        latitude:
          type: string
          nullable: true
          example: 34.46012
        longitude:
          type: string
          nullable: true
          example: 135.37012

    GpsDeviceListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/GpsDevice"
        message:
          type: string
          example: ok.

    GpsDevice:
      type: object
      required:
        - town_id
        - device_id
      properties:
        town_id:
          type: integer
          example: 1
        device_id:
          type: integer
          example: 10

    ApiGatewayGpsLocationRequestItem:
      type: object
      required:
        - device_id
      properties:
        device_id:
          type: integer
          example: 10

    ApiGatewayGpsLocation:
      type: object
      properties:
        device_id:
          type: integer
          example: 10
        lat:
          type: number
          format: double
          example: 34.46012
        long:
          type: number
          format: double
          example: 135.37012

    AreaTreeResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AreaNode"
        message:
          type: string
          example: ok.

    AreaNode:
      type: object
      required:
        - area_id
        - area_name
        - municipalities
      properties:
        area_id:
          type: integer
        area_name:
          type: string
        municipalities:
          type: array
          items:
            $ref: "#/components/schemas/MunicipalityNode"

    MunicipalityNode:
      type: object
      required:
        - municipality_id
        - municipality_name
        - unions
      properties:
        municipality_id:
          type: integer
        municipality_name:
          type: string
        unions:
          type: array
          items:
            $ref: "#/components/schemas/UnionNode"

    UnionNode:
      type: object
      required:
        - union_id
        - union_name
        - period_start
        - period_end
        - towns
      properties:
        union_id:
          type: integer
        union_name:
          type: string
        period_start:
          type: string
          format: date
          nullable: true
        period_end:
          type: string
          format: date
          nullable: true
        towns:
          type: array
          items:
            $ref: "#/components/schemas/TownNode"

    TownNode:
      type: object
      required:
        - town_id
        - town_name
        - town_icon
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        town_icon:
          type: string
          nullable: true

    TownDetailResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/TownDetailData"
        message:
          type: string
          example: ok.

    TownDetailData:
      type: object
      required:
        - town
        - courses
      properties:
        town:
          $ref: "#/components/schemas/TownDetail"
        courses:
          type: array
          items:
            $ref: "#/components/schemas/Course"

    TownDetail:
      type: object
      required:
        - town_id
        - union_id
        - name
        - icon_url
        - mikoshi_image_url
        - manufacture_year_text
        - artisan_text
        - sculpture_text
        - notes
        - storehouse_image_url
        - storehouse_latitude
        - storehouse_longitude
      properties:
        town_id:
          type: integer
        union_id:
          type: integer
        name:
          type: string
        icon_url:
          type: string
          nullable: true
        mikoshi_image_url:
          type: string
          nullable: true
        manufacture_year_text:
          type: string
          nullable: true
        artisan_text:
          type: string
          nullable: true
        sculpture_text:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        storehouse_image_url:
          type: string
          nullable: true
        storehouse_latitude:
          type: string
          nullable: true
          example: "34.460120"
        storehouse_longitude:
          type: string
          nullable: true
          example: "135.370120"

    Course:
      type: object
      required:
        - course_id
        - image_url
        - date
      properties:
        course_id:
          type: integer
        image_url:
          type: string
        date:
          type: string
          format: date
          example: "2026-06-15"

    UnionDetailResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/UnionDetail"
        message:
          type: string
          example: ok.

    UnionDetail:
      type: object
      required:
        - union_id
        - union_name
        - union_latitude
        - union_longitude
        - towns
        - sponsors
      properties:
        union_id:
          type: integer
        union_name:
          type: string
        union_latitude:
          type: string
          nullable: true
          example: "34.460120"
        union_longitude:
          type: string
          nullable: true
          example: "135.370120"
        towns:
          type: array
          items:
            $ref: "#/components/schemas/UnionTown"
        sponsors:
          type: array
          items:
            $ref: "#/components/schemas/UnionSponsorSummary"

    UnionTown:
      type: object
      required:
        - town_id
        - town_name
        - icon_url
        - storehouse_image_url
        - storehouse_latitude
        - storehouse_longitude
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        icon_url:
          type: string
          nullable: true
        storehouse_image_url:
          type: string
          nullable: true
        storehouse_latitude:
          type: string
          nullable: true
          example: "34.460120"
        storehouse_longitude:
          type: string
          nullable: true
          example: "135.370120"

    UnionSponsorSummary:
      type: object
      required:
        - sponsor_id
        - sponsor_name
        - sponsor_image_url
      properties:
        sponsor_id:
          type: integer
        sponsor_name:
          type: string
        sponsor_image_url:
          type: string
          nullable: true

    FacilityListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Facility"
        message:
          type: string
          example: ok.

    Facility:
      type: object
      required:
        - facility_code
        - location_latitude
        - location_longitude
      properties:
        facility_code:
          type: string
          example: toilet
        location_latitude:
          type: string
          example: "34.460120"
        location_longitude:
          type: string
          example: "135.370120"

    NewsFestivalTypeResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/NewsTownLatest"
        message:
          type: string
          example: ok.

    NewsTownLatest:
      type: object
      required:
        - town_id
        - created_at
        - municipality_name
        - union_name
        - town_name
        - is_read
      properties:
        town_id:
          type: integer
        created_at:
          type: string
          nullable: true
          description: NewsService::getByFestivalType は Y-m-d H:i:s 形式で返却します。
          example: "2026-06-15 12:00:00"
        municipality_name:
          type: string
        union_name:
          type: string
        town_name:
          type: string
        is_read:
          type: boolean

    NewsTownResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/NewsTownData"
        message:
          type: string
          example: ok.

    NewsTownData:
      type: object
      required:
        - town_id
        - town_name
        - news
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        news:
          type: array
          items:
            $ref: "#/components/schemas/NewsItem"

    NewsItem:
      type: object
      required:
        - news_id
        - title
        - content
        - created_at
        - updated_at
        - images
      properties:
        news_id:
          type: integer
        title:
          type: string
        content:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        images:
          type: array
          items:
            $ref: "#/components/schemas/NewsImage"

    NewsImage:
      type: object
      required:
        - image_url
      properties:
        image_url:
          type: string

    AnnouncementListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Announcement"
        message:
          type: string
          example: ok.

    Announcement:
      type: object
      required:
        - announcement_id
        - body
        - updated_at
      properties:
        announcement_id:
          type: integer
        body:
          type: string
        updated_at:
          type: string
          nullable: true
          description: NewsService::getAnnouncements は Y-m-d H:i:s 形式で返却します。
          example: "2026-06-15 12:00:00"

    NewsOperationPasswordRequest:
      type: object
      required:
        - password
      properties:
        password:
          type: string

    NewsCreateTokenResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/NewsCreateToken"
        message:
          type: string
          example: created.

    NewsCreateToken:
      type: object
      required:
        - town_id
        - town_name
        - token
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        token:
          type: string

    NewsOperationTokenResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/NewsOperationToken"
        message:
          type: string
          example: created.

    NewsOperationToken:
      type: object
      required:
        - town_id
        - town_name
        - news_id
        - news_title
        - news_content
        - news_images
        - token
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        news_id:
          type: integer
        news_title:
          type: string
        news_content:
          type: string
          nullable: true
        news_images:
          type: array
          items:
            $ref: "#/components/schemas/NewsImage"
        token:
          type: string

    NewsStoreRequest:
      type: object
      required:
        - token
        - title
        - password
      properties:
        token:
          type: string
        title:
          type: string
        body:
          type: string
          nullable: true
        password:
          type: string
        images:
          type: array
          items:
            type: string
            format: binary
          description: jpg / jpeg / png、各 5MB まで

    NewsDeleteRequest:
      type: object
      required:
        - token
      properties:
        token:
          type: string

    NewsMutationResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/NewsMutation"
        message:
          type: string
          example: ok.

    NewsMutation:
      type: object
      required:
        - news_id
        - town_id
        - title
        - content
        - created_at
        - updated_at
        - images
      properties:
        news_id:
          type: integer
        town_id:
          type: integer
        title:
          type: string
        content:
          type: string
          nullable: true
        created_at:
          type: string
          nullable: true
          description: NewsService::formatNews は Y-m-d H:i:s 形式で返却します。
          example: "2026-06-15 12:00:00"
        updated_at:
          type: string
          nullable: true
          description: NewsService::formatNews は Y-m-d H:i:s 形式で返却します。
          example: "2026-06-15 12:00:00"
        images:
          type: array
          items:
            type: string

    FestivalTypeListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/FestivalType"
        message:
          type: string
          example: ok.

    FestivalType:
      type: object
      required:
        - festival_type_id
        - festival_name
        - sort_order
        - is_active
      properties:
        festival_type_id:
          type: integer
        festival_name:
          type: string
        sort_order:
          type: integer
          nullable: true
        is_active:
          type: boolean

    SponsorListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: object
          description: tier ごとのスポンサー配列。JSON object のキーは tier 値です。
          additionalProperties:
            type: array
            items:
              $ref: "#/components/schemas/SponsorListItem"
          example:
            "1":
              - sponsor_id: 1
                sponsor_image_url: https://example.com/sponsor.jpg
        message:
          type: string
          example: ok.

    SponsorListItem:
      type: object
      required:
        - sponsor_id
        - sponsor_image_url
      properties:
        sponsor_id:
          type: integer
        sponsor_image_url:
          type: string
          nullable: true

    SponsorDetailResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/SponsorDetail"
        message:
          type: string
          example: ok.

    SponsorDetail:
      type: object
      required:
        - sponsor_id
        - sponsor_name
        - images
        - menus
      properties:
        sponsor_id:
          type: integer
        sponsor_name:
          type: string
        zip_code:
          type: string
          nullable: true
        address:
          type: string
          nullable: true
        tel:
          type: string
          nullable: true
        fax:
          type: string
          nullable: true
        introduction_text:
          type: string
          nullable: true
        sponsor_image_url:
          type: string
          nullable: true
        location_latitude:
          type: string
          nullable: true
        location_longitude:
          type: string
          nullable: true
        homepage_url:
          type: string
          nullable: true
        instagram_url:
          type: string
          nullable: true
        twitter_url:
          type: string
          nullable: true
        facebook_url:
          type: string
          nullable: true
        youtube_url:
          type: string
          nullable: true
        tiktok_url:
          type: string
          nullable: true
        access:
          type: string
          nullable: true
        business_hours:
          type: string
          nullable: true
        regular_holidays:
          type: string
          nullable: true
        images:
          type: array
          items:
            type: string
        menus:
          type: array
          items:
            $ref: "#/components/schemas/SponsorMenu"

    SponsorMenu:
      type: object
      required:
        - menu_name
        - price
      properties:
        menu_name:
          type: string
        price:
          type: string

    ScheduleResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/ScheduleData"
        message:
          type: string
          example: ok.

    ScheduleData:
      type: object
      required:
        - year
        - monthly
      properties:
        year:
          type: integer
          example: 2026
        monthly:
          type: array
          items:
            $ref: "#/components/schemas/MonthlySchedule"

    MonthlySchedule:
      type: object
      required:
        - month
        - schedules
      properties:
        month:
          type: integer
          minimum: 1
          maximum: 12
        schedules:
          type: array
          items:
            $ref: "#/components/schemas/ScheduleItem"

    ScheduleItem:
      type: object
      required:
        - day
        - union_name
        - municipality_name
        - content
      properties:
        day:
          type: integer
          minimum: 1
          maximum: 31
        union_name:
          type: string
        municipality_name:
          type: string
        content:
          type: string

    FavoriteListResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Favorite"
        message:
          type: string
          example: ok.

    Favorite:
      type: object
      required:
        - town_id
        - town_name
        - town_icon_url
        - union_name
        - municipality_name
      properties:
        town_id:
          type: integer
        town_name:
          type: string
        town_icon_url:
          type: string
          nullable: true
        union_name:
          type: string
        municipality_name:
          type: string

    FavoriteTownStatusResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          type: object
          required:
            - is_favorite
          properties:
            is_favorite:
              type: boolean
        message:
          type: string
          example: ok.

    InquiryStoreRequest:
      type: object
      required:
        - name
        - email
        - inquiry_type
        - body
      properties:
        name:
          type: string
          maxLength: 100
        email:
          type: string
          format: email
          maxLength: 255
        inquiry_type:
          type: string
          maxLength: 50
        body:
          type: string

    IapSessionTokenResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/IapSessionToken"
        message:
          type: string
          example: created.

    IapSessionToken:
      type: object
      required:
        - session_token
      properties:
        session_token:
          type: string
          description: Google Play acknowledge に渡す 10 分有効なセッショントークン

    SubscriptionAcknowledgeRequest:
      type: object
      required:
        - session_token
        - purchase_token
      properties:
        session_token:
          type: string
          maxLength: 255
        purchase_token:
          type: string
          maxLength: 1024
        product_id:
          type: string
          nullable: true
          maxLength: 255
        base_plan_id:
          type: string
          nullable: true
          maxLength: 255

    SubscriptionAcknowledgeResponse:
      type: object
      required:
        - data
        - message
      properties:
        data:
          $ref: "#/components/schemas/SubscriptionAcknowledgeData"
        message:
          type: string
          example: ok.

    SubscriptionAcknowledgeData:
      type: object
      required:
        - is_subscribed
        - status
        - current_period_end_at
      properties:
        is_subscribed:
          type: boolean
        status:
          type: string
          enum:
            - pending
            - active
            - grace_period
            - on_hold
            - canceled
            - expired
            - revoked
            - failed
            - unknown
        current_period_end_at:
          type: string
          format: date-time
          nullable: true

    GooglePlayWebhookRequest:
      type: object
      required:
        - message
      properties:
        message:
          type: object
          required:
            - data
          properties:
            data:
              type: string
              description: base64 encoded Google Play RTDN JSON payload.
            messageId:
              type: string
              nullable: true
            message_id:
              type: string
              nullable: true
        subscription:
          type: string
          nullable: true
