openapi: 3.0.3
info:
  title: honeyDue API
  description: |
    REST API for the honeyDue property management platform.
    Consumed by iOS (SwiftUI) and Android (Compose) mobile clients via Kotlin Multiplatform.

    ## Authentication
    Token-based authentication. After login/register, include the token in subsequent requests:
    ```
    Authorization: Token <token>
    ```
    `Bearer <token>` is also accepted.

    ## Common Headers
    | Header | Description | Required |
    |--------|-------------|----------|
    | `Authorization` | `Token <token>` or `Bearer <token>` | Protected routes |
    | `X-Timezone` | IANA timezone (e.g. `America/Chicago`) for overdue calculations | Optional |
    | `Accept-Language` | Locale for i18n error messages (e.g. `en`, `es`) | Optional |
    | `If-None-Match` | ETag for conditional requests on `/api/static_data/` | Optional |

    ## Error Responses
    All errors follow a consistent JSON shape. Validation errors include a `details` map.
  version: 2.0.0
  contact:
    name: honeyDue Team

servers:
  - url: https://honeyDue.treytartt.com/api
    description: Production
  - url: http://127.0.0.1:8000/api
    description: Local development (iOS simulator)
  - url: http://10.0.2.2:8000/api
    description: Local development (Android emulator)

tags:
  - name: Auth (Public)
    description: Authentication endpoints that do not require a token
  - name: Auth (Protected)
    description: Authentication endpoints that require a token
  - name: Static Data
    description: Lookup/reference data (public, cached with ETag)
  - name: Residences
    description: Property management (protected)
  - name: Tasks
    description: Task management and kanban board (protected)
  - name: Task Completions
    description: Task completion records (protected)
  - name: Contractors
    description: Contractor management (protected)
  - name: Documents
    description: Document and warranty management (protected)
  - name: Notifications
    description: Push notifications and preferences (protected)
  - name: Subscriptions
    description: In-app subscription management
  - name: Uploads
    description: File upload endpoints (protected)
  - name: Media
    description: Authenticated media serving (protected)

paths:
  # ===========================================================================
  # Auth (Public)
  # ===========================================================================
  /auth/login/:
    post:
      tags: [Auth (Public)]
      operationId: login
      summary: Login with email/password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Successful login
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Error'

  /auth/register/:
    post:
      tags: [Auth (Public)]
      operationId: register
      summary: Register a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Registration successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          $ref: '#/components/responses/Error'

  /auth/forgot-password/:
    post:
      tags: [Auth (Public)]
      operationId: forgotPassword
      summary: Send password reset code to email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForgotPasswordRequest'
      responses:
        '200':
          description: Reset code sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/ValidationError'

  /auth/verify-reset-code/:
    post:
      tags: [Auth (Public)]
      operationId: verifyResetCode
      summary: Verify password reset code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyResetCodeRequest'
      responses:
        '200':
          description: Code verified, returns reset token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResetCodeResponse'
        '400':
          $ref: '#/components/responses/Error'

  /auth/reset-password/:
    post:
      tags: [Auth (Public)]
      operationId: resetPassword
      summary: Reset password using reset token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
      responses:
        '200':
          description: Password reset successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/Error'

  /auth/apple-sign-in/:
    post:
      tags: [Auth (Public)]
      operationId: appleSignIn
      summary: Sign in with Apple ID token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppleSignInRequest'
      responses:
        '200':
          description: Successful Apple sign-in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialSignInResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'

  /auth/google-sign-in/:
    post:
      tags: [Auth (Public)]
      operationId: googleSignIn
      summary: Sign in with Google ID token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GoogleSignInRequest'
      responses:
        '200':
          description: Successful Google sign-in
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialSignInResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'

  # ===========================================================================
  # Auth (Protected)
  # ===========================================================================
  /auth/logout/:
    post:
      tags: [Auth (Protected)]
      operationId: logout
      summary: Logout (invalidate token)
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Logged out
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/me/:
    get:
      tags: [Auth (Protected)]
      operationId: getCurrentUser
      summary: Get current authenticated user with profile
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Current user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentUserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/profile/:
    put:
      tags: [Auth (Protected)]
      operationId: updateProfile
      summary: Update user profile
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProfileRequest'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentUserResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags: [Auth (Protected)]
      operationId: patchProfile
      summary: Partial update user profile
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProfileRequest'
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrentUserResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/verify-email/:
    post:
      tags: [Auth (Protected)]
      operationId: verifyEmail
      summary: Verify email with 6-digit code
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyEmailRequest'
      responses:
        '200':
          description: Email verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyEmailResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/resend-verification/:
    post:
      tags: [Auth (Protected)]
      operationId: resendVerification
      summary: Resend email verification code
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Verification email sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # Static Data (Public)
  # ===========================================================================
  /static_data/:
    get:
      tags: [Static Data]
      operationId: getStaticData
      summary: Get all lookup/reference data (ETag support)
      description: |
        Returns all seeded lookup data in a single payload. Supports conditional
        requests via `If-None-Match` header -- returns 304 when data has not changed.
      parameters:
        - in: header
          name: If-None-Match
          schema:
            type: string
          description: ETag from a previous response
      responses:
        '200':
          description: All lookup data
          headers:
            ETag:
              schema:
                type: string
              description: Content hash for conditional requests
            Cache-Control:
              schema:
                type: string
              description: "private, max-age=3600"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeededDataResponse'
        '304':
          description: Not Modified (client ETag matches)

  /residences/types/:
    get:
      tags: [Static Data]
      operationId: getResidenceTypes
      summary: List residence types
      responses:
        '200':
          description: Residence types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ResidenceTypeResponse'

  /tasks/categories/:
    get:
      tags: [Static Data]
      operationId: getTaskCategories
      summary: List task categories
      responses:
        '200':
          description: Task categories
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskCategoryResponse'

  /tasks/priorities/:
    get:
      tags: [Static Data]
      operationId: getTaskPriorities
      summary: List task priorities
      responses:
        '200':
          description: Task priorities
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskPriorityResponse'

  /tasks/frequencies/:
    get:
      tags: [Static Data]
      operationId: getTaskFrequencies
      summary: List task frequencies
      responses:
        '200':
          description: Task frequencies
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskFrequencyResponse'

  /contractors/specialties/:
    get:
      tags: [Static Data]
      operationId: getContractorSpecialties
      summary: List contractor specialties
      responses:
        '200':
          description: Contractor specialties
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ContractorSpecialtyResponse'

  /tasks/templates/:
    get:
      tags: [Static Data]
      operationId: getTaskTemplates
      summary: List all task templates
      responses:
        '200':
          description: Task templates
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskTemplateResponse'

  /tasks/templates/grouped/:
    get:
      tags: [Static Data]
      operationId: getTaskTemplatesGrouped
      summary: Get task templates grouped by category
      responses:
        '200':
          description: Templates grouped by category
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskTemplatesGroupedResponse'

  /tasks/templates/search/:
    get:
      tags: [Static Data]
      operationId: searchTaskTemplates
      summary: Search task templates by query
      parameters:
        - in: query
          name: q
          schema:
            type: string
          description: Search query string
      responses:
        '200':
          description: Matching templates
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskTemplateResponse'

  /tasks/templates/by-category/{category_id}/:
    get:
      tags: [Static Data]
      operationId: getTaskTemplatesByCategory
      summary: Get templates for a specific category
      parameters:
        - $ref: '#/components/parameters/CategoryIdParam'
      responses:
        '200':
          description: Templates for category
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskTemplateResponse'

  /tasks/templates/{id}/:
    get:
      tags: [Static Data]
      operationId: getTaskTemplate
      summary: Get a single task template
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskTemplateResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Residences
  # ===========================================================================
  /residences/:
    get:
      tags: [Residences]
      operationId: listResidences
      summary: List all residences the user has access to
      security:
        - tokenAuth: []
      responses:
        '200':
          description: List of residences
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ResidenceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Residences]
      operationId: createResidence
      summary: Create a new residence
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateResidenceRequest'
      responses:
        '201':
          description: Residence created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResidenceWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /residences/my-residences/:
    get:
      tags: [Residences]
      operationId: getMyResidences
      summary: Get user residences (optimized for main screen)
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Residences with metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MyResidencesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /residences/summary/:
    get:
      tags: [Residences]
      operationId: getResidenceSummary
      summary: Get summary counts across all residences
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Summary statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TotalSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /residences/join-with-code/:
    post:
      tags: [Residences]
      operationId: joinWithCode
      summary: Join a residence using a share code
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JoinWithCodeRequest'
      responses:
        '200':
          description: Joined residence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JoinResidenceResponse'
        '400':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Error'

  /residences/{id}/:
    get:
      tags: [Residences]
      operationId: getResidence
      summary: Get a single residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Residence detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResidenceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Residences]
      operationId: updateResidence
      summary: Update a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateResidenceRequest'
      responses:
        '200':
          description: Residence updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResidenceWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Residences]
      operationId: patchResidence
      summary: Partial update a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateResidenceRequest'
      responses:
        '200':
          description: Residence updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResidenceWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Residences]
      operationId: deleteResidence
      summary: Delete a residence (owner only)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Residence deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResidenceDeleteWithSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /residences/{id}/generate-share-code/:
    post:
      tags: [Residences]
      operationId: generateShareCode
      summary: Generate a share code for a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateShareCodeRequest'
      responses:
        '201':
          description: Share code generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateShareCodeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /residences/{id}/generate-share-package/:
    post:
      tags: [Residences]
      operationId: generateSharePackage
      summary: Generate a share package (.honeydue file metadata)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Share package data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SharePackageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /residences/{id}/share-code/:
    get:
      tags: [Residences]
      operationId: getShareCode
      summary: Get active share code for a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Active share code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShareCodeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /residences/{id}/users/:
    get:
      tags: [Residences]
      operationId: getResidenceUsers
      summary: List users who have access to a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: List of residence users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ResidenceUserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /residences/{id}/users/{user_id}/:
    delete:
      tags: [Residences]
      operationId: removeResidenceUser
      summary: Remove a user from a residence (owner only)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
        - in: path
          name: user_id
          required: true
          schema:
            type: integer
            format: uint
          description: ID of the user to remove
      responses:
        '200':
          description: User removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /residences/{id}/generate-tasks-report/:
    post:
      tags: [Residences]
      operationId: generateTasksReport
      summary: Generate a PDF tasks report for a residence
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: PDF report
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ===========================================================================
  # Tasks
  # ===========================================================================
  /tasks/:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: Get kanban board with all user tasks
      description: Returns tasks organized into kanban columns (overdue, due_soon, upcoming, completed, in_progress, archived).
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Kanban board
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanBoardResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Create a new task
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
      responses:
        '201':
          description: Task created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /tasks/bulk/:
    post:
      tags: [Tasks]
      operationId: bulkCreateTasks
      summary: Create multiple tasks atomically
      description: Inserts 1-50 tasks in a single database transaction. If any entry fails, the entire batch is rolled back. Used primarily by onboarding to create the user's initial task list in one request.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkCreateTasksRequest'
      responses:
        '201':
          description: All tasks created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkCreateTasksResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /tasks/by-residence/{residence_id}/:
    get:
      tags: [Tasks]
      operationId: getTasksByResidence
      summary: Get kanban board for a specific residence
      security:
        - tokenAuth: []
      parameters:
        - in: path
          name: residence_id
          required: true
          schema:
            type: integer
            format: uint
          description: Residence ID
      responses:
        '200':
          description: Kanban board for residence
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KanbanBoardResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /tasks/{id}/:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Get task detail
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Tasks]
      operationId: updateTask
      summary: Update a task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskRequest'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Tasks]
      operationId: patchTask
      summary: Partial update a task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskRequest'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Tasks]
      operationId: deleteTask
      summary: Delete a task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteWithSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /tasks/{id}/mark-in-progress/:
    post:
      tags: [Tasks]
      operationId: markTaskInProgress
      summary: Mark a task as in progress
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task marked in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /tasks/{id}/cancel/:
    post:
      tags: [Tasks]
      operationId: cancelTask
      summary: Cancel a task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /tasks/{id}/uncancel/:
    post:
      tags: [Tasks]
      operationId: uncancelTask
      summary: Uncancel a previously cancelled task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task uncancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /tasks/{id}/archive/:
    post:
      tags: [Tasks]
      operationId: archiveTask
      summary: Archive a task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task archived
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /tasks/{id}/unarchive/:
    post:
      tags: [Tasks]
      operationId: unarchiveTask
      summary: Unarchive a previously archived task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task unarchived
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskWithSummaryResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /tasks/{id}/quick-complete/:
    post:
      tags: [Tasks]
      operationId: quickCompleteTask
      summary: Quick complete a task (from widget, no notes/images)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskCompletionWithSummaryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /tasks/{id}/completions/:
    get:
      tags: [Tasks]
      operationId: getTaskCompletions
      summary: List completions for a specific task
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Task completions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskCompletionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Task Completions
  # ===========================================================================
  /task-completions/:
    get:
      tags: [Task Completions]
      operationId: listCompletions
      summary: List all task completions for the user
      security:
        - tokenAuth: []
      responses:
        '200':
          description: List of completions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskCompletionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Task Completions]
      operationId: createCompletion
      summary: Create a task completion (with optional images)
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskCompletionRequest'
      responses:
        '201':
          description: Completion created (includes updated task)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskCompletionWithSummaryResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /task-completions/{id}/:
    get:
      tags: [Task Completions]
      operationId: getCompletion
      summary: Get a single task completion
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Completion detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskCompletionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Task Completions]
      operationId: updateCompletion
      summary: Update a task completion
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskCompletionRequest'
      responses:
        '200':
          description: Completion updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskCompletionResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Task Completions]
      operationId: deleteCompletion
      summary: Delete a task completion
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Completion deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Contractors
  # ===========================================================================
  /contractors/:
    get:
      tags: [Contractors]
      operationId: listContractors
      summary: List all contractors accessible to the user
      security:
        - tokenAuth: []
      responses:
        '200':
          description: List of contractors
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ContractorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Contractors]
      operationId: createContractor
      summary: Create a new contractor
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContractorRequest'
      responses:
        '201':
          description: Contractor created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractorResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /contractors/by-residence/{residence_id}/:
    get:
      tags: [Contractors]
      operationId: listContractorsByResidence
      summary: List contractors for a specific residence
      security:
        - tokenAuth: []
      parameters:
        - in: path
          name: residence_id
          required: true
          schema:
            type: integer
            format: uint
          description: Residence ID
      responses:
        '200':
          description: Contractors for residence
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ContractorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /contractors/{id}/:
    get:
      tags: [Contractors]
      operationId: getContractor
      summary: Get a single contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Contractor detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Contractors]
      operationId: updateContractor
      summary: Update a contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContractorRequest'
      responses:
        '200':
          description: Contractor updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractorResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Contractors]
      operationId: patchContractor
      summary: Partial update a contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContractorRequest'
      responses:
        '200':
          description: Contractor updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContractorResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Contractors]
      operationId: deleteContractor
      summary: Delete a contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Contractor deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /contractors/{id}/toggle-favorite/:
    post:
      tags: [Contractors]
      operationId: toggleContractorFavorite
      summary: Toggle favorite status of a contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Favorite toggled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToggleFavoriteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /contractors/{id}/tasks/:
    get:
      tags: [Contractors]
      operationId: getContractorTasks
      summary: Get tasks associated with a contractor
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Contractor tasks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TaskResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Documents
  # ===========================================================================
  /documents/:
    get:
      tags: [Documents]
      operationId: listDocuments
      summary: List all documents accessible to the user
      security:
        - tokenAuth: []
      parameters:
        - in: query
          name: residence
          description: Filter by residence ID
          schema:
            type: integer
            format: uint
        - in: query
          name: document_type
          description: Filter by document type
          schema:
            type: string
        - in: query
          name: is_active
          description: Filter by active/inactive status (default true)
          schema:
            type: boolean
        - in: query
          name: expiring_soon
          description: Return warranties expiring in N days
          schema:
            type: integer
            minimum: 1
        - in: query
          name: search
          description: Case-insensitive search over title/description
          schema:
            type: string
      responses:
        '200':
          description: List of documents
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Documents]
      operationId: createDocument
      summary: Create a new document
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDocumentRequest'
      responses:
        '201':
          description: Document created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /documents/warranties/:
    get:
      tags: [Documents]
      operationId: listWarranties
      summary: List warranty documents
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Warranty documents
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /documents/{id}/:
    get:
      tags: [Documents]
      operationId: getDocument
      summary: Get a single document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Document detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Documents]
      operationId: updateDocument
      summary: Update a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDocumentRequest'
      responses:
        '200':
          description: Document updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Documents]
      operationId: patchDocument
      summary: Partial update a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDocumentRequest'
      responses:
        '200':
          description: Document updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Documents]
      operationId: deleteDocument
      summary: Delete a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Document deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /documents/{id}/activate/:
    post:
      tags: [Documents]
      operationId: activateDocument
      summary: Activate a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Document activated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  document:
                    $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /documents/{id}/deactivate/:
    post:
      tags: [Documents]
      operationId: deactivateDocument
      summary: Deactivate a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Document deactivated
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  document:
                    $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /documents/{id}/images/:
    post:
      tags: [Documents]
      operationId: uploadDocumentImage
      summary: Upload an image to a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - image
              properties:
                image:
                  type: string
                  format: binary
                  description: Image file to upload
                caption:
                  type: string
                  description: Optional caption for the image
      responses:
        '201':
          description: Image uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /documents/{id}/images/{imageId}/:
    delete:
      tags: [Documents]
      operationId: deleteDocumentImage
      summary: Delete an image from a document
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
        - in: path
          name: imageId
          required: true
          schema:
            type: integer
          description: Image ID
      responses:
        '200':
          description: Image deleted, returns updated document
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Notifications
  # ===========================================================================
  /notifications/:
    get:
      tags: [Notifications]
      operationId: listNotifications
      summary: List user notifications
      security:
        - tokenAuth: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
          description: Max number of notifications to return
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
          description: Offset for pagination
      responses:
        '200':
          description: Notifications list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/unread-count/:
    get:
      tags: [Notifications]
      operationId: getUnreadCount
      summary: Get count of unread notifications
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Unread count
          content:
            application/json:
              schema:
                type: object
                required: [unread_count]
                properties:
                  unread_count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/{id}/read/:
    post:
      tags: [Notifications]
      operationId: markNotificationRead
      summary: Mark a single notification as read
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Marked as read
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/mark-all-read/:
    post:
      tags: [Notifications]
      operationId: markAllNotificationsRead
      summary: Mark all notifications as read
      security:
        - tokenAuth: []
      responses:
        '200':
          description: All marked as read
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/devices/:
    post:
      tags: [Notifications]
      operationId: registerDevice
      summary: Register a push notification device
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterDeviceRequest'
      responses:
        '201':
          description: Device registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags: [Notifications]
      operationId: listDevices
      summary: List registered push devices
      security:
        - tokenAuth: []
      responses:
        '200':
          description: List of devices
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeviceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/devices/register/:
    post:
      tags: [Notifications]
      operationId: registerDeviceAlias
      summary: Register a push notification device (alias)
      description: Alias for POST /notifications/devices/ for mobile client compatibility.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterDeviceRequest'
      responses:
        '201':
          description: Device registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/devices/unregister/:
    post:
      tags: [Notifications]
      operationId: unregisterDevice
      summary: Unregister a push device by registration ID
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UnregisterDeviceRequest'
      responses:
        '200':
          description: Device unregistered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/devices/{id}/:
    delete:
      tags: [Notifications]
      operationId: deleteDevice
      summary: Delete a registered push device
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
        - in: query
          name: platform
          schema:
            type: string
            enum: [ios, android]
            default: ios
          description: Device platform
      responses:
        '200':
          description: Device removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notifications/preferences/:
    get:
      tags: [Notifications]
      operationId: getNotificationPreferences
      summary: Get notification preferences
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Notification preferences
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPreference'
        '401':
          $ref: '#/components/responses/Unauthorized'
    put:
      tags: [Notifications]
      operationId: updateNotificationPreferences
      summary: Update notification preferences
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePreferencesRequest'
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPreference'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
    patch:
      tags: [Notifications]
      operationId: patchNotificationPreferences
      summary: Partial update notification preferences
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePreferencesRequest'
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPreference'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # Subscriptions
  # ===========================================================================
  /subscription/:
    get:
      tags: [Subscriptions]
      operationId: getSubscription
      summary: Get current subscription
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Subscription details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/status/:
    get:
      tags: [Subscriptions]
      operationId: getSubscriptionStatus
      summary: Get subscription status with limits and usage
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Subscription status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/features/:
    get:
      tags: [Subscriptions]
      operationId: getFeatureBenefits
      summary: List feature benefits (free vs pro comparison)
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Feature benefits list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/FeatureBenefit'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/promotions/:
    get:
      tags: [Subscriptions]
      operationId: getPromotions
      summary: List active promotions for the user
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Active promotions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Promotion'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/upgrade-triggers/:
    get:
      tags: [Subscriptions]
      operationId: getAllUpgradeTriggers
      summary: Get all upgrade triggers (public, no auth required)
      description: Available without authentication so the app can display upgrade prompts before login.
      responses:
        '200':
          description: All upgrade triggers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UpgradeTriggerResponse'

  /subscription/upgrade-trigger/{key}/:
    get:
      tags: [Subscriptions]
      operationId: getUpgradeTrigger
      summary: Get a single upgrade trigger by key
      security:
        - tokenAuth: []
      parameters:
        - in: path
          name: key
          required: true
          schema:
            type: string
          description: Trigger key (e.g. "properties_limit", "tasks_limit")
      responses:
        '200':
          description: Upgrade trigger
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpgradeTriggerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /subscription/purchase/:
    post:
      tags: [Subscriptions]
      operationId: processPurchase
      summary: Process an in-app purchase (iOS or Android)
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessPurchaseRequest'
      responses:
        '200':
          description: Purchase processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  subscription:
                    $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/cancel/:
    post:
      tags: [Subscriptions]
      operationId: cancelSubscription
      summary: Cancel subscription
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Subscription cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  subscription:
                    $ref: '#/components/schemas/SubscriptionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/restore/:
    post:
      tags: [Subscriptions]
      operationId: restoreSubscription
      summary: Restore a previous subscription
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessPurchaseRequest'
      responses:
        '200':
          description: Subscription restored
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  subscription:
                    $ref: '#/components/schemas/SubscriptionResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/checkout/:
    post:
      tags: [Subscriptions]
      operationId: createCheckoutSession
      summary: Create a Stripe Checkout session for web subscription purchase
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [price_id, success_url, cancel_url]
              properties:
                price_id:
                  type: string
                  description: Stripe Price ID
                success_url:
                  type: string
                  format: uri
                cancel_url:
                  type: string
                  format: uri
      responses:
        '200':
          description: Checkout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  checkout_url:
                    type: string
                    format: uri
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Already subscribed on another platform
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  existing_platform:
                    type: string
                  message:
                    type: string

  /subscription/portal/:
    post:
      tags: [Subscriptions]
      operationId: createPortalSession
      summary: Create a Stripe Customer Portal session for managing web subscriptions
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [return_url]
              properties:
                return_url:
                  type: string
                  format: uri
      responses:
        '200':
          description: Portal session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  portal_url:
                    type: string
                    format: uri
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /subscription/webhook/stripe/:
    post:
      tags: [Subscriptions]
      operationId: handleStripeWebhook
      summary: Handle Stripe webhook events (server-to-server)
      description: |
        Receives Stripe webhook events for subscription lifecycle management.
        Verifies the webhook signature using the configured signing secret.
        No auth token required — uses Stripe signature verification.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Webhook processed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
        '400':
          $ref: '#/components/responses/Error'

  # ===========================================================================
  # Uploads
  # ===========================================================================
  /uploads/image/:
    post:
      tags: [Uploads]
      operationId: uploadImage
      summary: Upload an image file
      security:
        - tokenAuth: []
      parameters:
        - in: query
          name: category
          schema:
            type: string
            default: images
          description: Storage category/folder
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Upload result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadResult'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /uploads/document/:
    post:
      tags: [Uploads]
      operationId: uploadDocument
      summary: Upload a document file
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Upload result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadResult'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /uploads/completion/:
    post:
      tags: [Uploads]
      operationId: uploadCompletion
      summary: Upload a task completion image
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        '200':
          description: Upload result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadResult'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /uploads/:
    delete:
      tags: [Uploads]
      operationId: deleteUploadedFile
      summary: Delete an uploaded file
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: URL of the file to delete
      responses:
        '200':
          description: File deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # Media (Authenticated serving)
  # ===========================================================================
  /media/document/{id}:
    get:
      tags: [Media]
      operationId: serveDocument
      summary: Serve a document file (authenticated)
      description: Proxies the document file from S3 after verifying the user has access to the residence.
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: File content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /media/document-image/{id}:
    get:
      tags: [Media]
      operationId: serveDocumentImage
      summary: Serve a document image (authenticated)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Image content
          content:
            image/*:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /media/completion-image/{id}:
    get:
      tags: [Media]
      operationId: serveCompletionImage
      summary: Serve a task completion image (authenticated)
      security:
        - tokenAuth: []
      parameters:
        - $ref: '#/components/parameters/IdParam'
      responses:
        '200':
          description: Image content
          content:
            image/*:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /auth/account/:
    delete:
      tags: [Authentication]
      summary: Delete user account
      description: Permanently deletes the authenticated user's account and all associated data
      security:
        - tokenAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                password:
                  type: string
                  description: Required for email-auth users
                confirmation:
                  type: string
                  description: Must be "DELETE" for social-auth users
      responses:
        '200':
          description: Account deleted successfully
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/refresh/:
    post:
      tags: [Authentication]
      summary: Refresh auth token
      description: Returns a new token if current token is in the renewal window (60-90 days old)
      security:
        - tokenAuth: []
      responses:
        '200':
          description: Token refreshed
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /health/live:
    get:
      tags: [Health]
      summary: Liveness probe
      description: Simple liveness check, always returns 200
      responses:
        '200':
          description: Alive

  /tasks/suggestions/:
    get:
      tags: [Tasks]
      summary: Get personalized task template suggestions
      description: Returns task templates ranked by relevance to the residence's home profile
      security:
        - tokenAuth: []
      parameters:
        - name: residence_id
          in: query
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Suggestions with relevance scores
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        template:
                          $ref: '#/components/schemas/TaskTemplate'
                        relevance_score:
                          type: number
                        match_reasons:
                          type: array
                          items:
                            type: string
                  total_count:
                    type: integer
                  profile_completeness:
                    type: number
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

# =============================================================================
# Components
# =============================================================================
components:
  securitySchemes:
    tokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: 'Token-based auth. Format: `Token <token>` or `Bearer <token>`'

  parameters:
    IdParam:
      in: path
      name: id
      required: true
      schema:
        type: integer
        format: uint
      description: Resource ID
    CategoryIdParam:
      in: path
      name: category_id
      required: true
      schema:
        type: integer
        format: uint
      description: Category ID

  responses:
    Error:
      description: Error response
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ValidationError:
      description: Validation error with field details
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Access denied
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    # =========================================================================
    # Common
    # =========================================================================
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message (may be localized)
        details:
          type: object
          additionalProperties:
            type: string
          description: Field-level validation errors (field name -> message)

    MessageResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string

    # =========================================================================
    # Auth Requests
    # =========================================================================
    LoginRequest:
      type: object
      properties:
        username:
          type: string
          description: Username (required if email not provided)
        email:
          type: string
          format: email
          description: Email (required if username not provided)
        password:
          type: string
          minLength: 1
      required: [password]

    RegisterRequest:
      type: object
      required: [username, email, password]
      properties:
        username:
          type: string
          minLength: 3
          maxLength: 150
        email:
          type: string
          format: email
          maxLength: 254
        password:
          type: string
          minLength: 8
        first_name:
          type: string
          maxLength: 150
        last_name:
          type: string
          maxLength: 150

    ForgotPasswordRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email

    VerifyResetCodeRequest:
      type: object
      required: [email, code]
      properties:
        email:
          type: string
          format: email
        code:
          type: string
          minLength: 6
          maxLength: 6

    ResetPasswordRequest:
      type: object
      required: [reset_token, new_password]
      properties:
        reset_token:
          type: string
        new_password:
          type: string
          minLength: 8

    UpdateProfileRequest:
      type: object
      properties:
        email:
          type: string
          format: email
          maxLength: 254
          nullable: true
        first_name:
          type: string
          maxLength: 150
          nullable: true
        last_name:
          type: string
          maxLength: 150
          nullable: true

    VerifyEmailRequest:
      type: object
      required: [code]
      properties:
        code:
          type: string
          minLength: 6
          maxLength: 6

    AppleSignInRequest:
      type: object
      required: [id_token, user_id]
      properties:
        id_token:
          type: string
          description: Apple identity token (JWT)
        user_id:
          type: string
          description: Apple user ID (sub claim)
        email:
          type: string
          format: email
          nullable: true
          description: May be nil or private relay address
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true

    GoogleSignInRequest:
      type: object
      required: [id_token]
      properties:
        id_token:
          type: string
          description: Google ID token from Credential Manager

    # =========================================================================
    # Auth Responses
    # =========================================================================
    UserResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        username:
          type: string
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        is_active:
          type: boolean
        verified:
          type: boolean
        date_joined:
          type: string
          format: date-time
        last_login:
          type: string
          format: date-time
          nullable: true

    UserProfileResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        user_id:
          type: integer
          format: uint
        verified:
          type: boolean
        bio:
          type: string
        phone_number:
          type: string
        date_of_birth:
          type: string
          format: date-time
          nullable: true
        profile_picture:
          type: string

    LoginResponse:
      type: object
      required: [token, user]
      properties:
        token:
          type: string
        user:
          $ref: '#/components/schemas/UserResponse'

    RegisterResponse:
      type: object
      required: [token, user, message]
      properties:
        token:
          type: string
        user:
          $ref: '#/components/schemas/UserResponse'
        message:
          type: string

    SocialSignInResponse:
      type: object
      required: [token, user, is_new_user]
      properties:
        token:
          type: string
        user:
          $ref: '#/components/schemas/UserResponse'
        is_new_user:
          type: boolean

    CurrentUserResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        username:
          type: string
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        is_active:
          type: boolean
        date_joined:
          type: string
          format: date-time
        last_login:
          type: string
          format: date-time
          nullable: true
        profile:
          $ref: '#/components/schemas/UserProfileResponse'

    VerifyEmailResponse:
      type: object
      properties:
        message:
          type: string
        verified:
          type: boolean

    VerifyResetCodeResponse:
      type: object
      properties:
        message:
          type: string
        reset_token:
          type: string

    # =========================================================================
    # Static Data / Lookups
    # =========================================================================
    SeededDataResponse:
      type: object
      properties:
        residence_types:
          type: array
          items:
            $ref: '#/components/schemas/ResidenceTypeResponse'
        task_categories:
          type: array
          items:
            $ref: '#/components/schemas/TaskCategoryResponse'
        task_priorities:
          type: array
          items:
            $ref: '#/components/schemas/TaskPriorityResponse'
        task_frequencies:
          type: array
          items:
            $ref: '#/components/schemas/TaskFrequencyResponse'
        contractor_specialties:
          type: array
          items:
            $ref: '#/components/schemas/ContractorSpecialtyResponse'
        task_templates:
          $ref: '#/components/schemas/TaskTemplatesGroupedResponse'

    ResidenceTypeResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string

    TaskCategoryResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string
        description:
          type: string
        icon:
          type: string
        color:
          type: string
        display_order:
          type: integer

    TaskPriorityResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string
        level:
          type: integer
        color:
          type: string
        display_order:
          type: integer

    TaskFrequencyResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string
        days:
          type: integer
          nullable: true
          description: Number of days between occurrences (null for one-time)
        display_order:
          type: integer

    ContractorSpecialtyResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string
        description:
          type: string
        icon:
          type: string
        display_order:
          type: integer

    TaskTemplateResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        title:
          type: string
        description:
          type: string
        category_id:
          type: integer
          format: uint
          nullable: true
        category:
          $ref: '#/components/schemas/TaskCategoryResponse'
        frequency_id:
          type: integer
          format: uint
          nullable: true
        frequency:
          $ref: '#/components/schemas/TaskFrequencyResponse'
        icon_ios:
          type: string
        icon_android:
          type: string
        tags:
          type: array
          items:
            type: string
        display_order:
          type: integer
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TaskTemplateCategoryGroup:
      type: object
      properties:
        category_name:
          type: string
        category_id:
          type: integer
          format: uint
          nullable: true
        templates:
          type: array
          items:
            $ref: '#/components/schemas/TaskTemplateResponse'
        count:
          type: integer

    TaskTemplatesGroupedResponse:
      type: object
      properties:
        categories:
          type: array
          items:
            $ref: '#/components/schemas/TaskTemplateCategoryGroup'
        total_count:
          type: integer

    # =========================================================================
    # Residence
    # =========================================================================
    CreateResidenceRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        property_type_id:
          type: integer
          format: uint
          nullable: true
        street_address:
          type: string
          maxLength: 255
        apartment_unit:
          type: string
          maxLength: 50
        city:
          type: string
          maxLength: 100
        state_province:
          type: string
          maxLength: 100
        postal_code:
          type: string
          maxLength: 20
        country:
          type: string
          maxLength: 100
        bedrooms:
          type: integer
          nullable: true
        bathrooms:
          type: string
          description: Decimal value (e.g. "2.5")
          nullable: true
        square_footage:
          type: integer
          nullable: true
        lot_size:
          type: string
          description: Decimal value
          nullable: true
        year_built:
          type: integer
          nullable: true
        description:
          type: string
        purchase_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          description: Decimal value (e.g. "350000.00")
          nullable: true
        is_primary:
          type: boolean
          nullable: true

    UpdateResidenceRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          nullable: true
        property_type_id:
          type: integer
          format: uint
          nullable: true
        street_address:
          type: string
          maxLength: 255
          nullable: true
        apartment_unit:
          type: string
          maxLength: 50
          nullable: true
        city:
          type: string
          maxLength: 100
          nullable: true
        state_province:
          type: string
          maxLength: 100
          nullable: true
        postal_code:
          type: string
          maxLength: 20
          nullable: true
        country:
          type: string
          maxLength: 100
          nullable: true
        bedrooms:
          type: integer
          nullable: true
        bathrooms:
          type: string
          nullable: true
        square_footage:
          type: integer
          nullable: true
        lot_size:
          type: string
          nullable: true
        year_built:
          type: integer
          nullable: true
        description:
          type: string
          nullable: true
        purchase_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          nullable: true
        is_primary:
          type: boolean
          nullable: true

    JoinWithCodeRequest:
      type: object
      required: [code]
      properties:
        code:
          type: string
          minLength: 6
          maxLength: 6

    GenerateShareCodeRequest:
      type: object
      properties:
        expires_in_hours:
          type: integer
          description: Hours until code expires (default 24)

    ResidenceUserResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        username:
          type: string
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string

    ResidenceResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        owner_id:
          type: integer
          format: uint
        owner:
          $ref: '#/components/schemas/ResidenceUserResponse'
        users:
          type: array
          items:
            $ref: '#/components/schemas/ResidenceUserResponse'
        name:
          type: string
        property_type_id:
          type: integer
          format: uint
          nullable: true
        property_type:
          $ref: '#/components/schemas/ResidenceTypeResponse'
        street_address:
          type: string
        apartment_unit:
          type: string
        city:
          type: string
        state_province:
          type: string
        postal_code:
          type: string
        country:
          type: string
        bedrooms:
          type: integer
          nullable: true
        bathrooms:
          type: string
          nullable: true
          description: Decimal
        square_footage:
          type: integer
          nullable: true
        lot_size:
          type: string
          nullable: true
          description: Decimal
        year_built:
          type: integer
          nullable: true
        description:
          type: string
        purchase_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          nullable: true
          description: Decimal
        is_primary:
          type: boolean
        is_active:
          type: boolean
        overdue_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TotalSummary:
      type: object
      properties:
        total_residences:
          type: integer
        total_tasks:
          type: integer
        total_pending:
          type: integer
        total_overdue:
          type: integer
        tasks_due_next_week:
          type: integer
        tasks_due_next_month:
          type: integer

    MyResidencesResponse:
      type: object
      properties:
        residences:
          type: array
          items:
            $ref: '#/components/schemas/ResidenceResponse'

    ResidenceWithSummaryResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/ResidenceResponse'
        summary:
          $ref: '#/components/schemas/TotalSummary'

    ResidenceDeleteWithSummaryResponse:
      type: object
      properties:
        data:
          type: string
        summary:
          $ref: '#/components/schemas/TotalSummary'

    ShareCodeResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        code:
          type: string
        residence_id:
          type: integer
          format: uint
        created_by_id:
          type: integer
          format: uint
        is_active:
          type: boolean
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    JoinResidenceResponse:
      type: object
      properties:
        message:
          type: string
        residence:
          $ref: '#/components/schemas/ResidenceResponse'
        summary:
          $ref: '#/components/schemas/TotalSummary'

    GenerateShareCodeResponse:
      type: object
      properties:
        message:
          type: string
        share_code:
          $ref: '#/components/schemas/ShareCodeResponse'

    SharePackageResponse:
      type: object
      properties:
        share_code:
          type: string
        residence_name:
          type: string
        shared_by:
          type: string
        expires_at:
          type: string
          format: date-time
          nullable: true

    # =========================================================================
    # Task
    # =========================================================================
    CreateTaskRequest:
      type: object
      required: [residence_id, title]
      properties:
        residence_id:
          type: integer
          format: uint
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        category_id:
          type: integer
          format: uint
          nullable: true
        priority_id:
          type: integer
          format: uint
          nullable: true
        frequency_id:
          type: integer
          format: uint
          nullable: true
        custom_interval_days:
          type: integer
          nullable: true
          description: For "Custom" frequency, user-specified interval in days
        in_progress:
          type: boolean
          default: false
        assigned_to_id:
          type: integer
          format: uint
          nullable: true
        due_date:
          type: string
          description: Accepts "2025-11-27" or "2025-11-27T00:00:00Z"
          nullable: true
        estimated_cost:
          type: string
          description: Decimal value (e.g. "150.00")
          nullable: true
        contractor_id:
          type: integer
          format: uint
          nullable: true
        template_id:
          type: integer
          format: uint
          nullable: true
          description: TaskTemplate ID this task was spawned from (onboarding suggestion, browse-catalog pick). Omit for custom tasks.

    BulkCreateTasksRequest:
      type: object
      required: [residence_id, tasks]
      properties:
        residence_id:
          type: integer
          format: uint
          description: Residence that owns every task in the batch; overrides the per-entry residence_id.
        tasks:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: '#/components/schemas/CreateTaskRequest'

    BulkCreateTasksResponse:
      type: object
      properties:
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/TaskResponse'
        summary:
          $ref: '#/components/schemas/TotalSummary'
        created_count:
          type: integer

    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          nullable: true
        description:
          type: string
          nullable: true
        category_id:
          type: integer
          format: uint
          nullable: true
        priority_id:
          type: integer
          format: uint
          nullable: true
        frequency_id:
          type: integer
          format: uint
          nullable: true
        custom_interval_days:
          type: integer
          nullable: true
        in_progress:
          type: boolean
          nullable: true
        assigned_to_id:
          type: integer
          format: uint
          nullable: true
        due_date:
          type: string
          nullable: true
        estimated_cost:
          type: string
          nullable: true
        actual_cost:
          type: string
          nullable: true
        contractor_id:
          type: integer
          format: uint
          nullable: true

    TaskUserResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        username:
          type: string
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string

    TaskResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        residence_id:
          type: integer
          format: uint
        created_by_id:
          type: integer
          format: uint
        created_by:
          $ref: '#/components/schemas/TaskUserResponse'
        assigned_to_id:
          type: integer
          format: uint
          nullable: true
        assigned_to:
          $ref: '#/components/schemas/TaskUserResponse'
        title:
          type: string
        description:
          type: string
        category_id:
          type: integer
          format: uint
          nullable: true
        category:
          $ref: '#/components/schemas/TaskCategoryResponse'
        priority_id:
          type: integer
          format: uint
          nullable: true
        priority:
          $ref: '#/components/schemas/TaskPriorityResponse'
        frequency_id:
          type: integer
          format: uint
          nullable: true
        frequency:
          $ref: '#/components/schemas/TaskFrequencyResponse'
        in_progress:
          type: boolean
        due_date:
          type: string
          format: date
          nullable: true
        next_due_date:
          type: string
          format: date
          nullable: true
          description: For recurring tasks, updated after each completion
        estimated_cost:
          type: string
          nullable: true
          description: Decimal
        actual_cost:
          type: string
          nullable: true
          description: Decimal
        contractor_id:
          type: integer
          format: uint
          nullable: true
        is_cancelled:
          type: boolean
        is_archived:
          type: boolean
        parent_task_id:
          type: integer
          format: uint
          nullable: true
        template_id:
          type: integer
          format: uint
          nullable: true
          description: TaskTemplate this task was spawned from; nil for custom user tasks.
        completion_count:
          type: integer
        kanban_column:
          type: string
          description: "Computed column name: overdue, due_soon, upcoming, completed, in_progress, archived, cancelled"
          enum: [overdue, due_soon, upcoming, completed, in_progress, archived, cancelled]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    KanbanColumnResponse:
      type: object
      properties:
        name:
          type: string
          description: Column identifier
        display_name:
          type: string
          description: Human-readable column name
        button_types:
          type: array
          items:
            type: string
          description: Available action button types for tasks in this column
        icons:
          type: object
          additionalProperties:
            type: string
          description: Platform-specific icon names (ios, android)
        color:
          type: string
          description: Hex color for the column
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/TaskResponse'
        count:
          type: integer

    KanbanBoardResponse:
      type: object
      properties:
        columns:
          type: array
          items:
            $ref: '#/components/schemas/KanbanColumnResponse'
        days_threshold:
          type: integer
          description: Number of days used for "due soon" calculation (default 30)
        residence_id:
          type: string
          description: '"all" or the specific residence ID'

    TaskWithSummaryResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/TaskResponse'
        summary:
          $ref: '#/components/schemas/TotalSummary'

    DeleteWithSummaryResponse:
      type: object
      properties:
        data:
          type: string
        summary:
          $ref: '#/components/schemas/TotalSummary'

    # =========================================================================
    # Task Completion
    # =========================================================================
    CreateTaskCompletionRequest:
      type: object
      required: [task_id]
      properties:
        task_id:
          type: integer
          format: uint
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: Defaults to now if not provided
        notes:
          type: string
        actual_cost:
          type: string
          nullable: true
          description: Decimal
        rating:
          type: integer
          minimum: 1
          maximum: 5
          nullable: true
        image_urls:
          type: array
          items:
            type: string
          description: URLs of uploaded images to attach

    UpdateTaskCompletionRequest:
      type: object
      properties:
        notes:
          type: string
          nullable: true
        actual_cost:
          type: string
          nullable: true
        rating:
          type: integer
          minimum: 1
          maximum: 5
          nullable: true
        image_urls:
          type: array
          items:
            type: string
          description: Replaces all existing image associations

    TaskCompletionImageResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        image_url:
          type: string
          description: Direct S3 URL (may require auth)
        media_url:
          type: string
          description: "Authenticated proxy endpoint: /api/media/completion-image/{id}"
        caption:
          type: string

    TaskCompletionResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        task_id:
          type: integer
          format: uint
        completed_by:
          $ref: '#/components/schemas/TaskUserResponse'
        completed_at:
          type: string
          format: date-time
        notes:
          type: string
        actual_cost:
          type: string
          nullable: true
          description: Decimal
        rating:
          type: integer
          nullable: true
        images:
          type: array
          items:
            $ref: '#/components/schemas/TaskCompletionImageResponse'
        created_at:
          type: string
          format: date-time
        task:
          $ref: '#/components/schemas/TaskResponse'
          description: Included after create/quick-complete with updated task state

    TaskCompletionWithSummaryResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/TaskCompletionResponse'
        summary:
          $ref: '#/components/schemas/TotalSummary'

    # =========================================================================
    # Contractor
    # =========================================================================
    CreateContractorRequest:
      type: object
      required: [name]
      properties:
        residence_id:
          type: integer
          format: uint
          nullable: true
        name:
          type: string
          minLength: 1
          maxLength: 200
        company:
          type: string
          maxLength: 200
        phone:
          type: string
          maxLength: 20
        email:
          type: string
          format: email
          maxLength: 254
        website:
          type: string
          maxLength: 200
        notes:
          type: string
        street_address:
          type: string
          maxLength: 255
        city:
          type: string
          maxLength: 100
        state_province:
          type: string
          maxLength: 100
        postal_code:
          type: string
          maxLength: 20
        specialty_ids:
          type: array
          items:
            type: integer
            format: uint
        rating:
          type: number
          format: double
          nullable: true
        is_favorite:
          type: boolean
          nullable: true

    UpdateContractorRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          nullable: true
        company:
          type: string
          maxLength: 200
          nullable: true
        phone:
          type: string
          maxLength: 20
          nullable: true
        email:
          type: string
          format: email
          maxLength: 254
          nullable: true
        website:
          type: string
          maxLength: 200
          nullable: true
        notes:
          type: string
          nullable: true
        street_address:
          type: string
          maxLength: 255
          nullable: true
        city:
          type: string
          maxLength: 100
          nullable: true
        state_province:
          type: string
          maxLength: 100
          nullable: true
        postal_code:
          type: string
          maxLength: 20
          nullable: true
        specialty_ids:
          type: array
          items:
            type: integer
            format: uint
        rating:
          type: number
          format: double
          nullable: true
        is_favorite:
          type: boolean
          nullable: true
        residence_id:
          type: integer
          format: uint
          nullable: true

    ContractorResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        residence_id:
          type: integer
          format: uint
          nullable: true
        created_by_id:
          type: integer
          format: uint
        added_by:
          type: integer
          format: uint
          description: Alias for created_by_id (KMM compatibility)
        created_by:
          type: object
          properties:
            id:
              type: integer
              format: uint
            username:
              type: string
            first_name:
              type: string
            last_name:
              type: string
        name:
          type: string
        company:
          type: string
        phone:
          type: string
        email:
          type: string
        website:
          type: string
        notes:
          type: string
        street_address:
          type: string
        city:
          type: string
        state_province:
          type: string
        postal_code:
          type: string
        specialties:
          type: array
          items:
            $ref: '#/components/schemas/ContractorSpecialtyResponse'
        rating:
          type: number
          format: double
          nullable: true
        is_favorite:
          type: boolean
        is_active:
          type: boolean
        task_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ToggleFavoriteResponse:
      type: object
      properties:
        message:
          type: string
        is_favorite:
          type: boolean

    # =========================================================================
    # Document
    # =========================================================================
    DocumentType:
      type: string
      enum: [general, warranty, receipt, contract, insurance, manual]

    CreateDocumentRequest:
      type: object
      required: [residence_id, title]
      properties:
        residence_id:
          type: integer
          format: uint
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        document_type:
          $ref: '#/components/schemas/DocumentType'
        file_url:
          type: string
          maxLength: 500
        file_name:
          type: string
          maxLength: 255
        file_size:
          type: integer
          format: int64
          nullable: true
        mime_type:
          type: string
          maxLength: 100
        purchase_date:
          type: string
          format: date-time
          nullable: true
        expiry_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          nullable: true
          description: Decimal
        vendor:
          type: string
          maxLength: 200
        serial_number:
          type: string
          maxLength: 100
        model_number:
          type: string
          maxLength: 100
        task_id:
          type: integer
          format: uint
          nullable: true
        image_urls:
          type: array
          items:
            type: string
          description: URLs of uploaded images to attach

    UpdateDocumentRequest:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          nullable: true
        description:
          type: string
          nullable: true
        document_type:
          $ref: '#/components/schemas/DocumentType'
        file_url:
          type: string
          maxLength: 500
          nullable: true
        file_name:
          type: string
          maxLength: 255
          nullable: true
        file_size:
          type: integer
          format: int64
          nullable: true
        mime_type:
          type: string
          maxLength: 100
          nullable: true
        purchase_date:
          type: string
          format: date-time
          nullable: true
        expiry_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          nullable: true
        vendor:
          type: string
          maxLength: 200
          nullable: true
        serial_number:
          type: string
          maxLength: 100
          nullable: true
        model_number:
          type: string
          maxLength: 100
          nullable: true
        task_id:
          type: integer
          format: uint
          nullable: true

    DocumentImageResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        image_url:
          type: string
        media_url:
          type: string
          description: "Authenticated proxy endpoint: /api/media/document-image/{id}"
        caption:
          type: string

    DocumentResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        residence_id:
          type: integer
          format: uint
        residence:
          type: integer
          format: uint
          description: Alias for residence_id (KMM compatibility)
        created_by_id:
          type: integer
          format: uint
        created_by:
          type: object
          properties:
            id:
              type: integer
              format: uint
            username:
              type: string
            first_name:
              type: string
            last_name:
              type: string
        title:
          type: string
        description:
          type: string
        document_type:
          $ref: '#/components/schemas/DocumentType'
        file_url:
          type: string
        media_url:
          type: string
          description: "Authenticated proxy endpoint: /api/media/document/{id}"
        file_name:
          type: string
        file_size:
          type: integer
          format: int64
          nullable: true
        mime_type:
          type: string
        purchase_date:
          type: string
          format: date-time
          nullable: true
        expiry_date:
          type: string
          format: date-time
          nullable: true
        purchase_price:
          type: string
          nullable: true
          description: Decimal
        vendor:
          type: string
        serial_number:
          type: string
        model_number:
          type: string
        task_id:
          type: integer
          format: uint
          nullable: true
        is_active:
          type: boolean
        images:
          type: array
          items:
            $ref: '#/components/schemas/DocumentImageResponse'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    # =========================================================================
    # Notifications
    # =========================================================================
    NotificationType:
      type: string
      enum:
        - task_due_soon
        - task_overdue
        - task_completed
        - task_assigned
        - residence_shared
        - warranty_expiring

    Notification:
      type: object
      properties:
        id:
          type: integer
          format: uint
        user_id:
          type: integer
          format: uint
        notification_type:
          $ref: '#/components/schemas/NotificationType'
        title:
          type: string
        body:
          type: string
        task_id:
          type: integer
          format: uint
          nullable: true
        data:
          type: string
          description: JSON string with additional data
        sent:
          type: boolean
        sent_at:
          type: string
          format: date-time
          nullable: true
        read:
          type: boolean
        read_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    NotificationListResponse:
      type: object
      properties:
        count:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/Notification'

    NotificationPreference:
      type: object
      properties:
        id:
          type: integer
          format: uint
        user_id:
          type: integer
          format: uint
        task_due_soon:
          type: boolean
        task_overdue:
          type: boolean
        task_completed:
          type: boolean
        task_assigned:
          type: boolean
        residence_shared:
          type: boolean
        warranty_expiring:
          type: boolean
        daily_digest:
          type: boolean
        email_task_completed:
          type: boolean
        task_due_soon_hour:
          type: integer
          nullable: true
          description: UTC hour (0-23) for task due soon notifications
        task_overdue_hour:
          type: integer
          nullable: true
          description: UTC hour (0-23) for task overdue notifications
        warranty_expiring_hour:
          type: integer
          nullable: true
        daily_digest_hour:
          type: integer
          nullable: true

    UpdatePreferencesRequest:
      type: object
      properties:
        task_due_soon:
          type: boolean
          nullable: true
        task_overdue:
          type: boolean
          nullable: true
        task_completed:
          type: boolean
          nullable: true
        task_assigned:
          type: boolean
          nullable: true
        residence_shared:
          type: boolean
          nullable: true
        warranty_expiring:
          type: boolean
          nullable: true
        daily_digest:
          type: boolean
          nullable: true
        email_task_completed:
          type: boolean
          nullable: true
        task_due_soon_hour:
          type: integer
          nullable: true
        task_overdue_hour:
          type: integer
          nullable: true
        warranty_expiring_hour:
          type: integer
          nullable: true
        daily_digest_hour:
          type: integer
          nullable: true

    RegisterDeviceRequest:
      type: object
      required: [device_id, registration_id, platform]
      properties:
        name:
          type: string
        device_id:
          type: string
        registration_id:
          type: string
          description: APNs device token or FCM registration token
        platform:
          type: string
          enum: [ios, android]

    UnregisterDeviceRequest:
      type: object
      required: [registration_id]
      properties:
        registration_id:
          type: string
        platform:
          type: string
          enum: [ios, android]
          default: ios

    DeviceResponse:
      type: object
      properties:
        id:
          type: integer
          format: uint
        name:
          type: string
        device_id:
          type: string
        registration_id:
          type: string
        platform:
          type: string
          enum: [ios, android]
        active:
          type: boolean
        date_created:
          type: string
          format: date-time

    # =========================================================================
    # Subscriptions
    # =========================================================================
    SubscriptionResponse:
      type: object
      properties:
        tier:
          type: string
          enum: [free, pro]
        subscribed_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        auto_renew:
          type: boolean
        cancelled_at:
          type: string
          format: date-time
          nullable: true
        platform:
          type: string
          description: "ios or android"
        is_active:
          type: boolean
        is_pro:
          type: boolean

    UsageResponse:
      type: object
      properties:
        properties_count:
          type: integer
          format: int64
        tasks_count:
          type: integer
          format: int64
        contractors_count:
          type: integer
          format: int64
        documents_count:
          type: integer
          format: int64

    TierLimitsClientResponse:
      type: object
      properties:
        properties:
          type: integer
          nullable: true
          description: "null means unlimited"
        tasks:
          type: integer
          nullable: true
        contractors:
          type: integer
          nullable: true
        documents:
          type: integer
          nullable: true

    SubscriptionStatusResponse:
      type: object
      properties:
        tier:
          type: string
          description: 'Subscription tier (free or pro)'
        is_active:
          type: boolean
          description: Whether the subscription is currently active
        subscribed_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        auto_renew:
          type: boolean
        trial_start:
          type: string
          format: date-time
          nullable: true
        trial_end:
          type: string
          format: date-time
          nullable: true
        trial_active:
          type: boolean
        subscription_source:
          type: string
          nullable: true
          description: 'Platform source of the active subscription (ios, android, stripe, or null)'
        usage:
          $ref: '#/components/schemas/UsageResponse'
        limits:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/TierLimitsClientResponse'
          description: 'Map of tier name -> limits (e.g. {"free": {...}, "pro": {...}})'
        limitations_enabled:
          type: boolean
          description: Whether subscription limitations are currently enforced

    UpgradeTriggerResponse:
      type: object
      properties:
        trigger_key:
          type: string
        title:
          type: string
        message:
          type: string
        promo_html:
          type: string
        button_text:
          type: string

    FeatureBenefit:
      type: object
      properties:
        id:
          type: integer
          format: uint
        feature_name:
          type: string
        free_tier_text:
          type: string
        pro_tier_text:
          type: string
        display_order:
          type: integer
        is_active:
          type: boolean

    Promotion:
      type: object
      properties:
        id:
          type: integer
          format: uint
        promotion_id:
          type: string
        title:
          type: string
        message:
          type: string
        link:
          type: string
          nullable: true
        start_date:
          type: string
          format: date-time
        end_date:
          type: string
          format: date-time
        target_tier:
          type: string
          enum: [free, pro]
        is_active:
          type: boolean

    ProcessPurchaseRequest:
      type: object
      required: [platform]
      properties:
        receipt_data:
          type: string
          description: iOS StoreKit 1 receipt or StoreKit 2 JWS
        transaction_id:
          type: string
          description: iOS StoreKit 2 transaction ID
        purchase_token:
          type: string
          description: Android Google Play purchase token
        product_id:
          type: string
          description: Android product ID (helps identify subscription)
        platform:
          type: string
          enum: [ios, android]

    # =========================================================================
    # Uploads
    # =========================================================================
    UploadResult:
      type: object
      properties:
        url:
          type: string
          description: URL of the uploaded file
        file_name:
          type: string
        file_size:
          type: integer
          format: int64
        mime_type:
          type: string
