{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/frihet-io/json-schema/frihet-io-invoice-update-preconditions-schema.json",
  "title": "InvoiceUpdatePreconditions",
  "description": "Optional preconditions for PUT and PATCH /v1/invoices/{id}. Rejected on\nPOST /v1/invoices. Evaluated inside the update transaction against the live\ndocument, before any write or numbering side effect. They are never persisted\nand never returned. Omitting both leaves behaviour unchanged.\n",
  "x-generated": "2026-11-09",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/frihet-io-openapi.json#/components/schemas/InvoiceUpdatePreconditions",
  "type": "object",
  "properties": {
    "ifStatus": {
      "type": "string",
      "enum": [
        "draft",
        "sent",
        "partial",
        "paid",
        "overdue",
        "cancelled"
      ],
      "writeOnly": true,
      "description": "Expected current status. A mismatch returns 409\nINVOICE_STATUS_PRECONDITION_FAILED. With `draft` it also fails when the\ninvoice already carries issuance evidence. The list is closed: a VeriFactu\nhash, rectificativa numbering authority, sentAt, or any of faceStatus,\nfaceNumeroRegistro, faceSubmittedAt, tbaiStatus, tbaiCsv. Anything else is not\nchecked; in particular `eInvoice.sdiSubmission` (Italian SDI) and `ksefStatus`\nand related fields (Polish KSeF) do not block, so a draft with only those\nstill passes `ifStatus` draft.\nIt guards the lifecycle, for example a draft to sent promotion against a\nconcurrent promotion. It does NOT serialize two edits of the same draft:\nconcurrent writers that all send ifStatus draft all succeed and the last\none wins. ifUpdatedAt narrows that window only partly (see its limitation).\n"
    },
    "ifUpdatedAt": {
      "type": "string",
      "maxLength": 64,
      "writeOnly": true,
      "description": "The `updatedAt` value of the invoice, as returned by a previous GET.\nIt is an exact string comparison, not a date comparison (surrounding\nwhitespace is trimmed first): an equivalent instant in another format (no\nmilliseconds, a +01:00 offset) returns 409.\nA value that is not shaped like an ISO 8601 timestamp (date, time and zone;\nfor example month 13) is a 400, so a legacy updatedAt in another format\ncannot be echoed. A well-shaped value that is not a real calendar date, such\nas 2026-02-31T10:00:00Z, is not a 400: it never equals a stored token and\nreturns 409.\nLimitation: this is NOT a general guard against concurrent edits.\n`updatedAt` is not a complete revision counter. It detects only writes that\nraise `updatedAt`. Edits made from the Frihet app or the in-app assistant\nmay not raise it (the app writes the invoice as is, without refreshing\n`updatedAt`), and Payment Authority settlements, invoice amendments and\ndeposits do not either. A match therefore proves only that no\nupdatedAt-raising write happened since the token was read; it can still be\n200 after a person edited the same draft in the app, and an older write can\nrestore an earlier token. An invoice that has no stored `updatedAt` has no\ntoken: GET returns none and every ifUpdatedAt returns 409 with\n`actualUpdatedAt` null. Pair ifUpdatedAt with ifStatus for lifecycle\nsafety; do not rely on it to protect edits.\n"
    }
  }
}