openapi: 3.1.0
info:
  title: Sicket API
  version: 1.0.0
  description: >
    Canonical OpenAPI specification for Sicket. Public documentation uses the
    versioned /api/v1/* surface. Legacy /api/* aliases still exist in the
    backend for backward compatibility during migration.
servers:
  - url: https://api.sicket.app
    description: Production
  - url: http://localhost:8080
    description: Local development
tags:
  - name: Health
  - name: Authentication
  - name: Notifications
  - name: Invitations
  - name: Organizations
  - name: Buildings
  - name: Residential Units
  - name: Announcements
  - name: Users
  - name: Dashboard
  - name: Search
  - name: Tickets
  - name: Ticket Comments
  - name: Ticket Notes
  - name: Ticket Attachments
  - name: Analytics
  - name: Knowledge Base
  - name: Subscriptions
  - name: AI Ticket Assistance
  - name: Realtime
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    TicketRefParam:
      in: path
      name: id
      required: true
      description: >
        Public ticket reference used by the frontend. Prefer the human-facing
        ticketCode. The backend still accepts legacy numeric IDs during
        migration.
      schema:
        type: string
        example: 6WS42A
    TicketCodeParam:
      in: path
      name: ticketCode
      required: true
      description: Human-facing ticket reference such as 6WS42A.
      schema:
        type: string
        example: 6WS42A
    AttachmentIdParam:
      in: path
      name: id
      required: true
      schema:
        type: integer
        format: int64
        example: 55
    BuildingIdParam:
      in: path
      name: buildingId
      required: true
      schema:
        type: integer
        format: int64
        example: 9
  schemas:
    HealthResponse:
      type: object
      required: [status, timestamp, version, profile, service]
      properties:
        status:
          type: string
          example: UP
        timestamp:
          type: string
          format: date-time
        version:
          type: string
          example: 1.0.0
        profile:
          type: string
          example: local
        service:
          type: string
          example: sicket-api
    ApiErrorResponse:
      type: object
      required: [success, message, error, timestamp]
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
        error:
          type: string
        fieldErrors:
          type: object
          additionalProperties:
            type: string
        timestamp:
          type: string
          format: date-time
    ApiResponse_Login:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: Login successful
        data:
          $ref: '#/components/schemas/LoginResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_CurrentUser:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/CurrentUser'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Null:
      type: object
      required: [success, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          nullable: true
        timestamp:
          type: string
          format: date-time
    ApiResponse_Long:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: integer
          format: int64
        timestamp:
          type: string
          format: date-time
    ApiResponse_NotificationPreferences:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/NotificationPreferences'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Invitation:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/InvitationResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_InvitationList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/InvitationResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Organization:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/OrganizationResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedOrganizations:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedOrganizationResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedOrganizationUsers:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedUserWithRoleResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Announcement:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/AnnouncementResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedAnnouncements:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedAnnouncementResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_User:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/UserResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedUsers:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedUserWithRoleResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_UserList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/UserResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_UserWithRoleList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/UserWithRoleResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TenantResidentialUnit:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TenantResidentialUnitResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_EmailPreferences:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/EmailPreferencesResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_UserPreferences:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/UserPreferencesResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_DashboardSummary:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/DashboardSummaryResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_SearchResults:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/SearchResultResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_BulkInvitations:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/BulkCreateInvitationsResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_DashboardAnalytics:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/DashboardAnalyticsResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketVolumeTimeSeries:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TicketVolumeTimeSeriesResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_KnowledgeBaseEntry:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/KnowledgeBaseEntryResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_KnowledgeBaseEntryList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/KnowledgeBaseEntryResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_KnowledgeBaseSearchResults:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/KnowledgeBaseSearchResultResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_SubscriptionPlanList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/SubscriptionPlanResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_OrganizationSubscription:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/OrganizationSubscriptionResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_SubscriptionLimits:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/SubscriptionLimitsResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Building:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/BuildingResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedBuildings:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedBuildingResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_BuildingAssignment:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/BuildingAssignmentResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_BuildingAssignmentList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/BuildingAssignmentResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_ResidentialUnit:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/ResidentialUnitResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedResidentialUnits:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedResidentialUnitResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_AiSuggestedAnswer:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/AiSuggestedAnswer'
        timestamp:
          type: string
          format: date-time
    ApiResponse_Ticket:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TicketResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_PagedTickets:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/PagedTicketResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketRecommendations:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TicketRecommendations'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketActivityList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/TicketActivityResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketComment:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TicketCommentResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketCommentList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/TicketCommentResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketNote:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          $ref: '#/components/schemas/TicketNoteResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketNoteList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/TicketNoteResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_TicketAttachmentList:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          nullable: true
        data:
          type: array
          items:
            $ref: '#/components/schemas/TicketAttachmentResponse'
        timestamp:
          type: string
          format: date-time
    ApiResponse_BulkDeleteTickets:
      type: object
      required: [success, data, timestamp]
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: Tickets deleted successfully
        data:
          $ref: '#/components/schemas/BulkDeleteTicketsResponse'
        timestamp:
          type: string
          format: date-time
    LoginRequest:
      type: object
      required: [email, password]
      example:
        email: owner@example.com
        password: secret123
      properties:
        email:
          type: string
          format: email
          example: owner@example.com
        password:
          type: string
          format: password
          example: secret123
    LoginResponse:
      type: object
      required: [token, email, userId, platformRole]
      example:
        token: eyJhbGciOiJIUzI1NiJ9.example
        email: owner@example.com
        userId: 1
        platformRole: USER
      properties:
        token:
          type: string
        email:
          type: string
          format: email
        userId:
          type: integer
          format: int64
        platformRole:
          type: string
          enum: [ADMIN, USER]
    CurrentUser:
      type: object
      required:
        - id
        - email
        - firstName
        - lastName
        - fullName
        - platformRole
        - accountStatus
        - language
        - themePreference
        - memberships
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        fullName:
          type: string
        platformRole:
          type: string
          enum: [ADMIN, USER]
        accountStatus:
          type: string
        language:
          type: string
        themePreference:
          type: string
        memberships:
          type: array
          items:
            $ref: '#/components/schemas/UserMembership'
    UserMembership:
      type: object
      required: [organizationId, organizationName, organizationRole, active]
      properties:
        organizationId:
          type: integer
          format: int64
        organizationName:
          type: string
        organizationRole:
          type: string
          enum: [ORGANIZATION_ADMIN, LANDLORD, TENANT]
        active:
          type: boolean
    NotificationPreferences:
      type: object
      required:
        - userId
        - emailNotificationsEnabled
        - smsNotificationsEnabled
        - ticketUpdateNotificationsEnabled
        - announcementNotificationsEnabled
      properties:
        userId:
          type: integer
          format: int64
        emailNotificationsEnabled:
          type: boolean
        smsNotificationsEnabled:
          type: boolean
        ticketUpdateNotificationsEnabled:
          type: boolean
        announcementNotificationsEnabled:
          type: boolean
    UpdateNotificationPreferencesRequest:
      type: object
      properties:
        emailNotificationsEnabled:
          type: boolean
        smsNotificationsEnabled:
          type: boolean
        ticketUpdateNotificationsEnabled:
          type: boolean
        announcementNotificationsEnabled:
          type: boolean
    OrganizationStatus:
      type: string
      enum: [ACTIVE, TRIAL, SUSPENDED, CANCELLED]
    Language:
      type: string
      enum: [NL, EN]
    ThemePreference:
      type: string
      enum: [LIGHT, DARK, SYSTEM_DEFAULT]
    PlatformRole:
      type: string
      enum: [ADMIN, USER]
    AccountStatus:
      type: string
      enum: [ACTIVE, INVITED, PENDING_VERIFICATION, SUSPENDED, DELETED]
    OrganizationRole:
      type: string
      enum: [ORGANIZATION_ADMIN, LANDLORD, TENANT]
    BillingCycle:
      type: string
      enum: [MONTHLY, YEARLY]
    SubscriptionStatus:
      type: string
      enum: [TRIAL, ACTIVE, PAST_DUE, CANCELLED, EXPIRED]
    KnowledgeBaseSourceType:
      type: string
      enum: [CUSTOM_FAQ, POLICY, MAINTENANCE_GUIDE]
    OrganizationResponse:
      type: object
      required: [id, name, slug, status, createdAt, updatedAt]
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        slug:
          type: string
        billingEmail:
          type: string
          format: email
          nullable: true
        status:
          $ref: '#/components/schemas/OrganizationStatus'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateOrganizationRequest:
      type: object
      required: [name, slug, status]
      properties:
        name:
          type: string
          maxLength: 255
        slug:
          type: string
          maxLength: 100
        billingEmail:
          type: string
          format: email
          nullable: true
        status:
          $ref: '#/components/schemas/OrganizationStatus'
    UpdateOrganizationRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        billingEmail:
          type: string
          format: email
          nullable: true
        status:
          $ref: '#/components/schemas/OrganizationStatus'
    PagedOrganizationResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/OrganizationResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    UserWithRoleResponse:
      type: object
      required:
        - id
        - email
        - firstName
        - lastName
        - fullName
        - language
        - themePreference
        - accountStatus
        - platformRole
        - organizationRole
        - membershipActive
        - joinedAt
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        fullName:
          type: string
        phoneNumber:
          type: string
          nullable: true
        language:
          $ref: '#/components/schemas/Language'
        themePreference:
          $ref: '#/components/schemas/ThemePreference'
        accountStatus:
          $ref: '#/components/schemas/AccountStatus'
        platformRole:
          $ref: '#/components/schemas/PlatformRole'
        organizationRole:
          $ref: '#/components/schemas/OrganizationRole'
        membershipActive:
          type: boolean
        joinedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    PagedUserWithRoleResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/UserWithRoleResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    InvitationStatus:
      type: string
      enum: [PENDING, ACCEPTED, EXPIRED, CANCELLED]
    InvitationResponse:
      type: object
      required:
        - id
        - organizationId
        - organizationName
        - invitedById
        - invitedByName
        - email
        - role
        - token
        - expiresAt
        - status
        - createdAt
      example:
        id: 410
        organizationId: 12
        organizationName: Sicket Demo Organization
        invitedById: 1
        invitedByName: Tyler Demo
        email: landlord@example.com
        role: LANDLORD
        buildingId: 21
        buildingName: Keizersgracht 120
        residentialUnitId: null
        residentialUnitNumber: null
        token: inv_01HXYZABC123
        expiresAt: '2026-03-28T14:00:00'
        status: PENDING
        acceptedById: null
        acceptedAt: null
        createdAt: '2026-03-21T14:00:00'
      properties:
        id:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
        organizationName:
          type: string
        invitedById:
          type: integer
          format: int64
        invitedByName:
          type: string
        email:
          type: string
          format: email
        role:
          $ref: '#/components/schemas/OrganizationRole'
        buildingId:
          type: integer
          format: int64
          nullable: true
        buildingName:
          type: string
          nullable: true
        residentialUnitId:
          type: integer
          format: int64
          nullable: true
        residentialUnitNumber:
          type: string
          nullable: true
        token:
          type: string
        expiresAt:
          type: string
          format: date-time
        status:
          $ref: '#/components/schemas/InvitationStatus'
        acceptedById:
          type: integer
          format: int64
          nullable: true
        acceptedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
    CreateInvitationRequest:
      type: object
      required: [organizationId, email, role]
      example:
        organizationId: 12
        email: tenant@example.com
        role: TENANT
        buildingId: 21
      properties:
        organizationId:
          type: integer
          format: int64
          minimum: 1
        email:
          type: string
          format: email
        role:
          $ref: '#/components/schemas/OrganizationRole'
        buildingId:
          type: integer
          format: int64
          minimum: 1
          nullable: true
    AcceptInvitationRequest:
      type: object
      required: [token, firstName, lastName, password]
      example:
        token: inv_01HXYZABC123
        firstName: Bob
        lastName: Jansen
        password: secret123
        createResidentialUnit: true
        residentialUnitNumber: 3B
      properties:
        token:
          type: string
        firstName:
          type: string
          maxLength: 255
        lastName:
          type: string
          maxLength: 255
        password:
          type: string
          minLength: 8
        phoneNumber:
          type: string
          maxLength: 20
          nullable: true
        residentialUnitId:
          type: integer
          format: int64
          minimum: 1
          nullable: true
        residentialUnitNumber:
          type: string
          maxLength: 50
          nullable: true
        residentialUnitLabel:
          type: string
          maxLength: 255
          nullable: true
      description: >
        Tenant invitation acceptance requires either selecting an existing
        residentialUnitId or creating one with residentialUnitNumber. Do not
        send both selection modes at once.
    BulkCreateInvitationsRequest:
      type: object
      required: [invitations]
      properties:
        invitations:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/CreateInvitationRequest'
    BulkInvitationFailureResponse:
      type: object
      required: [email, code, message]
      properties:
        email:
          type: string
          format: email
        code:
          type: string
        message:
          type: string
    BulkCreateInvitationsResponse:
      type: object
      required: [created, failed]
      properties:
        created:
          type: array
          items:
            $ref: '#/components/schemas/InvitationResponse'
        failed:
          type: array
          items:
            $ref: '#/components/schemas/BulkInvitationFailureResponse'
    BuildingResponse:
      type: object
      required:
        - id
        - organizationId
        - displayName
        - active
        - createdAt
        - updatedAt
        - unitCount
        - openTickets
      example:
        id: 21
        organizationId: 12
        displayName: Keizersgracht 120
        country: Netherlands
        stateOrProvince: North Holland
        city: Amsterdam
        postcode: 1015CJ
        street: Keizersgracht
        houseNumberRange: 120-128
        active: true
        createdAt: '2026-03-01T09:00:00'
        updatedAt: '2026-03-20T18:45:00'
        unitCount: 18
        openTickets: 5
      properties:
        id:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
        displayName:
          type: string
        country:
          type: string
          nullable: true
        stateOrProvince:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        postcode:
          type: string
          nullable: true
        street:
          type: string
          nullable: true
        houseNumberRange:
          type: string
          nullable: true
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        unitCount:
          type: integer
          format: int64
        openTickets:
          type: integer
          format: int64
    CreateBuildingRequest:
      type: object
      required: [organizationId, displayName]
      example:
        organizationId: 12
        displayName: Keizersgracht 120
        country: Netherlands
        stateOrProvince: North Holland
        city: Amsterdam
        postcode: 1015CJ
        street: Keizersgracht
        houseNumberRange: 120-128
      properties:
        organizationId:
          type: integer
          format: int64
          minimum: 1
        displayName:
          type: string
          maxLength: 255
        country:
          type: string
          maxLength: 100
          nullable: true
        stateOrProvince:
          type: string
          maxLength: 100
          nullable: true
        city:
          type: string
          maxLength: 100
          nullable: true
        postcode:
          type: string
          maxLength: 20
          nullable: true
        street:
          type: string
          maxLength: 255
          nullable: true
        houseNumberRange:
          type: string
          maxLength: 50
          nullable: true
    UpdateBuildingRequest:
      type: object
      properties:
        displayName:
          type: string
          maxLength: 255
        country:
          type: string
          maxLength: 100
          nullable: true
        stateOrProvince:
          type: string
          maxLength: 100
          nullable: true
        city:
          type: string
          maxLength: 100
          nullable: true
        postcode:
          type: string
          maxLength: 20
          nullable: true
        street:
          type: string
          maxLength: 255
          nullable: true
        houseNumberRange:
          type: string
          maxLength: 50
          nullable: true
        active:
          type: boolean
    PagedBuildingResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/BuildingResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    BuildingAssignmentResponse:
      type: object
      required:
        - buildingId
        - buildingName
        - userId
        - userFullName
        - organizationRole
        - active
      properties:
        buildingId:
          type: integer
          format: int64
        buildingName:
          type: string
        userId:
          type: integer
          format: int64
        userFullName:
          type: string
        organizationRole:
          $ref: '#/components/schemas/OrganizationRole'
        residentialUnitId:
          type: integer
          format: int64
          nullable: true
        residentialUnitNumber:
          type: string
          nullable: true
        active:
          type: boolean
    AssignBuildingUserRequest:
      type: object
      required: [userId]
      properties:
        userId:
          type: integer
          format: int64
          minimum: 1
        residentialUnitId:
          type: integer
          format: int64
          minimum: 1
          nullable: true
    ResidentialUnitResponse:
      type: object
      required: [id, buildingId, unitNumber, active, createdAt, updatedAt]
      properties:
        id:
          type: integer
          format: int64
        buildingId:
          type: integer
          format: int64
        unitNumber:
          type: string
        label:
          type: string
          nullable: true
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateResidentialUnitRequest:
      type: object
      required: [buildingId, unitNumber]
      properties:
        buildingId:
          type: integer
          format: int64
          minimum: 1
        unitNumber:
          type: string
          maxLength: 50
        label:
          type: string
          maxLength: 255
          nullable: true
    UpdateResidentialUnitRequest:
      type: object
      properties:
        unitNumber:
          type: string
          maxLength: 50
        label:
          type: string
          maxLength: 255
          nullable: true
        active:
          type: boolean
    PagedResidentialUnitResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/ResidentialUnitResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    TicketAnalytics:
      type: object
      required:
        - openCount
        - inProgressCount
        - resolvedCount
        - closedCount
        - averageTimeToFirstResponseHours
        - averageTimeToResolutionHours
      properties:
        openCount:
          type: integer
          format: int64
        inProgressCount:
          type: integer
          format: int64
        resolvedCount:
          type: integer
          format: int64
        closedCount:
          type: integer
          format: int64
        averageTimeToFirstResponseHours:
          type: number
          format: double
        averageTimeToResolutionHours:
          type: number
          format: double
    TicketCountByType:
      type: object
      required: [ticketType, count]
      properties:
        ticketType:
          $ref: '#/components/schemas/TicketType'
        count:
          type: integer
          format: int64
    TicketCountByBuilding:
      type: object
      required: [buildingId, buildingName, count]
      properties:
        buildingId:
          type: integer
          format: int64
        buildingName:
          type: string
        count:
          type: integer
          format: int64
    UserAnalytics:
      type: object
      required: [activeTenants, activeLandlords, activeOrganizationAdmins]
      properties:
        activeTenants:
          type: integer
          format: int64
        activeLandlords:
          type: integer
          format: int64
        activeOrganizationAdmins:
          type: integer
          format: int64
    DashboardAnalyticsResponse:
      type: object
      required:
        - ticketAnalytics
        - ticketCountByType
        - ticketCountByBuilding
        - announcementCount
        - userAnalytics
      example:
        ticketAnalytics:
          openCount: 5
          inProgressCount: 3
          resolvedCount: 18
          closedCount: 7
          averageTimeToFirstResponseHours: 3.5
          averageTimeToResolutionHours: 28.4
        ticketCountByType:
          - ticketType: PERSONAL
            count: 19
          - ticketType: COMMUNITY
            count: 14
        ticketCountByBuilding:
          - buildingId: 21
            buildingName: Keizersgracht 120
            count: 17
          - buildingId: 22
            buildingName: Prinsengracht 88
            count: 16
        announcementCount: 4
        userAnalytics:
          activeTenants: 43
          activeLandlords: 3
          activeOrganizationAdmins: 2
      properties:
        ticketAnalytics:
          $ref: '#/components/schemas/TicketAnalytics'
        ticketCountByType:
          type: array
          items:
            $ref: '#/components/schemas/TicketCountByType'
        ticketCountByBuilding:
          type: array
          items:
            $ref: '#/components/schemas/TicketCountByBuilding'
        announcementCount:
          type: integer
          format: int64
        userAnalytics:
          $ref: '#/components/schemas/UserAnalytics'
    TicketVolumeTimeSeriesDataPoint:
      type: object
      required: [date, created, resolved, open]
      properties:
        date:
          type: string
        created:
          type: integer
          format: int64
        resolved:
          type: integer
          format: int64
        open:
          type: integer
          format: int64
    TicketVolumeTimeSeriesResponse:
      type: object
      required: [dataPoints, period, totalTickets]
      example:
        dataPoints:
          - date: '2026-03-17'
            created: 4
            resolved: 1
            open: 9
          - date: '2026-03-18'
            created: 2
            resolved: 3
            open: 8
          - date: '2026-03-19'
            created: 5
            resolved: 2
            open: 11
        period: DAILY
        totalTickets: 11
      properties:
        dataPoints:
          type: array
          items:
            $ref: '#/components/schemas/TicketVolumeTimeSeriesDataPoint'
        period:
          type: string
        totalTickets:
          type: integer
          format: int64
    KnowledgeBaseEntryResponse:
      type: object
      required:
        - id
        - organizationId
        - title
        - question
        - answer
        - published
        - visibleToTenants
        - buildingIds
        - createdById
        - updatedById
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
        title:
          type: string
        question:
          type: string
        answer:
          type: string
        category:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        published:
          type: boolean
        visibleToTenants:
          type: boolean
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
        priorityRank:
          type: integer
          nullable: true
        sourceType:
          $ref: '#/components/schemas/KnowledgeBaseSourceType'
        createdById:
          type: integer
          format: int64
        updatedById:
          type: integer
          format: int64
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateKnowledgeBaseEntryRequest:
      type: object
      required: [organizationId, title, question, answer]
      properties:
        organizationId:
          type: integer
          format: int64
        title:
          type: string
        question:
          type: string
        answer:
          type: string
        category:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        published:
          type: boolean
          nullable: true
        visibleToTenants:
          type: boolean
          nullable: true
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
        priorityRank:
          type: integer
          nullable: true
        sourceType:
          $ref: '#/components/schemas/KnowledgeBaseSourceType'
    UpdateKnowledgeBaseEntryRequest:
      type: object
      required: [organizationId, title, question, answer]
      properties:
        organizationId:
          type: integer
          format: int64
        title:
          type: string
        question:
          type: string
        answer:
          type: string
        category:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        published:
          type: boolean
          nullable: true
        visibleToTenants:
          type: boolean
          nullable: true
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
        priorityRank:
          type: integer
          nullable: true
        sourceType:
          $ref: '#/components/schemas/KnowledgeBaseSourceType'
    KnowledgeBaseSearchRequest:
      type: object
      required: [organizationId, query]
      example:
        organizationId: 12
        buildingId: 21
        query: How do I reset the heating in my apartment?
        limit: 5
      properties:
        organizationId:
          type: integer
          format: int64
        buildingId:
          type: integer
          format: int64
          nullable: true
        query:
          type: string
        limit:
          type: integer
          nullable: true
    KnowledgeBaseSearchResultResponse:
      type: object
      required: [id, title, question, answer, category, tags, relevanceScore, buildingIds, visibleToTenants]
      example:
        id: 91
        title: Heating reset instructions
        question: How do I reset the heating in my apartment?
        answer: Hold the reset button for 10 seconds and wait two minutes before trying again.
        category: Heating
        tags: [heating, reset, boiler]
        relevanceScore: 0.92
        buildingIds: [21]
        visibleToTenants: true
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        question:
          type: string
        answer:
          type: string
        category:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        relevanceScore:
          type: number
          format: double
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
        visibleToTenants:
          type: boolean
    SubscriptionPlanResponse:
      type: object
      required:
        - id
        - name
        - monthlyPriceEur
        - customPricing
        - maxUnits
        - maxBuildings
        - maxOrganizationAdmins
        - maxLandlords
        - maxTenants
        - aiDuplicateDetectionEnabled
        - aiSuggestionsEnabled
        - communityTicketsEnabled
        - announcementsEnabled
        - analyticsEnabled
        - customFaqKnowledgeBaseEnabled
        - prioritySupportEnabled
        - smsSupportEnabled
        - customIntegrationsEnabled
        - ssoAdvancedSecurityEnabled
        - phoneSupport247Enabled
        - customWorkflowsEnabled
        - slaGuaranteesEnabled
        - active
      example:
        id: 2
        name: Professional
        monthlyPriceEur: 249
        customPricing: false
        maxUnits: 500
        maxBuildings: null
        maxOrganizationAdmins: null
        maxLandlords: null
        maxTenants: null
        aiDuplicateDetectionEnabled: true
        aiSuggestionsEnabled: true
        communityTicketsEnabled: true
        announcementsEnabled: true
        analyticsEnabled: true
        customFaqKnowledgeBaseEnabled: true
        prioritySupportEnabled: true
        smsSupportEnabled: true
        customIntegrationsEnabled: false
        ssoAdvancedSecurityEnabled: false
        phoneSupport247Enabled: false
        customWorkflowsEnabled: false
        slaGuaranteesEnabled: false
        active: true
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        monthlyPriceEur:
          type: integer
          nullable: true
        customPricing:
          type: boolean
        maxUnits:
          type: integer
          nullable: true
        maxBuildings:
          type: integer
          nullable: true
        maxOrganizationAdmins:
          type: integer
          nullable: true
        maxLandlords:
          type: integer
          nullable: true
        maxTenants:
          type: integer
          nullable: true
        aiDuplicateDetectionEnabled:
          type: boolean
        aiSuggestionsEnabled:
          type: boolean
        communityTicketsEnabled:
          type: boolean
        announcementsEnabled:
          type: boolean
        analyticsEnabled:
          type: boolean
        customFaqKnowledgeBaseEnabled:
          type: boolean
        prioritySupportEnabled:
          type: boolean
        smsSupportEnabled:
          type: boolean
        customIntegrationsEnabled:
          type: boolean
        ssoAdvancedSecurityEnabled:
          type: boolean
        phoneSupport247Enabled:
          type: boolean
        customWorkflowsEnabled:
          type: boolean
        slaGuaranteesEnabled:
          type: boolean
        active:
          type: boolean
    OrganizationSubscriptionResponse:
      type: object
      required:
        - id
        - organizationId
        - subscriptionPlanId
        - planName
        - status
        - billingCycle
      example:
        id: 77
        organizationId: 12
        subscriptionPlanId: 2
        planName: Professional
        status: ACTIVE
        billingCycle: MONTHLY
        startsAt: '2026-03-01T00:00:00'
        endsAt: null
        trialEndsAt: null
        customMaxUnits: null
        customMaxBuildings: null
        customMaxOrganizationAdmins: null
        customMaxLandlords: null
        customMaxTenants: null
      properties:
        id:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
        subscriptionPlanId:
          type: integer
          format: int64
        planName:
          type: string
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
        billingCycle:
          $ref: '#/components/schemas/BillingCycle'
        startsAt:
          type: string
          format: date-time
          nullable: true
        endsAt:
          type: string
          format: date-time
          nullable: true
        trialEndsAt:
          type: string
          format: date-time
          nullable: true
        customMaxUnits:
          type: integer
          nullable: true
        customMaxBuildings:
          type: integer
          nullable: true
        customMaxOrganizationAdmins:
          type: integer
          nullable: true
        customMaxLandlords:
          type: integer
          nullable: true
        customMaxTenants:
          type: integer
          nullable: true
    SubscriptionLimitsResponse:
      type: object
      required:
        - planName
        - monthlyPriceEur
        - customPricing
        - maxUnits
        - maxBuildings
        - maxOrganizationAdmins
        - maxLandlords
        - maxTenants
        - aiDuplicateDetectionEnabled
        - aiSuggestionsEnabled
        - communityTicketsEnabled
        - announcementsEnabled
        - analyticsEnabled
        - customFaqKnowledgeBaseEnabled
        - prioritySupportEnabled
        - smsSupportEnabled
        - customIntegrationsEnabled
        - ssoAdvancedSecurityEnabled
        - phoneSupport247Enabled
        - customWorkflowsEnabled
        - slaGuaranteesEnabled
      example:
        planName: Professional
        monthlyPriceEur: 249
        customPricing: false
        maxUnits: 500
        maxBuildings: null
        maxOrganizationAdmins: null
        maxLandlords: null
        maxTenants: null
        aiDuplicateDetectionEnabled: true
        aiSuggestionsEnabled: true
        communityTicketsEnabled: true
        announcementsEnabled: true
        analyticsEnabled: true
        customFaqKnowledgeBaseEnabled: true
        prioritySupportEnabled: true
        smsSupportEnabled: true
        customIntegrationsEnabled: false
        ssoAdvancedSecurityEnabled: false
        phoneSupport247Enabled: false
        customWorkflowsEnabled: false
        slaGuaranteesEnabled: false
      properties:
        planName:
          type: string
        monthlyPriceEur:
          type: integer
          nullable: true
        customPricing:
          type: boolean
        maxUnits:
          type: integer
          nullable: true
        maxBuildings:
          type: integer
          nullable: true
        maxOrganizationAdmins:
          type: integer
          nullable: true
        maxLandlords:
          type: integer
          nullable: true
        maxTenants:
          type: integer
          nullable: true
        aiDuplicateDetectionEnabled:
          type: boolean
        aiSuggestionsEnabled:
          type: boolean
        communityTicketsEnabled:
          type: boolean
        announcementsEnabled:
          type: boolean
        analyticsEnabled:
          type: boolean
        customFaqKnowledgeBaseEnabled:
          type: boolean
        prioritySupportEnabled:
          type: boolean
        smsSupportEnabled:
          type: boolean
        customIntegrationsEnabled:
          type: boolean
        ssoAdvancedSecurityEnabled:
          type: boolean
        phoneSupport247Enabled:
          type: boolean
        customWorkflowsEnabled:
          type: boolean
        slaGuaranteesEnabled:
          type: boolean
    AnnouncementType:
      type: string
      enum: [INFO, MAINTENANCE, URGENT, NEWSLETTER]
    AnnouncementResponse:
      type: object
      required:
        - id
        - organizationId
        - createdById
        - title
        - description
        - announcementType
        - globalWithinOrganization
        - visibleToTenantsOnly
        - published
        - buildingIds
        - createdAt
        - updatedAt
      example:
        id: 611
        organizationId: 12
        createdById: 1
        title: Boiler maintenance on Tuesday
        description: Hot water may be unavailable between 10:00 and 12:00 while maintenance is performed.
        announcementType: MAINTENANCE
        globalWithinOrganization: false
        visibleToTenantsOnly: false
        startDate: '2026-03-25T10:00:00'
        endDate: '2026-03-25T12:00:00'
        pinnedUntil: '2026-03-25T12:00:00'
        published: true
        buildingIds: [21]
        createdAt: '2026-03-21T10:15:00'
        updatedAt: '2026-03-21T10:20:00'
      properties:
        id:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
        createdById:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
        announcementType:
          $ref: '#/components/schemas/AnnouncementType'
        globalWithinOrganization:
          type: boolean
        visibleToTenantsOnly:
          type: boolean
        startDate:
          type: string
          format: date-time
          nullable: true
        endDate:
          type: string
          format: date-time
          nullable: true
        pinnedUntil:
          type: string
          format: date-time
          nullable: true
        published:
          type: boolean
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateAnnouncementRequest:
      type: object
      required:
        - organizationId
        - createdById
        - title
        - description
        - announcementType
        - globalWithinOrganization
        - visibleToTenantsOnly
      example:
        organizationId: 12
        createdById: 1
        title: Boiler maintenance on Tuesday
        description: Hot water may be unavailable between 10:00 and 12:00 while maintenance is performed.
        announcementType: MAINTENANCE
        globalWithinOrganization: false
        visibleToTenantsOnly: false
        buildingIds: [21]
        startDate: '2026-03-25T10:00:00'
        endDate: '2026-03-25T12:00:00'
      properties:
        organizationId:
          type: integer
          format: int64
          minimum: 1
        createdById:
          type: integer
          format: int64
          minimum: 1
        title:
          type: string
          maxLength: 255
        description:
          type: string
        announcementType:
          $ref: '#/components/schemas/AnnouncementType'
        globalWithinOrganization:
          type: boolean
        visibleToTenantsOnly:
          type: boolean
        startDate:
          type: string
          format: date-time
          nullable: true
        endDate:
          type: string
          format: date-time
          nullable: true
        pinnedUntil:
          type: string
          format: date-time
          nullable: true
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
    UpdateAnnouncementRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: string
        announcementType:
          $ref: '#/components/schemas/AnnouncementType'
        globalWithinOrganization:
          type: boolean
        visibleToTenantsOnly:
          type: boolean
        startDate:
          type: string
          format: date-time
          nullable: true
        endDate:
          type: string
          format: date-time
          nullable: true
        pinnedUntil:
          type: string
          format: date-time
          nullable: true
        published:
          type: boolean
        buildingIds:
          type: array
          items:
            type: integer
            format: int64
    PagedAnnouncementResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/AnnouncementResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    UserResponse:
      type: object
      required:
        - id
        - email
        - firstName
        - lastName
        - fullName
        - language
        - themePreference
        - accountStatus
        - platformRole
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        fullName:
          type: string
        phoneNumber:
          type: string
          nullable: true
        language:
          $ref: '#/components/schemas/Language'
        themePreference:
          $ref: '#/components/schemas/ThemePreference'
        accountStatus:
          $ref: '#/components/schemas/AccountStatus'
        platformRole:
          $ref: '#/components/schemas/PlatformRole'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    UpdateUserRequest:
      type: object
      properties:
        firstName:
          type: string
          maxLength: 255
        lastName:
          type: string
          maxLength: 255
        email:
          type: string
          format: email
          maxLength: 255
        phoneNumber:
          type: string
          maxLength: 20
          nullable: true
        language:
          $ref: '#/components/schemas/Language'
        themePreference:
          $ref: '#/components/schemas/ThemePreference'
        accountStatus:
          $ref: '#/components/schemas/AccountStatus'
    UpdateUserPreferencesRequest:
      type: object
      required: [language, themePreference]
      example:
        language: EN
        themePreference: DARK
      properties:
        language:
          $ref: '#/components/schemas/Language'
        themePreference:
          $ref: '#/components/schemas/ThemePreference'
    UserPreferencesResponse:
      type: object
      required: [id, email, firstName, lastName, language, themePreference]
      properties:
        id:
          type: integer
          format: int64
        email:
          type: string
          format: email
        firstName:
          type: string
        lastName:
          type: string
        language:
          $ref: '#/components/schemas/Language'
        themePreference:
          $ref: '#/components/schemas/ThemePreference'
    EmailPreferencesResponse:
      type: object
      required: [userId, email, emailNotificationsEnabled, ticketUpdateEmailsEnabled, announcementEmailsEnabled]
      example:
        userId: 8
        email: tenant@example.com
        emailNotificationsEnabled: true
        ticketUpdateEmailsEnabled: true
        announcementEmailsEnabled: false
      properties:
        userId:
          type: integer
          format: int64
        email:
          type: string
          format: email
        emailNotificationsEnabled:
          type: boolean
        ticketUpdateEmailsEnabled:
          type: boolean
        announcementEmailsEnabled:
          type: boolean
    UpdateEmailPreferencesRequest:
      type: object
      properties:
        emailNotificationsEnabled:
          type: boolean
        ticketUpdateEmailsEnabled:
          type: boolean
        announcementEmailsEnabled:
          type: boolean
    TenantResidentialUnitResponse:
      type: object
      required: [userId, buildingId, buildingName, residentialUnitId, residentialUnitNumber, residentialUnitLabel]
      properties:
        userId:
          type: integer
          format: int64
        buildingId:
          type: integer
          format: int64
        buildingName:
          type: string
        residentialUnitId:
          type: integer
          format: int64
        residentialUnitNumber:
          type: string
        residentialUnitLabel:
          type: string
    UserBatchRequest:
      type: object
      required: [userIds]
      properties:
        userIds:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: integer
            format: int64
    DashboardSummaryResponse:
      type: object
      required:
        - openTicketsInMyScope
        - ticketsCreatedByMe
        - totalOpenTicketsInMyOrganizations
        - totalBuildingsInMyOrganizations
        - recentAnnouncements
      example:
        openTicketsInMyScope: 5
        ticketsCreatedByMe: 2
        totalOpenTicketsInMyOrganizations: 11
        totalBuildingsInMyOrganizations: 3
        recentAnnouncements:
          - id: 611
            organizationId: 12
            createdById: 1
            title: Boiler maintenance on Tuesday
            description: Hot water may be unavailable between 10:00 and 12:00 while maintenance is performed.
            announcementType: MAINTENANCE
            globalWithinOrganization: false
            visibleToTenantsOnly: false
            startDate: '2026-03-25T10:00:00'
            endDate: '2026-03-25T12:00:00'
            pinnedUntil: '2026-03-25T12:00:00'
            published: true
            buildingIds: [21]
            createdAt: '2026-03-21T10:15:00'
            updatedAt: '2026-03-21T10:20:00'
      properties:
        openTicketsInMyScope:
          type: integer
          format: int64
        ticketsCreatedByMe:
          type: integer
          format: int64
        totalOpenTicketsInMyOrganizations:
          type: integer
          format: int64
        totalBuildingsInMyOrganizations:
          type: integer
          format: int64
        recentAnnouncements:
          type: array
          items:
            $ref: '#/components/schemas/AnnouncementResponse'
    SearchResultResponse:
      type: object
      required: [results, totalResults]
      example:
        results:
          - type: ticket
            id: 4
            title: No hot water in bathroom
            description: Community ticket in Keizersgracht 120
            url: /tickets/6WS42A
            relevanceScore: 0.97
          - type: announcement
            id: 611
            title: Boiler maintenance on Tuesday
            description: Planned maintenance notice
            url: /announcements/611
            relevanceScore: 0.71
        totalResults: 2
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/SearchResult'
        totalResults:
          type: integer
    SearchResult:
      type: object
      required: [type, id, title, description, url, relevanceScore]
      properties:
        type:
          type: string
        id:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
        url:
          type: string
        relevanceScore:
          type: number
          format: double
    TicketType:
      type: string
      enum: [PERSONAL, COMMUNITY]
    TicketPriority:
      type: string
      enum: [CRITICAL, HIGH, MEDIUM, LOW]
    TicketStatus:
      type: string
      enum: [OPEN, IN_PROGRESS, RESOLVED, CLOSED]
    LocationType:
      type: string
      enum: [INSIDE_UNIT, COMMON_AREA, OUTSIDE_BUILDING, OTHER]
    TicketActivityType:
      type: string
      enum: [CREATED, UPDATED, STATUS_CHANGED, COMMENT_ADDED, NOTE_ADDED, REOPENED]
    TicketResponse:
      type: object
      required:
        - id
        - ticketCode
        - organizationId
        - buildingId
        - createdById
        - title
        - description
        - ticketType
        - ticketPriority
        - ticketStatus
        - anonymous
        - locationType
        - createdAt
        - updatedAt
      example:
        id: 4
        ticketCode: 6WS42A
        organizationId: 12
        buildingId: 21
        residentialUnitId: 305
        createdById: 8
        title: No hot water in bathroom
        description: The shower has had no hot water since this morning.
        ticketType: PERSONAL
        ticketPriority: HIGH
        ticketStatus: OPEN
        anonymous: false
        locationType: INSIDE_UNIT
        resolvedAt: null
        lastUpdatedById: 8
        aiPredictedCategory: Plumbing
        aiPredictionProvider: local
        aiPredictionModel: heuristic-v1
        aiPredictedAt: '2026-03-21T09:12:00'
        createdAt: '2026-03-21T09:12:00'
        updatedAt: '2026-03-21T09:12:00'
      properties:
        id:
          type: integer
          format: int64
        ticketCode:
          type: string
          example: 6WS42A
        organizationId:
          type: integer
          format: int64
        buildingId:
          type: integer
          format: int64
        residentialUnitId:
          type: integer
          format: int64
          nullable: true
        createdById:
          type: integer
          format: int64
        title:
          type: string
        description:
          type: string
        ticketType:
          $ref: '#/components/schemas/TicketType'
        ticketPriority:
          $ref: '#/components/schemas/TicketPriority'
        ticketStatus:
          $ref: '#/components/schemas/TicketStatus'
        anonymous:
          type: boolean
        locationType:
          $ref: '#/components/schemas/LocationType'
        resolvedAt:
          type: string
          format: date-time
          nullable: true
        lastUpdatedById:
          type: integer
          format: int64
          nullable: true
        aiPredictedCategory:
          type: string
          nullable: true
        aiPredictionProvider:
          type: string
          nullable: true
        aiPredictionModel:
          type: string
          nullable: true
        aiPredictedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateTicketRequest:
      type: object
      required:
        - organizationId
        - buildingId
        - residentialUnitId
        - title
        - description
        - ticketType
        - anonymous
        - locationType
      example:
        organizationId: 12
        buildingId: 21
        residentialUnitId: 305
        title: No hot water in bathroom
        description: The shower has had no hot water since this morning.
        ticketType: PERSONAL
        ticketPriority: HIGH
        anonymous: false
        locationType: INSIDE_UNIT
      properties:
        organizationId:
          type: integer
          format: int64
          minimum: 1
        buildingId:
          type: integer
          format: int64
          minimum: 1
        residentialUnitId:
          type: integer
          format: int64
          minimum: 1
        title:
          type: string
          maxLength: 255
        description:
          type: string
        ticketType:
          $ref: '#/components/schemas/TicketType'
        ticketPriority:
          $ref: '#/components/schemas/TicketPriority'
        anonymous:
          type: boolean
        locationType:
          $ref: '#/components/schemas/LocationType'
    UpdateTicketRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: string
        ticketType:
          $ref: '#/components/schemas/TicketType'
        ticketPriority:
          $ref: '#/components/schemas/TicketPriority'
        locationType:
          $ref: '#/components/schemas/LocationType'
    UpdateTicketStatusRequest:
      type: object
      required: [ticketStatus]
      properties:
        ticketStatus:
          $ref: '#/components/schemas/TicketStatus'
    PagedTicketResponse:
      type: object
      required: [content, page, size, totalElements, totalPages, first, last]
      properties:
        content:
          type: array
          items:
            $ref: '#/components/schemas/TicketResponse'
        page:
          type: integer
        size:
          type: integer
        totalElements:
          type: integer
          format: int64
        totalPages:
          type: integer
        first:
          type: boolean
        last:
          type: boolean
    FindSimilarTicketsRequest:
      type: object
      required: [title, buildingId]
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: string
          nullable: true
        buildingId:
          type: integer
          format: int64
          minimum: 1
        ticketType:
          $ref: '#/components/schemas/TicketType'
        locationType:
          $ref: '#/components/schemas/LocationType'
        limit:
          type: integer
          minimum: 1
          maximum: 5
          nullable: true
    TicketRecommendations:
      type: object
      required: [predictedPriority, predictedCategory, aiProvider, aiModel, similarTickets]
      properties:
        predictedPriority:
          $ref: '#/components/schemas/TicketPriority'
        predictedCategory:
          type: string
          nullable: true
        aiProvider:
          type: string
          nullable: true
        aiModel:
          type: string
          nullable: true
        similarTickets:
          type: array
          items:
            $ref: '#/components/schemas/SimilarTicket'
    SimilarTicket:
      type: object
      required:
        - id
        - ticketCode
        - title
        - description
        - ticketType
        - ticketStatus
        - ticketPriority
        - createdAt
        - similarityScore
      properties:
        id:
          type: integer
          format: int64
        ticketCode:
          type: string
          example: 6WS42A
        title:
          type: string
        description:
          type: string
        ticketType:
          type: string
        ticketStatus:
          type: string
        ticketPriority:
          $ref: '#/components/schemas/TicketPriority'
        createdAt:
          type: string
          format: date-time
        similarityScore:
          type: number
          format: double
    TicketActivityResponse:
      type: object
      required:
        - id
        - ticketId
        - ticketCode
        - type
        - description
        - createdAt
      properties:
        id:
          type: integer
          format: int64
        ticketId:
          type: integer
          format: int64
        ticketCode:
          type: string
        userId:
          type: integer
          format: int64
          nullable: true
        userName:
          type: string
          nullable: true
        type:
          $ref: '#/components/schemas/TicketActivityType'
        description:
          type: string
        metadata:
          type: object
          additionalProperties: true
          nullable: true
        createdAt:
          type: string
          format: date-time
    AddTicketCommentRequest:
      type: object
      required: [content]
      properties:
        content:
          type: string
        parentCommentId:
          type: integer
          format: int64
          minimum: 1
          nullable: true
    TicketCommentResponse:
      type: object
      required:
        - id
        - ticketId
        - authorName
        - anonymous
        - content
        - createdAt
        - updatedAt
      properties:
        id:
          type: integer
          format: int64
        ticketId:
          type: integer
          format: int64
        authorId:
          type: integer
          format: int64
          nullable: true
        authorName:
          type: string
        anonymous:
          type: boolean
        content:
          type: string
        parentCommentId:
          type: integer
          format: int64
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateTicketNoteRequest:
      type: object
      required: [content]
      properties:
        content:
          type: string
    TicketNoteResponse:
      type: object
      required: [id, ticketId, authorId, authorName, content, createdAt, updatedAt]
      properties:
        id:
          type: integer
          format: int64
        ticketId:
          type: integer
          format: int64
        authorId:
          type: integer
          format: int64
        authorName:
          type: string
        content:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TicketAttachmentUploadRequest:
      type: object
      required: [files]
      properties:
        files:
          type: array
          description: Repeated multipart file parts named files.
          items:
            type: string
            format: binary
    TicketAttachmentResponse:
      type: object
      required:
        - id
        - ticketId
        - ticketCode
        - fileName
        - contentType
        - sizeBytes
        - uploadedById
        - uploadedByName
        - createdAt
        - downloadUrl
      properties:
        id:
          type: integer
          format: int64
        ticketId:
          type: integer
          format: int64
        ticketCode:
          type: string
        fileName:
          type: string
        contentType:
          type: string
        sizeBytes:
          type: integer
          format: int64
        uploadedById:
          type: integer
          format: int64
        uploadedByName:
          type: string
        createdAt:
          type: string
          format: date-time
        downloadUrl:
          type: string
          example: /api/attachments/55/download
    BulkDeleteTicketsRequest:
      type: object
      required: [ticketCodes]
      properties:
        ticketCodes:
          type: array
          minItems: 1
          items:
            type: string
            example: 6WS42A
    BulkDeleteTicketsResponse:
      type: object
      required: [deletedCount]
      properties:
        deletedCount:
          type: integer
          example: 2
    TenantTicketAnswerRequest:
      type: object
      required:
        - organizationId
        - buildingId
        - title
        - description
        - ticketType
        - locationType
      properties:
        organizationId:
          type: integer
          format: int64
          minimum: 1
        buildingId:
          type: integer
          format: int64
          minimum: 1
        title:
          type: string
          maxLength: 255
        description:
          type: string
        ticketType:
          $ref: '#/components/schemas/TicketType'
        locationType:
          $ref: '#/components/schemas/LocationType'
    AiSuggestedAnswer:
      type: object
      required:
        - answer
        - sources
        - confidence
        - suggestedNextAction
        - safeForTenant
        - provider
        - model
        - usedFallback
      properties:
        answer:
          type: string
        sources:
          type: array
          items:
            $ref: '#/components/schemas/AiAnswerSource'
        confidence:
          type: number
          format: double
        suggestedNextAction:
          type: string
        safeForTenant:
          type: boolean
        provider:
          type: string
        model:
          type: string
        usedFallback:
          type: boolean
    AiAnswerSource:
      type: object
      required: [sourceType, title]
      properties:
        sourceType:
          type: string
        id:
          type: integer
          format: int64
          nullable: true
        reference:
          type: string
          nullable: true
        title:
          type: string
        excerpt:
          type: string
          nullable: true
        relevanceScore:
          type: number
          format: double
          nullable: true
    RealtimeEvent:
      type: object
      required:
        - id
        - eventType
        - entityType
        - entityId
        - message
        - refreshRecommended
        - createdAt
      properties:
        id:
          type: integer
          format: int64
        eventType:
          type: string
          example: TICKET_CREATED
        entityType:
          type: string
          example: ticket
        entityId:
          type: integer
          format: int64
        organizationId:
          type: integer
          format: int64
          nullable: true
        buildingId:
          type: integer
          format: int64
          nullable: true
        ticketCode:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        message:
          type: string
        refreshRecommended:
          type: boolean
        createdAt:
          type: string
          format: date-time
paths:
  /api/v1/health:
    get:
      tags: [Health]
      summary: Health check
      description: Simple application health endpoint used for smoke checks and uptime probes.
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: UP
                timestamp: '2026-03-21T22:00:00'
                version: 1.0.0
                profile: local
                service: sicket-api
  /api/v1/auth/login:
    post:
      tags: [Authentication]
      summary: Login
      description: Authenticate a user and receive a JWT bearer token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Login'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/auth/me:
    get:
      tags: [Authentication]
      summary: Get current user
      description: Get the currently authenticated user's profile and organization memberships.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current user loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_CurrentUser'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/auth/logout:
    post:
      tags: [Authentication]
      summary: Logout
      description: Stateless logout endpoint. The frontend should discard the bearer token after success.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Logout successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
              example:
                success: true
                message: Logout successful
                data: null
                timestamp: '2026-03-21T22:00:00'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/notifications/preferences:
    get:
      tags: [Notifications]
      summary: Get notification preferences
      description: Get the current user's email and SMS notification preferences.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Preferences loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_NotificationPreferences'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations:
    post:
      tags: [Invitations]
      summary: Create invitation
      description: >
        Create a single invitation. Organization admins can invite landlords and
        tenants, assigned landlords can invite tenants for their own buildings,
        and platform admins can also invite organization admins.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInvitationRequest'
      responses:
        '201':
          description: Invitation created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Invitation'
        '400':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Invitation conflicts with existing account or invitation state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/bulk-create:
    post:
      tags: [Invitations]
      summary: Bulk create invitations
      description: >
        Create multiple invitations in one request. Successful invitations are
        returned alongside per-item failures; the endpoint does not roll back
        successful items when one fails.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkCreateInvitationsRequest'
      responses:
        '200':
          description: Bulk invitation processing completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_BulkInvitations'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/{id}:
    get:
      tags: [Invitations]
      summary: Get invitation by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Invitation loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Invitation'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Invitations]
      summary: Cancel invitation
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Invitation cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to cancel this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/token/{token}:
    get:
      tags: [Invitations]
      summary: Get invitation by token
      description: Public endpoint used by the invite acceptance flow.
      parameters:
        - in: path
          name: token
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Invitation loaded by token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Invitation'
        '404':
          description: Invitation not found or token invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/organization/{organizationId}:
    get:
      tags: [Invitations]
      summary: List invitations by organization
      description: >
        Get invitations visible to the caller. Organization admins see all
        organization invitations; landlords see tenant invitations for their
        assigned buildings.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Invitations loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_InvitationList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to list invitations for this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/accept:
    post:
      tags: [Invitations]
      summary: Accept invitation
      description: >
        Accept an invitation, create the account if needed, attach the new user
        to the invited organization role, and for tenant invitations select or
        create the required residential unit in the invited building.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AcceptInvitationRequest'
      responses:
        '200':
          description: Invitation accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Invitation'
        '400':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Invitation not found or expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '409':
          description: Invitation cannot be accepted in its current state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/invitations/{id}/resend:
    post:
      tags: [Invitations]
      summary: Resend invitation
      description: >
        Resend an invitation email with a new token. Expired invitations are
        extended automatically.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Invitation resent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Invitation'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to resend this invitation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Invitation not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/organizations:
    get:
      tags: [Organizations]
      summary: List organizations
      description: Platform-admin organization listing.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Organizations loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedOrganizations'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Organizations]
      summary: Create organization
      description: Create a new organization. Intended for platform-admin onboarding flows.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrganizationRequest'
      responses:
        '201':
          description: Organization created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Organization'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create organizations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/organizations/{id}:
    get:
      tags: [Organizations]
      summary: Get organization by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Organization loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Organization'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Organizations]
      summary: Update organization
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrganizationRequest'
      responses:
        '200':
          description: Organization updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Organization'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Organizations]
      summary: Delete organization
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Organization deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/organizations/slug/{slug}:
    get:
      tags: [Organizations]
      summary: Get organization by slug
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Organization loaded by slug
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Organization'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Organization not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/organizations/{organizationId}/users:
    get:
      tags: [Organizations]
      summary: List organization users
      description: >
        Get users in one organization with optional filtering. Organization
        admins can manage their own organization roster.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
        - in: query
          name: role
          schema:
            $ref: '#/components/schemas/OrganizationRole'
        - in: query
          name: accountStatus
          schema:
            $ref: '#/components/schemas/AccountStatus'
        - in: query
          name: active
          schema:
            type: boolean
        - in: query
          name: searchQuery
          schema:
            type: string
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
        - in: query
          name: sortBy
          schema:
            type: string
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
      responses:
        '200':
          description: Organization users loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedOrganizationUsers'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this organization roster
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/buildings:
    get:
      tags: [Buildings]
      summary: List buildings
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: active
          schema:
            type: boolean
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
            default: 20
        - in: query
          name: sortBy
          schema:
            type: string
            default: id
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Buildings loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedBuildings'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Buildings]
      summary: Create building
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBuildingRequest'
      responses:
        '201':
          description: Building created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Building'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create buildings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/analytics/organization/{organizationId}:
    get:
      tags: [Analytics]
      summary: Get organization analytics
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Organization analytics loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_DashboardAnalytics'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view analytics for this organization
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/analytics/building/{buildingId}:
    get:
      tags: [Analytics]
      summary: Get building analytics
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: buildingId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Building analytics loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_DashboardAnalytics'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view analytics for this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/analytics/tickets/volume:
    get:
      tags: [Analytics]
      summary: Get ticket volume time series
      description: Get ticket volume over time for either an organization or a building.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: buildingId
          schema:
            type: integer
            format: int64
        - in: query
          name: startDate
          schema:
            type: string
            format: date
        - in: query
          name: endDate
          schema:
            type: string
            format: date
        - in: query
          name: period
          schema:
            type: string
      responses:
        '200':
          description: Ticket volume time series loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketVolumeTimeSeries'
        '400':
          description: Invalid query combination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view analytics in the requested scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/analytics/buildings/aggregate:
    get:
      tags: [Analytics]
      summary: Get aggregate analytics for multiple buildings
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: buildingIds
          required: true
          style: form
          explode: true
          schema:
            type: array
            items:
              type: integer
              format: int64
      responses:
        '200':
          description: Aggregate building analytics loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_DashboardAnalytics'
        '400':
          description: At least one buildingId is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view one or more requested buildings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/buildings/{id}:
    get:
      tags: [Buildings]
      summary: Get building by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Building loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Building'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Building not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Buildings]
      summary: Update building
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateBuildingRequest'
      responses:
        '200':
          description: Building updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Building'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Building not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Buildings]
      summary: Delete building
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Building deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Building not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/buildings/{id}/units:
    get:
      tags: [Buildings]
      summary: List residential units by building
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
        - in: query
          name: sortBy
          schema:
            type: string
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
      responses:
        '200':
          description: Building units loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedResidentialUnits'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view units for this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Building not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/buildings/{id}/assignments:
    get:
      tags: [Buildings]
      summary: List building assignments
      description: Get active building assignments for a building.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Building assignments loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_BuildingAssignmentList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view assignments for this building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Buildings]
      summary: Assign user to building
      description: Assign a landlord or tenant to a building. Tenant assignments may include a residential unit link.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignBuildingUserRequest'
      responses:
        '201':
          description: Building assignment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_BuildingAssignment'
        '400':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to assign this user to the building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/buildings/{id}/assignments/{userId}:
    delete:
      tags: [Buildings]
      summary: Remove user from building
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
        - in: path
          name: userId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Building assignment removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to remove this assignment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/residential-units:
    get:
      tags: [Residential Units]
      summary: List residential units
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: buildingId
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Residential units loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedResidentialUnits'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Residential Units]
      summary: Create residential unit
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateResidentialUnitRequest'
      responses:
        '201':
          description: Residential unit created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_ResidentialUnit'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create residential units
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/residential-units/{id}:
    get:
      tags: [Residential Units]
      summary: Get residential unit by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Residential unit loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_ResidentialUnit'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this residential unit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Residential unit not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/announcements:
    get:
      tags: [Announcements]
      summary: List announcements
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: buildingId
          schema:
            type: integer
            format: int64
        - in: query
          name: published
          schema:
            type: boolean
        - in: query
          name: activeNow
          schema:
            type: boolean
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
        - in: query
          name: sortBy
          schema:
            type: string
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
      responses:
        '200':
          description: Announcements loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedAnnouncements'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Announcements]
      summary: Create announcement
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAnnouncementRequest'
      responses:
        '201':
          description: Announcement created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Announcement'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create announcements in this scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/announcements/{id}:
    get:
      tags: [Announcements]
      summary: Get announcement by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Announcement loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Announcement'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this announcement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Announcement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Announcements]
      summary: Update announcement
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAnnouncementRequest'
      responses:
        '200':
          description: Announcement updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Announcement'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this announcement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Announcement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Announcements]
      summary: Delete announcement
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Announcement deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this announcement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Announcement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/announcements/{id}/publish:
    patch:
      tags: [Announcements]
      summary: Publish announcement
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Announcement published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Announcement'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to publish this announcement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Announcement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/announcements/{id}/unpublish:
    patch:
      tags: [Announcements]
      summary: Unpublish announcement
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Announcement unpublished
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Announcement'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to unpublish this announcement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Announcement not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Residential Units]
      summary: Update residential unit
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateResidentialUnitRequest'
      responses:
        '200':
          description: Residential unit updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_ResidentialUnit'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this residential unit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Residential unit not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Residential Units]
      summary: Delete residential unit
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Residential unit deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this residential unit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Residential unit not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    patch:
      tags: [Notifications]
      summary: Update notification preferences
      description: Update the current user's email and SMS notification preferences. Omitted fields keep their current values.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateNotificationPreferencesRequest'
      responses:
        '200':
          description: Preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_NotificationPreferences'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets:
    get:
      tags: [Tickets]
      summary: List tickets
      description: >
        List tickets within the caller's effective organization, building, and
        role scope. Tenants see only their own PERSONAL tickets and visible
        COMMUNITY tickets in their building. Staff users see tickets within
        their scoped buildings or organization.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: buildingId
          schema:
            type: integer
            format: int64
        - in: query
          name: status
          schema:
            $ref: '#/components/schemas/TicketStatus'
        - in: query
          name: ticketType
          schema:
            $ref: '#/components/schemas/TicketType'
        - in: query
          name: createdByMe
          schema:
            type: boolean
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
            default: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
            default: 20
        - in: query
          name: sortBy
          schema:
            type: string
            default: createdAt
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        '200':
          description: Tickets loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedTickets'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Tickets]
      summary: Create ticket
      description: >
        Create a ticket as a tenant in an assigned building and residential unit.
        COMMUNITY tickets can be surfaced to other tenants in the same building.
        PERSONAL tickets remain staff-visible.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTicketRequest'
      responses:
        '201':
          description: Ticket created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Ticket'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller cannot create a ticket in the requested scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{id}:
    get:
      tags: [Tickets]
      summary: Get ticket by reference
      description: Get a single ticket by public ticket reference. Prefer ticketCode.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      responses:
        '200':
          description: Ticket loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Ticket'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Tickets]
      summary: Update ticket
      description: >
        Update ticket content as the creator, an assigned-building landlord, an
        organization admin in scope, or a platform admin. Use the dedicated
        status endpoint for status changes.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTicketRequest'
      responses:
        '200':
          description: Ticket updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Ticket'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller cannot update this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Tickets]
      summary: Delete ticket
      description: Delete a ticket by public ticket reference.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      responses:
        '200':
          description: Ticket deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller cannot delete this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{id}/status:
    patch:
      tags: [Tickets]
      summary: Update ticket status
      description: >
        Update ticket status as the creator, an assigned-building landlord, an
        organization admin in scope, or a platform admin.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTicketStatusRequest'
      responses:
        '200':
          description: Ticket status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Ticket'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller cannot update ticket status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{ticketCode}/activity:
    get:
      tags: [Tickets]
      summary: Get ticket activity
      description: >
        Get persistent ticket activity history in reverse chronological order.
        Access rules match ticket visibility.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketCodeParam'
      responses:
        '200':
          description: Ticket activity loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketActivityList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this ticket activity
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/building/{buildingId}/count:
    get:
      tags: [Tickets]
      summary: Get ticket count for building
      description: Get the total number of tickets for a single building.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/BuildingIdParam'
      responses:
        '200':
          description: Ticket count loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Long'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view building ticket counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/bulk-delete:
    delete:
      tags: [Tickets]
      summary: Bulk delete tickets
      description: >
        Delete multiple tickets by their public ticket codes. Every requested
        ticket must be deletable by the caller or the request fails.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkDeleteTicketsRequest'
      responses:
        '200':
          description: Tickets deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_BulkDeleteTickets'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller cannot delete one or more requested tickets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/similar:
    post:
      tags: [Tickets]
      summary: Find similar tickets
      description: >
        Advisory duplicate-detection endpoint. Returns up to 5 similar COMMUNITY
        tickets in the tenant's building plus predicted priority and category for
        the draft ticket. This endpoint does not block ticket creation.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FindSimilarTicketsRequest'
      responses:
        '200':
          description: Similar tickets resolved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketRecommendations'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create tickets in the requested building
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{id}/comments:
    get:
      tags: [Ticket Comments]
      summary: List ticket comments
      description: >
        Get all comments for a ticket the caller can access. Anonymous ticket
        creators remain masked in their own comments for non-creators.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      responses:
        '200':
          description: Ticket comments loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketCommentList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this ticket's comments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Ticket Comments]
      summary: Add ticket comment
      description: >
        Add a comment to a ticket the caller can access. If the ticket is
        anonymous, comments written by the original ticket creator are returned
        as anonymous for non-creators.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddTicketCommentRequest'
      responses:
        '201':
          description: Comment created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketComment'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to comment on this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{id}/notes:
    get:
      tags: [Ticket Notes]
      summary: List ticket notes
      description: Get internal staff-only notes for a ticket. Tenants cannot access these notes.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      responses:
        '200':
          description: Ticket notes loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketNoteList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view ticket notes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Ticket Notes]
      summary: Add ticket note
      description: Add an internal staff-only note to a ticket.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketRefParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTicketNoteRequest'
      responses:
        '201':
          description: Ticket note created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketNote'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to add ticket notes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/tickets/{ticketCode}/attachments:
    get:
      tags: [Ticket Attachments]
      summary: List ticket attachments
      description: Get all attachments for a ticket using the human-facing ticketCode.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketCodeParam'
      responses:
        '200':
          description: Ticket attachments loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketAttachmentList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this ticket's attachments
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/knowledge-base:
    get:
      tags: [Knowledge Base]
      summary: List knowledge base entries
      description: List knowledge base entries in caller scope with optional filters.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
        - in: query
          name: category
          schema:
            type: string
        - in: query
          name: published
          schema:
            type: boolean
        - in: query
          name: search
          schema:
            type: string
        - in: query
          name: buildingId
          schema:
            type: integer
            format: int64
        - in: query
          name: visibleToTenants
          schema:
            type: boolean
      responses:
        '200':
          description: Knowledge base entries loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_KnowledgeBaseEntryList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to access this knowledge base scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Knowledge Base]
      summary: Create knowledge base entry
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateKnowledgeBaseEntryRequest'
      responses:
        '201':
          description: Knowledge base entry created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_KnowledgeBaseEntry'
        '400':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to create knowledge base entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/knowledge-base/{id}:
    get:
      tags: [Knowledge Base]
      summary: Get knowledge base entry
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Knowledge base entry loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_KnowledgeBaseEntry'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this knowledge base entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Knowledge base entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Knowledge Base]
      summary: Update knowledge base entry
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateKnowledgeBaseEntryRequest'
      responses:
        '200':
          description: Knowledge base entry updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_KnowledgeBaseEntry'
        '400':
          description: Validation or business rule error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this knowledge base entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Knowledge base entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    delete:
      tags: [Knowledge Base]
      summary: Delete knowledge base entry
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Knowledge base entry deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this knowledge base entry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Knowledge base entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/knowledge-base/search:
    post:
      tags: [Knowledge Base]
      summary: Search knowledge base
      description: Retrieve the best matching published knowledge base entries for AI/support flows.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KnowledgeBaseSearchRequest'
      responses:
        '200':
          description: Knowledge base search results loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_KnowledgeBaseSearchResults'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to search in this scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/subscriptions/plans:
    get:
      tags: [Subscriptions]
      summary: Get active subscription plans
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Active subscription plans loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SubscriptionPlanList'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/subscriptions/organizations/{organizationId}:
    get:
      tags: [Subscriptions]
      summary: Get organization subscription
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Organization subscription loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_OrganizationSubscription'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this organization's subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/subscriptions/organizations/{organizationId}/limits:
    get:
      tags: [Subscriptions]
      summary: Get organization subscription limits
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: organizationId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Organization subscription limits loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SubscriptionLimits'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this organization's limits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    post:
      tags: [Ticket Attachments]
      summary: Upload ticket attachments
      description: >
        Upload one or more attachments for a ticket using repeated multipart
        file parts named files.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketCodeParam'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/TicketAttachmentUploadRequest'
      responses:
        '201':
          description: Attachments uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TicketAttachmentList'
        '400':
          description: Validation or upload error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to upload attachments for this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/attachments/{id}:
    delete:
      tags: [Ticket Attachments]
      summary: Delete attachment
      description: Delete a single attachment by attachment ID.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AttachmentIdParam'
      responses:
        '200':
          description: Attachment deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_Null'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to delete this attachment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Attachment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/attachments/{id}/download:
    get:
      tags: [Ticket Attachments]
      summary: Download attachment
      description: Download a single attachment by attachment ID.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AttachmentIdParam'
      responses:
        '200':
          description: Attachment file stream
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to download this attachment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Attachment not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/ai/tickets/self-service-answer:
    post:
      tags: [AI Ticket Assistance]
      summary: Tenant self-service answer
      description: >
        Generate a tenant-safe AI answer using organization knowledge base
        content and similar resolved tickets in scope.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TenantTicketAnswerRequest'
      responses:
        '200':
          description: AI answer generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_AiSuggestedAnswer'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to use this flow in the requested scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/users:
    get:
      tags: [Users]
      summary: List users
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: role
          schema:
            $ref: '#/components/schemas/OrganizationRole'
        - in: query
          name: accountStatus
          schema:
            $ref: '#/components/schemas/AccountStatus'
        - in: query
          name: active
          schema:
            type: boolean
        - in: query
          name: searchQuery
          schema:
            type: string
        - in: query
          name: page
          schema:
            type: integer
            minimum: 1
        - in: query
          name: size
          schema:
            type: integer
            minimum: 1
        - in: query
          name: sortBy
          schema:
            type: string
        - in: query
          name: sortDirection
          schema:
            type: string
            enum: [asc, desc]
      responses:
        '200':
          description: Users loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_PagedUsers'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/users/{id}:
    get:
      tags: [Users]
      summary: Get user by ID
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: User loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_User'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to view this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
    put:
      tags: [Users]
      summary: Update user
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: User updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_User'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to update this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/users/{id}/activate:
    patch:
      tags: [Users]
      summary: Activate user
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: User activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_User'
  /api/v1/users/{id}/deactivate:
    patch:
      tags: [Users]
      summary: Deactivate user
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: User deactivated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_User'
  /api/v1/users/{id}/residential-unit:
    get:
      tags: [Users]
      summary: Get tenant residential unit
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Tenant residential unit loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TenantResidentialUnit'
  /api/v1/users/me/residential-unit:
    get:
      tags: [Users]
      summary: Get current user residential unit
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current user residential unit loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_TenantResidentialUnit'
  /api/v1/users/me/email-preferences:
    get:
      tags: [Users]
      summary: Get email preferences
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Email preferences loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_EmailPreferences'
    patch:
      tags: [Users]
      summary: Update email preferences
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateEmailPreferencesRequest'
      responses:
        '200':
          description: Email preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_EmailPreferences'
  /api/v1/users/search:
    get:
      tags: [Users]
      summary: Search users
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: q
          required: true
          schema:
            type: string
        - in: query
          name: organizationId
          schema:
            type: integer
            format: int64
        - in: query
          name: limit
          schema:
            type: integer
      responses:
        '200':
          description: User search results loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_UserWithRoleList'
  /api/v1/users/me/preferences:
    patch:
      tags: [Users]
      summary: Update current user preferences
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserPreferencesRequest'
      responses:
        '200':
          description: User preferences updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_UserPreferences'
  /api/v1/users/batch:
    post:
      tags: [Users]
      summary: Get users by IDs
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserBatchRequest'
      responses:
        '200':
          description: Users loaded by batch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_UserList'
  /api/v1/dashboard/summary:
    get:
      tags: [Dashboard]
      summary: Get dashboard summary
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Dashboard summary loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_DashboardSummary'
  /api/v1/search:
    get:
      tags: [Search]
      summary: Global search
      description: Search across tickets, users, buildings, and announcements.
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: q
          required: true
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
      responses:
        '200':
          description: Global search results loaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_SearchResults'
  /api/v1/ai/tickets/{ticketCode}/draft-reply:
    post:
      tags: [AI Ticket Assistance]
      summary: Staff draft reply
      description: >
        Generate a staff draft reply for a ticket using ticket context, scoped
        knowledge base content, similar resolved tickets, and internal staff
        notes/comments.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketCodeParam'
      responses:
        '200':
          description: Draft reply generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse_AiSuggestedAnswer'
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '403':
          description: Caller is not allowed to generate staff drafts for this ticket
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
        '404':
          description: Ticket not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
  /api/v1/realtime/events:
    get:
      tags: [Realtime]
      summary: Subscribe to realtime events
      description: >
        Opens an authenticated Server-Sent Events stream for lightweight live
        updates. Browser clients typically need a fetch-based SSE client because
        the request uses a bearer token.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: SSE stream established
          content:
            text/event-stream:
              schema:
                type: string
              examples:
                connected:
                  summary: Initial connection event
                  value: |
                    event: CONNECTED
                    data: {"id":1763742000001,"eventType":"CONNECTED","entityType":"connection","entityId":5,"message":"Realtime subscription established","refreshRecommended":false,"createdAt":"2026-03-21T22:00:00"}
                ticketCreated:
                  summary: Ticket creation event
                  value: |
                    event: TICKET_CREATED
                    data: {"id":1763742000002,"eventType":"TICKET_CREATED","entityType":"ticket","entityId":42,"organizationId":5,"buildingId":9,"ticketCode":"6WS42A","title":"Broken elevator","message":"A new ticket was created","refreshRecommended":true,"createdAt":"2026-03-21T22:01:00"}
        '401':
          description: Missing or invalid token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiErrorResponse'
