openapi: 3.0.3
info:
  title: Penfold Partner API
  version: "1.0"
  description: |
    Unified API for payroll partners to onboard employers, manage employees,
    contributions, and file uploads.

    ## Authentication

    OAuth2 client credentials via AWS Cognito.

    1. POST to the Cognito token endpoint with `grant_type=client_credentials`
       and scope `penfold-partner-api/read`.
    2. Include the access token as a Bearer token in the Authorization header.

    The token's `client_id` is mapped to your organisation, which scopes all
    requests to your employers only.

    ## Identifier conventions

    - **Employer ID** (`id`): UUID assigned by Penfold when the employer is
      created. Used as the `{employerId}` path parameter.
    - **External reference** (`externalReference`): The Penfold employer
      reference (e.g. `PEN12345678`). This is `PEN` + the Companies House
      number. Returned alongside `id` in employer responses for cross-referencing.
    - **Employee ID**: Unique identifier assigned by Penfold when the employee
      is enrolled.

servers:
  - url: https://partner-api.getpenfold.com/v1
    description: Production
  - url: https://partner-api.getpenfold.dev/v1
    description: Staging

security:
  - bearerAuth: []

tags:
  - name: Health
  - name: Employers
  - name: Payments
  - name: Employees
  - name: Documents
  - name: Transfers
  - name: Payroll Contributions
  - name: Payroll Uploads

paths:
  # ──────────────────────────────────────────────
  # Health
  # ──────────────────────────────────────────────
  /healthz:
    get:
      summary: Health check
      operationId: getHealth
      security: []
      tags: [Health]
      responses:
        "200":
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  # ──────────────────────────────────────────────
  # Employers
  # ──────────────────────────────────────────────
  /employers:
    get:
      summary: List employers
      operationId: getEmployers
      tags: [Employers]
      description: |
        Returns all employers belonging to your organisation.
        Each employer includes both `id` (UUID) and `externalReference`
        (PEN + company number) for cross-referencing.
      responses:
        "200":
          description: List of employers
          content:
            application/json:
              schema:
                type: object
                required: [employers]
                properties:
                  employers:
                    type: array
                    items:
                      $ref: "#/components/schemas/Employer"
              example:
                employers:
                  - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: Acme Ltd
                    externalReference: PEN12345678
                    companyNumber: "12345678"
                    status: Active
                    defaultEmployeeContributionsPercent: 5
                    defaultEmployerContributionsPercent: 3
                    contributionBasis: QualifyingEarnings
                    allowsSalarySacrifice: false
                    paymentMethod: BankTransfer
                    createdAt: "2025-01-15T10:00:00Z"
                  - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                    name: Widgets Inc
                    externalReference: PEN87654321
                    companyNumber: "87654321"
                    status: AwaitingAgreement
                    defaultEmployeeContributionsPercent: 5
                    defaultEmployerContributionsPercent: 3
                    contributionBasis: QualifyingEarnings
                    allowsSalarySacrifice: false
                    paymentMethod: BankTransfer
                    createdAt: "2025-03-01T10:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      summary: Create employer
      operationId: createEmployer
      tags: [Employers]
      description: |
        Creates a new employer under your organisation.

        The company number is validated against Companies House. If an employer
        with the same company number already exists in your organisation, a 409
        is returned with the existing employer's `externalReference`.

        ## Onboarding lifecycle

        Employer creation triggers an onboarding process:

        1. **AwaitingAgreement** — Employer record created, company validated
           against Companies House. The employer must complete onboarding on
           Penfold's platform (sign agreement, AML/KYB verification).
        2. **Pending** — Onboarding complete, awaiting first payroll submission.
        3. **Active** — First payroll processed. Employer is fully operational.

        Poll `GET /employers` or `GET /employers/{employerId}` to track
        status progression. The employer cannot process payroll until
        status is `Active`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEmployerRequest"
            example:
              companyNumber: "12345678"
              companyName: Acme Ltd
              primaryContactEmail: payroll@acme.com
              primaryContactName: Jane Smith
              primaryContactRole: PayrollManager
              payrollFrequencies:
                - Monthly
              expectedFirstPayPeriodStartDate: "2025-03-01"
              expectedPayPeriodCadence: Monthly
              defaultEmployeeContributionsPercent: 5
              defaultEmployerContributionsPercent: 3
              contributionBasis: QualifyingEarnings
              allowsSalarySacrifice: false
              paymentMethod: BankTransfer
              numberOfEmployees: 50
              directorName: John Smith
              directorDateOfBirth: "1980-05-15"
      responses:
        "201":
          description: Employer created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedEmployer"
              example:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                name: Acme Ltd
                externalReference: PEN12345678
                companyNumber: "12345678"
                status: AwaitingAgreement
                defaultEmployeeContributionsPercent: 5
                defaultEmployerContributionsPercent: 3
                contributionBasis: QualifyingEarnings
                allowsSalarySacrifice: false
                paymentMethod: BankTransfer
                createdAt: "2025-03-01T10:00:00Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Company not found in Companies House
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: company not found in Companies House
        "409":
          description: Employer already exists for this company number
          content:
            application/json:
              schema:
                type: object
                required: [error, externalReference]
                properties:
                  error:
                    type: string
                  externalReference:
                    type: string
                    description: The existing employer's Penfold reference
              example:
                error: employer already exists
                externalReference: PEN12345678
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}:
    patch:
      summary: Update employer
      operationId: updateEmployer
      tags: [Employers]
      description: |
        Update an employer's configuration. All fields are optional — only
        provided fields are updated.
      parameters:
        - $ref: "#/components/parameters/employerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmployerPatchBody"
            examples:
              updateContributions:
                summary: Update contribution rates
                value:
                  defaultEmployeeContributionsPercent: 6
                  defaultEmployerContributionsPercent: 4
              updateContact:
                summary: Update primary contact
                value:
                  primaryContactEmail: new.contact@acme.com
                  primaryContactName: John Doe
                  primaryContactRole: Finance
      responses:
        "200":
          description: Employer updated successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Employer"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Payments
  # ──────────────────────────────────────────────
  /employers/{employerId}/payments:
    get:
      summary: List payments
      operationId: getEmployerPayments
      tags: [Payments]
      description: |
        Returns all bank transfer payments for the specified employer, including
        past (received) and upcoming (outstanding) payments. Use the `status`
        field to distinguish between them.
      parameters:
        - $ref: "#/components/parameters/employerId"
      responses:
        "200":
          description: List of payments
          content:
            application/json:
              schema:
                type: object
                required: [payments]
                properties:
                  payments:
                    type: array
                    items:
                      $ref: "#/components/schemas/Payment"
              example:
                payments:
                  - id: "p1a2b3c4-d5e6-7890-abcd-ef1234567890"
                    amountPence: 850000
                    paymentReference: "M25030112345678-ZG"
                    payPeriodStart: "2025-03-01"
                    payPeriodEnd: "2025-03-31"
                    status: Outstanding
                    createdAt: "2025-03-15T10:00:00Z"
                  - id: "p2b3c4d5-e6f7-8901-bcde-f12345678902"
                    amountPence: 920000
                    paymentReference: "M25020112345678-AB"
                    payPeriodStart: "2025-02-01"
                    payPeriodEnd: "2025-02-28"
                    status: Received
                    createdAt: "2025-02-15T10:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Employees
  # ──────────────────────────────────────────────
  /employers/{employerId}/employees:
    get:
      summary: List employees
      operationId: getEmployees
      tags: [Employees]
      description: Retrieve a paginated list of employees for a specified employer.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of employees to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: A paginated list of employees for the specified employer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEmployees"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      summary: Create employees
      operationId: createEmployees
      tags: [Employees]
      description: |
        Enrol new employees for a specified employer. Employees are processed
        asynchronously — the response returns an upload ID that can be used
        to track processing status via the Uploads endpoints.
      parameters:
        - $ref: "#/components/parameters/employerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/EmployeeCreateBody"
      responses:
        "201":
          description: Submission accepted for processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Submission"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Pension Transfers
  # ──────────────────────────────────────────────
  /employers/{employerId}/employees/{employeeId}/transfers:
    post:
      summary: Initiate pension transfer
      operationId: initiateTransfer
      tags: [Transfers]
      description: |
        Initiate a pension transfer into Penfold for an employee. This
        requests the transfer of an existing pension pot from another
        provider into the employee's Penfold workplace pension.

        The transfer will be submitted to the previous provider (via Origo
        where supported) and progress through standard transfer statuses.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InitiateTransferRequest"
            example:
              providerName: "Scottish Widows"
              policyNumber: "SW12345678"
              estimatedAmountPence: 2500000
      responses:
        "201":
          description: Transfer initiated successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PensionTransfer"
              example:
                id: "t1a2b3c4-d5e6-7890-abcd-ef1234567890"
                reference: "PEN76432-1"
                providerName: "Scottish Widows"
                policyNumber: "SW12345678"
                estimatedAmountPence: 2500000
                status: Requested
                createdAt: "2025-03-15T14:30:00Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    get:
      summary: List pension transfers
      operationId: getEmployeeTransfers
      tags: [Transfers]
      description: List all pension transfers for a specified employee.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
      responses:
        "200":
          description: List of pension transfers for the employee.
          content:
            application/json:
              schema:
                type: object
                required: [transfers]
                properties:
                  transfers:
                    type: array
                    items:
                      $ref: "#/components/schemas/PensionTransfer"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/employees/{employeeId}:
    patch:
      summary: Update employee
      operationId: updateEmployee
      tags: [Employees]
      description: |
        Update an employee's information. This can be used to mark them as a
        leaver. Note that once marked as a leaver the employee record will no
        longer be accessible under the employer.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmployeePatchBody"
            examples:
              markAsLeaver:
                value:
                  exitDate: "2023-12-31"
      responses:
        "204":
          description: Successfully updated the employee's information.
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/employees/{employeeId}/pension-summary:
    get:
      summary: Get employee pension summary
      operationId: getEmployeePensionSummary
      tags: [Employees]
      description: |
        Returns pension summary for a specific employee.

        The employer must belong to your organisation. Returns 404 only if the
        employee is genuinely not found. Opted-out employees return 200 with
        `"status": "optedOut"` and null data fields.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
      responses:
        "200":
          description: Employee pension summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PensionSummary"
              examples:
                active:
                  summary: Active employee
                  value:
                    provider: Penfold
                    status: active
                    balance:
                      currentValuePence: 150000
                      totalContributionsPence: 140000
                      totalGainsPence: 10000
                      gainPercentage: 7.14
                    latestContributions:
                      employeePence: 5000
                      employerPence: 3000
                      period: "2024-01"
                    deepLink: "https://penfold.go.link"
                optedOut:
                  summary: Opted-out employee
                  value:
                    provider: Penfold
                    status: optedOut
                    balance: null
                    latestContributions: null
                    deepLink: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Documents
  # ──────────────────────────────────────────────
  /employers/{employerId}/employees/{employeeId}/documents:
    get:
      summary: List employee documents
      operationId: getEmployeeDocuments
      tags: [Documents]
      description: |
        Returns all documents for a specific employee, including contract notes,
        valuation statements, and direct debit notices.

        Each document includes a `downloadUrl` that returns a pre-signed S3 URL
        valid for 5 minutes. To download the PDF, follow the URL returned by
        `downloadUrl`.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
        - in: query
          name: type
          required: false
          description: Filter documents by type.
          schema:
            type: string
            enum:
              - AnnualBenefitStatement
              - ContractNote
              - DirectDebitMandateConfirmation
              - DirectDebitMandateNotice
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 200
          description: The maximum number of documents to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: A paginated list of documents for the employee.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedDocuments"
              example:
                pageNumber: 1
                pageSize: 200
                totalItems: 3
                items:
                  - id: "d2b3c4d5-f6a7-8901"
                    type: AnnualBenefitStatement
                    title: "Annual Benefit Statement - 2025"
                    createdAt: "2025-06-15T10:00:00Z"
                    downloadUrl: "/v1/employers/emp123/employees/ee456/documents/d2b3c4d5-f6a7-8901/download"
                  - id: "d3c4d5e6-a7b8-9012"
                    type: ContractNote
                    title: "Contract Note - Buy PenfoldLifetime"
                    createdAt: "2025-06-20T14:30:00Z"
                    downloadUrl: "/v1/employers/emp123/employees/ee456/documents/d3c4d5e6-a7b8-9012/download"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/employees/{employeeId}/documents/{documentId}/download:
    get:
      summary: Download document
      operationId: downloadEmployeeDocument
      tags: [Documents]
      description: |
        Returns a pre-signed URL for downloading the document PDF.
        The URL is valid for 5 minutes.

        The response body is a plain text URL. Redirect the user or make a
        GET request to the returned URL to download the PDF.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
        - in: path
          name: documentId
          required: true
          description: Unique identifier for the document
          schema:
            type: string
      responses:
        "200":
          description: Pre-signed S3 URL for the document PDF (valid for 5 minutes).
          content:
            text/plain:
              schema:
                type: string
                format: uri
              example: "https://s3.eu-west-1.amazonaws.com/penfold-client-comms/..."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Contributions
  # ──────────────────────────────────────────────
  /employers/{employerId}/employees/{employeeId}/contributions:
    get:
      summary: List employee contributions
      operationId: getEmployeeContributions
      tags: [Payroll Contributions]
      description: Retrieve the contributions for a specified employee.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/employeeId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of contributions to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
        - in: query
          name: sortBy
          description: Sort contributions by date field.
          required: false
          schema:
            type: string
            enum: [payPeriodStartDate, payPeriodEndDate, createdAt]
            default: createdAt
        - in: query
          name: sortOrder
          description: Sort order (ascending or descending).
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        "200":
          description: A paginated list of contributions for the specified employee.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedContributions"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/contributions:
    get:
      summary: List employer contributions
      operationId: getEmployerContributions
      tags: [Payroll Contributions]
      description: Retrieve a paginated list of contributions for a specified employer.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of contributions to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
        - in: query
          name: sortBy
          description: Sort contributions by date field.
          required: false
          schema:
            type: string
            enum: [payPeriodStartDate, payPeriodEndDate, createdAt]
            default: createdAt
        - in: query
          name: sortOrder
          description: Sort order (ascending or descending).
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        "200":
          description: A paginated list of contributions for the specified employer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedContributions"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    post:
      summary: Create contributions
      operationId: createContributions
      tags: [Payroll Contributions]
      description: |
        Create multiple contributions for a specified employer in a single request.
        Contributions are processed asynchronously — the response returns an upload
        ID that can be used to track processing status via the Uploads endpoints.
      parameters:
        - $ref: "#/components/parameters/employerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/ContributionBody"
      responses:
        "201":
          description: Submission accepted for processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Submission"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  # ──────────────────────────────────────────────
  # Uploads
  # ──────────────────────────────────────────────
  /employers/{employerId}/uploads:
    post:
      summary: Initiate file upload
      operationId: initiateUpload
      tags: [Payroll Uploads]
      description: |
        Initiate an upload of a Contribution or Enrolment file. Returns a
        pre-signed URL where the file should be sent via PUT request. We
        recommend using the AWS S3 client SDK to perform the upload.
      parameters:
        - $ref: "#/components/parameters/employerId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InitiateUploadBody"
      responses:
        "200":
          description: Successfully initiated the upload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Upload"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

    get:
      summary: List uploads
      operationId: getUploads
      tags: [Payroll Uploads]
      description: Get uploads for the specified employer.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: Retrieved the uploads successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedUploads"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/uploads/{uploadId}:
    get:
      summary: Get upload status
      operationId: getUpload
      tags: [Payroll Uploads]
      description: Get the details of a file upload, including the realtime status of its processing.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/uploadId"
      responses:
        "200":
          description: Retrieved the upload successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Upload"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/uploads/{uploadId}/errors:
    get:
      summary: Get upload errors
      operationId: getUploadErrors
      tags: [Payroll Uploads]
      description: |
        Get the errors of an upload. Will be an empty array unless upload
        status is "Error" or "PartiallyProcessed".
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/uploadId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of records to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: Retrieved the upload errors successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedUploadErrors"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/uploads/{uploadId}/contributions:
    get:
      summary: Get upload contributions
      operationId: getUploadContributions
      tags: [Payroll Uploads]
      description: Get the contributions created for an upload.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/uploadId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of records to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: Retrieved the upload contributions successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedContributions"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /employers/{employerId}/uploads/{uploadId}/enrolments:
    get:
      summary: Get upload enrolments
      operationId: getUploadEnrolments
      tags: [Payroll Uploads]
      description: Get the enrolments created for an upload, in the form of employee records.
      parameters:
        - $ref: "#/components/parameters/employerId"
        - $ref: "#/components/parameters/uploadId"
        - in: query
          name: pageSize
          schema:
            type: integer
            minimum: 100
            maximum: 500
            default: 200
          description: The maximum number of records to return per page.
        - in: query
          name: pageNumber
          schema:
            type: integer
            minimum: 1
            default: 1
          description: The page number to return.
      responses:
        "200":
          description: Retrieved the upload enrolments successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEmployees"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalServerError"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Cognito OAuth2 access token with scope `penfold-partner-api/read`

  parameters:
    employerId:
      in: path
      name: employerId
      required: true
      description: Penfold employer UUID
      schema:
        type: string
        format: uuid
    employeeId:
      in: path
      name: employeeId
      required: true
      description: Unique identifier for the employee
      schema:
        type: string
    uploadId:
      in: path
      name: uploadId
      required: true
      description: Unique identifier for the upload
      schema:
        type: string

  schemas:
    # ──────────────────────────────────────────────
    # Partner API schemas
    # ──────────────────────────────────────────────

    PensionSummary:
      type: object
      required: [provider, status, balance, latestContributions, deepLink]
      properties:
        provider:
          type: string
          enum: [Penfold]
          description: Pension provider name
        status:
          type: string
          enum: [active, optedOut]
          description: |
            - `active`: employee is enrolled, balance and contributions are populated
            - `optedOut`: employee opted out of the scheme, all data fields are null
        balance:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Balance"
        latestContributions:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/LatestContributions"
          description: Most recent contribution breakdown. Null if opted out or no contributions yet.
        deepLink:
          nullable: true
          type: string
          format: uri
          description: URL for "View & manage your pension" CTA. Null if opted out.
          example: "https://penfold.go.link"

    Balance:
      type: object
      required:
        - currentValuePence
        - totalContributionsPence
        - totalGainsPence
        - gainPercentage
      properties:
        currentValuePence:
          type: integer
          description: Current total pot value in pence
          example: 150000
        totalContributionsPence:
          type: integer
          description: Total contributions paid in, in pence
          example: 140000
        totalGainsPence:
          type: integer
          description: Investment gains (or losses if negative) in pence
          example: 10000
        gainPercentage:
          type: number
          format: double
          description: Gain as percentage of total contributions, rounded to 2dp
          example: 7.14

    LatestContributions:
      type: object
      required:
        - employeePence
        - employerPence
        - period
      properties:
        employeePence:
          type: integer
          description: Employee contribution in pence
          example: 5000
        employerPence:
          type: integer
          description: Employer contribution in pence
          example: 3000
        period:
          type: string
          pattern: "^\\d{4}-\\d{2}$"
          description: Pay period (YYYY-MM)
          example: "2024-01"

    Employer:
      type: object
      required:
        - id
        - name
        - externalReference
        - companyNumber
        - status
        - defaultEmployeeContributionsPercent
        - defaultEmployerContributionsPercent
        - contributionBasis
        - allowsSalarySacrifice
        - paymentMethod
        - createdAt
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the employer
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        name:
          type: string
          description: Employer name
          example: Acme Ltd
        externalReference:
          type: string
          description: Penfold employer reference (`PEN` + Companies House number)
          example: PEN12345678
        companyNumber:
          type: string
          description: Companies House number
          example: "12345678"
        status:
          type: string
          enum: [AwaitingAgreement, Pending, Active, Closed]
          description: |
            Current status of the employer:
            - `AwaitingAgreement`: Employer must complete onboarding on Penfold's platform (agreement, AML/KYB)
            - `Pending`: Onboarding complete, awaiting first payroll submission
            - `Active`: Fully operational, can process payroll
            - `Closed`: Employer account closed
          example: Active
        defaultEmployeeContributionsPercent:
          type: number
          description: Default employee contribution percentage
          example: 5
        defaultEmployerContributionsPercent:
          type: number
          description: Default employer contribution percentage
          example: 3
        contributionBasis:
          type: string
          enum: [QualifyingEarnings, TotalPay, BasicPay]
          description: Basis on which pension contributions are calculated
          example: QualifyingEarnings
        allowsSalarySacrifice:
          type: boolean
          description: Whether the employer allows salary sacrifice
          example: false
        paymentMethod:
          type: string
          enum: [BankTransfer]
          description: Payment method for pension contributions
          example: BankTransfer
        createdAt:
          type: string
          format: date-time
          description: When the employer was created (ISO 8601)
          example: "2025-03-01T10:00:00Z"

    CreateEmployerRequest:
      type: object
      required:
        - companyNumber
        - companyName
        - primaryContactEmail
        - primaryContactName
        - payrollFrequencies
        - expectedFirstPayPeriodStartDate
        - expectedPayPeriodCadence
        - defaultEmployeeContributionsPercent
        - defaultEmployerContributionsPercent
        - contributionBasis
        - paymentMethod
        - numberOfEmployees
        - directorName
        - directorDateOfBirth
      properties:
        companyNumber:
          type: string
          description: Companies House registration number
          example: "12345678"
        companyName:
          type: string
          description: Registered company name
          example: Acme Ltd
        primaryContactEmail:
          type: string
          format: email
          description: Email address of the primary contact at the employer
          example: payroll@acme.com
        primaryContactName:
          type: string
          description: Full name of the primary contact at the employer
          example: Jane Smith
        primaryContactRole:
          type: string
          enum: [CompanyDirector, Finance, HR, PayrollManager]
          description: Role of the primary contact at the employer
          example: PayrollManager
        directorName:
          type: string
          description: Full name of a company director. Required for AML verification.
          example: John Smith
        directorDateOfBirth:
          type: string
          format: date
          description: Date of birth of the company director (YYYY-MM-DD). Required for AML verification.
          example: "1980-05-15"
        payrollFrequencies:
          type: array
          items:
            type: string
            enum: [Weekly, Fortnightly, FourWeekly, Monthly]
          description: How often the employer runs payroll
          example: [Monthly]
        expectedFirstPayPeriodStartDate:
          type: string
          format: date
          description: Expected start date of the first pay period (YYYY-MM-DD)
          example: "2025-03-01"
        expectedPayPeriodCadence:
          type: string
          enum: [Weekly, Fortnightly, FourWeekly, Monthly]
          description: Expected cadence for pay periods
          example: Monthly
        defaultEmployeeContributionsPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Default employee contribution percentage
          example: 5
        defaultEmployerContributionsPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Default employer contribution percentage
          example: 3
        contributionBasis:
          type: string
          enum: [QualifyingEarnings, TotalPay, BasicPay]
          description: Basis on which pension contributions are calculated
          example: QualifyingEarnings
        allowsSalarySacrifice:
          type: boolean
          description: Whether the employer allows salary sacrifice arrangements
          default: false
          example: false
        paymentMethod:
          type: string
          enum: [BankTransfer]
          description: How the employer will pay pension contributions
          example: BankTransfer
        numberOfEmployees:
          type: integer
          minimum: 1
          description: Approximate number of employees to be enrolled
          example: 50

    EmployerPatchBody:
      type: object
      description: All fields are optional. Only provided fields are updated.
      properties:
        primaryContactEmail:
          type: string
          format: email
          description: Email address of the primary contact at the employer
          example: payroll@acme.com
        primaryContactName:
          type: string
          description: Full name of the primary contact at the employer
          example: Jane Smith
        primaryContactRole:
          type: string
          enum: [CompanyDirector, Finance, HR, PayrollManager]
          description: Role of the primary contact at the employer
          example: PayrollManager
        defaultEmployeeContributionsPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Default employee contribution percentage
          example: 5
        defaultEmployerContributionsPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Default employer contribution percentage
          example: 3
        contributionBasis:
          type: string
          enum: [QualifyingEarnings, TotalPay, BasicPay]
          description: Basis on which pension contributions are calculated
          example: QualifyingEarnings
        allowsSalarySacrifice:
          type: boolean
          description: Whether the employer allows salary sacrifice arrangements
          example: false
        payrollFrequencies:
          type: array
          items:
            type: string
            enum: [Weekly, Fortnightly, FourWeekly, Monthly]
          description: How often the employer runs payroll
          example: [Monthly]

    CreatedEmployer:
      description: Alias for Employer — returned from POST /employers.
      allOf:
        - $ref: "#/components/schemas/Employer"

    Payment:
      type: object
      required:
        - id
        - amountPence
        - paymentReference
        - payPeriodStart
        - payPeriodEnd
        - status
        - createdAt
      properties:
        id:
          type: string
          description: Unique identifier for the payment
          example: "p1a2b3c4-d5e6-7890-abcd-ef1234567890"
        amountPence:
          type: integer
          description: Total payment amount in pence
          example: 850000
        paymentReference:
          type: string
          description: >-
            Bank transfer reference to use when making the payment.
            Format: {frequencyPrefix}{yyMMdd}{employerNumber}-{suffix}
            where frequencyPrefix is M(onthly)/W(eekly)/4(weekly)/2(weekly),
            date is the pay period start, and employer number is the external
            reference without the PEN prefix. Max 18 characters.
          example: "M25030112345678-ZG"
        payPeriodStart:
          type: string
          format: date
          description: Start date of the pay period this payment covers (ISO 8601 date)
          example: "2025-03-01"
        payPeriodEnd:
          type: string
          format: date
          description: End date of the pay period this payment covers (ISO 8601 date)
          example: "2025-03-31"
        status:
          type: string
          description: Whether Penfold has received this payment.
          enum:
            - Outstanding
            - Received
          example: "Outstanding"
        createdAt:
          type: string
          format: date-time
          description: When the payment record was created (ISO 8601)
          example: "2025-03-15T10:00:00Z"

    Document:
      type: object
      required:
        - id
        - type
        - title
        - createdAt
        - downloadUrl
      properties:
        id:
          type: string
          description: Unique identifier for the document.
          example: "d1a2b3c4-e5f6-7890"
        type:
          type: string
          enum:
            - AnnualBenefitStatement
            - ContractNote
            - DirectDebitMandateConfirmation
            - DirectDebitMandateNotice
          description: |
            The type of document:
            - `AnnualBenefitStatement`: Annual benefit statement (SMPI)
            - `ContractNote`: Generated when funds are bought, sold, or switched
            - `DirectDebitMandateConfirmation`: Direct debit setup confirmation
            - `DirectDebitMandateNotice`: Direct debit advance notice
          example: AnnualBenefitStatement
        title:
          type: string
          description: Human-readable document title.
          example: "Contract Note - Buy PenfoldLifetime"
        createdAt:
          type: string
          format: date-time
          description: When the document was created (ISO 8601).
          example: "2025-06-15T10:00:00Z"
        downloadUrl:
          type: string
          description: |
            Relative URL to download the document PDF. Returns a pre-signed
            S3 URL valid for 5 minutes.
          example: "/v1/employers/emp123/employees/ee456/documents/d1a2b3c4-e5f6-7890/download"

    PaginatedDocuments:
      type: object
      required:
        - pageNumber
        - pageSize
        - totalItems
        - items
      properties:
        pageNumber:
          type: integer
          description: The current page number.
          example: 1
        pageSize:
          type: integer
          description: The number of items per page.
          example: 200
        totalItems:
          type: integer
          description: The total number of items available.
          example: 3
        items:
          type: array
          items:
            $ref: "#/components/schemas/Document"
          description: An array of Document objects on the current page.

    InitiateTransferRequest:
      type: object
      required:
        - providerName
        - policyNumber
        - estimatedAmountPence
      properties:
        providerName:
          type: string
          description: Name of the pension provider the pot is being transferred from.
          example: "Scottish Widows"
        policyNumber:
          type: string
          description: Policy or plan number at the previous provider.
          example: "SW12345678"
        estimatedAmountPence:
          type: integer
          description: Estimated value of the pension pot in pence.
          example: 2500000

    PensionTransfer:
      type: object
      required:
        - id
        - reference
        - providerName
        - policyNumber
        - estimatedAmountPence
        - status
        - createdAt
      properties:
        id:
          type: string
          description: Unique identifier for the transfer.
          example: "t1a2b3c4-d5e6-7890-abcd-ef1234567890"
        reference:
          type: string
          description: Penfold transfer reference (e.g. PEN76432-1).
          example: "PEN76432-1"
        providerName:
          type: string
          description: Name of the previous pension provider.
          example: "Scottish Widows"
        policyNumber:
          type: string
          description: Policy or plan number at the previous provider.
          example: "SW12345678"
        estimatedAmountPence:
          type: integer
          description: Estimated value of the pension pot in pence.
          example: 2500000
        finalAmountPence:
          type: integer
          nullable: true
          description: Actual amount received in pence. Null until funds arrive.
          example: null
        status:
          type: string
          description: |
            Simplified status of the transfer. Internally Penfold tracks ~25
            granular statuses; these are collapsed here assuming most transfers
            follow the happy path. Rejected transfers will typically need to
            be resolved on the Penfold platform directly.

            - `Requested`: Transfer has been submitted
            - `InProgress`: Transfer is being processed with the previous provider
            - `FundsReceived`: Funds have arrived at Penfold
            - `Complete`: Funds invested in the employee's pension pot
            - `Rejected`: Previous provider rejected the transfer — resolve via Penfold platform
            - `Cancelled`: Transfer was cancelled
          enum:
            - Requested
            - InProgress
            - FundsReceived
            - Complete
            - Rejected
            - Cancelled
          example: Requested
        rejectionReason:
          type: string
          nullable: true
          description: Reason the transfer was rejected (if applicable).
          example: null
        createdAt:
          type: string
          format: date-time
          description: When the transfer was initiated (ISO 8601).
          example: "2025-03-15T14:30:00Z"

    # ──────────────────────────────────────────────
    # Schemas ported from Payroll API v3 (converted to camelCase)
    # ──────────────────────────────────────────────

    Employee:
      type: object
      required:
        - id
        - email
        - forename
        - surname
        - title
        - dateOfBirth
        - nationalInsuranceNumber
        - employmentStartDate
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: Unique identifier for the employee.
          example: "e1234-abcd-5678-efgh"
          readOnly: true
        email:
          type: string
          description: Email address of the employee.
          example: "john.doe@example.com"
          format: email
        forename:
          type: string
          description: The employee's first name.
          example: "John"
        surname:
          type: string
          description: The employee's last name.
          example: "Doe"
        title:
          type: string
          description: The employee's title, e.g., Mr., Mrs., Dr., etc.
          example: "Mr."
        dateOfBirth:
          type: string
          description: The employee's date of birth, in YYYY-MM-DD format.
          example: "1990-01-01"
          format: date
        addressLine1:
          type: string
          description: The first line of the employee's address.
          example: "123 Main Street"
        postcode:
          type: string
          description: The postal code of the employee's address.
          example: "AB12 3CD"
        nationalInsuranceNumber:
          type: string
          description: The employee's National Insurance number.
          example: "AA123456C"
        employmentStartDate:
          type: string
          description: The date the employee started their employment, in YYYY-MM-DD format.
          example: "2023-01-01"
          format: date
        status:
          type: string
          description: |
            The employee's current status on the Penfold platform:
            - `Enrolled`: Employee has been enrolled but has not yet activated their Penfold account
            - `Active`: Employee has activated their Penfold account
            - `OptedOut`: Employee opted out of the pension scheme
            - `Left`: Employee has left employment
          enum:
            - Enrolled
            - Active
            - OptedOut
            - Left
          example: Active
        exitDate:
          type: string
          nullable: true
          description: The date the employee's employment ended, in YYYY-MM-DD format (if applicable).
          example: null
          format: date
        optOutDate:
          type: string
          nullable: true
          description: The date the employee opted out of the pension scheme, in YYYY-MM-DD format (if applicable).
          example: null
          format: date
        optInDate:
          type: string
          nullable: true
          description: The date the employee opted into the pension scheme, in YYYY-MM-DD format (if applicable).
          example: null
          format: date
        optOutWindowStartDate:
          type: string
          nullable: true
          description: The date the employee's opt out window begins.
          example: "2023-01-01"
          format: date
        optOutWindowEndDate:
          type: string
          nullable: true
          description: The date the employee's opt out window ends.
          example: "2023-01-31"
          format: date
        createdAt:
          type: string
          description: The date and time the employee record was created, in ISO 8601 format.
          example: "2023-03-01T12:00:00Z"
          format: date-time
          readOnly: true
        updatedAt:
          type: string
          description: The date and time the employee record was last updated, in ISO 8601 format.
          example: "2023-03-15T12:00:00Z"
          format: date-time
          readOnly: true

    EmployeeCreateBody:
      type: object
      required:
        - email
        - dateOfBirth
        - forename
        - surname
        - title
        - addressLine1
        - postcode
        - nationalInsuranceNumber
        - employmentStartDate
      properties:
        email:
          type: string
          description: Email address of the employee.
          example: "john.doe@example.com"
          format: email
        forename:
          type: string
          description: The employee's first name.
          example: "John"
        surname:
          type: string
          description: The employee's last name.
          example: "Doe"
        title:
          type: string
          description: The employee's title, e.g. Mr., Mrs., Dr., etc.
          example: "Mr."
        dateOfBirth:
          type: string
          description: The employee's date of birth, in YYYY-MM-DD format.
          example: "1990-01-01"
          format: date
        addressLine1:
          type: string
          description: The first line of the employee's address.
          example: "123 Main Street"
        postcode:
          type: string
          description: The postal code of the employee's address.
          example: "AB12 3CD"
        nationalInsuranceNumber:
          type: string
          description: The employee's National Insurance number.
          example: "AA123456C"
        employmentStartDate:
          type: string
          description: The date the employee started their employment, in YYYY-MM-DD format.
          example: "2023-01-01"
          format: date

    EmployeePatchBody:
      type: object
      properties:
        exitDate:
          type: string
          description: Date the employee's employment ended, in YYYY-MM-DD format.
          example: "2023-12-31"
          format: date

    PaginatedEmployees:
      type: object
      required:
        - pageNumber
        - pageSize
        - totalItems
        - items
      properties:
        pageNumber:
          type: integer
          description: The current page number.
          example: 1
        pageSize:
          type: integer
          description: The number of items per page.
          example: 200
        totalItems:
          type: integer
          description: The total number of items available.
          example: 1
        items:
          type: array
          items:
            $ref: "#/components/schemas/Employee"
          description: An array of Employee objects on the current page.

    Submission:
      type: object
      required:
        - id
        - status
        - createdAt
      properties:
        id:
          type: string
          description: Upload ID for tracking the submission. Use the Uploads endpoints to check processing status.
          example: "u1234-abcd-5678-efgh"
        status:
          type: string
          description: Initial status of the submission.
          example: ReceivedFile
          enum:
            - ReceivedFile
            - Processing
        createdAt:
          type: string
          format: date-time
          description: When the submission was created (ISO 8601).
          example: "2023-03-01T12:00:00Z"

    Contribution:
      type: object
      required:
        - id
        - employeeId
        - employerContributionsAmount
        - employeeContributionsAmount
        - createdAt
        - payPeriodStartDate
        - payPeriodEndDate
        - status
      properties:
        id:
          type: string
          description: Unique identifier for the contribution record.
          example: "c1234-abcd-5678-efgh"
        uploadId:
          type: string
          description: ID of the file upload from which the contribution was created (if applicable).
        employeeId:
          type: string
          description: Identifier for the employee associated with the contribution.
          example: "e9876-wxyz-4321-stuv"
        employerContributionsAmount:
          type: number
          description: The amount of the employer's contribution for the given pay period.
          example: 1000.00
        employeeContributionsAmount:
          type: number
          description: The amount of the employee's contribution for the given pay period.
          example: 250.00
        createdAt:
          type: string
          format: date-time
          description: The date and time the contribution record was created, in ISO 8601 format.
          example: "2023-03-22T12:00:00Z"
          readOnly: true
        payPeriodStartDate:
          type: string
          format: date
          description: The start date of the pay period for which the contributions were made, in YYYY-MM-DD format.
          example: "2023-03-01"
        payPeriodEndDate:
          type: string
          format: date
          description: The end date of the pay period for which the contributions were made, in YYYY-MM-DD format.
          example: "2023-03-15"
        status:
          type: string
          description: >
            The status of the contribution. Possible values are:
            - `Cancelled`: The contribution has been cancelled.
            - `NotSubmitted`: The contribution has not been submitted.
            - `Deleted`: The contribution has been deleted.
            - `Pending`: The contribution is currently being processed.
            - `Completed`: The contribution has been successfully processed.
          enum:
            - Cancelled
            - NotSubmitted
            - Deleted
            - Pending
            - Completed
          example: "Pending"

    ContributionBody:
      type: object
      required:
        - employeeId
        - employerContributionsAmount
        - employeeContributionsAmount
        - payPeriodStartDate
        - payPeriodEndDate
      properties:
        employeeId:
          type: string
          description: Identifier for the employee associated with the contribution.
          example: "e9876-wxyz-4321-stuv"
        employerContributionsAmount:
          type: number
          description: The amount of the employer's contribution for the given pay period.
          example: 1200.00
        employeeContributionsAmount:
          type: number
          description: The amount of the employee's contribution for the given pay period.
          example: 300.00
        payPeriodStartDate:
          type: string
          format: date
          description: The start date of the pay period, in YYYY-MM-DD format.
          example: "2023-03-01"
        payPeriodEndDate:
          type: string
          format: date
          description: The end date of the pay period, in YYYY-MM-DD format.
          example: "2023-03-15"

    PaginatedContributions:
      type: object
      required:
        - pageNumber
        - pageSize
        - totalItems
        - items
      properties:
        pageNumber:
          type: integer
          description: The current page number.
          example: 1
        pageSize:
          type: integer
          description: The number of items per page.
          example: 200
        totalItems:
          type: integer
          description: The total number of items available.
          example: 1
        items:
          type: array
          items:
            $ref: "#/components/schemas/Contribution"
          description: An array of Contribution objects on the current page.

    Upload:
      type: object
      required:
        - id
        - putDestinationUrl
        - status
        - updatedAt
        - createdAt
        - processingStarted
        - processingEnded
        - processingTime
        - totalErrors
        - contributionsCreated
        - contributionsAlreadyExisted
        - employerContributions
        - employeeContributions
        - totalContributions
        - filename
      properties:
        id:
          type: string
          description: ID of the upload.
        employerId:
          type: string
          description: ID of the employer the upload was made for.
          nullable: true
        putDestinationUrl:
          type: string
          description: >
            Destination URL where the actual file should be sent, using a PUT
            request. Only present when status is AwaitingFile. We recommend
            using the AWS S3 client SDK to perform the upload.
          nullable: true
        createdAt:
          type: string
          format: date-time
          description: Datetime the upload was created.
        updatedAt:
          type: string
          format: date-time
          description: Datetime the upload was last updated.
        processingStarted:
          type: string
          nullable: true
          format: date-time
          description: Datetime of when processing of the file began.
          example: "2023-03-01T11:00:00Z"
        processingEnded:
          type: string
          format: date-time
          nullable: true
          description: Datetime of when processing of the file ended, or null if it has not ended.
          example: "2023-03-01T12:00:00Z"
        processingTime:
          type: number
          description: The number of seconds it took to process the file end-to-end (if applicable).
          example: 47
          nullable: true
        totalErrors:
          type: number
          description: >
            The number of errors produced in processing the file. Zero if there
            are none or processing is not finished. Use the
            `{uploadId}/errors` endpoint to retrieve detailed information.
          example: 10
        contributionsCreated:
          type: number
          description: The number of contributions created during processing. Zero if processing is not finished.
          example: 5
        contributionsUnprocessed:
          type: number
          description: The number of contributions that failed to be created during processing. Zero if processing is not finished.
          example: 0
        contributionsAlreadyExisted:
          type: number
          description: The number of contributions that already existed prior to processing and were skipped. Zero if processing is not finished.
          example: 0
        employerContributions:
          type: number
          description: The total of employer contributions created for the upload, excluding those that already existed. Zero if processing is not finished.
          example: 500.34
        employeeContributions:
          type: number
          description: The total of employee contributions created for the upload, excluding those that already existed. Zero if processing is not finished.
          example: 734.11
        totalContributions:
          type: number
          description: The total of all contributions created for the upload, excluding those that already existed. Zero if processing is not finished.
          example: 1234.45
        filename:
          type: string
          description: The filename of the uploaded file (if applicable).
          example: "papdis.csv"
          nullable: true
        status:
          type: string
          description: >
            The status of the upload. Error indicates the file was not able to
            be processed. PartiallyProcessed indicates that some but not all
            contributions or enrolments were created.
          example: Processed
          enum:
            - AwaitingFile
            - ReceivedFile
            - Processing
            - Processed
            - Error
            - Timeout
            - PartiallyProcessed

    InitiateUploadBody:
      type: object
      required:
        - filename
        - purpose
      properties:
        filename:
          type: string
          description: The filename of the upload.
          example: "papdis.csv"
        purpose:
          type: string
          description: >
            The purpose of the file upload. If "contribution", both
            contributions and enrolments will be processed. If "enrolment",
            only enrolments will be processed.
          example: "contribution"
          enum:
            - contribution
            - enrolment

    PaginatedUploads:
      type: object
      properties:
        pageNumber:
          type: integer
          description: The current page number.
          example: 1
        pageSize:
          type: integer
          description: The number of items per page.
          example: 200
        totalItems:
          type: integer
          description: The total number of items available.
          example: 1
        items:
          type: array
          items:
            $ref: "#/components/schemas/Upload"
          description: An array of Upload objects on the current page.

    UploadError:
      type: object
      properties:
        uploadId:
          type: string
          description: ID of the upload where the error occurred.
        scope:
          type: string
          enum: [File, AllRows, Row]
          description: >
            The scope of the error. Row indicates an error in a specific row.
            AllRows indicates an error that applies to all rows. File indicates
            an error with the file itself.
        rowIndex:
          type: number
          description: The row in the file where the error occurred. Starting at row 1, the first non-header row. Set when scope=Row.
          nullable: true
        code:
          type: string
          description: An identifier for the class of the error.
          example: NationalInsuranceNumberInvalid
        message:
          type: string
          description: Human readable error message to help diagnose the issue.
          example: "The provided National Insurance Number is invalid."

    PaginatedUploadErrors:
      type: object
      properties:
        pageNumber:
          type: integer
          description: The current page number.
          example: 1
        pageSize:
          type: integer
          description: The number of items per page.
          example: 200
        totalItems:
          type: integer
          description: The total number of items available.
          example: 1
        items:
          type: array
          items:
            $ref: "#/components/schemas/UploadError"
          description: An array of UploadError objects on the current page.

    # ──────────────────────────────────────────────
    # Shared error schemas
    # ──────────────────────────────────────────────

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string

    ValidationErrorDetail:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: A descriptive error message.
        validationErrors:
          type: array
          items:
            type: object
            required: [field, message]
            properties:
              field:
                type: string
                description: The name of the field that failed validation.
              message:
                type: string
                description: A descriptive error message.

  responses:
    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: unauthorized

    ValidationError:
      description: Request body failed validation
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrorDetail"
          example:
            error: validation failed
            validationErrors:
              - field: companyNumber
                message: companyNumber is required
              - field: primaryContactEmail
                message: must be a valid email address

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: not found

    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: internal server error
