openapi: 3.0.3
info:
  title: Payti Cloud API
  version: "1.0.0"
  description: >
    Public API contract for Payti Cloud MVP. This document describes the
    existing backend behavior and is intended to become the source for Scalar UI.

servers:
  - url: https://api.pay-ti.ru
    description: Production
  - url: http://localhost:8080
    description: Local development

tags:
  - name: Health
  - name: Auth
  - name: Workspaces
  - name: Projects
  - name: Tasks
  - name: Time Entries
  - name: Reports

paths:
  /healthz:
    get:
      tags: [Health]
      operationId: getHealthz
      summary: Health check
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusResponse"

  /readyz:
    get:
      tags: [Health]
      operationId: getReadyz
      summary: Readiness check
      responses:
        "200":
          description: Ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusResponse"
        "503":
          description: Not ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatusResponse"

  /v1/auth/register:
    post:
      tags: [Auth]
      operationId: register
      summary: Register user and send verification email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "202":
          description: Verification email sent
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "409":
          description: Email already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: email_already_exists
                  message: email is already registered
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/login:
    post:
      tags: [Auth]
      operationId: login
      summary: Login and issue token pair
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginRequest"
      responses:
        "200":
          description: Token pair
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenPairResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: invalid_credentials
                  message: invalid credentials
        "403":
          description: Email not verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: email_not_verified
                  message: email is not verified
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/refresh:
    post:
      tags: [Auth]
      operationId: refresh
      summary: Refresh token pair
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshRequest"
      responses:
        "200":
          description: Token pair
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenPairResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          description: Invalid refresh token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: refresh_token_invalid
                  message: refresh token is invalid
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/logout:
    post:
      tags: [Auth]
      operationId: logout
      summary: Revoke current refresh session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshRequest"
      responses:
        "204":
          description: Logged out
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/register/verify:
    post:
      tags: [Auth]
      operationId: verifyEmail
      summary: Verify registration email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmailVerificationRequest"
      responses:
        "204":
          description: Email verified
        "400":
          description: Invalid request or verification token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidRequest:
                  value:
                    error:
                      code: invalid_request
                      message: validation failed
                invalidToken:
                  value:
                    error:
                      code: verification_token_invalid
                      message: email verification token is invalid
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/password/forgot:
    post:
      tags: [Auth]
      operationId: requestPasswordReset
      summary: Request password reset email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordResetRequest"
      responses:
        "202":
          description: Request accepted
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/password/reset:
    post:
      tags: [Auth]
      operationId: resetPassword
      summary: Reset password with token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PasswordResetConfirmRequest"
      responses:
        "204":
          description: Password updated
        "400":
          description: Invalid request or reset token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalidRequest:
                  value:
                    error:
                      code: invalid_request
                      message: validation failed
                invalidToken:
                  value:
                    error:
                      code: reset_token_invalid
                      message: password reset token is invalid
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/auth/me:
    get:
      tags: [Auth]
      operationId: me
      summary: Current authenticated user
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: user_not_found
                  message: user not found
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/workspaces:
    get:
      tags: [Workspaces]
      operationId: listWorkspaces
      summary: List active workspaces of current user
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Workspace list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/workspaces/current:
    get:
      tags: [Workspaces]
      operationId: currentWorkspace
      summary: Current workspace selected by backend MVP policy
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Current workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects:
    get:
      tags: [Projects]
      operationId: listProjects
      summary: List active projects of current workspace
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Project list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectSummaryListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Projects]
      operationId: createProject
      summary: Create project in current workspace
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectWriteRequest"
      responses:
        "201":
          description: Created project
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/summary:
    get:
      tags: [Projects]
      operationId: workspaceProjectSummary
      summary: Aggregate metrics for all active projects in current workspace
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Workspace project summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceProjectSummaryResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}:
    parameters:
      - $ref: "#/components/parameters/ProjectID"
    patch:
      tags: [Projects]
      operationId: updateProject
      summary: Update project in current workspace
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProjectWriteRequest"
      responses:
        "200":
          description: Updated project
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Projects]
      operationId: archiveProject
      summary: Archive project in current workspace
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Archived
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/report:
    get:
      tags: [Reports]
      operationId: projectReport
      summary: Project report for selected period
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProjectID"
        - name: period_start
          in: query
          required: true
          schema:
            type: string
            format: date-time
        - name: period_end
          in: query
          required: true
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Project report
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectReportResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/tasks:
    get:
      tags: [Tasks]
      operationId: listWorkspaceTasks
      summary: List active tasks across all active projects in current workspace
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Task list across workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceTaskSummaryListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks:
    parameters:
      - $ref: "#/components/parameters/ProjectID"
    get:
      tags: [Tasks]
      operationId: listProjectTasks
      summary: List active tasks of project in current workspace
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Task list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSummaryListResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Create task in project
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskWriteRequest"
      responses:
        "201":
          description: Created task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks/{task_id}:
    parameters:
      - $ref: "#/components/parameters/ProjectID"
      - $ref: "#/components/parameters/TaskID"
    patch:
      tags: [Tasks]
      operationId: updateTask
      summary: Update task in project
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaskWriteRequest"
      responses:
        "200":
          description: Updated task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [Tasks]
      operationId: archiveTask
      summary: Archive task in project
      security:
        - bearerAuth: []
      responses:
        "204":
          description: Archived
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks/{task_id}/timer/start:
    post:
      tags: [Time Entries]
      operationId: startTimer
      summary: Start timer for task
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProjectID"
        - $ref: "#/components/parameters/TaskID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "201":
          description: Timer entry created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks/{task_id}/timer/stop:
    post:
      tags: [Time Entries]
      operationId: stopTimer
      summary: Stop active timer for task
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProjectID"
        - $ref: "#/components/parameters/TaskID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "200":
          description: Timer entry completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks/{task_id}/time/add:
    post:
      tags: [Time Entries]
      operationId: addManualTime
      summary: Add manual time to task
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProjectID"
        - $ref: "#/components/parameters/TaskID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DurationRequest"
      responses:
        "201":
          description: Manual entry created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/projects/{project_id}/tasks/{task_id}/time/subtract:
    post:
      tags: [Time Entries]
      operationId: subtractManualTime
      summary: Subtract manual time from task
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/ProjectID"
        - $ref: "#/components/parameters/TaskID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DurationRequest"
      responses:
        "201":
          description: Manual entry created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimeEntryResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"

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

  parameters:
    ProjectID:
      name: project_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TaskID:
      name: task_id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  responses:
    InvalidRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: invalid_request
              message: request body must be valid JSON
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: unauthorized
              message: authorization required
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: not_found
              message: resource not found
    Conflict:
      description: Conflict
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: conflict
              message: conflict
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: internal_error
              message: internal server error

  schemas:
    StatusResponse:
      type: object
      additionalProperties: false
      properties:
        status:
          type: string
      required: [status]

    ErrorEnvelope:
      type: object
      additionalProperties: false
      properties:
        code:
          type: string
        message:
          type: string
      required: [code, message]

    ErrorResponse:
      type: object
      additionalProperties: false
      properties:
        error:
          $ref: "#/components/schemas/ErrorEnvelope"
      required: [error]

    Pagination:
      type: object
      additionalProperties: false
      properties:
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
      required: [limit, offset, total]

    RegisterRequest:
      type: object
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
        password:
          type: string
        display_name:
          type: string
      required: [email, password, display_name]

    LoginRequest:
      type: object
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
        password:
          type: string
      required: [email, password]

    RefreshRequest:
      type: object
      additionalProperties: false
      properties:
        refresh_token:
          type: string
      required: [refresh_token]

    PasswordResetRequest:
      type: object
      additionalProperties: false
      properties:
        email:
          type: string
          format: email
      required: [email]

    PasswordResetConfirmRequest:
      type: object
      additionalProperties: false
      properties:
        token:
          type: string
        new_password:
          type: string
      required: [token, new_password]

    EmailVerificationRequest:
      type: object
      additionalProperties: false
      properties:
        token:
          type: string
      required: [token]

    TokenPairResponse:
      type: object
      additionalProperties: false
      properties:
        access_token:
          type: string
        refresh_token:
          type: string
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          format: int64
      required: [access_token, refresh_token, token_type, expires_in]

    UserResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        display_name:
          type: string
        system_role:
          $ref: "#/components/schemas/SystemRole"
      required: [id, email, display_name, system_role]

    MeResponse:
      allOf:
        - $ref: "#/components/schemas/UserResponse"

    WorkspaceMode:
      type: string
      enum: [personal, team_shared, team_managed]

    WorkspaceMemberRole:
      type: string
      enum: [owner, admin, member]

    WorkspaceMemberStatus:
      type: string
      enum: [active, disabled]

    SystemRole:
      type: string
      enum: [user, system_admin]

    WorkspaceResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mode:
          $ref: "#/components/schemas/WorkspaceMode"
        role:
          $ref: "#/components/schemas/WorkspaceMemberRole"
        status:
          $ref: "#/components/schemas/WorkspaceMemberStatus"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required: [id, name, mode, role, status, created_at, updated_at]

    WorkspaceListResponse:
      type: object
      additionalProperties: false
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required: [items, pagination]

    BillingType:
      type: string
      enum: [hourly, fixed]

    FixedBillingMode:
      type: string
      enum: [project_total, task_prices]

    ProjectRevenueMode:
      type: string
      enum: [hourly, fixed_project_total, fixed_task_prices]

    TaskRevenueMode:
      type: string
      enum: [hourly, fixed_task_price, not_applicable]

    ProjectWriteRequest:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
        billing_type:
          $ref: "#/components/schemas/BillingType"
        fixed_billing_mode:
          allOf:
            - $ref: "#/components/schemas/FixedBillingMode"
          nullable: true
        fixed_price_minor:
          type: integer
          format: int64
          nullable: true
        hourly_rate_minor:
          type: integer
          format: int64
          nullable: true
      required: [title, billing_type]

    ProjectResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        billing_type:
          $ref: "#/components/schemas/BillingType"
        fixed_billing_mode:
          allOf:
            - $ref: "#/components/schemas/FixedBillingMode"
          nullable: true
        fixed_price_minor:
          type: integer
          format: int64
          nullable: true
        hourly_rate_minor:
          type: integer
          format: int64
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - title
        - billing_type
        - created_at
        - updated_at

    ProjectSummaryResponse:
      allOf:
        - $ref: "#/components/schemas/ProjectResponse"
        - type: object
          additionalProperties: false
          properties:
            tasks_total:
              type: integer
              format: int64
            tasks_completed:
              type: integer
              format: int64
            tracked_seconds:
              type: integer
              format: int64
            revenue_minor:
              type: integer
              format: int64
            revenue_mode:
              $ref: "#/components/schemas/ProjectRevenueMode"
          required:
            - tasks_total
            - tasks_completed
            - tracked_seconds
            - revenue_minor
            - revenue_mode

    ProjectSummaryListResponse:
      type: object
      additionalProperties: false
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ProjectSummaryResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required: [items, pagination]

    WorkspaceProjectSummaryResponse:
      type: object
      additionalProperties: false
      properties:
        projects_total:
          type: integer
          format: int64
        tasks_total:
          type: integer
          format: int64
        tasks_completed:
          type: integer
          format: int64
        tracked_seconds:
          type: integer
          format: int64
        revenue_minor:
          type: integer
          format: int64
      required: [projects_total, tasks_total, tasks_completed, tracked_seconds, revenue_minor]

    TaskStatus:
      type: string
      enum: [new, in_progress, paused, completed]

    TaskWriteRequest:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
        status:
          $ref: "#/components/schemas/TaskStatus"
        fixed_price_minor:
          type: integer
          format: int64
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
      required: [title, status]

    TaskResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        title:
          type: string
        status:
          $ref: "#/components/schemas/TaskStatus"
        fixed_price_minor:
          type: integer
          format: int64
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - project_id
        - title
        - status
        - created_at
        - updated_at

    TaskSummaryResponse:
      allOf:
        - $ref: "#/components/schemas/TaskResponse"
        - type: object
          additionalProperties: false
          properties:
            tracked_seconds:
              type: integer
              format: int64
            revenue_minor:
              type: integer
              format: int64
              nullable: true
            revenue_mode:
              $ref: "#/components/schemas/TaskRevenueMode"
          required: [tracked_seconds, revenue_mode]

    WorkspaceTaskSummaryResponse:
      allOf:
        - $ref: "#/components/schemas/TaskSummaryResponse"
        - type: object
          additionalProperties: false
          properties:
            project_title:
              type: string
          required: [project_title]

    TaskSummaryListResponse:
      type: object
      additionalProperties: false
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/TaskSummaryResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required: [items, pagination]

    WorkspaceTaskSummaryListResponse:
      type: object
      additionalProperties: false
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceTaskSummaryResponse"
        pagination:
          $ref: "#/components/schemas/Pagination"
      required: [items, pagination]

    TimeEntrySource:
      type: string
      enum: [timer, manual]

    TimeAdjustmentKind:
      type: string
      enum: [add, subtract]

    DurationRequest:
      type: object
      additionalProperties: false
      properties:
        duration_seconds:
          type: integer
          format: int32
      required: [duration_seconds]

    TimeEntryResponse:
      type: object
      additionalProperties: false
      properties:
        id:
          type: string
          format: uuid
        task_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        source:
          $ref: "#/components/schemas/TimeEntrySource"
        adjustment_kind:
          allOf:
            - $ref: "#/components/schemas/TimeAdjustmentKind"
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        ended_at:
          type: string
          format: date-time
          nullable: true
        duration_seconds:
          type: integer
          format: int32
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - task_id
        - user_id
        - source
        - duration_seconds
        - created_at
        - updated_at

    ProjectReportTaskRowResponse:
      type: object
      additionalProperties: false
      properties:
        task_id:
          type: string
          format: uuid
        title:
          type: string
        status:
          $ref: "#/components/schemas/TaskStatus"
        completed_at:
          type: string
          format: date-time
          nullable: true
        tracked_seconds:
          type: integer
          format: int64
        revenue_minor:
          type: integer
          format: int64
          nullable: true
        revenue_mode:
          $ref: "#/components/schemas/TaskRevenueMode"
      required: [task_id, title, status, tracked_seconds, revenue_mode]

    ProjectReportResponse:
      type: object
      additionalProperties: false
      properties:
        project_id:
          type: string
          format: uuid
        project_title:
          type: string
        period_start:
          type: string
          format: date-time
        period_end:
          type: string
          format: date-time
        generated_at:
          type: string
          format: date-time
        billing_type:
          $ref: "#/components/schemas/BillingType"
        fixed_billing_mode:
          allOf:
            - $ref: "#/components/schemas/FixedBillingMode"
          nullable: true
        fixed_price_minor:
          type: integer
          format: int64
          nullable: true
        hourly_rate_minor:
          type: integer
          format: int64
          nullable: true
        tracked_seconds:
          type: integer
          format: int64
        revenue_minor:
          type: integer
          format: int64
        revenue_mode:
          $ref: "#/components/schemas/ProjectRevenueMode"
        task_rows:
          type: array
          items:
            $ref: "#/components/schemas/ProjectReportTaskRowResponse"
      required:
        - project_id
        - project_title
        - period_start
        - period_end
        - generated_at
        - billing_type
        - tracked_seconds
        - revenue_minor
        - revenue_mode
        - task_rows
