> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ctrl-hub.com/llms.txt
> Use this file to discover all available pages before exploring further.

# List cost roll-ups

> Read what a node of the job hierarchy costs, and what everything beneath it
costs.

A roll-up is derived and read-only — there is no create, update or delete. It
is keyed on the cost centre rather than on a cost sheet, so a contract nobody
has costed directly still returns a real total from the work orders beneath
it, without an empty sheet having to exist for it.

Filter by `organisation`, `cost_centre` and `cost_centre_type` together for the
node you want rolled up. All three are required as a set; supplying some but
not all of them is rejected rather than answered.

By default this returns exactly one element: the roll-up for the node named
by the filter. Add `eq(depth,all)` to the filter to get one element per node
in the subtree beneath and including that node instead, so a client can see
which scheme, work order or operation is holding the money rather than only
how much is below in total. Any other `depth` value is rejected rather than
answered.

Every element, in either mode, also carries `parent_cost_centre_id` and
`parent_cost_centre_type` (both null on the subtree's root), `name`, `code`
and `has_cost_sheet` — enough on its own to rebuild the shape of the subtree
and label each node, since a `depth=all` response is otherwise just a bag of
totals with no relationships between them.

Each roll-up returns the node's own `direct` total, the `rolled` total that
includes every descendant, and `by_status` — `rolled` split by the status of
the sheet each line sits on, so a client can sum only the statuses it cares
about without asking again. Soft-deleted nodes are left out of the walk.

Totals are sums of the snapshots stored on the cost lines themselves, so
revising a rate or a margin in the cost register never moves a roll-up.

This read is gated on permission to view cost lines rather than cost sheets: a
roll-up returns nothing but money.




## OpenAPI

````yaml /api-reference/openapi.yaml get /v3/cost-rollups
openapi: 3.1.0
info:
  contact:
    email: support@ctrl-hub.com
    name: Ctrl Hub
    url: https://www.ctrl-hub.com
  description: >
    Ctrl Hub is the all-in-one platform for high-risk industries like utilities,
    construction, infrastructure, and renewables. We help teams manage
    everything from risk assessments and HAVS exposure to vehicle and equipment
    checks, with a guaranteed minimum of 200% ROI.
  license:
    name: MIT License
    url: https://opensource.org/licenses/MIT
  summary: An API for managing your compliance and risk posture
  termsOfService: https://www.ctrl-hub.com/terms-conditions
  title: Ctrl Hub
  version: 1.0.0
servers:
  - description: Production
    url: https://api.ctrl-hub.com
  - description: Staging
    url: https://api.ctrl-hub.dev
  - description: Development
    url: https://api.ctrl-hub.run
security: []
tags:
  - description: |
      Actions are follow-ups assigned to users and teams, produced manually or
      by domain producers such as the data-capture workflow runner.
    name: Actions
  - description: |
      Audit events are the events that are logged by the system.
    name: Audit Events
  - description: |
      View the platform's health and availability.
    name: Status
  - description: >
      User-owned dashboards composed of cards on a fixed-slot bento layout.
      Cards come from a per-domain registry; the API stores their config as
      opaque JSON.
    name: Dashboards
  - description: >
      A record of documents the platform generated and handed over, retained as
      evidence of what

      was issued. `subject_type` says what kind of records an export covers, and
      it is what decides

      how the request is authorised: there is no export permission of its own,
      so if you can see the

      records you can export them.
    name: Exports
  - description: >
      Scheduled delivery of a saved query's results, as a CSV attachment or a
      summary email. Every run is retained with the file it sent, so what a
      recipient received stays retrievable. A report runs as the person who
      created it, under their own access.
    name: Reports
  - description: |
      Manage appointments for work to be carried out with your customers
    name: Customer Appointments
  - description: |
      Manage interactions you have with your customers
    name: Customer Interactions
  - description: |
      Manage accounts for your customers
    name: Customer Accounts and Contacts
  - description: |
      Qualifications are the skills and knowledge that an organisation requires.
    name: Qualifications
  - description: |
      Workflows allow you to automate your processes.
    name: Workflows
  - description: |
      Manage documents
    name: Documents
  - description: |
      Manage documents
    name: Folders
  - description: |
      Manage documents
    name: Document Reviews
  - description: |
      Manage feature configurations for an organisation.
    name: Feature Configurations
  - description: |
      Equipment are the physical assets that an organisation manages.
    name: Equipment
  - description: |
      The central cost register: time-versioned rate codes for material, labour,
      contractor and miscellaneous spend, used to cost work against the job
      hierarchy.
    name: Costs
  - description: >
      Locations are places an organisation manages, optionally classified by a
      location type.
    name: Locations
  - description: |
      Manage your forms and their schemas
    name: Forms, Schemas and Categories
  - description: |
      Create and view form submissions
    name: Submissions
  - description: |
      View the roles available in the system.
    name: IAM Roles
  - description: >
      IAM role groups can be assigned to principals to manage authorisation
      centrally.
    name: IAM Role Groups
  - description: |
      Manage service accounts which can access the API programmatically.
    name: Service Accounts
  - description: |
      Manage bridges between organisations.
    name: Bridges
  - description: |
      Manage settings for an organisation.
    name: Settings
  - description: |
      Manage teams within an organisation.
    name: Teams
  - description: |
      Manage job roles within an organisation.
    name: Job Roles
  - description: |
      Manage users and accounts.
    name: Users
  - description: |
      Invite and manage invitations to organisations.
    name: Invitations
  - description: >
      IAM grants are the asignment of roles or permissions to principals to
      manage resource access.
    name: IAM Grants
  - description: |
      View the permissions available in the system.
    name: IAM Permissions
  - description: |
      SSO providers are the identity providers for an organisation.
    name: SSO Providers
  - description: |
      Whoami returns information about the currently authenticated principal.
    name: Whoami
  - description: |
      Manage your images
    name: Images
  - description: >
      AI agent personas that synthesise data into role-specific intelligent
      briefings.
    name: Agents
  - description: |
      Briefings generated by AI agents, including reasoning traces.
    name: Briefings
  - description: >
      Organisations are the center point for most resources in the platform.
      Most other endpoints are subresources of an organisation.
    name: Organisations
  - description: |
      Permits managements, integrated with street manager.
    name: Permits
  - description: |
      Projects manage your work and governance.
    name: Projects
  - description: >
      Import templates allow users to save and reuse their CSV importer
      configuration as named templates.
    name: Import Templates
  - description: |
      Properties are the physical locations.
    name: Properties
  - description: |
      Search across schemes, work orders, and operations.
    name: Search
  - description: |
      Provides the API specification in JSON and YAML formats
    name: Specifications
  - description: |
      Streets are the physical roads.
    name: Streets
  - description: |
      Integration with street manager
    name: Street Manager
  - description: |
      Vehicles are the physical vehicles that an organisation manages.
    name: Vehicles
  - description: >
      Scheme contracts (also known as regions) group schemes allocated from the
      network to a contractor.
    name: Scheme Contracts
  - description: >
      Scheme shares allow you to share your schemes with other organisations
      across bridges.
    name: Scheme Shares
  - description: |
      Schemes are large programmes of work
    name: Schemes
  - description: |
      Work orders the component parts of a scheme.
    name: Work Orders
  - description: |
      Operations are the work to be carried out within work orders.
    name: Operations
externalDocs:
  description: More documentation and resources
  url: https://docs.ctrl-hub.com
paths:
  /v3/cost-rollups:
    get:
      tags:
        - Costs
      summary: List cost roll-ups
      description: >
        Read what a node of the job hierarchy costs, and what everything beneath
        it

        costs.


        A roll-up is derived and read-only — there is no create, update or
        delete. It

        is keyed on the cost centre rather than on a cost sheet, so a contract
        nobody

        has costed directly still returns a real total from the work orders
        beneath

        it, without an empty sheet having to exist for it.


        Filter by `organisation`, `cost_centre` and `cost_centre_type` together
        for the

        node you want rolled up. All three are required as a set; supplying some
        but

        not all of them is rejected rather than answered.


        By default this returns exactly one element: the roll-up for the node
        named

        by the filter. Add `eq(depth,all)` to the filter to get one element per
        node

        in the subtree beneath and including that node instead, so a client can
        see

        which scheme, work order or operation is holding the money rather than
        only

        how much is below in total. Any other `depth` value is rejected rather
        than

        answered.


        Every element, in either mode, also carries `parent_cost_centre_id` and

        `parent_cost_centre_type` (both null on the subtree's root), `name`,
        `code`

        and `has_cost_sheet` — enough on its own to rebuild the shape of the
        subtree

        and label each node, since a `depth=all` response is otherwise just a
        bag of

        totals with no relationships between them.


        Each roll-up returns the node's own `direct` total, the `rolled` total
        that

        includes every descendant, and `by_status` — `rolled` split by the
        status of

        the sheet each line sits on, so a client can sum only the statuses it
        cares

        about without asking again. Soft-deleted nodes are left out of the walk.


        Totals are sums of the snapshots stored on the cost lines themselves, so

        revising a rate or a margin in the cost register never moves a roll-up.


        This read is gated on permission to view cost lines rather than cost
        sheets: a

        roll-up returns nothing but money.
      operationId: ListCostRollups
      parameters:
        - $ref: '#/components/parameters/cost_rollup_filter'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
      responses:
        '200':
          $ref: '#/components/responses/ListCostRollups'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorised'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - Session: []
        - OAuth2: []
        - Cookie: []
components:
  parameters:
    cost_rollup_filter:
      name: filter
      in: query
      description: >
        Filters the response data based on the value provided.


        Available filters:


        - `organisation`: Filter by the ID of the organisation the node belongs
        to

        - `cost_centre`: Filter by the ID of the node to roll up

        - `cost_centre_type`: Filter by the type of the node to roll up
          (`scheme-contracts`, `schemes`, `work-orders`, `operations`)
        - `depth`: Optional. How much of the subtree to return: `self` (the
          default, and the same as omitting the term) returns one element, the
          roll-up for the filtered node; `all` returns one element per node in
          the subtree beneath and including it, so a client can see which
          scheme, work order or operation is holding the money rather than only
          the total beneath it. Matching is exact lowercase; any other value is
          rejected with `CH.004.211`.

        A roll-up is derived from a cost centre, so `organisation`,
        `cost_centre`

        and `cost_centre_type` must be given **together** — they identify the
        node

        whose subtree is summed. Supplying some but not all of them is rejected

        with `CH.004.211` rather than answered, because a partly-filtered
        roll-up

        would return a total for the wrong scope. Supplying none of them returns

        an empty collection, as any unfiltered collection read does.


        Wrap them in `and()`, as in

        `and(eq(organisation,c000c344-8847-47da-a091-32e75902d3b1),eq(cost_centre,7f3a2c18-64bd-4f0e-9a52-c8d1b73e0f45),eq(cost_centre_type,work-orders),eq(depth,all))`;

        filters after a bare top-level comma are dropped.


        For more information on using named filters, see [the
        docs](https://docs.ctrl-hub.com/api-reference/features#filtering)
      required: false
      schema:
        type: string
        default: ''
    limit:
      name: limit
      in: query
      description: Limit the number of resources returned by the API
      required: false
      schema:
        type: integer
        format: int64
        minimum: 1
        default: 100
    offset:
      name: offset
      in: query
      description: Offset the resources returned by the API
      required: false
      schema:
        type: integer
        format: int64
        minimum: 0
        default: 0
  responses:
    ListCostRollups:
      description: List of cost roll-ups.
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            allOf:
              - type: object
                required:
                  - data
                properties:
                  data:
                    title: A list of cost roll-ups
                    type: array
                    items:
                      $ref: '#/components/schemas/CostRollup'
              - $ref: '#/components/schemas/DocumentMeta'
              - $ref: '#/components/schemas/JSONAPI'
    BadRequest:
      description: >
        There was an error with the request - this could be due to an invalid
        body, query parameters,

        or headers that were sent to the API.
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: 98ca4a78-b66f-4234-9719-aaf832ee6669
            status: '400'
            title: A validation error was encountered
            source:
              parameter: include
            meta:
              resource: wrong_value
    Unauthorised:
      description: Authentication failed
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: 05fc9c8d-73b9-4697-9337-57f7a567a48f
            status: '401'
            title: You are not authorised to access this resource
            detail: In order to access this resource, you need the 'admin' role.
            code: AUTH.001
    Forbidden:
      description: >-
        The authenticated principal does not hold the grant required for this
        action
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            errors:
              - id: 1b3f2c30-2d56-4d3a-9c44-9be9c2cbf2f0
                status: '403'
                title: Forbidden
                detail: You do not hold the grant required for this action.
                code: AUTH.002
    InternalServerError:
      description: There was a problem handling the request on the server side
      headers:
        Content-Type:
          $ref: '#/components/headers/content-type'
        Content-Length:
          $ref: '#/components/headers/content-length'
        X-Request-ID:
          $ref: '#/components/headers/x-request-id'
      content:
        application/vnd.api+json:
          schema:
            type: object
            properties:
              errors:
                type: array
                items:
                  $ref: '#/components/schemas/Error'
          example:
            id: fe9d9a69-f0a7-4fdc-bb2c-176027f316c5
            status: '500'
            title: Internal Server Error
            detail: An unexpected error occurred on the server.
  headers:
    content-type:
      description: The content type of the response
      schema:
        type: string
      example: application/vnd.api+json
    content-length:
      description: The length of the response body in bytes
      schema:
        type: integer
        format: int32
      example: 1234
    x-request-id:
      description: >-
        An ID that can be provided when reporting bugs to help identify the
        issue
      schema:
        type: string
      example: 8470f56af4cf25e22be08e72c70dbbdc
  schemas:
    CostRollup:
      type: object
      description: >
        A cost roll-up — what one node of the job hierarchy costs, and what
        everything

        beneath it costs.


        The resource is derived and entirely read-only: there is nothing to
        create,

        update or delete. It is keyed on the cost centre rather than on a cost
        sheet,

        because roll-up is a property of the node. A contract nobody has costed

        directly still has a real total from the work orders beneath it, so its

        roll-up is readable without an empty sheet having to exist for it. The
        `id` is

        the cost centre's own identifier.


        It is gated on the same permission as cost lines, not cost sheets: a
        roll-up

        returns nothing but money.
      required:
        - id
        - type
        - attributes
      properties:
        id:
          type: string
          format: uuid
          description: >
            The unique identifier of the cost centre the roll-up is for. A
            roll-up has

            no identity of its own — it is derived from the node and the sheets

            beneath it.
        type:
          type: string
          const: cost-rollups
        attributes:
          $ref: '#/components/schemas/CostRollupAttributes'
    DocumentMeta:
      type: object
      description: Document level meta about the resources on the server in list endpoints.
      required:
        - meta
      properties:
        meta:
          type: object
          required:
            - pagination
          properties:
            features:
              $ref: '#/components/schemas/Features'
            pagination:
              $ref: '#/components/schemas/Pagination'
    JSONAPI:
      type: object
      description: JSON API response object
      required:
        - jsonapi
      properties:
        jsonapi:
          type: object
          required:
            - version
          properties:
            version:
              type: string
              description: The version of the JSON API specification
              examples:
                - '1.0'
    Error:
      type: object
      description: An error response
      properties:
        id:
          description: >-
            A unique identifier for this particular occurrence of the problem.
            If you encounter this, please provide us with the error ID and we
            can investigate it on our side.
          type: string
          format: uuid
          examples:
            - 05fc9c8d-73b9-4697-9337-57f7a567a48f
        status:
          description: >-
            The status code for the error. This might not match the HTTP status
            code if there are more that one errors to return with different
            status codes.
          type: string
          examples:
            - '401'
            - '500'
        title:
          description: A human readable title for the error.
          type: string
          examples:
            - You are not authorised to access this resource
        detail:
          description: >-
            Where there is more detail that we can provide outside of the title,
            we will provide it here.
          type: string
          examples:
            - In order to access this resource, you need the 'admin' role.
        code:
          description: >-
            A unique code for the error that may help us to diagnose the issue.
            Not all errors have codes, so this is usually empty.
          type: string
          examples:
            - AUTH.001
        source:
          description: A JSON object containing additional information about the error.
          type: object
          properties:
            pointer:
              description: >-
                A JSON Pointer to the value in the request that caused the
                error.
              type: string
              examples:
                - /data/attributes/email
            parameter:
              description: >-
                A string indicating which query parameter in the request caused
                the error.
              type: string
              examples:
                - include
      required:
        - id
        - status
        - title
    CostRollupAttributes:
      type: object
      description: Attributes for a cost roll-up
      required:
        - cost_centre_id
        - cost_centre_type
        - parent_cost_centre_id
        - parent_cost_centre_type
        - name
        - code
        - has_cost_sheet
        - direct
        - rolled
        - by_status
        - estimated
      properties:
        cost_centre_id:
          type: string
          format: uuid
          description: >
            The unique identifier of the node the roll-up is for. Read it
            together

            with `cost_centre_type`, which names the resource type it points at.
          readOnly: true
          examples:
            - 7f3a2c18-64bd-4f0e-9a52-c8d1b73e0f45
        cost_centre_type:
          allOf:
            - $ref: '#/components/schemas/CostCentreType'
          description: The level of the job hierarchy the roll-up is for.
          readOnly: true
        parent_cost_centre_id:
          type:
            - string
            - 'null'
          format: uuid
          description: |
            The unique identifier of this node's immediate parent in the job
            hierarchy. Null on the subtree's root — the node named by the filter
            has nothing above it in this response to point at.
          readOnly: true
          examples:
            - c000c344-8847-47da-a091-32e75902d3b1
        parent_cost_centre_type:
          description: >
            The resource type of `parent_cost_centre_id`. It travels with the id

            for the same reason `cost_centre_type` travels with
            `cost_centre_id`:

            the four hierarchy levels are separate id spaces, so an id on its
            own

            identifies nothing a client can navigate to. Null exactly when

            `parent_cost_centre_id` is null.
          readOnly: true
          oneOf:
            - $ref: '#/components/schemas/CostCentreType'
            - type: 'null'
        name:
          type: string
          description: |
            The node's display name. Empty on the subtree's root: the walk reads
            a node's children to build their roll-ups and never reads the root's
            own document, so there is nothing to read the root's name from.
          readOnly: true
        code:
          type: string
          description: |
            The node's code. Empty for a scheme contract, which has no code.
          readOnly: true
        has_cost_sheet:
          type: boolean
          description: >
            Whether this node has a cost sheet of its own, even one with no
            lines

            on it yet. Separates "someone started costing this node and entered

            nothing" from "nobody has costed it" — both otherwise total zero in

            `direct`.
          readOnly: true
        direct:
          allOf:
            - $ref: '#/components/schemas/CostTotals'
          description: >
            What this node's own cost sheet comes to, ignoring everything
            beneath it.

            Zero where the node has never been costed directly.
          readOnly: true
        rolled:
          allOf:
            - $ref: '#/components/schemas/CostTotals'
          description: >
            `direct` plus every descendant's direct total. For an operation — a
            leaf —

            this equals `direct`.


            Soft-deleted nodes are left out of the walk, so deleting a work
            order

            removes its costs from every ancestor's rolled total.
          readOnly: true
        by_status:
          type: object
          description: >
            `rolled` split by the status of the cost sheet each line sits on, so
            a

            client can answer "what is my approved liability, ignoring anything
            still

            quoted" by summing only the statuses it wants — no second request
            per

            combination. Summing all three entries gives `rolled` back.
          readOnly: true
          required:
            - quoted
            - approved
            - completed
          properties:
            quoted:
              allOf:
                - $ref: '#/components/schemas/CostTotals'
              description: >-
                The part of `rolled` sitting on sheets that are still priced
                estimates.
            approved:
              allOf:
                - $ref: '#/components/schemas/CostTotals'
              description: The part of `rolled` sitting on sheets that have been agreed.
            completed:
              allOf:
                - $ref: '#/components/schemas/CostTotals'
              description: The part of `rolled` sitting on sheets whose costs are final.
        estimated:
          type: object
          description: >
            What this node's plan comes to, alongside what it actually cost.

            `direct`, `rolled` and `by_status` above are unchanged by this field
            and

            keep their existing meaning — they walk only lines with

            `line_type: actual`, exactly as before this was added. `estimated`
            is

            the same walk over the same hierarchy, but over lines with

            `line_type: estimated` instead.


            Because both sides come back on the one roll-up,
            estimate-versus-actual

            is a subtraction the client does locally — `estimated.rolled` less

            `rolled` — rather than a second request against a separate resource.


            An estimate covers all the work beneath the node that carries it, so

            the estimate side counts a node's estimate unless something ABOVE
            it,

            inside this subtree, has already estimated the same work. A scheme's

            own estimate therefore shadows everything under it; a work order

            estimated directly shadows its operations; and a sibling work order

            that was only estimated at its operations still contributes those,

            because nothing above them estimated that work.


            This is per branch, not per level: two branches estimated at
            different

            depths both count in full. Only the work actually covered twice is

            dropped.


            It differs from `rolled`, which always sums every sheet in the
            subtree,

            because counting both a work order's estimate and its own
            operations'

            would count the same work twice.
          readOnly: true
          required:
            - direct
            - rolled
          properties:
            direct:
              allOf:
                - $ref: '#/components/schemas/CostTotals'
              description: >
                What this node's own cost sheet estimates, ignoring everything

                beneath it. Zero where the node has no estimate lines of its
                own.
            rolled:
              allOf:
                - $ref: '#/components/schemas/CostTotals'
              description: |
                Every estimate in the subtree that no ancestor estimate already
                covers — the nearest estimate to the top of each branch, summed
                across branches. Where nothing beneath the node carries an
                estimate, this is zero.
    Features:
      type: object
      description: Represents feature configurations of the API
      properties:
        include:
          type: object
          properties:
            options:
              type: array
              items:
                type: string
                examples:
                  - related.resource
    Pagination:
      type: object
      description: Represents pagination details for API responses
      required:
        - counts
        - current_page
        - offsets
        - requested
      properties:
        counts:
          type: object
          required:
            - pages
            - resources
          properties:
            pages:
              type: integer
              examples:
                - 1
            resources:
              type: integer
              examples:
                - 1
        current_page:
          type: integer
          examples:
            - 1
        offsets:
          type: object
          required:
            - next
            - previous
          properties:
            next:
              type:
                - integer
                - 'null'
              examples:
                - null
            previous:
              type:
                - integer
                - 'null'
              examples:
                - null
        requested:
          type: object
          required:
            - limit
            - offset
          properties:
            limit:
              type: integer
              examples:
                - 10
            offset:
              type: integer
              examples:
                - 0
    CostCentreType:
      type: string
      description: >
        The level of the job hierarchy a cost sheet is attached to. Each value
        is the

        JSON:API resource type of the entity the sheet costs, so a sheet's
        target can

        be resolved from the `cost_centre_type` / `cost_centre_id` pair without
        a

        lookup table.
      enum:
        - scheme-contracts
        - schemes
        - work-orders
        - operations
    CostTotals:
      type: object
      description: >
        What a set of cost lines comes to.


        `cost` is what the work cost, `charge` what it sells for, and `profit`
        the

        difference. All three are summed from the snapshots stored on the lines

        themselves — none is re-derived from the cost register at read time, so
        a

        total is as stable as the lines beneath it. Revising a rate or a margin

        changes what future work costs and leaves recorded work alone.
      required:
        - cost
        - charge
        - profit
        - categories
      properties:
        cost:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The sum of every line's amount — what the work cost.
        charge:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The sum of every line's charge — what the work sells for.
        profit:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: >-
            `charge` less `cost`. Negative where the work sells for less than it
            cost.
        categories:
          allOf:
            - $ref: '#/components/schemas/CostCategoryTotals'
          description: >
            `cost` split by the kind of spend it represents. The split
            reconciles: the

            three categories sum to `cost`.
    Money:
      type: object
      description: >
        A monetary amount held in the currency's minor units (pence for GBP)
        alongside

        its ISO 4217 currency code. Amounts are never decimals, so repeated
        arithmetic

        cannot accumulate rounding error.
      required:
        - amount
        - currency
      properties:
        amount:
          type: integer
          format: int64
          description: >-
            The amount in the currency's minor units, for example 9250 for GBP
            92.50
          examples:
            - 9250
        currency:
          type: string
          description: The ISO 4217 alphabetic currency code
          minLength: 3
          maxLength: 3
          pattern: ^[A-Z]{3}$
          examples:
            - GBP
    CostCategoryTotals:
      type: object
      description: >
        A cost split by the kind of spend it represents.


        The split covers cost only, not charge: it answers "where did the money
        go",

        which is a question about what was bought rather than what it sold for.
        The

        categories always sum to the `cost` they were split from.
      required:
        - material
        - labour
        - misc
        - contractor
      properties:
        material:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The part of the cost spent on materials.
        labour:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The part of the cost spent on labour.
        misc:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The part of the cost spent on anything else.
        contractor:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The part of the cost spent on contractors.
  securitySchemes:
    Session:
      description: |
        Session token for authentication.
      in: header
      name: X-Session-Token
      type: apiKey
    OAuth2:
      description: |
        OAuth2 token for authentication.
      flows:
        clientCredentials:
          scopes: {}
          tokenUrl: https://auth.ctrl-hub.com/oauth2/token
      type: oauth2
    Cookie:
      description: |
        Cookie token for authentication.
      in: cookie
      name: ctrl_hub_session
      type: apiKey

````