openapi: 3.0.3
info:
  title: Folkyn API
  version: 1.0.0
  license:
    name: Proprietary
    url: https://folkyn.com
  description: Team API key; responses in `data`, errors in `error` with `X-Request-Id`. Spec at `/api/v1/openapi.yaml` and `/api/v1/openapi.json` (no key). **60 requests/minute** per key on `/api/v1/*`.
servers:
  - url: https://api.folkyn.com
    description: Production

tags:
  - name: Specification
    description: OpenAPI document download (no authentication)
  - name: Project codes
    description: Team project codes
  - name: Team tags
    description: Team tags
  - name: Teams
    description: Organizational teams (team units) and member lists
  - name: Missions
    description: Team missions
  - name: Freelancers
    description: Team freelancers and invitations
  - name: Activity reports
    description: Activity reports (CRA)
  - name: Accounting
    description: Accounting entries

security:
  - bearerAuth: []
  - ApiKeyHeader: []

paths:
  /api/v1/openapi.yaml:
    get:
      operationId: getOpenApiYaml
      summary: Download OpenAPI specification (YAML)
      description: Returns the OpenAPI 3 document as YAML (no authentication).
      tags: [Specification]
      security: []
      responses:
        '200':
          description: OpenAPI 3.0 document
          content:
            text/yaml:
              schema:
                type: string
        '404':
          description: Specification file missing on server
          content:
            text/plain:
              schema:
                type: string
        '500':
          description: Invalid or unreadable specification
          content:
            text/plain:
              schema:
                type: string

  /api/v1/openapi.json:
    get:
      operationId: getOpenApiJson
      summary: Download OpenAPI specification (JSON)
      description: Returns the OpenAPI 3 document as JSON (no authentication; body is the spec root, not wrapped in `data`).
      tags: [Specification]
      security: []
      responses:
        '200':
          description: OpenAPI 3.0 document as JSON object
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '404':
          description: Specification file missing on server
          content:
            text/plain:
              schema:
                type: string
        '500':
          description: Invalid or unreadable specification
          content:
            text/plain:
              schema:
                type: string

  /api/v1/project-codes:
    get:
      operationId: listProjectCodes
      summary: List Project Codes
      description: Lists team project codes for the API key's team.
      tags: [Project codes]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedProjectCodes'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createProjectCode
      summary: Create Project Code
      description: Creates a team project code.
      tags: [Project codes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                external_id:
                  type: string
                  nullable: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectCodeCreated'
        '400':
          $ref: '#/components/responses/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/project-codes/{id}:
    get:
      operationId: getProjectCode
      summary: Retrieve Project Code
      description: Retrieves one project code by id, including linked missions.
      tags: [Project codes]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ProjectCodeDetailPublic'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Project code not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/team-tags:
    get:
      operationId: listTeamTags
      summary: List Team Tags
      description: Lists team tags for the API key's team.
      tags: ['Team tags']
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedTeamTags'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createTeamTag
      summary: Create Team Tag
      description: Creates a team tag.
      tags: ['Team tags']
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TeamTagCreated'
        '400':
          $ref: '#/components/responses/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/team-tags/{id}:
    get:
      operationId: getTeamTag
      summary: Retrieve Team Tag
      description: Retrieves team tag information by id.
      tags: ['Team tags']
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TeamTagDetailPublic'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Team tag not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/teams:
    get:
      operationId: listTeams
      summary: List Teams
      description: Lists organizational teams (team units) for the API key's team.
      tags: [Teams]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedTeams'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createTeam
      summary: Create Team
      description: Creates a new organizational team (team unit) for the API key's team. Use **`parent_id`** to create a child node (depth is derived from the parent). Omit **`parent_id`** to create a root node at depth 1.
      tags: [Teams]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Team name (must be non-empty).
                  example: Engineering
                parent_id:
                  type: string
                  format: uuid
                  nullable: true
                  description: Parent organizational team UUID. Omit to create a root node.
                external_id:
                  type: string
                  nullable: true
                  maxLength: 255
                  description: External reference for integrations (unique per team when set).
                sort_order:
                  type: integer
                  minimum: 0
                  default: 0
                  description: Sort order among siblings (ascending).
      responses:
        '201':
          description: Team created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TeamListItemPublic'
        '400':
          $ref: '#/components/responses/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/teams/{id}:
    get:
      operationId: getTeam
      summary: Retrieve Team
      description: Retrieves organizational team information by id.
      tags: [Teams]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TeamDetailPublic'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Team not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/missions:
    get:
      operationId: listMissions
      summary: List Missions
      description: |
        Lists missions for the API key's team. All query parameters are optional.
      tags: [Missions]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/MissionStatusFilter'
        - $ref: '#/components/parameters/MissionStartDateFrom'
        - $ref: '#/components/parameters/MissionEndDateFrom'
        - $ref: '#/components/parameters/MissionEndDateTo'
        - $ref: '#/components/parameters/MissionCreatedAtFrom'
        - $ref: '#/components/parameters/MissionCreatedAtTo'
        - $ref: '#/components/parameters/MissionProjectCodeIdFilter'
        - $ref: '#/components/parameters/MissionHasProjectCodeFilter'
        - $ref: '#/components/parameters/MissionHasPurchaseOrderFilter'
        - name: freelancer_id
          in: query
          description: Restrict to missions assigned to this freelancer (UUID).
          schema:
            type: string
            format: uuid
        - name: external_id
          in: query
          description: Restrict to the mission whose **`external_id`** matches exactly (your ERP / third-party reference set via **`PATCH /api/v1/missions/{id}`**).
          schema:
            type: string
            example: ERP-MISSION-2026-001
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedMissions'
        '400':
          description: |
            Validation (e.g. invalid **`created_at_*`** / **`end_date_*`** date or range, unknown **`status`** or **`has_*`** value,
            both **`project_code_id`** and **`has_project_code`**, or **`project_code_id`** not found for the team).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createMission
      summary: Create Mission
      description: |
        Creates a mission for the API key's team.

        For `hourly`, `daily`, or `monthly`, send `rate_amount` > 0; for `deliverable`, send `deliverables` instead of `rate_amount`. Dates: **YYYY-MM-DD**.

        **Generate a services-agreement PDF** (`contract_type: services_agreement`, default) — `use_folkyn_template: true` + `team_signer_id`, then either:
        - **Default template:** `template_locale` = `fr` or `en`.
        - **Custom template:** `folkyn_template_id` = prestation template UUID.

        **Generate a master-agreement PDF** (`contract_type: master_agreement`): `master_agreement_template_id` + `master_agreement_valid_until` + `team_signer_id`. If the freelancer already has a framework agreement in progress or awaiting signature (same team, not expired), that agreement is **reused and linked** to the mission — no new PDF is generated and template fields are **ignored**. Send template fields only when no current framework agreement exists (e.g. first mission under master agreement, or previous agreement expired).

        For services agreements only, omit template fields to create a mission without a generated document. Master-agreement missions require either a reusable framework agreement or `master_agreement_template_id` / uploaded `master_agreement_file`. Optional: `purchase_order`, `project_code_id`, `approver_id`. The freelance referent is set on the freelancer profile (`PATCH /api/v1/freelancers/{id}`), not on mission create.
      tags: [Missions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MissionCreateRequest'
            examples:
              services_agreement_folkyn_locale:
                summary: Services agreement — Folkyn template by language
                value:
                  name: 'Backend API — Q3'
                  description: 'API development and maintenance for the Q3 release.'
                  mission_type: daily
                  contract_type: services_agreement
                  rate_amount: 650
                  start_date: '2026-07-01'
                  end_date: '2026-12-31'
                  freelancer_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  team_signer_id: 'bbbbbbbb-cccc-4ddd-eeee-ffffffffffff'
                  use_folkyn_template: true
                  template_locale: fr
              services_agreement_with_template:
                summary: Services agreement — team template UUID
                value:
                  name: 'Backend API — Q3'
                  description: 'API development and maintenance for the Q3 release.'
                  mission_type: daily
                  contract_type: services_agreement
                  rate_amount: 650
                  start_date: '2026-07-01'
                  end_date: '2026-12-31'
                  freelancer_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  team_signer_id: 'bbbbbbbb-cccc-4ddd-eeee-ffffffffffff'
                  use_folkyn_template: true
                  folkyn_template_id: 'cccccccc-dddd-4eee-ffff-000000000001'
              services_agreement_simple:
                summary: Services agreement — no generated document
                value:
                  name: 'Backend API — Q3'
                  description: 'API development and maintenance for the Q3 release.'
                  mission_type: daily
                  contract_type: services_agreement
                  rate_amount: 650
                  start_date: '2026-07-01'
                  end_date: '2026-12-31'
                  freelancer_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  purchase_order: 'PO-2026-001'
              services_agreement_with_approver:
                summary: Services agreement — pending admin approval before freelancer
                value:
                  name: 'Backend API — Q3 (review)'
                  description: 'API development and maintenance for the Q3 release.'
                  mission_type: daily
                  contract_type: services_agreement
                  rate_amount: 650
                  start_date: '2026-07-01'
                  end_date: '2026-12-31'
                  freelancer_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  approver_id: 'bbbbbbbb-cccc-4ddd-eeee-ffffffffffff'
              master_agreement:
                summary: Master agreement with Folkyn template
                value:
                  name: 'Framework — Jane Doe'
                  description: 'Framework agreement for consulting services.'
                  mission_type: daily
                  contract_type: master_agreement
                  rate_amount: 600
                  start_date: '2026-07-01'
                  end_date: '2026-12-31'
                  freelancer_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  team_signer_id: 'bbbbbbbb-cccc-4ddd-eeee-ffffffffffff'
                  master_agreement_template_id: 'cccccccc-dddd-4eee-ffff-000000000001'
                  master_agreement_valid_until: '2027-06-30'
      responses:
        '201':
          description: Created — same object as GET /api/v1/missions/{id}
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MissionDetail'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden (e.g. subscription required)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/missions/{id}:
    get:
      operationId: getMission
      summary: Retrieve Mission
      description: Retrieves mission information by id.
      tags: [Missions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MissionDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: patchMissionEnterprise
      summary: Update Mission Enterprise Fields
      description: Updates name, purchase_order, external_id, and/or project code for the API key's team mission.
      tags: [Missions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MissionEnterprisePatchRequest'
            examples:
              project_code_name_and_external_id:
                summary: Project code — name + optional ERP id (same object)
                description: >
                  **`name`** is always required inside **`project_code`**. Add **`external_id`** in that same object when you want Folkyn to match or create by **(team, external_id)**.
                value:
                  project_code:
                    name: 'ACME — renewal 2026'
                    external_id: 'ERP-PROJ-88421'
              project_code_name_only:
                summary: Project code — display name only (no ERP id)
                value:
                  project_code:
                    name: 'Internal initiative'
              project_code_id_link:
                summary: Link mission to an existing project code UUID
                value:
                  project_code_id: 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
              project_code_unlink:
                summary: Remove project code from the mission
                description: >
                  **`project_code_id`: null** or **`""`** unlinks. At runtime you may also send **`"project_code": null`**; this example uses **`project_code_id`** so it validates against the published schema.
                value:
                  project_code_id: null
              rename_mission_only:
                summary: Update mission title only
                value:
                  name: 'Renamed mission title'
              set_external_id:
                summary: Set the mission external identifier
                value:
                  external_id: 'ERP-MISSION-2026-001'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MissionDetail'
        '400':
          description: Validation (unknown fields, empty name, both project code keys, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Mission not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/missions/{id}/terminate:
    post:
      operationId: terminateMission
      summary: Terminate Mission Early
      description: >
        Early-terminates an **`in-progress`** mission at a given effective date (**`end_date`**, `YYYY-MM-DD`).
        If the date is today or in the past, the mission is terminated immediately (status `terminated`).
        If the date is in the future, the termination is scheduled and finalized automatically on that date.
        Same business rules as the Folkyn app.
      tags: [Missions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MissionTerminateRequest'
            examples:
              terminate_today:
                summary: Terminate immediately (today)
                value:
                  end_date: '2026-06-29'
                  reason: 'Client ended the engagement early.'
              schedule_future:
                summary: Schedule termination on a future date
                value:
                  end_date: '2026-08-31'
      responses:
        '200':
          description: Success — same object as GET /api/v1/missions/{id}
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MissionDetail'
        '400':
          description: Validation (missing/invalid end_date, mission not in-progress, already terminated/scheduled).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Mission not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/freelancers:
    get:
      operationId: listFreelancers
      summary: List Freelancers
      description: Lists freelancers for the API key's team.
      tags: [Freelancers]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: >
            Comma-separated roster segment: `staffed`, `unstaffed`, `pending`, `archived`, or `all`.
            `staffed` = at least one active mission; `unstaffed` = active link without such mission; `pending` = invitation not accepted (`id` is a team invitation id);
            `archived` = archived link. Omit `status` to exclude `archived` and `pending`; use `all` for no filter.
          schema:
            type: string
            example: staffed,unstaffed
        - name: tag_ids
          in: query
          description: Comma-separated tag UUIDs (AND — freelancer must have all listed tags).
          schema:
            type: string
        - name: kyc_complete
          in: query
          schema:
            type: string
            enum: ['true', 'false']
        - name: master_agreement_active
          in: query
          schema:
            type: string
            enum: ['true', 'false']
        - name: external_id
          in: query
          description: Filter by exact `external_id` (case-insensitive) — matches the team-set value or, when unset, the value derived from the freelancer's name.
          schema:
            type: string
            example: clementbalea
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedPartnerFreelancers'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/freelancers/invitations:
    post:
      operationId: inviteFreelancer
      summary: Invite Freelancer
      description: |
        Invites a freelancer by email.

        Track pending invitations with `GET /api/v1/freelancers?status=pending`.
      tags: [Freelancers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FreelancerInvitationCreateRequest'
            example:
              email: jane.doe@example.com
              first_name: Jane
              last_name: Doe
              job_title: Senior Developer
              tag_ids:
                - 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
              invite_locale: en
      responses:
        '201':
          description: Invitation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/FreelancerInvitationCreated'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/freelancers/{id}:
    patch:
      operationId: patchFreelancerTeamFields
      summary: Update Freelancer Team Fields
      description: Updates team-scoped freelancer fields.
      tags: [Freelancers]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Freelancer profile UUID (not a pending invitation id).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FreelancerPartnerPatchRequest'
            examples:
              external_id_and_note:
                summary: External id and internal note
                value:
                  external_id: 'ERP-F-88421'
                  note: 'Preferred for Q3 staffing'
              tags_and_teams:
                summary: Replace tags and teams
                value:
                  tag_ids:
                    - 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee'
                  team_ids:
                    - 'bbbbbbbb-cccc-4ddd-eeee-ffffffffffff'
              job_title_and_referent:
                summary: Job title and team referent
                value:
                  job_title: 'Senior React Developer'
                  referent_id: 'cccccccc-dddd-4eee-ffff-000000000001'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/FreelancerListItemPublic'
        '400':
          description: Validation (unknown fields, invalid arrays, referent not in team, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Freelancer not found for this team
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/users:
    get:
      operationId: listUsers
      summary: List Team Users
      description: |
        Lists company users (team admins and members), including pending invitations.

        Default `status` filter: `active,pending`. Other values: `expired`, `undeliverable`, or `all`.

        For `status: active`, `id` is the profile UUID. For invitations, `id` is the invitation UUID.
      tags: [Users]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          description: Comma-separated segments — `active`, `pending`, `expired`, `undeliverable`, or `all`. Default `active,pending`.
          schema:
            type: string
            example: active,pending
        - name: role
          in: query
          description: Filter by team role — `team-admin` or `team-member`. Omit for all roles.
          schema:
            type: string
            enum: [team-admin, team-member]
        - name: email
          in: query
          description: Filter by exact email address (case-insensitive). Matches both active members and invitations.
          schema:
            type: string
            format: email
            example: jane.doe@acme.com
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedPartnerUsers'
        '400':
          description: Validation (unknown status or role)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/users/invitations:
    post:
      operationId: inviteUser
      summary: Invite Team User
      description: |
        Invites a team admin or member by email.

        Track pending invitations with `GET /api/v1/users?status=pending`.
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInvitationCreateRequest'
            example:
              email: admin@example.com
              first_name: Alex
              last_name: Martin
              role: team-admin
              job_title: HR Manager
              invite_locale: fr
      responses:
        '201':
          description: Invitation created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/UserInvitationCreated'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/activity-reports:
    get:
      operationId: listActivityReports
      summary: List Activity Reports
      description: Lists activity reports (CRA) for the API key's team.
      tags: [Activity reports]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/ActivityReportStatusFilter'
        - name: submission_date
          in: query
          description: Filter by day (YYYY-MM-DD).
          schema:
            type: string
            format: date
            example: '2026-04-08'
        - name: mission_id
          in: query
          description: Mission UUID.
          schema:
            type: string
            format: uuid
        - name: freelancer_id
          in: query
          description: Freelancer UUID.
          schema:
            type: string
            format: uuid
        - name: period
          in: query
          description: |
            Filter by CRA calendar month, same as stored **`report_month`** (`YYYY-MM`).
            Combines with other filters (logical AND).
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
            example: '2026-04'
        - $ref: '#/components/parameters/ActivityReportSort'
        - $ref: '#/components/parameters/ActivityReportSortDirection'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedActivityReports'
        '400':
          $ref: '#/components/responses/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/activity-reports/{id}:
    get:
      operationId: getActivityReport
      summary: Retrieve Activity Report
      description: Retrieves activity report information by id.
      tags: [Activity reports]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ActivityReportDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/accounting/entries:
    get:
      operationId: listAccountingEntries
      summary: List Accounting Entries
      description: |
        Lists accounting entries for validated activity reports on the API key's team.

        Each item includes `invoice_number`, `file_name`, and a `freelancer` object.
        Optional **`sort`** / **`direction`** query params order the list (default **`created_at`** **`desc`**).
      tags: [Accounting]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/AccountingEntrySort'
        - $ref: '#/components/parameters/AccountingEntrySortDirection'
        - name: status
          in: query
          schema:
            type: string
            enum: [paid, waiting_payment]
        - name: type
          in: query
          schema:
            type: string
            enum: [invoice, expenses]
        - name: activity_report_id
          in: query
          description: Filter by CRA UUID (validated, in-team scope).
          schema:
            type: string
            format: uuid
        - name: has_sent_to_accounting
          in: query
          description: >
            Presence filter on whether the entry was sent to the accounting tool (**not a date range**).
            Case-insensitive accepted values: **`with`**, **`without`**, **`true`**, **`false`** (also **`1`** / **`0`**).
            **`with`** / **`true`**: **`sent_to_accounting_at`** is set (already sent). **`without`** / **`false`**: not sent yet (`null`).
            Returns **`400`** if the value is invalid.
          schema:
            type: string
            enum: [with, without, true, false]
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/PaginatedAccountingEntries'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/v1/accounting/entries/{id}:
    patch:
      operationId: patchAccountingEntryMarkPaid
      summary: Mark Accounting Entry as Paid
      description: Marks the accounting entry as paid.
      tags: [Accounting]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountingEntryMarkPaidRequest'
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [entry]
                    properties:
                      entry:
                        $ref: '#/components/schemas/AccountingEntryPublic'
        '400':
          description: Validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicError'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: 'Use `Authorization: Bearer <your_api_key>`'
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-Api-Key

  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 10
    MissionStatusFilter:
      name: status
      in: query
      description: |
        Comma-separated mission statuses (kebab-case, normalized to lowercase).
        Filters on the same values returned in the `status` field — see `MissionStatus`.

        **Allowed values:** `draft`, `pending-freelancer-review`, `pending-company-review`, `pending-freelancer-extension`, `in-progress`, `completed`, `early-terminated`, `cancelled`, `rejected`, `deleted`.

        An unknown value returns `400`.
      schema:
        type: string
      example: in-progress,completed
    MissionStartDateFrom:
      name: start_date_from
      in: query
      description: Calendar **YYYY-MM-DD**. Returns missions whose `start_date` is on or after this day.
      schema:
        type: string
        pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
      example: '2025-01-01'
    MissionCreatedAtFrom:
      name: created_at_from
      in: query
      description: Inclusive lower bound on mission creation date. Calendar **YYYY-MM-DD** (UTC day start). Pairs with `created_at_to`.
      schema:
        type: string
        pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
      example: '2025-06-01'
    MissionCreatedAtTo:
      name: created_at_to
      in: query
      description: Inclusive upper bound on mission creation date. Calendar **YYYY-MM-DD** (UTC day end). Must be ≥ `created_at_from` when both are set.
      schema:
        type: string
        pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
      example: '2025-06-30'
    MissionEndDateFrom:
      name: end_date_from
      in: query
      description: >
        Inclusive lower bound on the mission end date. Calendar **YYYY-MM-DD**. Pairs with `end_date_to`.
        Matches on the effective end date: `early_termination_date` when set, otherwise `end_date`.
      schema:
        type: string
        pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
      example: '2025-07-01'
    MissionEndDateTo:
      name: end_date_to
      in: query
      description: >
        Inclusive upper bound on the mission end date. Calendar **YYYY-MM-DD**. Must be ≥ `end_date_from` when both are set.
        Matches on the effective end date: `early_termination_date` when set, otherwise `end_date`.
      schema:
        type: string
        pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
      example: '2025-07-31'
    MissionProjectCodeIdFilter:
      name: project_code_id
      in: query
      description: |
        Filter missions linked to this team project code (UUID from **`GET /api/v1/project-codes`**).
        Mutually exclusive with **`has_project_code`**. Returns **`400`** if the id is not a valid UUID or does not belong to the API key's team.
      schema:
        type: string
        format: uuid
      example: aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee
    MissionHasProjectCodeFilter:
      name: has_project_code
      in: query
      description: |
        Presence filter on **`project_code_id`**. Case-insensitive accepted values: **`with`**, **`without`**, **`true`**, **`false`** (also **`1`** / **`0`**).
        **`with`** / **`true`**: mission has a project code. **`without`** / **`false`**: no project code.
        Mutually exclusive with **`project_code_id`**.
      schema:
        type: string
        enum: [with, without, 'true', 'false', '1', '0']
      example: with
    MissionHasPurchaseOrderFilter:
      name: has_purchase_order
      in: query
      description: |
        Presence filter on **`purchase_order`**. Same accepted values as **`has_project_code`**.
        **`with`** / **`true`**: non-empty purchase order (after trim). **`without`** / **`false`**: null, empty, or whitespace-only.
      schema:
        type: string
        enum: [with, without, 'true', 'false', '1', '0']
      example: without
    ActivityReportStatusFilter:
      name: status
      in: query
      description: |
        Comma-separated activity report statuses. Case-insensitive (normalized to uppercase).

        **Allowed values:** `PENDING`, `APPROVED`, `REJECTED`.

        Response JSON uses a lowercase **`status`** field (`pending`, `approved`, `rejected`, `paid`) — see `ActivityReportDisplayStatus`.
      schema:
        type: string
      example: PENDING,APPROVED
    ActivityReportSort:
      name: sort
      in: query
      description: >
        Sort field for the list (pairs with `direction`). Default `created_at`.
        `review_date` is an alias for `reviewed_at` (last review decision; null until reviewed).
      schema:
        type: string
        enum: [created_at, reviewed_at, review_date]
        default: created_at
    ActivityReportSortDirection:
      name: direction
      in: query
      description: Sort direction. Case-insensitive. Omit for **`desc`**.
      schema:
        type: string
        enum: [asc, desc]
        default: desc
    AccountingEntrySort:
      name: sort
      in: query
      description: >
        Sort field for accounting entries (pairs with **`direction`**). Default **`created_at`**.
        **`payment_date`** aliases **`paid_at`**. **`sent_to_accounting`** and **`accounting_sent_at`**
        alias **`sent_to_accounting_at`**.
      schema:
        type: string
        enum:
          - created_at
          - paid_at
          - payment_date
          - sent_to_accounting_at
          - sent_to_accounting
          - accounting_sent_at
        default: created_at
    AccountingEntrySortDirection:
      name: direction
      in: query
      description: Sort direction for **`GET /api/v1/accounting/entries`**. Case-insensitive. Omit for **`desc`**.
      schema:
        type: string
        enum: [asc, desc]
        default: desc

  schemas:
    PartnerValidationErrorReason:
      type: string
      description: Machine-readable validation subtype (HTTP 400) for create tag / project code.
      enum:
        - invalid_json
        - required_field
        - duplicate_name
    PublicErrorDetails:
      type: object
      description: Optional structured hints (validation, scope, rate limit).
      additionalProperties: true
      properties:
        reason:
          $ref: '#/components/schemas/PartnerValidationErrorReason'
        field:
          type: string
          description: JSON body field name when applicable (e.g. `name`).
          example: name
        requiredScope:
          type: string
          description: Required OAuth-like scope string when `code` is 403 (insufficient scope).
          example: tags:write
        retryAfterSeconds:
          type: integer
          minimum: 0
          description: Hint when `code` is 429.
    PublicError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: integer
              description: Same as HTTP status; use `message` and `details` to narrow.
              enum: [400, 401, 403, 404, 429, 500]
              example: 401
            message:
              type: string
            request_id:
              type: string
              format: uuid
              description: Correlates with the X-Request-Id response header.
            details:
              $ref: '#/components/schemas/PublicErrorDetails'
    PaginatedProjectCodes:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ProjectCode'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    PaginatedTeamTags:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TeamTag'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    PaginatedMissions:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MissionListItemPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    PartnerFreelancerListStatus:
      type: string
      enum: [staffed, unstaffed, pending, archived]
      description: >
        Lifecycle status on each list item.
        `staffed` = active mission on team; `unstaffed` = active link, no such mission;
        `pending` = invitation not yet accepted; `archived` = archived link.
    FreelancerTagPublic:
      type: object
      required: [id, name]
      description: Team tag attached to the freelancer.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    FreelancerTeamPublic:
      type: object
      required: [id, name, depth, path, parent_id]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        depth:
          type: integer
          minimum: 1
          maximum: 3
          description: Depth in the organizational tree (1 = root).
        path:
          type: string
          description: Ancestor path from root to this node (e.g. `Direction › Engineering › Backend`).
        parent_id:
          type: string
          format: uuid
          nullable: true
          description: Parent organizational team UUID; null for root nodes.
    FreelancerListItemPublic:
      type: object
      required:
        - id
        - email
        - external_id
        - job_title
        - note
        - referent
        - status
        - tags
        - teams
        - kyc_complete
        - master_agreement_active
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Profile UUID, or invitation UUID when `status` is `pending`.
        email:
          type: string
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        external_id:
          type: string
          description: >
            External identifier for integrations. Uses the team-stored value when set;
            otherwise computed from lowercased `first_name` + `last_name` with spaces and Latin diacritics
            stripped (e.g. Clément Balea → `clementbalea`). German `ß` becomes `ss`.
            Empty string if both names are missing and no stored value.
        job_title:
          type: string
          nullable: true
        note:
          type: string
          nullable: true
          description: Internal team note on the freelancer; null if absent.
        referent:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PartnerFreelancerReferentPublic'
          description: Team referent profile; null if none.
        status:
          $ref: '#/components/schemas/PartnerFreelancerListStatus'
        tags:
          type: array
          description: Tags for this freelancer on the team (`tag_ids` query uses AND across listed UUIDs).
          items:
            $ref: '#/components/schemas/FreelancerTagPublic'
        teams:
          type: array
          description: Organizational teams (team units) this freelancer belongs to.
          items:
            $ref: '#/components/schemas/FreelancerTeamPublic'
        kyc_complete:
          type: boolean
        master_agreement_active:
          type: boolean
          description: Signed master agreement with a future `valid_until` date.
        created_at:
          type: string
          format: date-time
    PaginatedPartnerFreelancers:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FreelancerListItemPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    FreelancerPartnerPatchRequest:
      type: object
      description: |
        At least one field required. `tag_ids` and `team_ids` replace the full assignment for this team.
        `referent_id` can be reassigned to another team member but cannot be cleared.
      minProperties: 1
      properties:
        external_id:
          type: string
          nullable: true
          description: Team-stored external identifier; null clears it.
        note:
          type: string
          nullable: true
          description: Internal team note on the freelancer; null clears it.
        tag_ids:
          type: array
          description: Team tag UUIDs to assign (replaces existing tags for this team).
          items:
            type: string
            format: uuid
        team_ids:
          type: array
          maxItems: 1
          description: Organizational team UUID to assign (replaces existing assignment for this team). At most one team per person. Send an empty array to clear.
          items:
            type: string
            format: uuid
        department_ids:
          type: array
          maxItems: 1
          deprecated: true
          description: Deprecated alias for `team_ids`. Use `team_ids` instead.
          items:
            type: string
            format: uuid
        job_title:
          type: string
          nullable: true
          description: Job title on the team link.
        referent_id:
          type: string
          format: uuid
          description: Team member profile UUID as referent; must belong to the API key's team. Cannot be cleared.
    FreelancerInvitationCreateRequest:
      type: object
      required: [email, first_name, last_name]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        job_title:
          type: string
          description: Optional job title stored on the invitation.
        referent_id:
          type: string
          format: uuid
          description: Active team member profile UUID as referent.
        tag_ids:
          type: array
          items:
            type: string
            format: uuid
        team_ids:
          type: array
          maxItems: 1
          description: Organizational team UUID on the invitation. At most one team per person.
          items:
            type: string
            format: uuid
        invited_by_member_id:
          type: string
          format: uuid
          description: Team member profile UUID shown as email sender; defaults to first active team-admin.
        invite_locale:
          type: string
          enum: [fr, en]
          description: Locale for the invitation email (`fr` or `en`).
    FreelancerInvitationCreated:
      type: object
      required: [invitation_id, email, status, email_sent, is_existing_freelancer]
      properties:
        invitation_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        status:
          type: string
          enum: [pending]
        email_sent:
          type: boolean
        is_existing_freelancer:
          type: boolean
          description: True when the email belongs to an existing freelancer account on the platform.
    MissionCreateRequest:
      type: object
      required: [name, description, mission_type, start_date, end_date, freelancer_id]
      properties:
        name:
          type: string
        description:
          type: string
          description: Mission objectives, deliverables, and working conditions.
        mission_type:
          $ref: '#/components/schemas/MissionBillingType'
        contract_type:
          type: string
          enum: [services_agreement, master_agreement]
          default: services_agreement
        rate_amount:
          type: number
          description: Required for hourly, daily, and monthly missions (HT unit rate). Omit for deliverable missions.
        deliverables:
          type: array
          description: Required when mission_type is deliverable.
          items:
            type: object
            required: [name, quantity, amount_ht]
            properties:
              name:
                type: string
              quantity:
                type: number
              amount_ht:
                type: number
        start_date:
          type: string
          format: date
          description: YYYY-MM-DD
        end_date:
          type: string
          format: date
          description: YYYY-MM-DD
        freelancer_id:
          type: string
          format: uuid
          description: Active freelancer profile UUID on the team.
        team_signer_id:
          type: string
          format: uuid
          description: Required when generating a contract from a template (`folkyn_template_id` or `master_agreement_template_id`). Active team member profile UUID as company signatory.
        purchase_order:
          type: string
        project_code_id:
          type: string
          format: uuid
        approver_id:
          type: string
          format: uuid
          description: Active team-admin UUID — mission stays in `draft` until approved.
        use_folkyn_template:
          type: boolean
          description: Set `true` to generate a services-agreement PDF from a DOCX template.
        template_locale:
          type: string
          enum: [fr, en]
          description: Default prestation template language. Use instead of `folkyn_template_id` when you do not have the template UUID.
        folkyn_template_id:
          type: string
          format: uuid
          description: Prestation template UUID. Alternative to `template_locale`.
        master_agreement_template_id:
          type: string
          format: uuid
          description: Team DOCX template UUID (`master_agreement` only). Requires `team_signer_id` and `master_agreement_valid_until`. Ignored when the freelancer already has a current framework agreement (in progress or awaiting signature).
        master_agreement_valid_until:
          type: string
          format: date
          description: Framework agreement end date (YYYY-MM-DD). Required with `master_agreement_template_id`. Ignored when an existing framework agreement is reused.
    PaginatedPartnerUsers:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/UserListItemPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    UserListItemPublic:
      type: object
      required: [id, email, role, status, tags, teams]
      properties:
        id:
          type: string
          format: uuid
          description: Profile UUID when active; invitation UUID when pending/expired/undeliverable.
        email:
          type: string
          format: email
        first_name:
          type: string
          nullable: true
        last_name:
          type: string
          nullable: true
        role:
          type: string
          enum: [team-admin, team-member]
        job_title:
          type: string
          nullable: true
        status:
          type: string
          enum: [active, pending, expired, undeliverable]
        tags:
          type: array
          items:
            $ref: '#/components/schemas/PartnerTagPublic'
        teams:
          type: array
          description: Organizational teams assigned to the user or invitation.
          items:
            $ref: '#/components/schemas/PartnerTeamUnitPublic'
        created_at:
          type: string
          format: date-time
          nullable: true
    PartnerTagPublic:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    PartnerTeamUnitPublic:
      type: object
      required: [id, name, depth, path, parent_id]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        depth:
          type: integer
          minimum: 1
          maximum: 3
        path:
          type: string
          description: Ancestor path from root to this node.
        parent_id:
          type: string
          format: uuid
          nullable: true
    UserInvitationCreateRequest:
      type: object
      required: [email, first_name, last_name, role]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        role:
          type: string
          enum: [team-admin, team-member]
        job_title:
          type: string
        tag_ids:
          type: array
          items:
            type: string
            format: uuid
        team_ids:
          type: array
          maxItems: 1
          description: Organizational team UUID on the invitation. At most one team per person.
          items:
            type: string
            format: uuid
        invited_by_member_id:
          type: string
          format: uuid
          description: Team member profile UUID shown as email sender; defaults to first active team-admin.
        invite_locale:
          type: string
          enum: [fr, en]
    UserInvitationCreated:
      type: object
      required: [invitation_id, email, status, email_sent, role, resent]
      properties:
        invitation_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        status:
          type: string
          enum: [pending]
        email_sent:
          type: boolean
        role:
          type: string
          enum: [team-admin, team-member]
        resent:
          type: boolean
          description: True when an existing pending invitation was updated and the email was resent.
    PaginatedActivityReports:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ActivityReportListItemPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    MissionBillingType:
      type: string
      description: >
        Billing model for the mission (Postgres enum `mission_type`).
        `hourly` / `daily` / `monthly` use **`rate_amount`** as the price per that unit in **`currency`**.
        `deliverable` missions bill via the **deliverables catalog** (quantities and line amounts); `rate_amount` may still be set as a reference but line totals come from catalog rows on mission detail.
      enum:
        - hourly
        - daily
        - monthly
        - deliverable
    MissionStatus:
      type: string
      description: >
        Mission status. Derived from dates, pending documents, and pending addenda —
        the same value used for filtering (see the `status` query parameter).
      enum:
        - draft
        - pending-freelancer-review
        - pending-company-review
        - pending-freelancer-extension
        - in-progress
        - completed
        - early-terminated
        - cancelled
        - rejected
        - deleted
    ActivityReportDisplayStatus:
      type: string
      description: >
        Normalized status in response JSON (lowercase). Values:
        `PENDING` → `pending`, `APPROVED` → `approved` or **`paid`** when invoicing or expenses are marked paid, `REJECTED` → `rejected`.
      enum:
        - pending
        - approved
        - rejected
        - paid
    MissionListItemPublic:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        mission_type:
          $ref: '#/components/schemas/MissionBillingType'
        contract_kind:
          type: string
          description: >
            Contract channel: **`master_agreement`** = framework agreement path, **`services_agreement`** = standalone services agreement.
            Only these two values are returned.
          enum: [master_agreement, services_agreement]
        has_document:
          type: boolean
        currency:
          type: string
          description: ISO 4217 alphabetic code (e.g. `EUR`, `USD`) as stored on the mission.
          example: EUR
        rate_amount:
          type: number
          description: >
            Unit price in **`currency`** according to **`mission_type`**: per hour, per day, per month, or mission-level reference for deliverable missions (see `MissionBillingType`).
            Not a daily rate derived by dividing elsewhere — the unit is implied by `mission_type`.
        client_rate_amount:
          type: number
          nullable: true
          description: >
            ESN teams only: unit price excl. VAT billed to the end client for the same unit as **`rate_amount`** (per hour, day, or month per **`mission_type`**). Null for non-ESN teams, deliverable missions, or when unset.
        start_date:
          type: string
          format: date
        end_date:
          type: string
          format: date
        status:
          $ref: '#/components/schemas/MissionStatus'
        early_termination_date:
          type: string
          format: date
          nullable: true
          description: Effective end date chosen when the mission was terminated early (**YYYY-MM-DD**). Null if the mission was never terminated early.
        terminated_by:
          type: string
          format: uuid
          nullable: true
          description: UUID of the team user who terminated the mission early. Null if the mission was never terminated early.
        external_referent:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ActivityReportExternalReferentPublic'
        purchase_order:
          type: string
          nullable: true
        external_id:
          type: string
          nullable: true
          description: External identifier for the mission set by the team (e.g. an ERP or third-party system reference); null if unset.
        project_code_id:
          type: string
          format: uuid
          nullable: true
        project_code:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ActivityReportMissionProjectCodePublic'
          description: Primary linked project code; null if the mission has none.
        project_codes:
          type: array
          description: >
            All project codes linked to the mission (`id`, `name`, `external_id`). Populated on **`GET /api/v1/missions`**
            and **`GET /api/v1/missions/{id}`**. Empty when the mission has no linked codes.
          items:
            $ref: '#/components/schemas/ActivityReportMissionProjectCodePublic'
        deliverables_catalog:
          type: array
          description: >
            Mission deliverables catalog. Empty on **`GET /api/v1/missions`**; filled on **`GET /api/v1/missions/{id}`**.
          items:
            $ref: '#/components/schemas/MissionDeliverableCatalogItem'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        created_by:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PartnerPerson'
        freelancer:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/MissionFreelancerPublic'
        team_signer:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/PartnerPerson'
    MissionEnterprisePatchRequest:
      type: object
      description: |
        At least one field required. Send only one of `project_code_id` or `project_code` when updating the project code (not both in the same request).
      minProperties: 1
      properties:
        name:
          type: string
          maxLength: 255
          description: Mission title.
        purchase_order:
          type: string
          nullable: true
          maxLength: 255
        external_id:
          type: string
          nullable: true
          maxLength: 255
          description: >
            External identifier for the mission (e.g. an ERP or third-party system reference).
            Send **`null`** or **`""`** to clear it.
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: >
            Link the mission to an existing team project code by **UUID** (the row must belong to the same team as the API key).
            Send **`null`** or **`""`** to unlink the mission from any project code.
            Mutually exclusive with **`project_code`** in the same request.
        project_code_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >
            Replace the full list of project codes linked to the mission (requires the team setting for multiple codes).
            Send an empty array to unlink all codes. Mutually exclusive with **`project_code_id`** and **`project_code`**.
        project_code:
          type: object
          description: >
            Create or reuse a team project code and link the mission in one request.
            Send **`null`** to unlink. Mutually exclusive with **`project_code_id`**.
            Field-level rules are on **`name`** and **`external_id`** below.
          nullable: true
          allOf:
            - $ref: '#/components/schemas/MissionEnterprisePatchProjectCode'
    MissionEnterprisePatchProjectCode:
      type: object
      description: |
        Send **`name`** and, when needed, **`external_id`** together in the same JSON object (see request examples on **`PATCH /api/v1/missions/{id}`**).

        **Unlink** the mission from any project code by sending **`"project_code": null`** on the PATCH body (not by sending this object).

        **How the row is chosen**
        - **Non-empty `external_id` (after trim):** matched by `external_id` for your team. If found, reused; `name` may be updated unless another project code on the team already uses that `name` (`400`). If not found, a new project code is created.
        - **Empty or omitted `external_id`:** matched by `name` only; reuse if found, otherwise create.
      properties:
        name:
          type: string
          maxLength: 255
          description: >
            Required. Display name for the project code. When **`external_id`** is empty or whitespace-only after trim,
            Folkyn matches an existing row by this **`name`** for your team before creating one.
        external_id:
          type: string
          nullable: true
          maxLength: 255
          description: >
            Optional. Stable id from your ERP (max 255 characters; leading/trailing spaces are trimmed).
            When non-empty after trim, matched by `external_id` for your team first, then the rules above apply.
      required: [name]
    MissionTerminateRequest:
      type: object
      description: |
        Early-terminate an **`in-progress`** mission. `end_date` is the effective end date.
        Today/past → immediate termination; future → scheduled and finalized on that date.
      properties:
        end_date:
          type: string
          format: date
          description: >
            Effective end date (`YYYY-MM-DD`). Must be **>= today (UTC)**, **>= mission start date**,
            and **<= current mission end date**.
        reason:
          type: string
          nullable: true
          description: Optional free-text reason kept on the mission for your records.
      required: [end_date]
    MissionDetail:
      $ref: '#/components/schemas/MissionListItemPublic'
    MissionFreelancerPublic:
      type: object
      description: >
        Assigned freelancer. `referent` is the team member assigned as referent for this freelancer, when configured — not the external client referent on the mission.
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        referent:
          type: object
          nullable: true
          description: Team referent (id, name, email); null if none.
          allOf:
            - $ref: '#/components/schemas/PartnerFreelancerReferentPublic'
    ProjectCode:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        external_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        mission_count:
          type: integer
    ProjectCodeCreated:
      type: object
      required: [id, name, external_id, created_at]
      description: Payload returned by POST /api/v1/project-codes (no mission_count or updated_at).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        external_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    TeamTag:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        user_count:
          type: integer
        freelancer_count:
          type: integer
    TeamTagCreated:
      type: object
      required: [id, name, created_at]
      description: Payload returned by POST /api/v1/team-tags (no counts or updated_at).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
    PartnerProfileIdEmail:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
    ProjectCodeMissionSummary:
      type: object
      required: [id, name, freelancer_id]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        freelancer_id:
          type: string
          format: uuid
          nullable: true
          description: Assigned freelancer profile id; null when the mission has no freelancer.
    ProjectCodeDetailPublic:
      type: object
      required: [id, name, missions, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        external_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        missions:
          type: array
          items:
            $ref: '#/components/schemas/ProjectCodeMissionSummary'
    TeamTagDetailPublic:
      type: object
      required: [id, name, freelancers, users, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        freelancers:
          type: array
          items:
            $ref: '#/components/schemas/PartnerProfileIdEmail'
        users:
          type: array
          items:
            $ref: '#/components/schemas/PartnerProfileIdEmail'
    TeamListItemPublic:
      type: object
      required:
        - id
        - name
        - depth
        - path
        - parent_id
        - external_id
        - sort_order
        - is_archived
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        depth:
          type: integer
          minimum: 1
          maximum: 3
        path:
          type: string
        parent_id:
          type: string
          format: uuid
          nullable: true
        external_id:
          type: string
          nullable: true
        sort_order:
          type: integer
          minimum: 0
        is_archived:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TeamDetailPublic:
      type: object
      required:
        - id
        - name
        - depth
        - path
        - parent_id
        - external_id
        - sort_order
        - is_archived
        - freelancers
        - users
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        depth:
          type: integer
          minimum: 1
          maximum: 3
        path:
          type: string
        parent_id:
          type: string
          format: uuid
          nullable: true
        external_id:
          type: string
          nullable: true
        sort_order:
          type: integer
          minimum: 0
        is_archived:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        freelancers:
          type: array
          items:
            $ref: '#/components/schemas/PartnerProfileIdEmail'
        users:
          type: array
          items:
            $ref: '#/components/schemas/PartnerProfileIdEmail'
    PaginatedTeams:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TeamListItemPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    PartnerPerson:
      type: object
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
        last_name:
          type: string
    PartnerFreelancerReferentPublic:
      type: object
      description: Team referent (id, first name, last name, email).
      required: [id, first_name, last_name, email]
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
    ActivityReportFreelancerPublic:
      type: object
      nullable: true
      description: >
        Assigned freelancer; null if missing. `referent` matches the team referent on mission payloads (see `MissionFreelancerPublic`).
      properties:
        id:
          type: string
          format: uuid
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
        external_id:
          type: string
          description: >
            External identifier for integrations. Uses the team-stored value when set;
            otherwise computed from lowercased `first_name` + `last_name` with spaces and Latin diacritics
            stripped (e.g. Clément Balea → `clementbalea`). German `ß` becomes `ss`.
            Empty string if both names are missing and no stored value.
        siren:
          type: string
          nullable: true
          description: >
            Freelancer company SIREN (9 digits, no spaces); null when unset or fewer than 9 digits.
        referent:
          type: object
          nullable: true
          description: Team referent (id, name, email); null if none.
          allOf:
            - $ref: '#/components/schemas/PartnerFreelancerReferentPublic'
    ActivityReportMissionProjectCodePublic:
      type: object
      required: [id, name, external_id]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        external_id:
          type: string
          nullable: true
          description: Optional client / ERP reference for the project code.
    ProjectCodeAmountPublic:
      type: object
      required: [id, name, external_id, amount_excl_vat]
      description: Project code with invoice amount excl. VAT (excludes expense refacturation).
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        external_id:
          type: string
          nullable: true
        amount_excl_vat:
          type: number
          description: Invoice amount excl. VAT attributed to this project code for the activity report or invoice accounting entry.
    ActivityReportSupplementaryItemPublic:
      type: object
      required: [id, description, amount_excl_vat, project_code_id]
      properties:
        id:
          type: string
          format: uuid
        description:
          type: string
          nullable: true
        amount_excl_vat:
          type: number
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: >
            Project code billed on this supplementary line. Set when the mission has multiple project codes; otherwise `null`.
    ActivityReportMissionPublic:
      type: object
      required: [id, name, mission_type, currency, rate_amount, purchase_order, project_code]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mission_type:
          $ref: '#/components/schemas/MissionBillingType'
        currency:
          type: string
          description: ISO 4217 currency code of the mission's amounts (e.g. `EUR`, `USD`). Defaults to `EUR`.
        rate_amount:
          type: number
          description: Freelancer unit rate excl. VAT (in **`currency`**); unit follows **`mission_type`**.
        client_rate_amount:
          type: number
          nullable: true
          description: >
            ESN: unit rate excl. VAT resold to the end client (same currency and unit as **`rate_amount`**). Null when not applicable or unset.
        purchase_order:
          type: string
          nullable: true
          description: Purchase order number (bon de commande) on the mission, if set.
        project_code:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ActivityReportMissionProjectCodePublic'
          description: Primary linked team project code; null if the mission has none. Use root **`project_codes`** on the activity report for the full list with amounts.
    ActivityReportExternalReferentPublic:
      type: object
      required: [id, email, company, siren, address, postal_code, city]
      description: External client referent on the mission (id, email, company name, SIREN, address fields). Distinct from `freelancer.referent` (team referent).
      properties:
        id:
          type: string
          format: uuid
          nullable: true
          description: Client company UUID; null when unset.
        email:
          type: string
        company:
          type: string
        siren:
          type: string
          nullable: true
          pattern: '^[0-9]{9}$'
          description: Client company SIREN (9 digits, no spaces); null when unset or fewer than 9 digits.
        address:
          type: string
          nullable: true
          description: Street address line.
        postal_code:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
    ActivityReportListItemPublic:
      type: object
      properties:
        id:
          type: string
          format: uuid
        mission:
          $ref: '#/components/schemas/ActivityReportMissionPublic'
        external_referent:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/ActivityReportExternalReferentPublic'
        report_month:
          type: string
          pattern: '^[0-9]{4}-[0-9]{2}$'
          description: >
            Month key as stored (**`YYYY-MM`**). **`report_period_start`** / **`report_period_end`** are RFC 3339
            **`date-time`** strings with explicit offset, covering the **first civil day** through the **last civil day**
            of that month in **`report_period_time_zone`** (**`Europe/Paris`** — legal time, including DST). Safe to
            parse with standard `date-time` clients (`new Date(...)` in JavaScript). Null if **`report_month`** is
            missing or invalid.
          example: '2024-03'
        report_period_time_zone:
          type: string
          enum: [Europe/Paris]
          description: IANA time zone applied to **`report_period_start`** / **`report_period_end`** (always this value).
        report_period_start:
          type: string
          format: date-time
          nullable: true
          description: >
            First instant of the first civil day of **`report_month`** in **`Europe/Paris`** (RFC 3339 with offset).
            Null if missing or invalid.
          example: '2024-03-01T00:00:00.000+01:00'
        report_period_end:
          type: string
          format: date-time
          nullable: true
          description: >
            Last instant of the last civil day of **`report_month`** in **`Europe/Paris`** (handles February leap years
            and DST). Null if invalid.
          example: '2024-03-31T23:59:59.999+02:00'
        submission_date:
          type: string
          format: date-time
          nullable: true
        status:
          $ref: '#/components/schemas/ActivityReportDisplayStatus'
        reviewed_at:
          type: string
          format: date-time
          nullable: true
          description: >
            Timestamp of the last review decision (approve or reject). Null if never reviewed.
        rejection_reason:
          type: string
          nullable: true
        freelancer:
          $ref: '#/components/schemas/ActivityReportFreelancerPublic'
        invoicing_is_paid:
          type: boolean
        expenses_is_paid:
          type: boolean
        invoice_amount_excl_vat:
          type: number
          description: Invoice line amount excluding VAT.
        expense_amount_excl_vat:
          type: number
          description: Expense line amount excluding VAT.
        project_codes:
          type: array
          description: >
            Invoice amount excl. VAT split by project code (same rules as invoice billing; excludes expense refacturation).
            One entry with the full **`invoice_amount_excl_vat`** when the mission has a single linked code; per-line split when
            multiple codes are linked. Empty when no project codes are linked.
          items:
            $ref: '#/components/schemas/ProjectCodeAmountPublic'
        supplementary_items:
          type: array
          description: >
            Supplementary billing lines. When the mission has multiple project codes, each line includes **`project_code_id`**.
          items:
            $ref: '#/components/schemas/ActivityReportSupplementaryItemPublic'
        created_at:
          type: string
          format: date-time
        accounting_entries:
          type: array
          description: Accounting entry ids linked to this activity report (`invoice` / `expenses`). Empty until validated.
          items:
            $ref: '#/components/schemas/ActivityReportAccountingStubPublic'
        timesheets:
          type: array
          description: All timesheet lines for this activity report (discriminated by `type`). Empty array if none.
          items:
            $ref: '#/components/schemas/ActivityReportTimesheetLinePublic'
    MissionDeliverableCatalogItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        quantity:
          type: integer
        amount_excl_vat:
          type: number
        order_index:
          type: integer
    ActivityReportTimesheetLinePublic:
      description: |
        Timesheet lines for the activity report. `type` discriminates the object shape.
        For `hourly` and `daily` missions, the API returns one row per calendar day in `report_month`: days without declared time appear as zero-fill lines with `hours: 0` or `days: 0`, `description: null`, and a deterministic UUID in `id`. Declared days use their own `id`. Lines are sorted by `date`, then `id`. `monthly` / `deliverable` rows follow day-based lines.
        `description` is `null` when empty (never an empty string).
      oneOf:
        - $ref: '#/components/schemas/ActivityReportTimesheetLineHourlyPublic'
        - $ref: '#/components/schemas/ActivityReportTimesheetLineDailyPublic'
        - $ref: '#/components/schemas/ActivityReportTimesheetLineMonthlyPublic'
        - $ref: '#/components/schemas/ActivityReportTimesheetLineDeliverablePublic'
      discriminator:
        propertyName: type
        mapping:
          hourly: '#/components/schemas/ActivityReportTimesheetLineHourlyPublic'
          daily: '#/components/schemas/ActivityReportTimesheetLineDailyPublic'
          monthly: '#/components/schemas/ActivityReportTimesheetLineMonthlyPublic'
          deliverable: '#/components/schemas/ActivityReportTimesheetLineDeliverablePublic'
    ActivityReportTimesheetLineHourlyPublic:
      type: object
      required: [id, type, description]
      description: 'Hours per calendar day. Missing days in the month are returned as zero-fill rows with `hours: 0`.'
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [hourly]
        description:
          type: string
          nullable: true
          description: Comment or line description on the CRA; null when empty.
        date:
          type: string
          format: date
          nullable: true
          description: Activity day (YYYY-MM-DD).
        hours:
          type: number
          nullable: true
          description: Declared hours for this line; `0` on zero-fill days.
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: >
            Project code billed on this line. Present when the mission has multiple project codes; omitted or `null` for single-code missions.
    ActivityReportTimesheetLineDailyPublic:
      type: object
      required: [id, type, description]
      description: 'Days per calendar day. Missing days in the month are returned as zero-fill rows with `days: 0`.'
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [daily]
        description:
          type: string
          nullable: true
          description: Line comment on the CRA; null when empty.
        date:
          type: string
          format: date
          nullable: true
        days:
          type: number
          nullable: true
          description: Declared days for this line; `0` on zero-fill days.
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: Project code billed on this line when the mission has multiple project codes.
    ActivityReportTimesheetLineMonthlyPublic:
      type: object
      required: [id, type, description]
      description: Activity or billing line for a calendar month (quantities and totals live on the CRA / mission elsewhere).
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [monthly]
        description:
          type: string
          nullable: true
          description: Line comment on the CRA; null when empty.
        month:
          type: string
          nullable: true
          pattern: '^[0-9]{4}-[0-9]{2}$'
          description: Month key YYYY-MM.
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: Project code billed on this line when the mission has multiple project codes.
    ActivityReportTimesheetLineDeliverablePublic:
      type: object
      required: [id, type, description, comment]
      description: >
        One deliverable line. Structured payloads may return `comment` separately with `description` set to null (deliverable name in `deliverable`).
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [deliverable]
        description:
          type: string
          nullable: true
          description: Optional label; null when only structured deliverable fields are present (use `deliverable` + `comment`).
        comment:
          type: string
          nullable: true
          description: Optional deliverable comment from the stored JSON (`comment` field); `null` when unset.
        deliverable:
          type: string
          nullable: true
          description: Free-text name or label of the deliverable.
        date:
          type: string
          format: date
          nullable: true
        amount:
          type: number
          nullable: true
        project_code_id:
          type: string
          format: uuid
          nullable: true
          description: Project code billed on this line when the mission has multiple project codes.
    ActivityReportAccountingStubPublic:
      type: object
      required: [id, type]
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum: [invoice, expenses]
    AccountingEntryPublic:
      type: object
      required:
        - id
        - activity_report_id
        - type
        - status
        - amount_excl_vat
        - vat_amount
        - amount_incl_vat
        - currency
        - invoice_number
        - file_name
        - freelancer
        - paid_at
        - sent_to_accounting_at
        - created_at
        - project_codes
      properties:
        id:
          type: string
          format: uuid
        activity_report_id:
          type: string
          format: uuid
        type:
          type: string
          enum: [invoice, expenses]
        status:
          type: string
          enum: [waiting_payment, paid]
        amount_excl_vat:
          type: number
        vat_amount:
          type: number
        amount_incl_vat:
          type: number
        currency:
          type: string
          description: ISO 4217 currency code of the entry's amounts, fixed at entry creation (e.g. `EUR`, `USD`). Defaults to `EUR`.
        invoice_number:
          type: string
          nullable: true
          description: Freelancer invoice number when **`type`** is **`invoice`**; null for **`expenses`** or if unset.
        file_name:
          type: string
          nullable: true
          description: >
            Uploaded invoice file name (`invoice`) or generated expense summary PDF name (`expenses`, `note-de-frais-{activity_report_id}.pdf`).
        freelancer:
          $ref: '#/components/schemas/ActivityReportFreelancerPublic'
        paid_at:
          type: string
          format: date-time
          nullable: true
        sent_to_accounting_at:
          type: string
          format: date-time
          nullable: true
          description: >
            Timestamp when the entry was sent to the team accounting tool (email export). `null` if not yet sent.
        created_at:
          type: string
          format: date-time
        project_codes:
          type: array
          description: >
            For **`type: invoice`**, invoice amount excl. VAT split by project code (same rules as activity reports).
            Empty for **`type: expenses`** entries.
          items:
            $ref: '#/components/schemas/ProjectCodeAmountPublic'
    PaginatedAccountingEntries:
      type: object
      required: [items, total, current_page, per_page]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AccountingEntryPublic'
        total:
          type: integer
        current_page:
          type: integer
        per_page:
          type: integer
    AccountingEntryMarkPaidRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [paid]
          description: Must be **`paid`** (only marking as paid is supported via this API).
    ActivityReportDetail:
      $ref: '#/components/schemas/ActivityReportListItemPublic'

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
    ForbiddenError:
      description: Insufficient scope or forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
    RateLimited:
      description: Too many requests
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
    InternalError:
      description: Server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
    PublicError:
      description: Client error (typically HTTP 400).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PublicError'
