{
  "openapi": "3.1.0",
  "info": {
    "title": "GroundIT — Billing",
    "version": "0.1.0",
    "license": {
      "name": "Proprietary — GroundIT/Sysmedac internal"
    },
    "description": "Client invoicing v1 for the Finance console (`/billing`, §W only — there is no mobile billing surface and none is planned). The deliberately thin client register, effective-dated rate cards resolved **as-of the work date** (most-specific window wins), invoices drafted from **approved** timesheet hours consumed by event, gapless per-legal-entity numbering, simple percentage tax lines, the maker-checker'd `DRAFT → SENT → PAID` (+ `VOID`) lifecycle, an append-only payment ledger, and the receivables aging aggregate. Money is exact decimal end to end — never float — and `currency_code` always comes from the owning legal entity, never typed per row. See ../../api-docs/00-api-overview-and-conventions.md.\n"
  },
  "servers": [
    {
      "url": "/api/v1"
    }
  ],
  "security": [
    {
      "bearerJWT": []
    }
  ],
  "tags": [
    {
      "name": "billing",
      "description": "Client register, rate cards, invoices, taxes, payments, receivables and number series."
    },
    {
      "name": "client"
    },
    {
      "name": "billing_rate"
    },
    {
      "name": "invoice"
    },
    {
      "name": "invoice_line"
    },
    {
      "name": "invoice_tax"
    },
    {
      "name": "invoice_payment"
    },
    {
      "name": "receivables"
    },
    {
      "name": "invoice_sequence"
    }
  ],
  "x-reuse-anchors": {
    "parameters": {
      "path_id": {
        "$ref": "#/components/parameters/PathId"
      },
      "page_size": {
        "$ref": "#/components/parameters/PageSize"
      },
      "page_after": {
        "$ref": "#/components/parameters/PageAfter"
      },
      "page_before": {
        "$ref": "#/components/parameters/PageBefore"
      },
      "sort_param": {
        "$ref": "#/components/parameters/SortParam"
      },
      "accept_language": {
        "$ref": "#/components/parameters/AcceptLanguage"
      },
      "idempotency_key": {
        "$ref": "#/components/parameters/IdempotencyKey"
      },
      "if_match": {
        "$ref": "#/components/parameters/IfMatch"
      }
    },
    "responses": {
      "unauthorized": {
        "$ref": "#/components/responses/Unauthorized"
      },
      "forbidden": {
        "$ref": "#/components/responses/Forbidden"
      },
      "not_found": {
        "$ref": "#/components/responses/NotFound"
      },
      "conflict": {
        "$ref": "#/components/responses/Conflict"
      },
      "unprocessable": {
        "$ref": "#/components/responses/UnprocessableEntity"
      },
      "locked": {
        "$ref": "#/components/responses/Locked"
      },
      "too_many": {
        "$ref": "#/components/responses/TooManyRequests"
      },
      "precondition_required": {
        "$ref": "#/components/responses/PreconditionRequired"
      },
      "precondition_failed": {
        "$ref": "#/components/responses/PreconditionFailed"
      }
    },
    "headers": {
      "etag": {
        "$ref": "#/components/headers/ETag"
      },
      "location": {
        "$ref": "#/components/headers/Location"
      },
      "idem_replayed": {
        "$ref": "#/components/headers/IdempotencyReplayed"
      }
    }
  },
  "paths": {
    "/clients": {
      "get": {
        "operationId": "billing.client.list",
        "summary": "List billing clients",
        "description": "The register of organisations the tenant invoices — `BIL-S01`'s grid, and the `ACTIVE`-only client picker on `BIL-S05` step 1 (db 17 §1 `clients`). Deliberately thin: name, billing address, tax identifiers, currency and status, plus the derived **Outstanding** and **Last invoice** grid columns. No contact, owner, pipeline or opportunity field exists on this resource (ADR 0026 §(d) — the boundary that keeps the future `sales` round open).\n",
        "tags": [
          "billing",
          "client"
        ],
        "x-token": "billing.client.list",
        "x-realizes-features": [
          "BIL-F01"
        ],
        "x-screens": [
          "BIL-S01",
          "BIL-S05"
        ],
        "x-touches-entities": [
          "billing.clients",
          "billing.invoices"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ClientStatus"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search on `display_name` (the `BIL-S01` toolbar search).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `display_name`, `-display_name`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of billing clients.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "billing.client.create",
        "summary": "Create a billing client",
        "description": "Adds a client to the register (`BIL-S01` **New client** → editor `PT-MODAL`). `display_name` is unique per tenant (a second client with the same billing name is a data-entry error, not a second counterparty) → `409`. `currency_code` is **defaulted from the legal entity that will issue this client's invoices, never freely typed**, and the tax identifier is **pack-validated** (`XC-F01`, `gstin` for India / `vat_no` for KSA) — never a hardcoded per-country regex. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "client"
        ],
        "x-token": "billing.client.create",
        "x-realizes-features": [
          "BIL-F01"
        ],
        "x-screens": [
          "BIL-S01"
        ],
        "x-touches-entities": [
          "billing.clients",
          "org.legal_entities"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client created (`status=ACTIVE`).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/clients/{id}": {
      "get": {
        "operationId": "billing.client.get",
        "summary": "Get one billing client",
        "description": "One client for the `BIL-S01` editor and the `BIL-S04` invoice header block (name, billing address and tax identifiers are printed onto the invoice and its PDF).\n",
        "tags": [
          "billing",
          "client"
        ],
        "x-token": "billing.client.get",
        "x-realizes-features": [
          "BIL-F01"
        ],
        "x-screens": [
          "BIL-S01",
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.clients"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The client.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "billing.client.update",
        "summary": "Update a billing client",
        "description": "Edits the register fields (`BIL-S01` editor). `If-Match` carries the row `version` — several Finance users share this surface. Changing `currency_code` once the client has invoices is **blocked** (`409`), because an issued document's currency is inherited from the issuing legal entity and must stay re-explainable. Tax-identifier format is pack-validated (`XC-F01`). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "client"
        ],
        "x-token": "billing.client.update",
        "x-realizes-features": [
          "BIL-F01"
        ],
        "x-screens": [
          "BIL-S01"
        ],
        "x-touches-entities": [
          "billing.clients",
          "org.legal_entities"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClientUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated client.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/clients/{id}/archive": {
      "post": {
        "operationId": "billing.client.archive",
        "summary": "Archive a billing client (status flip, never a delete)",
        "description": "Sets `status = 'ARCHIVED'` (`BIL-S01` row action, destructive `PT-CTA` + confirm). **Archiving is a state, not a delete** — an issued document must never lose its counterparty. An archived client is excluded from `BIL-S05`'s client picker and from new rate cards, while **its existing invoices stay readable and payable**. There is no delete operation on this resource, by design (db 17 §1). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "client"
        ],
        "x-token": "billing.client.archive",
        "x-realizes-features": [
          "BIL-F01"
        ],
        "x-screens": [
          "BIL-S01"
        ],
        "x-touches-entities": [
          "billing.clients"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArchiveInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Client archived.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/billing-rates": {
      "get": {
        "operationId": "billing.billing_rate.list",
        "summary": "List billing rate cards for a client",
        "description": "Effective-dated rate cards per client, optionally narrowed to a project and/or grade (`BIL-S02` grid). Default scope is rows **in force today**; `include_superseded=true` reveals past and future windows rather than hiding them permanently — an issued invoice must always be re-explainable (db 17 §1). The derived *Scope* column (`Client base` / `Project: X` / `Grade: Y` / `Project X · Grade Y`) makes the resolution precedence legible inline.\n",
        "tags": [
          "billing",
          "billing_rate"
        ],
        "x-token": "billing.billing_rate.list",
        "x-realizes-features": [
          "BIL-F02"
        ],
        "x-screens": [
          "BIL-S02",
          "BIL-S05"
        ],
        "x-touches-entities": [
          "billing.client_billing_rates",
          "billing.clients",
          "work.projects",
          "org.grades"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "description": "The client whose rate cards are listed (`BIL-S02` is always client-scoped).",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "project_ref",
            "in": "query",
            "required": false,
            "description": "Soft `ref→work.projects`, projection-resolved — never a `work` read.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "grade_ref",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Return the windows in force on this **work** date (defaults to today).",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "include_superseded",
            "in": "query",
            "required": false,
            "description": "`true` reveals superseded and future windows (the **Show superseded** toggle).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `effective_from`, `-effective_from`, `rate`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of rate cards for the client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRatePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "billing.billing_rate.create",
        "summary": "Create a billing rate card",
        "description": "Opens a new effective-dated window (`BIL-S02` **New rate**). **Rates are effective-dated, not edited in place**: a price change is a new row plus a closed predecessor window, so March's invoices are never rewritten by an April rate. Setting `supersede=true` auto-closes the predecessor at `effective_from − 1 day` **in the same transaction** — the \"close the old window / open the new one\" review the editor shows before save. Windows may not overlap within one `(client, project_ref, grade_ref)` specificity → `409` naming the conflicting window (never a silent last-write-wins). `currency_code` is inherited from the client / issuing legal entity, never independently chosen. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "billing_rate"
        ],
        "x-token": "billing.billing_rate.create",
        "x-realizes-features": [
          "BIL-F02"
        ],
        "x-screens": [
          "BIL-S02"
        ],
        "x-touches-entities": [
          "billing.client_billing_rates",
          "billing.clients",
          "work.projects",
          "org.grades"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BillingRateCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rate card created (and the superseded predecessor window closed, when requested).",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Effective-window overlap at this specificity — the response names the conflicting row's window so the form error is legible (`BIL-S02` *form/submit error*), or an idempotency-key reuse.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/RateOverlapProblem"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/billing-rates/{id}": {
      "get": {
        "operationId": "billing.billing_rate.get",
        "summary": "Get one billing rate card",
        "description": "One rate window for the `BIL-S02` editor — the read the edit form loads before it writes, and the re-explain read for a card cited by an `invoice_lines.rate_id` long after its window closed (db 17 §1). Returns the row's `version` in the `ETag` for the `If-Match` write that follows.\n",
        "tags": [
          "billing",
          "billing_rate"
        ],
        "x-token": "billing.billing_rate.get",
        "x-realizes-features": [
          "BIL-F02"
        ],
        "x-screens": [
          "BIL-S02"
        ],
        "x-touches-entities": [
          "billing.client_billing_rates",
          "billing.clients",
          "work.projects",
          "org.grades"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The rate card.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "billing.billing_rate.update",
        "summary": "Update a billing rate card",
        "description": "Corrects a rate window that has **not** yet priced an issued line (`BIL-S02` editor). `If-Match` carries the row `version`. The **overlap guard** is re-evaluated on every write: two rows within one `(client, project_ref, grade_ref)` specificity may not hold overlapping `[effective_from, effective_to]` ranges, and at most one open-ended row may exist per specificity → `409` naming the conflicting window. A row already cited by an `invoice_lines.rate_id` is **never deleted** and its priced history is never restated — retire it with `billing.billing_rate.end` instead (db 17 §1). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "billing_rate"
        ],
        "x-token": "billing.billing_rate.update",
        "x-realizes-features": [
          "BIL-F02"
        ],
        "x-screens": [
          "BIL-S02"
        ],
        "x-touches-entities": [
          "billing.client_billing_rates",
          "work.projects",
          "org.grades"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BillingRateUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated rate card.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Effective-window overlap at this specificity (conflicting window named), or idempotency-key reuse.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/RateOverlapProblem"
                }
              }
            }
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/billing-rates/{id}/end": {
      "post": {
        "operationId": "billing.billing_rate.end",
        "summary": "Close a rate card's effective window",
        "description": "Sets `effective_to` (inclusive) — the **Close window** row action on `BIL-S02`, and the only retirement path for a card that has priced an invoice line. There is deliberately **no delete**: an issued invoice must stay re-explainable years later, so the window is closed and the row survives with its `rate_id` provenance intact (db 17 §1, `BIL-F02`). `effective_to` must be `>= effective_from` and must not reopen an overlap with a successor window → `409`. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "billing_rate"
        ],
        "x-token": "billing.billing_rate.end",
        "x-realizes-features": [
          "BIL-F02"
        ],
        "x-screens": [
          "BIL-S02"
        ],
        "x-touches-entities": [
          "billing.client_billing_rates"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BillingRateEndInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Window closed.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingRate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Closing this window would overlap or orphan a successor window, or idempotency-key reuse.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/RateOverlapProblem"
                }
              }
            }
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices": {
      "get": {
        "operationId": "billing.invoice.list",
        "summary": "List invoices (receivables grid)",
        "description": "The Finance console's receivables surface (`BIL-S03`) — filterable by client, status, legal entity, billed period and **aging bucket**. `outstanding` is derived (`grand_total − amount_paid`); it is `0` for `PAID` and **not reported at all** for `VOID` (a cancelled document is not a receivable). The `aging_bucket` filter applies **only to `SENT` rows with an outstanding balance**, exactly like the `BIL-S03` chips — a `DRAFT` carries no due-date obligation and including any other status would inflate the aging read Finance steers on. Aging arithmetic is calendar-day and always computes on the **Gregorian** `due_date` (a Hijri string is display-only, `XC-F11`).\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.list",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F06"
        ],
        "x-screens": [
          "BIL-S03"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "billing.clients",
          "org.legal_entities"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            }
          },
          {
            "name": "legal_entity_ref",
            "in": "query",
            "required": false,
            "description": "Soft `ref→org.legal_entities`, projection-resolved.",
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "period_from",
            "in": "query",
            "required": false,
            "description": "Lower bound of the billed **work** period (not the document date).",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "period_to",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "aging_bucket",
            "in": "query",
            "required": false,
            "description": "Restricts to `SENT` rows with an outstanding balance falling in this bucket.",
            "schema": {
              "$ref": "#/components/schemas/AgingBucket"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text search on `invoice_no`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `issue_date`, `-issue_date`, `due_date`, `-due_date`, `invoice_no`, `grand_total`, `status`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoicePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/generate-from-timesheets": {
      "post": {
        "operationId": "billing.invoice.generate_from_timesheets",
        "summary": "Generate a draft invoice from approved timesheet hours",
        "description": "The `BIL-S05` wizard's **Create draft**. Builds a `DRAFT` invoice, its `invoice_lines` and its pack-supplied `invoice_taxes` from **approved hours only** — projected from `work.timesheet.approved` events into `billing`'s own read model, **never a cross-schema read into `work`** (architecture-docs/03-domain-modules §4). Each candidate row is priced by the `BIL-F02` resolution rule **as-of the work date** (most specific window wins: `project + grade` → `project` → `grade` → client base) and the winning card's `id` is stamped onto the line as `rate_id`. **`invoice_no` is `null` on a `DRAFT`** — the number is allocated from `invoice_sequences` **inside the send transaction**, never here (`billing.invoice.send`, db 17 §2). A draft that is edited away, superseded or discarded must not burn a number: allocating at creation would leave permanent holes in a per-entity series that has to be gapless (`BIL-F03`). Generation is **idempotent by projection**: hours already billed on an earlier invoice are excluded, so a re-run **extends** rather than duplicates. Creation is deliberately **not** maker-checker'd — a draft commits nothing to a client; the control point is `billing.invoice.send`. **Missing rates are never a silent skip**: any candidate row with no rate window in force on its work date is returned in a `422` naming those rows (`unpriced_rows`), and the caller must either fix the rates or pass `exclude_unpriced=true` as an explicit, counted choice. Dropping unpriced hours quietly under-bills a client by an amount nobody ever sees. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.generate_from_timesheets",
        "x-realizes-features": [
          "BIL-F03",
          "BIL-F02",
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S05"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "billing.invoice_lines",
          "billing.invoice_taxes",
          "billing.client_billing_rates",
          "billing.clients",
          "org.legal_entities",
          "org.grades",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "billing.invoice.generated",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceGenerateInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft invoice created with its lines and tax lines; `invoice_no` is `null` until send.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Validation failed, **or** one or more candidate rows have no rate window in force on their work date. In the missing-rate case `unpriced_rows` enumerates every blocked row (project · grade · period · hours) so `BIL-S05` can render each with a deep-link to `BIL-S02`. Rows are **never** dropped silently.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceGenerationProblem"
                }
              }
            }
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}": {
      "get": {
        "operationId": "billing.invoice.get",
        "summary": "Get one invoice with lines, taxes and payments",
        "description": "The `BIL-S04` document — header, totals, lines, tax lines and the append-only payments ledger (newest first), plus the grant set the footer `PT-CTA` cluster is rendered from. Lifecycle CTAs are built from the grants the caller **actually holds** for this invoice, never from a role name guessed on the client. A `SENT`/`PAID`/`VOID` invoice is read-only: it is not a form any more.\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.get",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F05",
          "BIL-F06",
          "BIL-F07"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "billing.invoice_lines",
          "billing.invoice_taxes",
          "billing.invoice_payments",
          "billing.clients",
          "billing.client_billing_rates",
          "org.legal_entities",
          "people.employees",
          "docs.documents"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          }
        ],
        "responses": {
          "200": {
            "description": "The invoice with its lines, taxes and payments.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "billing.invoice.update",
        "summary": "Update a draft invoice header",
        "description": "Edits `issue_date`, `due_date` and the billed period — **`DRAFT` only**. `If-Match` carries the row `version`: a draft invoice is exactly the shape that loses writes without it, several Finance users editing lines and dates on one document (db 17 §2 **Notes**). **A `SENT` invoice is immutable** — no header, line or tax row may change once sent, there is no \"unsend\" and no correction-in-place, so this operation answers **`409`** with `code=STATE_TRANSITION_INVALID` on any non-`DRAFT` invoice. Corrections are `billing.invoice.void` + reissue. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.update",
        "x-realizes-features": [
          "BIL-F04"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoices"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated draft invoice.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/send": {
      "post": {
        "operationId": "billing.invoice.send",
        "summary": "Send (issue) a draft invoice — maker-checker'd",
        "description": "`DRAFT → SENT` (`BIL-S04` **Send**). This **issues the invoice and starts the receivable — it does not email the client**; delivery is a Later facet of `BIL-F07` and the confirm copy says so. **MC-1 maker-checker** (security-docs/04 §2.1): the request is raised into the unified approvals inbox as request type **`INVOICE`** (ADR 0027 §(e), routed/escalated by `XC-F16`, honouring delegation `XC-F14`) and answers **`202`** while the invoice sits *Awaiting approval*; the checker's decision is **applied by this endpoint under its own tokens** (ADR 0027 §(d)) and answers `200`. The maker's token cannot approve → `403 MAKER_EQUALS_CHECKER`; an inbox decision this endpoint would refuse is refused here and the refusal recorded. On approval the transaction **allocates the next gapless `invoice_no` from `invoice_sequences`** for the issuing legal entity (`SELECT … FOR UPDATE` on the series row, incremented in the same transaction so a rollback returns the number rather than burning it), stamps `sent_at`, and **freezes `subtotal`, `tax_total`, `grand_total` and the tax lines** (`BIL-F05`) — a later rate or pack change can never restate an issued document. Notified via `XC-F05`, audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.send",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F03",
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "billing.invoice_sequences",
          "billing.invoice_lines",
          "billing.invoice_taxes",
          "xc.approval_inbox"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "billing.invoice.sent",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceTransitionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice issued — `status=SENT`, `invoice_no` allocated, `sent_at` stamped, totals frozen.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "202": {
            "description": "Maker-checker request raised as an `INVOICE` inbox item (`XC-F12`); the invoice is *Awaiting approval* and stays `DRAFT` until a checker actions it.\n",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceApprovalAccepted"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/void": {
      "post": {
        "operationId": "billing.invoice.void",
        "summary": "Void an invoice (reason required) — maker-checker'd on SENT",
        "description": "`DRAFT → VOID` or `SENT → VOID` (`BIL-S04` **Void**, destructive `PT-CTA` + confirm). **`reason` is required** — a cancelled tax document must say why — and is persisted to `void_reason` alongside `voided_at`. **The invoice number is retained and never reused**: a missing number is a question a tax authority asks, a voided one is an answer (db 17 §2). Voiding a **`SENT`** invoice is **MC-1 maker-checker'd** and routes as request type **`INVOICE`** (ADR 0027 §(e)) — `202` while awaiting a checker, `200` once applied by this endpoint under its own tokens (ADR 0027 §(d)). **Voiding a `DRAFT` (never sent) is plain** — the band binds to the externally-visible retraction (security-docs/04 §2.1). A `PAID` invoice is not voidable → `409`. Corrections to a sent invoice are `VOID` + **reissue**, which allocates a new number; there is no credit note in v1 (ADR 0026 §(d)). Notified via `XC-F05`, audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.void",
        "x-realizes-features": [
          "BIL-F04"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "xc.approval_inbox"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "billing.invoice.voided",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceVoidInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice voided — `status=VOID`, `voided_at` + `void_reason` stamped, number retained.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "202": {
            "description": "Void of a `SENT` invoice raised as an `INVOICE` inbox item (`XC-F12`); the invoice is *Awaiting approval* and stays `SENT` until a checker actions it.\n",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceApprovalAccepted"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/render": {
      "post": {
        "operationId": "billing.invoice.render",
        "summary": "Render the invoice PDF (async, server-side)",
        "description": "`BIL-S04` **Download PDF**. Requests a **server-side** render through the [ADR 0023](../../architecture-docs/adr/0023-server-side-pdf-rendering.md) path — never a browser render, because the stored artifact must be the same evidence the client received. Answers **`202`** with a job handle; the caller polls until the artifact is ready and then fetches it through a short-lived **presigned** URL (`XC-F07`). Heavy/batch rendering runs on the jobs tier (`XC-F08`). The finished document reference lands on `invoices.pdf_document_ref` (`ref→docs.documents`); a `SENT` invoice's PDF is immutable alongside it, and a **void/reissue produces a new document, never an overwrite** (`BIL-F07`). The PDF carries **no IRN/ZATCA clearance mark** — e-invoicing is deferred (ADR 0026 §(d)) and a clearance-shaped mark would read as a filing claim.\n",
        "tags": [
          "billing",
          "invoice"
        ],
        "x-token": "billing.invoice.render",
        "x-realizes-features": [
          "BIL-F07"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "docs.documents"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "async",
        "x-emits-event": "billing.invoice.rendered",
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceRenderInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Render accepted; poll for the finished document reference.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceRenderAccepted"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/lines": {
      "post": {
        "operationId": "billing.invoice_line.create",
        "summary": "Add a line to a draft invoice",
        "description": "Adds a `MANUAL` line to a `DRAFT` invoice (`BIL-S04` lines table). `amount = quantity × unit_rate` is **computed by the service and never hand-typed** — expressible exactly because both operands are decimal (db 17 §2). `TIMESHEET` lines are produced only by `billing.invoice.generate_from_timesheets`; a manual line may still cite the card it was priced from via `rate_id`. Re-rolls `subtotal`/`grand_total` in the same transaction. **`DRAFT` only** — a `SENT` invoice is immutable → `409`. `If-Match` carries the **parent invoice's** `version` (the document, not the line, is the concurrency unit). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_line"
        ],
        "x-token": "billing.invoice_line.create",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_lines",
          "billing.invoices",
          "billing.client_billing_rates",
          "work.projects"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceLineCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Line added; invoice totals re-rolled.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceLine"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoice-lines/{id}": {
      "patch": {
        "operationId": "billing.invoice_line.update",
        "summary": "Edit a line on a draft invoice",
        "description": "Edits description, quantity, unit rate or the covered work period on a `DRAFT` invoice's line; `amount` is recomputed by the service and the invoice totals are re-rolled in the same transaction. **`DRAFT` only** — lines are immutable once the invoice is `SENT` → `409` (db 17 §2 **Lifecycle**). `If-Match` carries the **parent invoice's** `version`. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_line"
        ],
        "x-token": "billing.invoice_line.update",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_lines",
          "billing.invoices"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceLineUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Line updated; invoice totals re-rolled.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceLine"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "billing.invoice_line.delete",
        "summary": "Remove a line from a draft invoice",
        "description": "Removes a line from a `DRAFT` invoice and re-rolls its totals. **`DRAFT` only** — removing a line from a `SENT` invoice would restate a document the client already holds → `409`; the correction path is `VOID` + reissue. `If-Match` carries the **parent invoice's** `version`. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_line"
        ],
        "x-token": "billing.invoice_line.delete",
        "x-realizes-features": [
          "BIL-F04",
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_lines",
          "billing.invoices"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Line removed; invoice totals re-rolled.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/taxes": {
      "post": {
        "operationId": "billing.invoice_tax.create",
        "summary": "Add a tax line to a draft invoice",
        "description": "Adds a **percentage** tax line to a `DRAFT` invoice and re-rolls `tax_total`/`grand_total` (`BIL-S04` tax-lines table). `label` and `rate_percent` come from the legal entity's bound **compliance pack** (`XC-F01`) and are **stamped onto the row, never derived at render time** — that is what lets a two-year-old invoice still print the rate it was actually taxed at. India **GST** and KSA **VAT** are the same mechanism with different pack-supplied values: no forked table, no forked column, no hardcoded country string. **Simple percentage lines only in v1** (`BIL-F05`) — no place-of-supply engine, reverse charge or withholding, and **no IRN/ZATCA clearance field exists anywhere in this module**. **`DRAFT` only** → `409` on `SENT`. `If-Match` carries the **parent invoice's** `version`. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_tax"
        ],
        "x-token": "billing.invoice_tax.create",
        "x-realizes-features": [
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_taxes",
          "billing.invoices",
          "org.legal_entities"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceTaxCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tax line added; invoice totals re-rolled.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceTax"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoice-taxes/{id}": {
      "delete": {
        "operationId": "billing.invoice_tax.delete",
        "summary": "Remove a tax line from a draft invoice",
        "description": "Removes a tax line from a `DRAFT` invoice and re-rolls its totals. **`DRAFT` only** — tax lines are **frozen at send** (`BIL-F05`), so removing one from a `SENT` invoice would restate an issued tax document → `409`. There is no update operation: a wrong rate on a draft is removed and re-added from the pack. `If-Match` carries the **parent invoice's** `version`. Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_tax"
        ],
        "x-token": "billing.invoice_tax.delete",
        "x-realizes-features": [
          "BIL-F05"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_taxes",
          "billing.invoices"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "responses": {
          "204": {
            "description": "Tax line removed; invoice totals re-rolled.",
            "headers": {
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoices/{id}/payments": {
      "get": {
        "operationId": "billing.invoice_payment.list",
        "summary": "List payments recorded against an invoice",
        "description": "The `BIL-S04` payments panel — the **append-only** payment ledger for one invoice, newest value date first. Reversal rows are returned alongside the originals and carry a **positive** `amount` identified by `reversal_of_payment_id`, so no reader has to infer sign semantics (db 17 §3).\n",
        "tags": [
          "billing",
          "invoice_payment"
        ],
        "x-token": "billing.invoice_payment.list",
        "x-realizes-features": [
          "BIL-F06"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_payments",
          "people.employees"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": true,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `paid_on`, `-paid_on`, `created_at`, `-created_at`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of payment ledger rows for the invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoicePaymentPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "billing.invoice_payment.create",
        "summary": "Record a payment (or a reversal) against a sent invoice",
        "description": "`BIL-S04` **Record payment** — manual by decision: amount, **value** date (when the money moved, not the entry date) and a free-text bank/UTR/cheque reference. **This is a receivables ledger, not a bank feed**: there is no statement import, matching or clearing anywhere in this module (`BIL-F06` constraint). **Partial payments are normal** and leave the invoice `SENT` in its aging bucket; the ledger sum maintains `invoices.amount_paid` in the same transaction (`Σ normal − Σ reversals`) and the invoice transitions **`SENT → PAID`** — stamping `paid_at` and emitting `billing.invoice.paid` — **only** when the outstanding balance reaches nil. **Over-payment is rejected at entry, not clamped** (`422`): the amount someone typed and the amount stored must never differ, and `amount_paid <= grand_total` is the DB floor under it. `paid_on` must be `>= invoices.issue_date`. A payment against a `VOID` or `DRAFT` invoice is refused (`409`). **Corrections are reversals, never edits** — the row is append-only (no update, no delete, no `version`); a mis-keyed payment is corrected by inserting a row carrying `reversal_of_payment_id` with an amount **equal** to the reversed row's, and a payment may be reversed **once** (`409` on a double reversal). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_payment"
        ],
        "x-token": "billing.invoice_payment.create",
        "x-realizes-features": [
          "BIL-F06",
          "BIL-F04"
        ],
        "x-screens": [
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_payments",
          "billing.invoices",
          "people.employees"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": "billing.invoice.paid",
        "x-rls-scope": "tenant",
        "x-append-only": true,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoicePaymentCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment recorded; `invoices.amount_paid` re-derived and the invoice moved to `PAID` when settled in full.\n",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoicePaymentResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/receivables/aging": {
      "get": {
        "operationId": "billing.receivables.aging",
        "summary": "Receivables aging summary (buckets by client and legal entity)",
        "description": "The `BIL-S03` aging strip — an **aggregate over `invoices`**, not a materialized table: the numbers must never be able to disagree with the invoices they summarise, and at this volume the aggregate is cheap (db 17 \"The downstream read\"). Scope is `status = 'SENT'` **and** `grand_total − amount_paid > 0`, bucketed on `due_date` into **Current · 1–30 · 31–60 · 61–90 · 90+**, with a count and an outstanding total per bucket, broken down per client and per legal entity. `DRAFT` (no due-date obligation), `PAID` (settled) and `VOID` (not a receivable) rows are excluded — including any of them would inflate the read Finance steers on. Arithmetic is calendar-day and always computes on the **Gregorian** `due_date`; a Hijri string is display-only (`XC-F11`). This carries its **own token** because the strip is a money rollup, not a row read; console reads are scope-bounded (ADR 0026 §(b)).\n",
        "tags": [
          "billing",
          "receivables"
        ],
        "x-token": "billing.receivables.aging",
        "x-realizes-features": [
          "BIL-F06"
        ],
        "x-screens": [
          "BIL-S03"
        ],
        "x-touches-entities": [
          "billing.invoices",
          "billing.clients",
          "org.legal_entities"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "legal_entity_ref",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "Bucket the outstanding balances as at this Gregorian date (defaults to today).",
            "schema": {
              "$ref": "#/components/schemas/DateOnlyRef"
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "required": false,
            "description": "Breakdown axis for the `by_group` rows.",
            "schema": {
              "type": "string",
              "enum": [
                "client",
                "legal_entity"
              ],
              "default": "client"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Aging buckets with counts and outstanding totals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgingSummary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoice-sequences": {
      "get": {
        "operationId": "billing.invoice_sequence.list",
        "summary": "List the per-legal-entity invoice number series",
        "description": "The number series that produce `invoices.invoice_no` — one row per `(legal entity, prefix)`, with the `next_no` to be handed out. Numbering is **gapless within a legal entity**: a number is allocated by locking the series row and incrementing it inside the invoice insert transaction, so a rollback returns the number rather than burning it. A Postgres `sequence` is deliberately **not** used — sequences are non-transactional and leak numbers on rollback, which is the exact defect this table exists to prevent (db 17 §2).\n",
        "tags": [
          "billing",
          "invoice_sequence"
        ],
        "x-token": "billing.invoice_sequence.list",
        "x-realizes-features": [
          "BIL-F03"
        ],
        "x-screens": [
          "BIL-S05",
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_sequences",
          "org.legal_entities"
        ],
        "x-idempotent": false,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "name": "legal_entity_ref",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/UuidRef"
            }
          },
          {
            "$ref": "#/components/parameters/PageSize"
          },
          {
            "$ref": "#/components/parameters/PageAfter"
          },
          {
            "$ref": "#/components/parameters/PageBefore"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Sort whitelist: `prefix`, `-prefix`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of invoice number series.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSequencePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/invoice-sequences/{id}": {
      "patch": {
        "operationId": "billing.invoice_sequence.update",
        "summary": "Configure an invoice number series (prefix / next number)",
        "description": "Guarded configuration of a legal entity's series. **`next_no` is the only column mutable in normal operation** and may only move **forward** — it is never rewound and a number is never reused, because a **void keeps its number** and a gap in the series is precisely what this table exists to prevent (`409` on a decrease). **Rolling a series at year end is a new row with a new prefix, not a rewind** — the prefix may embed a fiscal-year token. `If-Match` carries the row `version`; `(legal_entity_ref, prefix)` stays unique per tenant (`409`). Audited (`XC-F06`).\n",
        "tags": [
          "billing",
          "invoice_sequence"
        ],
        "x-token": "billing.invoice_sequence.update",
        "x-realizes-features": [
          "BIL-F03"
        ],
        "x-screens": [
          "BIL-S05",
          "BIL-S04"
        ],
        "x-touches-entities": [
          "billing.invoice_sequences",
          "org.legal_entities"
        ],
        "x-idempotent": true,
        "x-market": "both",
        "x-sync-async": "sync",
        "x-emits-event": null,
        "x-rls-scope": "tenant",
        "x-append-only": false,
        "x-entitlement": "billing",
        "x-provisional": null,
        "parameters": [
          {
            "$ref": "#/components/parameters/PathId"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": true,
            "$ref": "#/components/parameters/IfMatch"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceSequenceUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated number series.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSequence"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "412": {
            "$ref": "#/components/responses/PreconditionFailed"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          },
          "428": {
            "$ref": "#/components/responses/PreconditionRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerJWT": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Product session token. Two mint paths, one contract (ADR 0010): employees/managers authenticate against Keycloak (mobile + web); workspace staff arrive from the One portal via bridge-token SSO (`POST /api/sso/exchange` verifies the platform's Ed25519 token and mints the product session). The token carries IDENTITY ONLY — `sub`, `tenantUid`, `principal_class`, MFA level, session ref, `exp`. Roles/permissions are re-resolved server-side per request. Enforcement is layered: gateway (TLS/WAF/routing only — NEVER trusted for auth) → NestJS auth guard (validates token, builds the request auth-context) → entitlement middleware (subscription-status → feature-flag → numeric-limit, ADR 0009) → `SET LOCAL app.tenant_id` / `app.user_id` → Postgres FORCED RLS. `tenantUid` is NEVER a path, query, or body parameter.\n"
      }
    },
    "schemas": {
      "UuidRef": {
        "$ref": "#/components/schemas/Uuid"
      },
      "DateOnlyRef": {
        "$ref": "#/components/schemas/DateOnly"
      },
      "TimestampRef": {
        "$ref": "#/components/schemas/Timestamp"
      },
      "HijriDisplayRef": {
        "$ref": "#/components/schemas/HijriDisplay"
      },
      "DecimalHoursRef": {
        "$ref": "#/components/schemas/DecimalHours"
      },
      "RateRef": {
        "$ref": "#/components/schemas/Rate"
      },
      "MoneyRef": {
        "$ref": "#/components/schemas/Money"
      },
      "CurrencyCodeRef": {
        "$ref": "#/components/schemas/CurrencyCode"
      },
      "BusinessNoRef": {
        "$ref": "#/components/schemas/BusinessNo"
      },
      "FileDownloadRef": {
        "$ref": "#/components/schemas/FileDownload"
      },
      "AuditMetaRef": {
        "$ref": "#/components/schemas/AuditMeta"
      },
      "AppendOnlyMetaRef": {
        "$ref": "#/components/schemas/AppendOnlyMeta"
      },
      "CursorPageRef": {
        "$ref": "#/components/schemas/CursorPage"
      },
      "ProblemRef": {
        "$ref": "#/components/schemas/Problem"
      },
      "ValidationProblemRef": {
        "$ref": "#/components/schemas/ValidationProblem"
      },
      "ClientStatus": {
        "type": "string",
        "enum": [
          "ACTIVE",
          "ARCHIVED"
        ],
        "description": "billing.client_status (db 17 §1). `ACTIVE` billable and in pickers · `ARCHIVED` retired — excluded from new rate cards and from BIL-S05 generation, while its existing invoices stay readable and payable. Archiving is a state, never a delete.\n"
      },
      "RateUnit": {
        "type": "string",
        "enum": [
          "HOURLY",
          "DAILY"
        ],
        "description": "billing.rate_unit (db 17 §1). Pairs with the line quantity unit, so an `HOURLY` card never silently prices a day quantity.\n"
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": [
          "DRAFT",
          "SENT",
          "PAID",
          "VOID"
        ],
        "description": "billing.invoice_status (db 17 §2). `DRAFT` assembled and editable, commits nothing · `SENT` issued and a live receivable · `PAID` settled in full (terminal) · `VOID` cancelled (terminal) — **the number is retained and never reissued**. `VOID` is reachable from `DRAFT` and `SENT`; `SENT → PAID` is driven by the payment ledger, never by an operator.\n"
      },
      "InvoiceLineSource": {
        "type": "string",
        "enum": [
          "TIMESHEET",
          "MANUAL"
        ],
        "description": "billing.invoice_line_source (db 17 §2). `TIMESHEET` generated from **approved** hours via the projection (BIL-F03) · `MANUAL` added on the draft by Finance.\n"
      },
      "AgingBucket": {
        "type": "string",
        "enum": [
          "CURRENT",
          "DAYS_1_30",
          "DAYS_31_60",
          "DAYS_61_90",
          "DAYS_90_PLUS"
        ],
        "description": "**API-surface enum, not a DB enum** — the receivables aging read is an aggregate over `invoices` bucketed on `due_date`, with no materialized aging table (db 17 \"The downstream read\"). `CURRENT` = not yet due. Buckets render on `BIL-S03` as chips (amber ≤ 30 days, red > 30) and apply only to `SENT` rows with an outstanding balance.\n"
      },
      "ArchiveInput": {
        "type": "object",
        "description": "Optional note recorded with a status-flip action (audited, XC-F06).",
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "InvoiceTransitionInput": {
        "type": "object",
        "description": "Optional note carried on a maker-checker'd lifecycle transition (audited, XC-F06).",
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "ClientBillingAddress": {
        "type": "object",
        "description": "`clients.billing_address` jsonb (db 17 §1) — sparse and **print-oriented**: rendered onto the invoice header and PDF, never queried or joined.\n",
        "additionalProperties": false,
        "properties": {
          "line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO-3166 alpha-2; follows the issuing legal entity's pack country list."
          }
        }
      },
      "ClientTaxIds": {
        "type": "object",
        "description": "`clients.tax_ids` jsonb (db 17 §1) — market tax identifiers **keyed by kind**, so a tenant operating both markets holds both without a forked column or table. Format is **pack-validated in the service** (`XC-F01`), never a hardcoded per-country regex.\n",
        "additionalProperties": {
          "type": "string"
        },
        "properties": {
          "gstin": {
            "type": [
              "string",
              "null"
            ],
            "description": "India GSTIN (15 chars, pack format)."
          },
          "vat_no": {
            "type": [
              "string",
              "null"
            ],
            "description": "KSA VAT number (15 digits, pack format)."
          }
        }
      },
      "LegalEntityRef": {
        "type": "object",
        "description": "Minimal `org.legal_entities` projection (own read-model — `org` owns the write model, 00 §5). Supplies the invoice's currency, tax pack and number series.\n",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCodeRef"
          }
        }
      },
      "ProjectRef": {
        "type": "object",
        "description": "Minimal `work.projects` projection resolved **by event projection only** — `billing` holds no privilege on the `work` schema, so this is never a join (03-domain-modules §4).\n",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "GradeRef": {
        "type": "object",
        "description": "Minimal `org.grades` projection — the grade a rate override targets.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "EmployeeRef": {
        "type": "object",
        "description": "Minimal `people.employees` projection — the Finance user who keyed a payment.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "employee_no": {
            "$ref": "#/components/schemas/BusinessNoRef"
          },
          "full_name": {
            "type": "string"
          }
        }
      },
      "ClientRef": {
        "type": "object",
        "description": "Minimal `billing.clients` projection for embedding on an invoice header.",
        "readOnly": true,
        "properties": {
          "id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "display_name": {
            "type": "string"
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCodeRef"
          }
        }
      },
      "Client": {
        "description": "billing.clients — an organisation the tenant invoices (db 17 §1). **Deliberately thin**: it holds exactly what an invoice header needs. There is no contact, contact-person, phone, owner, account-manager, pipeline, stage or opportunity property here and **none may be added** — the rich account entity belongs to a future `sales` module (ADR 0026 §(d)).\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "display_name",
              "billing_address",
              "tax_ids",
              "currency_code",
              "status"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "display_name": {
                "type": "string",
                "minLength": 2,
                "maxLength": 200,
                "description": "The client's billing name, printed on the invoice. Unique per tenant. UTF-8; Arabic/RTL inline."
              },
              "billing_address": {
                "$ref": "#/components/schemas/ClientBillingAddress"
              },
              "tax_ids": {
                "$ref": "#/components/schemas/ClientTaxIds"
              },
              "currency_code": {
                "$ref": "#/components/schemas/CurrencyCodeRef",
                "description": "Must equal the currency of the legal entity that issues this client's invoices — defaulted from that entity, never freely typed.\n"
              },
              "status": {
                "$ref": "#/components/schemas/ClientStatus"
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "outstanding_amount": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived grid column: Σ (grand_total − amount_paid) over this client's `SENT` invoices (BIL-S01)."
              },
              "last_invoice_date": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived grid column: MAX(invoices.issue_date); null for a client with no invoice yet."
              }
            }
          }
        ]
      },
      "ClientCreate": {
        "type": "object",
        "required": [
          "display_name",
          "billing_address",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 200
          },
          "billing_address": {
            "$ref": "#/components/schemas/ClientBillingAddress"
          },
          "tax_ids": {
            "$ref": "#/components/schemas/ClientTaxIds"
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCodeRef"
          }
        }
      },
      "ClientUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 200
          },
          "billing_address": {
            "$ref": "#/components/schemas/ClientBillingAddress"
          },
          "tax_ids": {
            "$ref": "#/components/schemas/ClientTaxIds"
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCodeRef",
            "description": "Blocked once the client has invoices (409)."
          },
          "status": {
            "$ref": "#/components/schemas/ClientStatus",
            "description": "Prefer `billing.client.archive` for the retire path; provided here for reactivation."
          }
        }
      },
      "ClientPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          }
        ]
      },
      "BillingRate": {
        "description": "billing.client_billing_rates — an **effective-dated** billing rate for a client, optionally narrowed to a project and/or grade (db 17 §1). Generation resolves the rate **as-of the work date**, most-specific window wins.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "client_id",
              "rate",
              "rate_unit",
              "currency_code",
              "effective_from"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "client_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "project_ref": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Soft `ref→work.projects` (projection-resolved, never a join); null = every project."
              },
              "grade_ref": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Soft `ref→org.grades`; null = every grade."
              },
              "rate": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "The billed price per `rate_unit` — `> 0`, decimal, never float."
              },
              "rate_unit": {
                "$ref": "#/components/schemas/RateUnit"
              },
              "currency_code": {
                "$ref": "#/components/schemas/CurrencyCodeRef",
                "description": "Inherited from the client / issuing legal entity, never independently chosen."
              },
              "effective_from": {
                "$ref": "#/components/schemas/DateOnlyRef",
                "description": "Applies to work performed **on or after** this date."
              },
              "effective_to": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Inclusive end; null = open-ended (the normal case, and at most one per specificity)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "scope_label": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Derived precedence label for the `BIL-S02` *Scope* column — `Client base` / `Project: X` / `Grade: Y` / `Project X · Grade Y`.\n"
              },
              "in_force": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "Derived against the `as_of` date: true = In force, false with a future `effective_from` = Future, otherwise Superseded."
              },
              "project": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "grade": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/GradeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "BillingRateCreate": {
        "type": "object",
        "required": [
          "client_id",
          "rate",
          "effective_from"
        ],
        "additionalProperties": false,
        "properties": {
          "client_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "project_ref": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "grade_ref": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "rate": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "rate_unit": {
            "$ref": "#/components/schemas/RateUnit"
          },
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "supersede": {
            "type": "boolean",
            "default": false,
            "description": "Auto-close the predecessor window at `effective_from − 1 day` in the same transaction (the `BIL-S02` supersede ergonomic). `false` and an overlap exists → 409.\n"
          }
        }
      },
      "BillingRateUpdate": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "rate": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "rate_unit": {
            "$ref": "#/components/schemas/RateUnit"
          },
          "effective_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          }
        }
      },
      "BillingRateEndInput": {
        "type": "object",
        "required": [
          "effective_to"
        ],
        "additionalProperties": false,
        "properties": {
          "effective_to": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "Inclusive end date; must be `>= effective_from`."
          },
          "note": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "BillingRatePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BillingRate"
                }
              }
            }
          }
        ]
      },
      "RateOverlapProblem": {
        "description": "409 for the effective-window overlap guard — extends Problem with the conflicting window so the `BIL-S02` form error can name it rather than surfacing a raw constraint violation (db 17 §1).\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ProblemRef"
          },
          {
            "type": "object",
            "properties": {
              "conflicting_rate_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "conflicting_effective_from": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "conflicting_effective_to": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "specificity": {
                "type": "string",
                "description": "The `(client, project_ref, grade_ref)` tuple whose windows collide, rendered as the Scope label."
              }
            }
          }
        ]
      },
      "Invoice": {
        "description": "billing.invoices — one invoice issued by a legal entity to a client for a billed period (db 17 §2). Totals are **stored columns frozen at send**, not computed-on-read expressions, so a later rate or pack change can never restate an issued document. There is **no IRN, clearance status, signed-XML reference or QR property** anywhere on this model (ADR 0026 §(d)).\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "client_id",
              "legal_entity_ref",
              "status",
              "issue_date",
              "due_date",
              "currency_code",
              "period_from",
              "period_to",
              "subtotal",
              "tax_total",
              "grand_total",
              "amount_paid"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "invoice_no": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/BusinessNoRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "**`null` until `SENT`** — allocated from `invoice_sequences` inside the send transaction, so a discarded draft never burns a number. Once allocated it is **unique per legal entity, gapless and never reused** — a void keeps its number. `BIL-S03`/`BIL-S04` render a `DRAFT` as an em-dash, not an empty string. Read-model field, never a path key.\n"
              },
              "client_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "legal_entity_ref": {
                "$ref": "#/components/schemas/UuidRef",
                "description": "Soft `ref→org.legal_entities` — the issuing entity; supplies currency, tax pack and number series."
              },
              "status": {
                "$ref": "#/components/schemas/InvoiceStatus"
              },
              "issue_date": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "due_date": {
                "$ref": "#/components/schemas/DateOnlyRef",
                "description": "`>= issue_date`; drives the receivables aging buckets (BIL-F06)."
              },
              "currency_code": {
                "$ref": "#/components/schemas/CurrencyCodeRef",
                "description": "From `legal_entity_ref`, never typed."
              },
              "period_from": {
                "$ref": "#/components/schemas/DateOnlyRef",
                "description": "Start of the billed **work** period (not the document date)."
              },
              "period_to": {
                "$ref": "#/components/schemas/DateOnlyRef"
              },
              "subtotal": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "Σ `invoice_lines.amount`."
              },
              "tax_total": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "Σ `invoice_taxes.amount`."
              },
              "grand_total": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "`subtotal + tax_total`, exact because the operands are decimal; **frozen at send**."
              },
              "amount_paid": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "Maintained from `invoice_payments` — never hand-set; `<= grand_total` (the over-payment floor); `0` while `DRAFT`."
              },
              "sent_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "paid_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "voided_at": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/TimestampRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "void_reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "**Required** on `VOID` and null otherwise — a cancelled tax document must say why."
              },
              "pdf_document_ref": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Soft `ref→docs.documents` — the rendered PDF (BIL-F07 / ADR 0023); null until rendered. A void/reissue produces a new document, never an overwrite."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "outstanding_amount": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/MoneyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived `grand_total − amount_paid`; `0` for `PAID`, and **omitted (null) for `VOID`** — a void is not a receivable (BIL-S03)."
              },
              "aging_bucket": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/AgingBucket"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Derived; present **only** for `SENT` rows with an outstanding balance."
              },
              "issue_date_hijri": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/HijriDisplayRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "due_date_hijri": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/HijriDisplayRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "client": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ClientRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "legal_entity": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/LegalEntityRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "approval_pending": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "True while a send/void `INVOICE` request is open in the approvals inbox (XC-F12) — the *Awaiting approval* state on BIL-S04."
              },
              "granted_actions": {
                "type": "array",
                "readOnly": true,
                "items": {
                  "type": "string"
                },
                "description": "The `billing.*` tokens the caller actually holds for **this** invoice. `BIL-S04` renders its lifecycle CTA cluster from this, never from a role name guessed on the client.\n"
              }
            }
          }
        ]
      },
      "InvoiceUpdate": {
        "type": "object",
        "description": "`DRAFT` only — a `SENT` invoice is immutable (409).",
        "additionalProperties": false,
        "properties": {
          "issue_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "period_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "period_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          }
        }
      },
      "InvoiceVoidInput": {
        "type": "object",
        "required": [
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Persisted to `void_reason`; required because a cancelled tax document must say why."
          }
        }
      },
      "InvoiceGenerateInput": {
        "type": "object",
        "description": "`BIL-S05` wizard scope — step 1 (client, entity, projects, period) plus the step-3 unpriced-row decision.",
        "required": [
          "client_id",
          "legal_entity_ref",
          "period_from",
          "period_to",
          "issue_date",
          "due_date"
        ],
        "additionalProperties": false,
        "properties": {
          "client_id": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "`ACTIVE` clients only — archived clients are excluded from the picker."
          },
          "legal_entity_ref": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "Sets currency, tax pack and number series; its currency must match the client's (422 otherwise)."
          },
          "project_refs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UuidRef"
            },
            "description": "Soft `ref→work.projects`, projection-resolved. Omitted/empty = **all** the client's projects."
          },
          "period_from": {
            "$ref": "#/components/schemas/DateOnlyRef",
            "description": "The **work** period — drives both hour selection and rate as-of resolution."
          },
          "period_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "issue_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "due_date": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "exclude_unpriced": {
            "type": "boolean",
            "default": false,
            "description": "The **explicit, counted** choice from `BIL-S05` step 3 (\"Exclude N rows — they will not be billed on this invoice\"). `false` (the default) makes an unpriced row a blocking `422`; quietly dropping unpriced hours under-bills a client by an amount nobody ever sees.\n"
          }
        }
      },
      "InvoicePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          }
        ]
      },
      "InvoiceDetail": {
        "description": "`BIL-S04` document view — the invoice plus its lines, tax lines and payment ledger.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Invoice"
          },
          {
            "type": "object",
            "properties": {
              "lines": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoiceLine"
                }
              },
              "taxes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoiceTax"
                }
              },
              "payments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoicePayment"
                },
                "description": "Append-only ledger, newest value date first."
              }
            }
          }
        ]
      },
      "InvoiceApprovalAccepted": {
        "type": "object",
        "description": "202 envelope for a maker-checker'd transition raised into the unified approvals inbox as request type `INVOICE` (ADR 0027 §(e)). Authority is **not** transferred to the inbox: the outcome is applied by this module's transition endpoint under its own tokens (ADR 0027 §(d)).\n",
        "readOnly": true,
        "required": [
          "invoice_id",
          "approval_request_id",
          "request_type",
          "status"
        ],
        "properties": {
          "invoice_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "approval_request_id": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "The `xc.approval_inbox` row to deep-link to (XC-S15)."
          },
          "request_type": {
            "type": "string",
            "enum": [
              "INVOICE"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "AWAITING_APPROVAL"
            ]
          },
          "current_step": {
            "type": [
              "string",
              "null"
            ],
            "description": "Routing/escalation step label (XC-F16)."
          }
        }
      },
      "InvoiceRenderInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "locale": {
            "type": "string",
            "enum": [
              "en",
              "ar"
            ],
            "description": "Render locale; defaults to the legal entity's pack default (XC-F11). RTL mirrors for `ar`."
          },
          "force": {
            "type": "boolean",
            "default": false,
            "description": "Re-render even when `pdf_document_ref` is already populated (a new document, never an overwrite)."
          }
        }
      },
      "InvoiceRenderAccepted": {
        "type": "object",
        "description": "202 handle for the ADR 0023 server-side render job. Poll until `status=READY`, then fetch the artifact through the `docs` presigned, expiring URL (XC-F07) — bytes never transit this API.\n",
        "readOnly": true,
        "required": [
          "render_job_id",
          "status"
        ],
        "properties": {
          "render_job_id": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "status": {
            "type": "string",
            "enum": [
              "QUEUED",
              "RUNNING",
              "READY",
              "FAILED"
            ]
          },
          "document_ref": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UuidRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "`ref→docs.documents` — populated once the artifact lands; also stamped onto `invoices.pdf_document_ref`."
          },
          "download": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/FileDownloadRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Presigned, expiring handle, present only once the render is `READY`."
          }
        }
      },
      "InvoiceGenerationProblem": {
        "description": "422 for `billing.invoice.generate_from_timesheets`. Extends the standard field-level validation problem with `unpriced_rows` — **every** candidate row with no rate window in force on its work date, enumerated so `BIL-S05` can render each as a blocked row with a deep-link to `BIL-S02`. Rows are never dropped silently (BIL-F03).\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/ValidationProblemRef"
          },
          {
            "type": "object",
            "properties": {
              "unpriced_rows": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "work_date_from",
                    "work_date_to",
                    "quantity"
                  ],
                  "properties": {
                    "project_ref": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/UuidRef"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "project_name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "grade_ref": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/UuidRef"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "grade_name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "work_date_from": {
                      "$ref": "#/components/schemas/DateOnlyRef"
                    },
                    "work_date_to": {
                      "$ref": "#/components/schemas/DateOnlyRef"
                    },
                    "quantity": {
                      "$ref": "#/components/schemas/DecimalHoursRef",
                      "description": "Approved hours/days that would go unbilled."
                    }
                  }
                }
              },
              "unpriced_row_count": {
                "type": "integer",
                "description": "Count for the \"Exclude N rows\" confirm copy."
              }
            }
          }
        ]
      },
      "InvoiceLine": {
        "description": "billing.invoice_lines — one billable row (db 17 §2). `amount = quantity × unit_rate` is **stored, service-computed, never hand-typed**. The grade that selected the applied rate is recoverable through `rate_id → client_billing_rates.grade_ref` and deliberately not duplicated here.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "invoice_id",
              "source",
              "description",
              "quantity",
              "unit_rate",
              "amount"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "invoice_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "source": {
                "$ref": "#/components/schemas/InvoiceLineSource"
              },
              "project_ref": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Soft `ref→work.projects` (projection-resolved) — required for `TIMESHEET` lines."
              },
              "period_from": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Start of the work period this line covers — required for `TIMESHEET` lines."
              },
              "period_to": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/DateOnlyRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "description": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "The line as the client reads it."
              },
              "quantity": {
                "$ref": "#/components/schemas/DecimalHoursRef",
                "description": "`> 0` — hours or days per the applied `rate_unit`; numeric(9,2) decimal, never float."
              },
              "unit_rate": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "The price applied per unit (`>= 0`)."
              },
              "amount": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "`quantity × unit_rate` — stored, not computed on read; never hand-typed."
              },
              "rate_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "`fk→client_billing_rates` — **which card priced this line**; null for a MANUAL line that cites none."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "project": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ProjectRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "rate_scope_label": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The winning rate window's Scope label, so pricing stays explainable inline (`BIL-S05` step 3)."
              }
            }
          }
        ]
      },
      "InvoiceLineCreate": {
        "type": "object",
        "description": "A `MANUAL` line added on a DRAFT invoice. `amount` is computed by the service and is not accepted here.",
        "required": [
          "description",
          "quantity",
          "unit_rate"
        ],
        "additionalProperties": false,
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "quantity": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "unit_rate": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "project_ref": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "period_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "period_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "rate_id": {
            "$ref": "#/components/schemas/UuidRef",
            "description": "Optional pricing provenance — a manual line may legitimately cite the card it was priced from."
          }
        }
      },
      "InvoiceLineUpdate": {
        "type": "object",
        "description": "`DRAFT` only. `amount` is recomputed by the service and is not accepted here.",
        "additionalProperties": false,
        "properties": {
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "quantity": {
            "$ref": "#/components/schemas/DecimalHoursRef"
          },
          "unit_rate": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "project_ref": {
            "$ref": "#/components/schemas/UuidRef"
          },
          "period_from": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "period_to": {
            "$ref": "#/components/schemas/DateOnlyRef"
          }
        }
      },
      "InvoiceTax": {
        "description": "billing.invoice_taxes — a percentage tax line (db 17 §2). India GST and KSA VAT are the same mechanism with different pack-supplied values; the `label` is **stamped, not derived at render time**, which is what lets a two-year-old invoice print the rate it was actually taxed at.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "invoice_id",
              "label",
              "rate_percent",
              "amount"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "invoice_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "description": "The line as printed, e.g. `GST 18%` / `VAT 15%` — **from the pack (XC-F01), never hardcoded copy**."
              },
              "rate_percent": {
                "$ref": "#/components/schemas/RateRef",
                "description": "numeric(9,6) applied rate, `>= 0`, as a string — never a float."
              },
              "amount": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "The computed tax amount in the invoice currency; rounding follows the pack."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          }
        ]
      },
      "InvoiceTaxCreate": {
        "type": "object",
        "description": "Values are resolved from the legal entity's bound compliance pack (`XC-F01`) and stamped onto the row; the client supplies which pack tax to apply, not an invented label/rate pair.\n",
        "required": [
          "label",
          "rate_percent"
        ],
        "additionalProperties": false,
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "rate_percent": {
            "$ref": "#/components/schemas/RateRef"
          }
        }
      },
      "InvoicePayment": {
        "description": "billing.invoice_payments — **append-only** (db 17 §3): no update, no delete, no `version`, `app_rw` holds SELECT/INSERT only. Corrections are **reversal rows**, never edits — a reversal carries a **positive** `amount` and is identified by `reversal_of_payment_id`, so no reader has to infer sign semantics.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "invoice_id",
              "amount",
              "paid_on",
              "recorded_by"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "invoice_id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "amount": {
                "$ref": "#/components/schemas/MoneyRef",
                "description": "`> 0` **including reversal rows**; in the invoice's currency, decimal."
              },
              "paid_on": {
                "$ref": "#/components/schemas/DateOnlyRef",
                "description": "The **value** date (when the money moved), not the entry date; `>= invoices.issue_date`."
              },
              "reference": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 100,
                "description": "Free-text bank / UTR / cheque reference, keyed by hand."
              },
              "recorded_by": {
                "$ref": "#/components/schemas/UuidRef",
                "description": "Soft `ref→people.employees` — the Finance user who keyed it."
              },
              "reversal_of_payment_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/UuidRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Set on a **reversal** row (amount equals the reversed row's); a payment may be reversed exactly once."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AppendOnlyMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "recorded_by_employee": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/EmployeeRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "paid_on_hijri": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/HijriDisplayRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "InvoicePaymentCreate": {
        "type": "object",
        "description": "Amount must be `> 0` and `<= outstanding` — **over-payment is rejected (422), never clamped**. Partials are normal. Set `reversal_of_payment_id` to correct a mis-keyed row; the original survives untouched, because someone believed the money arrived on that day for that reference and the ledger has to keep saying so.\n",
        "required": [
          "amount",
          "paid_on"
        ],
        "additionalProperties": false,
        "properties": {
          "amount": {
            "$ref": "#/components/schemas/MoneyRef"
          },
          "paid_on": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "reference": {
            "type": "string",
            "maxLength": 100
          },
          "reversal_of_payment_id": {
            "$ref": "#/components/schemas/UuidRef"
          }
        }
      },
      "InvoicePaymentResult": {
        "description": "The recorded ledger row plus the invoice state it drove, so `BIL-S04` refreshes both panels in one round trip.",
        "allOf": [
          {
            "$ref": "#/components/schemas/InvoicePayment"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "invoice_status": {
                "$ref": "#/components/schemas/InvoiceStatus"
              },
              "invoice_amount_paid": {
                "$ref": "#/components/schemas/MoneyRef"
              },
              "invoice_outstanding_amount": {
                "$ref": "#/components/schemas/MoneyRef"
              },
              "settled": {
                "type": "boolean",
                "description": "True when this entry took the outstanding balance to nil — the invoice moved `SENT → PAID` and `billing.invoice.paid` was emitted."
              }
            }
          }
        ]
      },
      "InvoicePaymentPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoicePayment"
                }
              }
            }
          }
        ]
      },
      "AgingBucketSummary": {
        "type": "object",
        "readOnly": true,
        "required": [
          "bucket",
          "invoice_count",
          "outstanding_amount"
        ],
        "properties": {
          "bucket": {
            "$ref": "#/components/schemas/AgingBucket"
          },
          "invoice_count": {
            "type": "integer",
            "minimum": 0
          },
          "outstanding_amount": {
            "$ref": "#/components/schemas/MoneyRef"
          }
        }
      },
      "AgingSummary": {
        "type": "object",
        "description": "The `BIL-S03` aging strip. An aggregate over `invoices` — `status='SENT'`, `grand_total − amount_paid > 0`, bucketed on `due_date` — with **no materialized aging table**, so the numbers can never disagree with the invoices they summarise (db 17). Chip counts and the grid must always agree, which is why the same filter vocabulary (`AgingBucket`) drives both.\n",
        "readOnly": true,
        "required": [
          "as_of",
          "buckets",
          "total_outstanding",
          "invoice_activity_count"
        ],
        "properties": {
          "as_of": {
            "$ref": "#/components/schemas/DateOnlyRef"
          },
          "buckets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgingBucketSummary"
            },
            "description": "One entry per `AgingBucket`, in order Current → 90+."
          },
          "total_outstanding": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MoneyRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null when issued invoices span multiple currencies; use by_group totals per currency."
          },
          "invoice_activity_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Count of issued invoice rows (SENT",
            "PAID": null,
            "or VOID)": null,
            "including fully settled history.": null
          },
          "by_group": {
            "type": "array",
            "description": "Per-client or per-legal-entity breakdown, per the `group_by` parameter.",
            "items": {
              "type": "object",
              "required": [
                "group_id",
                "buckets",
                "total_outstanding"
              ],
              "properties": {
                "group_id": {
                  "$ref": "#/components/schemas/UuidRef"
                },
                "group_label": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "currency_code": {
                  "$ref": "#/components/schemas/CurrencyCodeRef"
                },
                "buckets": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AgingBucketSummary"
                  }
                },
                "total_outstanding": {
                  "$ref": "#/components/schemas/MoneyRef"
                }
              }
            }
          }
        }
      },
      "InvoiceSequence": {
        "description": "billing.invoice_sequences — the **per-legal-entity invoice number series** (db 17 §2). It *produces* `invoices.invoice_no` as a value and owns no rows. Concurrent issues against one entity serialize on this row — an accepted, bounded cost at invoice volumes, recorded so it is not later mistaken for a defect.\n",
        "allOf": [
          {
            "type": "object",
            "required": [
              "id",
              "legal_entity_ref",
              "prefix",
              "next_no"
            ],
            "properties": {
              "id": {
                "$ref": "#/components/schemas/UuidRef"
              },
              "legal_entity_ref": {
                "$ref": "#/components/schemas/UuidRef",
                "description": "Soft `ref→org.legal_entities` — the entity whose series this is. Unique with `prefix` per tenant."
              },
              "prefix": {
                "type": "string",
                "maxLength": 32,
                "description": "Human-readable series prefix (e.g. `INV-`, `GIT/25-26/`); may embed a fiscal-year token."
              },
              "next_no": {
                "type": "integer",
                "minimum": 1,
                "description": "The next number to hand out; incremented **inside** the invoice insert transaction."
              }
            }
          },
          {
            "$ref": "#/components/schemas/AuditMetaRef"
          },
          {
            "type": "object",
            "readOnly": true,
            "properties": {
              "legal_entity": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/LegalEntityRef"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        ]
      },
      "InvoiceSequenceUpdate": {
        "type": "object",
        "description": "`next_no` may only move **forward** (409 on a decrease) — it is never rewound and a number is never reused. Rolling a series at year end is a **new row with a new prefix**, not a rewind.\n",
        "additionalProperties": false,
        "properties": {
          "prefix": {
            "type": "string",
            "maxLength": 32
          },
          "next_no": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "InvoiceSequencePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CursorPageRef"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/InvoiceSequence"
                }
              }
            }
          }
        ]
      },
      "Uuid": {
        "type": "string",
        "format": "uuid",
        "description": "UUIDv7 surrogate primary key (db-docs/00 §3). Never the business identifier."
      },
      "DateOnly": {
        "type": "string",
        "format": "date",
        "description": "Calendar-only value (pay-period date, leave date, due date)."
      },
      "Timestamp": {
        "type": "string",
        "format": "date-time",
        "description": "timestamptz, serialized UTC ISO-8601. Presentation timezone is a client concern."
      },
      "HijriDisplay": {
        "type": "string",
        "readOnly": true,
        "description": "Formatted Umm al-Qura display string (`*_hijri`, db-docs/00 §6) accompanying a canonical Gregorian value on KSA-facing read-models (GOSI/WPS periods, Iqama expiry, KSA payslips). NEVER the source of truth; never accepted as input.\n"
      },
      "DecimalHours": {
        "type": "string",
        "pattern": "^-?\\d+(\\.\\d{1,2})?$",
        "description": "numeric(9,2) decimal hours/days as a string (OT hours, leave days). Never a float."
      },
      "Rate": {
        "type": "string",
        "pattern": "^-?\\d+(\\.\\d{1,6})?$",
        "description": "numeric(9,6) fraction as a string, e.g. \"0.120000\" for the 12% EPF rate. Never a float; percentages are stored as fractions."
      },
      "Money": {
        "type": "object",
        "description": "Exact decimal money (db-docs/00 §6). `amount` is a STRING so float never enters the wire.",
        "required": [
          "amount",
          "currency_code"
        ],
        "additionalProperties": false,
        "properties": {
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d{1,2})?$",
            "description": "numeric(18,2) as a string, e.g. \"45000.00\"."
          },
          "currency_code": {
            "$ref": "#/components/schemas/CurrencyCode"
          }
        }
      },
      "CurrencyCode": {
        "type": "string",
        "enum": [
          "INR",
          "SAR"
        ],
        "description": "ISO-4217. A legal entity operates in a single currency (db-docs/00 §6)."
      },
      "BusinessNo": {
        "type": "string",
        "description": "Tenant-unique, prefixed human reference (`employee_no`, `requisition_no`, `offer_no`, `payslip_no`, `claim_no`, `ticket_no`, `asset_no`, `case_no`). Read-model field; never a path key.\n"
      },
      "FileDownload": {
        "type": "object",
        "description": "Authorized file handle (db-docs/00 §14, xc.files). Bytes never transit the API — the backend mints a time-limited presigned URL after authorization. Clients never see storage keys or hold storage credentials; the presigned URL is never persisted.\n",
        "required": [
          "file_id",
          "file_name",
          "url",
          "expires_at"
        ],
        "properties": {
          "file_id": {
            "$ref": "#/components/schemas/Uuid"
          },
          "file_name": {
            "type": "string"
          },
          "mime_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "content_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "SHA-256",
            "present where tamper-evidence matters (payslips": null,
            "letters": null,
            "e-sign artifacts).": null
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Time-limited presigned URL."
          },
          "expires_at": {
            "$ref": "#/components/schemas/Timestamp"
          }
        }
      },
      "AuditMeta": {
        "type": "object",
        "description": "Standard mutable-entity columns (db-docs/00 §5). Read-only; present on every mutable read-model.",
        "readOnly": true,
        "properties": {
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "created_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "null = system/jobs"
          },
          "updated_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "updated_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ]
          },
          "deleted_at": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Timestamp"
              },
              {
                "type": "null"
              }
            ],
            "description": "soft-delete marker; live rows are null. Deleted rows are excluded by default scope."
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-lock counter (where present); surfaces as the ETag."
          }
        }
      },
      "AppendOnlyMeta": {
        "type": "object",
        "description": "Standard append-only/immutable columns (db-docs/00 §5/§8). No update/version/delete; corrections are new compensating rows.",
        "readOnly": true,
        "properties": {
          "created_at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "created_by": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Uuid"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "CursorPage": {
        "type": "object",
        "description": "Generic cursor-pagination envelope. List operations compose it via allOf to type `data`, e.g. `allOf: [ {$ref CursorPage}, { properties: { data: { items: {$ref Employee} } } } ]`.\n",
        "required": [
          "data",
          "page"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "page": {
            "type": "object",
            "required": [
              "has_more"
            ],
            "properties": {
              "next_cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prev_cursor": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "has_more": {
                "type": "boolean"
              },
              "total_est": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Optional, capped, APPROXIMATE row estimate for grid \"X of Z\" display only — never an exact COUNT(*) on large tables (attend.attendance_records, xc.notifications, audit.*).\n"
              }
            }
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail (application/problem+json). The platform-wide error envelope (03 §1).",
        "required": [
          "type",
          "title",
          "status"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "default": "about:blank",
            "description": "Problem-type URI."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary (stable per type)."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, duplicated for convenience."
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation specific to this occurrence."
          },
          "instance": {
            "type": "string",
            "description": "URI reference for this specific occurrence."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "correlation_id": {
            "$ref": "#/components/schemas/Uuid"
          }
        }
      },
      "ValidationProblem": {
        "description": "422 field-level validation failure; extends Problem with a per-field error array. `detail` is ALWAYS present on a 422 (#1251) and is the human summary of `errors[]`: one offending field renders as `\"<field>: <its message>\"` (`withholding_amount: is required for an India entity`), several as `\"N fields were refused: a, b, c.\"`, capped at five names. It is display copy derived from members already in the same body — clients keep branching on `code` and mapping `errors[].pointer` back to a control, never parsing this sentence.\n",
        "allOf": [
          {
            "$ref": "#/components/schemas/Problem"
          },
          {
            "type": "object",
            "required": [
              "detail",
              "errors"
            ],
            "properties": {
              "detail": {
                "type": "string",
                "description": "Human summary of `errors[]`, always populated on a 422 so a client never has to fall back to generic copy for the one status that names a fixable field.\n",
                "example": "withholding_amount: is required for an India entity"
              },
              "errors": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "pointer",
                    "rule"
                  ],
                  "properties": {
                    "pointer": {
                      "type": "string",
                      "description": "JSON Pointer to the offending field, e.g. /claim_amount"
                    },
                    "rule": {
                      "type": "string",
                      "enum": [
                        "required",
                        "format",
                        "length",
                        "range",
                        "cross-field",
                        "async-server",
                        "consent-gated",
                        "uniqueness-business",
                        "not_found"
                      ],
                      "description": "FSD validation taxonomy rule (fsd-docs/00 §8.2). `not_found` is the server-side-lookup arm: a body field that REFERENCES another resource (e.g. `project_id` on a work entry) and did not resolve for this caller. It is reported here, under the field's pointer, and NOT as a 404 — the request addresses its own resource, so the failure belongs on the form field the client can actually fix (#805).\n"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "ErrorCode": {
        "type": "string",
        "description": "Machine-stable error code (paired with the HTTP status; drives client handling, not display copy).",
        "enum": [
          "VALIDATION_FAILED",
          "IDEMPOTENCY_KEY_REUSE",
          "VERSION_CONFLICT",
          "PRECONDITION_REQUIRED",
          "MAKER_EQUALS_CHECKER",
          "STEP_UP_REQUIRED",
          "CONSENT_REQUIRED",
          "TOKEN_DENIED",
          "SCOPE_DENIED",
          "OWNERSHIP_DENIED",
          "STATE_TRANSITION_INVALID",
          "FACE_MISMATCH",
          "FACE_VERIFICATION_REQUIRED",
          "WITHHOLDING_REVIEW_REQUIRED",
          "FEATURE_NOT_IN_PLAN",
          "PLAN_LIMIT_EXCEEDED",
          "TENANT_PAST_DUE",
          "TENANT_SUSPENDED",
          "TENANT_CANCELLED",
          "RATE_LIMITED",
          "NOT_FOUND"
        ]
      }
    },
    "parameters": {
      "PathId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "UUIDv7 surrogate key of the target resource. Business numbers (`employee_no`, `claim_no`, …) are read-model fields, never path keys.",
        "schema": {
          "$ref": "#/components/schemas/Uuid"
        }
      },
      "PageSize": {
        "name": "page[size]",
        "in": "query",
        "required": false,
        "description": "Max items per page. Cursor pagination only (03 §2); offset pagination is rejected (ADR 0015).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "PageAfter": {
        "name": "page[after]",
        "in": "query",
        "required": false,
        "description": "Opaque forward keyset cursor (from a prior page's `page.next_cursor`).",
        "schema": {
          "type": "string"
        }
      },
      "PageBefore": {
        "name": "page[before]",
        "in": "query",
        "required": false,
        "description": "Opaque backward keyset cursor (from a prior page's `page.prev_cursor`).",
        "schema": {
          "type": "string"
        }
      },
      "SortParam": {
        "name": "sort",
        "in": "query",
        "required": false,
        "description": "Comma-separated sort keys; leading `-` = descending. Each key MUST be in the operation's documented sort whitelist (free-form sort is rejected so the keyset cursor stays stable).\n",
        "schema": {
          "type": "string"
        }
      },
      "AcceptLanguage": {
        "name": "Accept-Language",
        "in": "header",
        "required": false,
        "description": "Locale for server-rendered/localized text (LocalizedText resolution, letters, notifications). Active locale set comes from the legal entity's compliance pack; KSA tenants default `ar`.\n",
        "schema": {
          "type": "string",
          "enum": [
            "en",
            "ar"
          ],
          "default": "en"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Client-generated unique key. REQUIRED on every mutation (this round tightens ADR 0015's \"platform + retryable mutations\" floor to ALL mutations for uniformity — offline punch/leave sync depends on it). Scoped (tenant, principal, route, key); a replay within the ~24h window returns the stored response with `Idempotency-Replayed: true`; the same key with a different body → 409 IDEMPOTENCY_KEY_REUSE (04 §1).\n",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 255
        }
      },
      "IfMatch": {
        "name": "If-Match",
        "in": "header",
        "required": true,
        "description": "Optimistic-concurrency precondition for mutating a VERSIONED mutable entity (db-docs/00 §5 applies `version` where concurrent edits are likely). Value is the entity's current ETag (the row `version`). Absent → 428; stale → 412 (04 §2). N/A for append-only entities and for unversioned low-contention entities (their update ops simply omit this parameter).\n",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or invalid session token (no authenticated principal).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated but denied — permission token not granted, out of scope (self/team/branch), not the owner, tenant suspended, or an MC-2 operation without a fresh step-up challenge. `code` ∈ TOKEN_DENIED | SCOPE_DENIED | OWNERSHIP_DENIED | MAKER_EQUALS_CHECKER | STEP_UP_REQUIRED | CONSENT_REQUIRED | TENANT_SUSPENDED. A plan feature-flag being off is 402 FEATURE_NOT_IN_PLAN, not 403 (see PaymentRequired).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource does not exist OR is masked by RLS (tenant/self/team/branch scope) — the API does not distinguish, so existence is never confirmed across a scope boundary (02 §4 disclosure posture).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Conflict": {
        "description": "Idempotency-key reuse with a different body, an invalid state transition, or a uniqueness/business-rule violation (fsd 00 §8.2). For database-backed uniqueness rules, detail names the exact violated rule; unmapped constraints are not collapsed into a generic 409.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Field-level validation failed. The body carries both `errors[]` (per-field, pointer-addressed) and a `detail` summarising them — never an empty `detail` (#1251).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationProblem"
            },
            "examples": {
              "singleField": {
                "summary": "One refused field — `detail` names it",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "withholding_amount: is required for an India entity",
                  "errors": [
                    {
                      "pointer": "/withholding_amount",
                      "rule": "required",
                      "message": "is required for an India entity"
                    }
                  ]
                }
              },
              "severalFields": {
                "summary": "Several refused fields — `detail` counts and names them",
                "value": {
                  "type": "about:blank",
                  "title": "Unprocessable Entity",
                  "status": 422,
                  "code": "VALIDATION_FAILED",
                  "detail": "2 fields were refused: effective_from, cap_amount.amount.",
                  "errors": [
                    {
                      "pointer": "/effective_from",
                      "rule": "cross-field",
                      "message": "Overlaps an existing version."
                    },
                    {
                      "pointer": "/cap_amount/amount",
                      "rule": "range",
                      "message": "Must be a non-negative decimal string."
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "Locked": {
        "description": "Tenant subscription `past_due` (ADR 0009): WRITES are blocked (423), reads still succeed. `code` = TENANT_PAST_DUE. Mutations return this; list/get operations do not.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Per-principal/route rate limit exceeded.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PreconditionRequired": {
        "description": "If-Match header absent on a mutation of a versioned mutable entity (428).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PreconditionFailed": {
        "description": "If-Match / ETag mismatch — the row changed since it was read (412, VERSION_CONFLICT).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "headers": {
      "ETag": {
        "description": "Strong validator of the row `version` (e.g. `\"v7\"`). Use as `If-Match` on the next mutation.",
        "schema": {
          "type": "string"
        }
      },
      "Location": {
        "description": "URI of the created/affected resource.",
        "schema": {
          "type": "string",
          "format": "uri-reference"
        }
      },
      "IdempotencyReplayed": {
        "description": "`true` when a stored idempotent response was replayed rather than freshly computed.",
        "schema": {
          "type": "boolean"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (on 429 / 503).",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}