openapi: 3.1.0
info:
  title: Flo Web API — Matters
  description: |
    Upload matter entries to a Perform review cycle from a CSV file.

    Two endpoints are recommended for new integrations: `POST /matters-csv/upsert`
    (idempotent write) and `POST /matters-csv/validate-upsert` (pre-flight validation
    against the same rules). The older `POST /matters-csv/upload-csv` and
    `POST /matters-csv/validate-csv` endpoints are retained for backwards compatibility
    only — they behave differently in ways that make retries and re-uploads unsafe.

    Authentication is session-based: the caller must present a valid Flo admin
    session (established via the login flow). Requests without a valid admin session
    are rejected with `401 Unauthorized` or `403 Forbidden`.
  version: 2.0.0
servers:
  - url: https://florecruit.com/api/v2
    description: Flo production API
paths:
  /matters-csv/upload-csv:
    post:
      deprecated: true
      description: Use POST /matters-csv/upsert instead for idempotent behavior. This
        endpoint silently skips rows that already exist in the review cycle. Creates
        new matter for each row in the csv file. Rejects all rows if any single row
        contain errors.
      operationId: uploadMattersCSV
      parameters:
      - description: The UUID of the review cycle to be associated with the uploaded
          matters.
        in: query
        name: reviewCycleId
        required: true
        schema:
          type: string
          format: uuid
          description: The UUID of the review cycle to be associated with the uploaded
            matters.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: |
                    Comma-separated value file. Must be less than 10MB in size.
                    Header row is required, and each subsequent row corresponds to one matter entry.
                    Header row must match: "Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description"
                    The columns must follow these requirements:
                    - Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.
                    - Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
                    - Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Billing Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.
                    - End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.
                    - Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.
                    - Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.
                    - Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
              required:
              - file
      responses:
        "200":
          description: Matters created successfully (no response body)
        "400":
          content:
            '*/*':
              schema:
                type: string
          description: "Invalid CSV file format, or CSV file contains one or more\
            \ invalid rows. Use /validate-csv for row level error details."
        "401":
          description: Unauthorized
        "403":
          content:
            '*/*':
              schema:
                type: string
          description: Forbidden
        "404":
          content:
            '*/*':
              schema:
                type: string
          description: Review Cycle not found
        "409":
          content:
            '*/*':
              schema:
                type: string
          description: Conflict
        "500":
          content:
            '*/*':
              schema:
                type: string
          description: Internal Server Error
      summary: Uploads matters from the csv file
      tags:
      - matter-csv-controller
  /matters-csv/upsert:
    post:
      description: |
        Upserts a matter entry for each row in the CSV file. Uniqueness of a
        matter entry within a review cycle is by the combined (Matter ID,
        Client Name, Attorney Email) key.

        CSV formatting:
        - Field values containing a comma, double quote, or newline must be
          wrapped in double quotes. Embedded double quotes are escaped by
          doubling. Example: a Description of Smith, Jones & Associates
          must be written as "Smith, Jones & Associates".

        Behavior:
        - If a matter entry with that composite key already exists in the cycle,
          its non-key fields are updated in place when they differ from the row.
          Rows whose non-key fields already match the stored entry are no-ops
          and are not counted in the response's updated total. The row's id is
          preserved, so any existing review-cycle assignments referencing the
          entry survive.
        - If no such entry exists, a new one is created.
        - Multiple entries under the same Matter ID + Client Name (different
          Attorney Emails) are supported - they share a single Client Matter
          parent whose canonical Matter Name and Description also refresh on
          each upsert.

        Idempotency:
        - Uploading the same CSV twice produces the same end state - safe to retry.
        - Non-key fields updated: Matter Name, Billing Attorney Email, Start Date,
          End Date, Hours Billed, Description.
        - Matter Name and Description also refresh on the shared Client Matter
          parent; all existing entries under the same Matter ID + Client Name see
          the update.

        Validation:
        - All rows must pass validation or the entire upload is rejected (no
          partial commits).
        - Rows are rejected when:
            - Any field-level validation fails (see per-column rules below).
            - Two rows share the same upsert key (Matter ID, Client Name, Attorney
              Email). Each matter entry must appear in the CSV at most once.
            - Two rows share the same Client Matter (Matter ID, Client Name) but
              disagree on any Client Matter field (Matter Name or Description).
        - Pre-flight validation is available with POST /matters-csv/validate-upsert;
          it returns per-row, per-column error details without committing.

        Limits:
        - File size <= 10 MB.
        - Row count <= 10 000.
      operationId: upsertMattersCSV
      parameters:
      - description: The UUID of the review cycle to associate with the upserted matter
          entries.
        in: query
        name: reviewCycleId
        required: true
        schema:
          type: string
          format: uuid
          description: The UUID of the review cycle to associate with the upserted
            matter entries.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: |
                    Comma-separated value file. Must be less than 10MB in size.
                    Header row is required, and each subsequent row corresponds to one matter entry.
                    Header row must match: "Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description"
                    The columns must follow these requirements:
                    - Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.
                    - Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
                    - Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Billing Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.
                    - End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.
                    - Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.
                    - Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.
                    - Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
              required:
              - file
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MattersCSVUpsertResponse"
          description: Matter entries upserted successfully. Response body reports
            created/updated counts.
        "400":
          content:
            '*/*':
              schema:
                type: string
          description: |-
            The CSV was rejected. Causes:
            - Unsupported file format or empty file.
            - File exceeds 10 MB or contains more than 10 000 rows.
            - One or more rows failed field-level validation.
            - Two rows share the same upsert key (Matter ID, Client Name,
              Attorney Email).
            - Two rows share the same Client Matter (Matter ID, Client Name)
              but disagree on any Client Matter field (Matter Name, Description).
            Use POST /matters-csv/validate-upsert to identify the specific rows
            and columns with errors.
        "401":
          content:
            '*/*':
              schema:
                type: string
          description: Unauthorized
        "403":
          content:
            '*/*':
              schema:
                type: string
          description: Forbidden
        "404":
          content:
            '*/*':
              schema:
                type: string
          description: Review Cycle not found for the given reviewCycleId.
        "409":
          content:
            '*/*':
              schema:
                type: string
          description: Conflict
        "500":
          content:
            '*/*':
              schema:
                type: string
          description: Internal Server Error
      summary: Upserts matter entries from a CSV file
      tags:
      - matter-csv-controller
  /matters-csv/validate-csv:
    post:
      deprecated: true
      description: Use POST /matters-csv/validate-upsert instead. This endpoint validates
        against the /upload-csv rules and does not flag duplicate upsert keys within
        a file. Returns row and column level errors for matters in csv file. Only
        performs validation and does not upload the file. Email errors occur if no
        members exist with the specified email.
      operationId: validateMattersCSV
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: |
                    Comma-separated value file. Must be less than 10MB in size.
                    Header row is required, and each subsequent row corresponds to one matter entry.
                    Header row must match: "Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description"
                    The columns must follow these requirements:
                    - Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.
                    - Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
                    - Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Billing Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.
                    - End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.
                    - Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.
                    - Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.
                    - Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
              required:
              - file
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MattersCSVValidationResponse"
          description: Validation performed. Response includes rows numbers and column
            names that contain errors. Email errors occur if no members exist with
            the specified email.
        "400":
          content:
            '*/*':
              schema:
                type: string
          description: Bad Request
        "401":
          content:
            '*/*':
              schema:
                $ref: "#/components/schemas/MattersCSVValidationResponse"
          description: Unauthorized
        "403":
          content:
            '*/*':
              schema:
                type: string
          description: Forbidden
        "404":
          content:
            '*/*':
              schema:
                type: string
          description: Not Found
        "409":
          content:
            '*/*':
              schema:
                type: string
          description: Conflict
        "500":
          content:
            '*/*':
              schema:
                type: string
          description: Internal Server Error
      summary: Validates matters csv file
      tags:
      - matter-csv-controller
  /matters-csv/validate-upsert:
    post:
      description: |
        Validates the CSV against the same rules POST /matters-csv/upsert enforces.
        Returns per-row, per-column error details. Does not persist any data.
        Uniqueness of a matter entry within a review cycle is by the combined
        (Matter ID, Client Name, Attorney Email) key.
      operationId: validateUpsertCSV
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: |
                    Comma-separated value file. Must be less than 10MB in size.
                    Header row is required, and each subsequent row corresponds to one matter entry.
                    Header row must match: "Matter ID,Matter Name,Attorney Email,Billing Attorney Email,Start Date,End Date,Hours Billed,Client Name,Description"
                    The columns must follow these requirements:
                    - Matter ID: String, Required. Together with Client Name, identifies the Client Matter this entry rolls up to. Multiple rows may share the same (Matter ID, Client Name) - one row per attorney working the matter; when they do, all Client Matter fields (Matter Name, Description) must be identical across those rows.
                    - Matter Name: String, Required. Must be <= 255 characters. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
                    - Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Billing Attorney Email: String, Required. Email must match an existing Perform member's email.
                    - Start Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 6/30/2025. Blank persists as null on the entry.
                    - End Date: Date, Optional. When present, format must be M/d/yyyy, e.g. 10/5/2025. When both dates are present, End Date must be on or after Start Date. Blank persists as null on the entry.
                    - Hours Billed: Integer or Float, Required. Must be between 0 and 999999.99.
                    - Client Name: String, Required. Must be <= 255 characters. Together with Matter ID, identifies the Client Matter this entry rolls up to.
                    - Description: String, Optional. Must be <= 500 characters. This is the matter's description, not a description of the individual attorney's work on that matter. A Client Matter field: must be identical across all rows sharing the same (Matter ID, Client Name).
              required:
              - file
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MattersCSVValidationResponse"
          description: Validation performed. Response includes row numbers and column
            names that contain errors.
        "400":
          content:
            '*/*':
              schema:
                type: string
          description: Bad Request
        "401":
          content:
            '*/*':
              schema:
                type: string
          description: Unauthorized
        "403":
          content:
            '*/*':
              schema:
                type: string
          description: Forbidden
        "404":
          content:
            '*/*':
              schema:
                type: string
          description: Not Found
        "409":
          content:
            '*/*':
              schema:
                type: string
          description: Conflict
        "500":
          content:
            '*/*':
              schema:
                type: string
          description: Internal Server Error
      summary: Pre-flight-validates a matter entry CSV for the upsert endpoint
      tags:
      - matter-csv-controller
components:
  schemas:
    MatterCSVFileError:
      type: object
      properties:
        description:
          type: string
          description: "A human-readable explanation of the rejection, with enough\
            \ detail to correct the file."
          example: Row 2 has a different number of columns than the header row. Ensure
            any field that contains a comma or line break is wrapped in double quotes.
        rowNumber:
          type: integer
          format: int32
          description: The file row that could not be parsed (the header is row 1).
            Populated only for COLUMN_COUNT_MISMATCH.
          example: 2
        type:
          type: string
          description: The kind of file-level rejection.
          enum:
          - INVALID_FILE_SIZE
          - INVALID_ROW_COUNT
          - COLUMN_COUNT_MISMATCH
          - UNREADABLE_FILE
          example: COLUMN_COUNT_MISMATCH
      required:
      - description
      - type
    MatterCSVRowErrors:
      type: object
      properties:
        columnsWithErrors:
          type: array
          description: The column(s) that failed validation. Emails will fail validation
            if the addresses are not associated with existing perform member accounts.
          example:
          - Matter Name
          - Attorney Email
          items:
            type: string
          uniqueItems: true
        rowNumber:
          type: integer
          format: int32
          description: The row number for the error.
          example: 1
    MattersCSVUpsertResponse:
      type: object
      description: Counts from a matter entry CSV upsert.
      properties:
        created:
          type: integer
          format: int32
          description: Number of matter entries newly created by this upsert.
        updated:
          type: integer
          format: int32
          description: Number of existing matter entries whose stored fields were
            changed by this upsert. Rows whose non-key fields already matched the
            stored entry are not counted.
      required:
      - created
      - updated
    MattersCSVValidationResponse:
      type: object
      properties:
        fileError:
          $ref: "#/components/schemas/MatterCSVFileError"
          description: Present only when the whole file was rejected before per-row
            validation; null when the file parsed.
        numValidRows:
          type: integer
          format: int32
          description: The number of valid rows in the CSV file.
          example: 1
        rowErrors:
          type: array
          description: Details for each row containing errors. Empty if no rows have
            errors.
          example:
          - rowNumber: 1
            columnsWithErrors:
            - Matter Name
            - Attorney Email
          items:
            $ref: "#/components/schemas/MatterCSVRowErrors"
