openapi: 3.0.3
info:
  title: GymFlow Mobile API
  version: "1.0.0"
  description: >
    Per-gym mobile API. Base URL is fixed per gym build (e.g.
    https://xfitness.gymflowpos.com/api/mobile/v1). Scope: MVP only
    (see docs/mobile-permission-matrix.md and spec §50). Partner finder,
    comments, video posts, and online store checkout are deferred.
servers:
  - url: /api/mobile/v1

security:
  - bearerAuth: []

tags:
  - name: Config
  - name: Auth
  - name: Me
  - name: Feed
  - name: Member
  - name: Trainer
  - name: Admin

paths:
  # ---------------------------------------------------------------- Config
  /config/bootstrap:
    get:
      tags: [Config]
      summary: Gym identity, enabled modules, legal URLs, maintenance status
      security: []
      responses:
        "200":
          description: Bootstrap config
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          gymName: { type: string }
                          branchName: { type: string }
                          currency: { type: string, example: PKR }
                          timezone: { type: string, example: Asia/Karachi }
                          modules:
                            type: object
                            description: Mirrors mobile_app_settings.*_enabled flags
                          maintenanceMode: { type: boolean }
                          maintenanceMessage: { type: string, nullable: true }
                          minimumAndroidVersion: { type: string, nullable: true }
                          minimumIosVersion: { type: string, nullable: true }
                          forceUpdateAndroid: { type: boolean }
                          forceUpdateIos: { type: boolean }
                          termsUrl: { type: string, nullable: true }
                          privacyUrl: { type: string, nullable: true }
        "503":
          $ref: "#/components/responses/Maintenance"

  /config/theme:
    get:
      tags: [Config]
      summary: Light/dark theme tokens + brand assets
      security: []
      responses:
        "200":
          description: Theme tokens
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          light: { $ref: "#/components/schemas/ThemeTokens" }
                          dark: { $ref: "#/components/schemas/ThemeTokens" }
                          logoUrl: { type: string, nullable: true }
                          splashUrl: { type: string, nullable: true }
                          loginBackgroundUrl: { type: string, nullable: true }

  /config/legal:
    get:
      tags: [Config]
      summary: Terms, privacy, community guideline document URLs
      security: []
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /config/app-version:
    get:
      tags: [Config]
      summary: Minimum/latest supported version + force-update flags
      security: []
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  # ------------------------------------------------------------------ Auth
  /auth/discover-account:
    post:
      tags: [Auth]
      summary: Check contact number eligibility before signup (masked result only)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber]
              properties:
                contactNumber: { type: string }
      responses:
        "200":
          description: Masked eligibility result — never reveals whether contact exists if ineligible
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          eligible: { type: boolean }
                          accountType: { type: string, enum: [MEMBER, TRAINER, ADMIN], nullable: true }
                          maskedName: { type: string, nullable: true }
                          maskedContact: { type: string, nullable: true }
                          alreadyRegistered: { type: boolean }

  /auth/signup/request-otp:
    post:
      tags: [Auth]
      summary: Send OTP to an eligible contact (rate-limited, see §35)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber, purpose]
              properties:
                contactNumber: { type: string }
                purpose: { type: string, enum: [SIGNUP] }
                invitationToken:
                  type: string
                  nullable: true
                  description: Required only when target account type is ADMIN (invite-only, see mobile_account_invitations)
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/signup/verify-otp:
    post:
      tags: [Auth]
      summary: Verify signup OTP
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber, otp]
              properties:
                contactNumber: { type: string }
                otp: { type: string, minLength: 6, maxLength: 6 }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /auth/signup/check-username:
    post:
      tags: [Auth]
      summary: Availability check for a chosen username
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /auth/signup/complete:
    post:
      tags: [Auth]
      summary: Create mobile_users + mobile_user_links, issue tokens
      security: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber, username, password, displayName, termsAccepted]
              properties:
                contactNumber: { type: string }
                username: { type: string }
                password: { type: string, format: password }
                displayName: { type: string }
                termsAccepted: { type: boolean }
                deviceId: { type: string }
                platform: { type: string, enum: [ANDROID, IOS] }
      responses:
        "201": { $ref: "#/components/responses/AuthSession" }
        "409": { $ref: "#/components/responses/Conflict" }

  /auth/login:
    post:
      tags: [Auth]
      summary: Username/contact + password login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [identifier, password]
              properties:
                identifier: { type: string, description: "username or normalized contact number" }
                password: { type: string, format: password }
                deviceId: { type: string }
                platform: { type: string, enum: [ANDROID, IOS] }
      responses:
        "200": { $ref: "#/components/responses/AuthSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Rotate refresh token, issue new access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken, deviceId]
              properties:
                refreshToken: { type: string }
                deviceId: { type: string }
      responses:
        "200": { $ref: "#/components/responses/AuthSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke current device's refresh token
      responses:
        "204": { description: Logged out }

  /auth/logout-all:
    post:
      tags: [Auth]
      summary: Revoke all refresh tokens for this identity
      responses:
        "204": { description: All sessions revoked }

  /auth/forgot-password/request-otp:
    post:
      tags: [Auth]
      summary: Request password-reset OTP (generic response regardless of existence)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber]
              properties:
                contactNumber: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /auth/forgot-password/verify-otp:
    post:
      tags: [Auth]
      summary: Verify reset OTP, receive short-lived reset token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactNumber, otp]
              properties:
                contactNumber: { type: string }
                otp: { type: string }
      responses:
        "200":
          description: Reset token issued (10 minute expiry, single use, reset-scope only)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          resetToken: { type: string }
                          expiresInSeconds: { type: integer, example: 600 }

  /auth/forgot-password/reset:
    post:
      tags: [Auth]
      summary: Set new password using reset token, revokes all refresh tokens
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [resetToken, newPassword]
              properties:
                resetToken: { type: string }
                newPassword: { type: string, format: password }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /auth/change-password:
    post:
      tags: [Auth]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword: { type: string, format: password }
                newPassword: { type: string, format: password }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /auth/sessions:
    get:
      tags: [Auth]
      summary: List active devices/sessions for this identity
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /auth/sessions/{sessionId}:
    delete:
      tags: [Auth]
      summary: Revoke a specific device/session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: string }
      responses:
        "204": { description: Revoked }

  /auth/deactivate-account:
    post:
      tags: [Auth]
      summary: Deactivate mobile account (does not delete underlying member/staff record)
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  # ------------------------------------------------------------------- Me
  /me:
    get:
      tags: [Me]
      summary: Unified identity, roles, linked branches, active branch
      responses:
        "200":
          description: Current identity
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MeIdentity" }
    patch:
      tags: [Me]
      summary: Update editable profile fields (display name request, etc.)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /me/preferences:
    patch:
      tags: [Me]
      summary: Update theme mode, notification toggles, biometric login flag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                themeMode: { type: string, enum: [SYSTEM, LIGHT, DARK] }
                biometricLoginEnabled: { type: boolean }
                membershipReminders: { type: boolean }
                feedNotifications: { type: boolean }
                planNotifications: { type: boolean }
                paymentNotifications: { type: boolean }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /me/notifications:
    get:
      tags: [Me]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }

  /me/notifications/{id}/read:
    post:
      tags: [Me]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  /me/device:
    post:
      tags: [Me]
      summary: Register/update device push token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [deviceId, platform]
              properties:
                deviceId: { type: string }
                platform: { type: string, enum: [ANDROID, IOS] }
                pushToken: { type: string, nullable: true }
                appVersion: { type: string }
                osVersion: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }

  # ----------------------------------------------------------------- Feed
  /feed:
    get:
      tags: [Feed]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /posts:
    post:
      tags: [Feed]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [postType]
              properties:
                postType: { type: string, enum: [TEXT, IMAGE] }
                caption: { type: string, maxLength: 1000 }
                mediaUrls:
                  type: array
                  items: { type: string }
                  maxItems: 4
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /posts/{postId}:
    get:
      tags: [Feed]
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
    patch:
      tags: [Feed]
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
    delete:
      tags: [Feed]
      summary: Soft-delete (own post, or any post if Admin)
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "204": { description: Deleted }
  /posts/{postId}/like:
    post:
      tags: [Feed]
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
    delete:
      tags: [Feed]
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "204": { description: Unliked }
  /content-reports:
    post:
      tags: [Feed]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entityType, entityId, reasonCode]
              properties:
                entityType: { type: string, enum: [POST, PROFILE] }
                entityId: { type: integer }
                reasonCode:
                  type: string
                  enum: [SPAM, HARASSMENT, INAPPROPRIATE, PRIVACY, MISINFORMATION, OTHER]
                notes: { type: string }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }

  # --------------------------------------------------------------- Member
  /member/memberships/current:
    get:
      tags: [Member]
      responses:
        "200":
          description: Current/latest membership summary
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/CurrentMembership" }
  /member/memberships:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /member/payments:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /member/packages:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /member/payment-accounts:
    get:
      tags: [Member]
      summary: Gym accounts with show_in_mobile_app = 1 only
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /member/renewal-requests:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Member]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [requestedPackageId, paymentMethod, screenshotUrl]
              properties:
                requestedPackageId: { type: integer }
                paymentAccountId: { type: integer, nullable: true }
                declaredAmount: { type: number }
                paymentMethod:
                  type: string
                  enum: [BANK_TRANSFER, MOBILE_WALLET, CASH_DEPOSIT, OTHER]
                transactionReference: { type: string }
                paymentDate: { type: string, format: date }
                screenshotUrl: { type: string }
                memberNote: { type: string }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /member/renewal-requests/{requestId}/cancel:
    post:
      tags: [Member]
      parameters:
        - name: requestId
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /member/plans:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /member/progress:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /member/measurements:
    get:
      tags: [Member]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Member]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MeasurementInput" }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }

  # -------------------------------------------------------------- Trainer
  /trainer/members:
    get:
      tags: [Trainer]
      summary: Linked members only (active membership first, then latest expired)
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /trainer/members/{memberId}/measurements:
    get:
      tags: [Trainer]
      parameters: [{ $ref: "#/components/parameters/MemberId" }]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Trainer]
      parameters:
        - $ref: "#/components/parameters/MemberId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MeasurementInput" }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/members/{memberId}/notes:
    get:
      tags: [Trainer]
      parameters: [{ $ref: "#/components/parameters/MemberId" }]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Trainer]
      parameters: [{ $ref: "#/components/parameters/MemberId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [noteText]
              properties:
                noteText: { type: string }
                visibility: { type: string, enum: [TRAINER_AND_ADMIN, ADMINS_ONLY] }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/plans:
    get:
      tags: [Trainer]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Trainer]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PlanTemplateInput" }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/plans/{planId}/assign:
    post:
      tags: [Trainer]
      parameters:
        - name: planId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [memberId, startDate]
              properties:
                memberId: { type: integer }
                membershipId: { type: integer, nullable: true }
                startDate: { type: string, format: date }
                endDate: { type: string, format: date, nullable: true }
                trainerNotes: { type: string }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/profile:
    get:
      tags: [Trainer]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
    patch:
      tags: [Trainer]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/certificates:
    get:
      tags: [Trainer]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
    post:
      tags: [Trainer]
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /trainer/payments:
    get:
      tags: [Trainer]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }

  # ---------------------------------------------------------------- Admin
  /admin/attendance/today:
    get:
      tags: [Admin]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /admin/financials/daily-closing:
    get:
      tags: [Admin]
      summary: Read-only — reuses existing web daily-closing calculation, never recalculated independently
      parameters:
        - name: date
          in: query
          schema: { type: string, format: date }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /admin/posts:
    get:
      tags: [Admin]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /admin/posts/{postId}/hide:
    post:
      tags: [Admin]
      parameters: [{ $ref: "#/components/parameters/PostId" }]
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /admin/content-reports:
    get:
      tags: [Admin]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
  /admin/content-reports/{reportId}:
    patch:
      tags: [Admin]
      parameters:
        - name: reportId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status: { type: string, enum: [REVIEWING, RESOLVED, DISMISSED] }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /admin/renewal-requests/{requestId}/approve:
    post:
      tags: [Admin]
      summary: Approves via the same service the web portal renewal flow uses (spec §18)
      parameters:
        - name: requestId
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /admin/renewal-requests/{requestId}/reject:
    post:
      tags: [Admin]
      parameters:
        - name: requestId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [rejectionReason]
              properties:
                rejectionReason: { type: string }
      responses:
        "200": { $ref: "#/components/responses/GenericSuccess" }
  /admin/notifications:
    get:
      tags: [Admin]
      responses:
        "200": { $ref: "#/components/responses/PagedSuccess" }
    post:
      tags: [Admin]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, body, targetType, targetValue]
              properties:
                title: { type: string }
                body: { type: string }
                targetType: { type: string, enum: [USER, ROLE, BRANCH, TOPIC] }
                targetValue: { type: string }
                scheduledAt: { type: string, format: date-time, nullable: true }
      responses:
        "201": { $ref: "#/components/responses/GenericSuccess" }
  /admin/notifications/{notificationId}/send:
    post:
      tags: [Admin]
      parameters:
        - name: notificationId
          in: path
          required: true
          schema: { type: integer }
      responses:
        "202": { description: Accepted for processing via mobile_event_outbox }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, default: 20, maximum: 50 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, format: uuid }
    PostId:
      name: postId
      in: path
      required: true
      schema: { type: integer }
    MemberId:
      name: memberId
      in: path
      required: true
      schema: { type: integer }

  schemas:
    SuccessEnvelope:
      type: object
      properties:
        success: { type: boolean, example: true }
        message: { type: string }
        data: {}
        meta:
          type: object
          properties:
            requestId: { type: string }
            timestamp: { type: string, format: date-time }

    ErrorEnvelope:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            fields:
              type: object
              additionalProperties:
                type: array
                items: { type: string }
        meta:
          type: object
          properties:
            requestId: { type: string }

    ThemeTokens:
      type: object
      properties:
        primaryClr: { type: string }
        secondaryClr: { type: string }
        backgroundClr: { type: string }
        surfaceClr: { type: string }
        textClr: { type: string }
        textMutedClr: { type: string }
        borderClr: { type: string }
        tintClr: { type: string }
        tabActiveClr: { type: string }
        tabInactiveClr: { type: string }
        cardClr: { type: string }
        inputClr: { type: string }
        successClr: { type: string }
        warningClr: { type: string }
        errorClr: { type: string }

    MeIdentity:
      type: object
      properties:
        user:
          type: object
          properties:
            id: { type: integer }
            role: { type: string, enum: [MEMBER, TRAINER, ADMIN] }
            displayName: { type: string }
        branches:
          type: array
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              memberId: { type: integer, nullable: true }
              staffId: { type: integer, nullable: true }
              isPrimary: { type: boolean }
        activeBranchId: { type: integer }

    CurrentMembership:
      type: object
      properties:
        membershipId: { type: integer }
        package: { type: string }
        startDate: { type: string, format: date }
        expiryDate: { type: string, format: date }
        status: { type: string, enum: [ACTIVE, EXPIRED, FROZEN] }
        daysRemaining: { type: integer }
        totalAmount: { type: string }
        paidAmount: { type: string }
        outstandingAmount: { type: string }
        assignedTrainer: { type: string, nullable: true }
        branch: { type: string }
        renewalEligible: { type: boolean }

    MeasurementInput:
      type: object
      required: [measurementDate]
      properties:
        measurementDate: { type: string, format: date }
        heightCm: { type: number, nullable: true }
        weightKg: { type: number, nullable: true }
        bodyFatPercent: { type: number, nullable: true }
        muscleMassKg: { type: number, nullable: true }
        waistCm: { type: number, nullable: true }
        chestCm: { type: number, nullable: true }
        notes: { type: string, nullable: true }

    PlanTemplateInput:
      type: object
      required: [planType, title, durationType]
      properties:
        planType: { type: string, enum: [DIET, MEAL, TRAINING] }
        title: { type: string }
        description: { type: string }
        durationType: { type: string, enum: [DAILY, WEEKLY, MONTHLY, CUSTOM] }
        durationDays: { type: integer, nullable: true }

  responses:
    GenericSuccess:
      description: Standard success envelope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/SuccessEnvelope" }
    PagedSuccess:
      description: Cursor-paginated success envelope
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - type: object
                properties:
                  meta:
                    type: object
                    properties:
                      nextCursor: { type: string, nullable: true }
                      hasMore: { type: boolean }
    AuthSession:
      description: Access + refresh token issued
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - type: object
                properties:
                  data:
                    type: object
                    properties:
                      accessToken: { type: string }
                      refreshToken: { type: string }
                      accessTokenExpiresIn: { type: integer, example: 1200 }
                      user: { $ref: "#/components/schemas/MeIdentity" }
    Unauthorized:
      description: Invalid credentials or expired/revoked token
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    ValidationError:
      description: Field-level validation failure
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Conflict:
      description: Resource already exists (e.g. account already registered)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    RateLimited:
      description: Too many requests
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Maintenance:
      description: App in maintenance mode
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
