{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.kodemed.ch/_common/CodingResult.schema.json",
  "title": "KodeMed Coding Result",
  "description": "Per-case grouper output + coded code list. Surfaced by every flow that returns coding output: HIS REST webhook payload (after Apply), WebSocket `case.coded` / `grouping.refreshed` message, COM `GetResults()` return inside each `cases[]` entry. Same shape everywhere — partners write ONE parser, drift fails the uniformity test (#640).\n\n## Field coverage\n\nThe field set matches the DLL's existing `CodingResults.CodingResult` class (`KodeMed.Interface/Models/CodingResults.cs:393-559`) so every datum the COM partners already receive can ride the canonical envelope unchanged. Where the DLL had unstructured `List<string>` fields (`secondaryDiagnoses`, `procedures`), the canonical uses structured objects (code + catalog + flags) so a single parser works for every transport.",
  "type": "object",
  "_comment_required": "Pre-2026-06-02 review: diagnoses + procedures were in required. Audit (CRITICAL #1) showed OutboundWebhookDispatcher emits {caseId} only — partners enabling validation reject every webhook. Reality: when the grouper hasn't (yet) run on a case, diagnoses/procedures are not yet populated; the schema must accept that intermediate state. The full canonical contract is preserved in the property descriptions; partners that need the arrays still get them when the grouper has run. caseId stays required (it IS the per-case correlator with SPIGES <Fall fall_id>).",
  "required": ["caseId"],
  "properties": {
    "caseId": {
      "$ref": "OpaqueIdentifier.schema.json",
      "description": "Case identifier echoed from the SPIGES `<Fall fall_id>` attribute. NOT a UUID — SPIGES allows the institution to use its own scheme (numeric, alphanumeric, prefixed). $ref OpaqueIdentifier (non-blank string ≤100) instead of Identifier (strict UUID)."
    },
    "tariff": {
      "type": ["string", "null"],
      "enum": ["SWISSDRG", "TARPSY", "STREHA", "SPLG", null],
      "description": "Tariff system whose grouper produced this result. `SWISSDRG` (acute somatic), `TARPSY` (psychiatric), `STREHA` (rehabilitation), `SPLG` (planned — Spitalplanungsleistungsgrouper). Null when no grouper ran."
    },
    "drg": {
      "type": ["string", "null"],
      "description": "Final group code from the active grouper — DRG (SwissDRG), PCG (TARPSY), RCG (ST-Reha), SPLG group. Null if grouping was not run or no code could be assigned.",
      "examples": ["G22C", "P67D", "TP21A", "TR01A"]
    },
    "drgDescription": {
      "type": ["string", "null"],
      "description": "Localised description of the assigned group code."
    },
    "mdc": {
      "type": ["string", "null"],
      "description": "Major Diagnostic Category from the grouper. Null when no DRG assigned or for tariffs without MDC."
    },
    "partition": {
      "type": ["string", "null"],
      "description": "Medical / Surgical / Other partition flag from the grouper."
    },
    "pccl": {
      "type": ["integer", "null"],
      "minimum": 0,
      "maximum": 4,
      "description": "Patient Clinical Complexity Level (0–4 for SwissDRG). Null if not computed or not applicable (TARPSY, ST-Reha)."
    },
    "costWeight": {
      "type": ["number", "null"],
      "minimum": 0,
      "description": "Cost weight assigned by the grouper. For SwissDRG this is the effective cost weight (CWeff) after LOS adjustments — equivalent to `effectiveCostWeight`. Provided as a top-level convenience for partners that only need the headline figure."
    },
    "baseCostWeight": {
      "type": ["number", "null"],
      "minimum": 0,
      "description": "Base cost weight from the catalogue, BEFORE LOS adjustments. Null for tariffs that don't expose this split."
    },
    "effectiveCostWeight": {
      "type": ["number", "null"],
      "minimum": 0,
      "description": "Effective cost weight after LOS / outlier adjustments. For SwissDRG this typically equals `costWeight`."
    },
    "los": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Length of stay (days) used by the grouper. For TARPSY / ST-Reha this drives per-diem calculation."
    },
    "ltp": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Lower trim point (days). Stays below LTP get a discount."
    },
    "htp": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Upper trim point (days). Stays above HTP get a per-day outlier supplement."
    },
    "alos": {
      "type": ["number", "null"],
      "minimum": 0,
      "description": "Catalogue average length of stay (days) for the assigned group."
    },
    "dayRate": {
      "type": ["number", "null"],
      "minimum": 0,
      "description": "Per-day rate applied to outlier days (above HTP) or to the entire per-diem stay (TARPSY / ST-Reha)."
    },
    "transferDiscount": {
      "type": ["boolean", "null"],
      "description": "Whether the grouper applied a transfer discount (patient transferred to / from another hospital)."
    },
    "diagnoses": {
      "type": "array",
      "description": "Ordered list of diagnoses on the case, with the Hauptdiagnose first. Structured (NOT a plain string list) so partners get the dagger/asterisk pairing + POA flag in one parse.",
      "items": {
        "type": "object",
        "required": ["code", "catalog"],
        "additionalProperties": false,
        "properties": {
          "code":            { "type": "string", "examples": ["E11.9", "G30.0", "K35.2"] },
          "catalog":         { "type": "string", "enum": ["ICD-10-GM", "ICD-10-WHO"] },
          "isHauptdiagnose": { "type": "boolean" },
          "isDagger":        { "type": "boolean" },
          "isAsterisk":      { "type": "boolean" },
          "isExclamation":   { "type": "boolean" },
          "zusatz":          { "type": ["string", "null"], "description": "Paired dagger code if this row is an asterisk." },
          "poa":             { "type": ["string", "null"], "description": "Present-on-Admission flag (Y/N/U/W)." }
        }
      }
    },
    "procedures": {
      "type": "array",
      "description": "Ordered list of CHOP procedures on the case.",
      "items": {
        "type": "object",
        "required": ["code", "catalog"],
        "additionalProperties": false,
        "properties": {
          "code":       { "type": "string", "examples": ["00.66.00", "39.95.21", "47.01"] },
          "catalog":    { "type": "string", "enum": ["CHOP"] },
          "performedAt": { "$ref": "Timestamp.schema.json" },
          "laterality": { "type": ["string", "null"], "enum": ["L", "R", "B", null], "description": "Left / Right / Bilateral when the code requires it." }
        }
      }
    },
    "supplements": {
      "type": "array",
      "description": "Additional payments (Zusatzentgelte / supplements) the grouper attached to this case. Each carries the ZE code, localised description, monetary amount, and count.",
      "items": {
        "type": "object",
        "required": ["code", "amount"],
        "additionalProperties": false,
        "properties": {
          "code":        { "type": "string", "examples": ["ZE-2026-01.01", "RZE-2026-02.15"] },
          "description": { "type": ["string", "null"] },
          "amount":      { "type": "number", "minimum": 0, "description": "Amount in CHF." },
          "count":       { "type": "integer", "minimum": 0 }
        }
      }
    },
    "warnings": {
      "type": "array",
      "description": "Non-fatal validation findings from grouping or plausibility checks. Structured (code + message + severity) so partners can filter / locale-translate.",
      "items": {
        "type": "object",
        "required": ["code", "message"],
        "additionalProperties": false,
        "properties": {
          "code":     { "type": "string", "examples": ["AGE_RESTRICTION", "POA_REQUIRED"] },
          "message":  { "type": "string" },
          "severity": { "type": "string", "enum": ["INFO", "WARNING", "ERROR"] }
        }
      }
    },
    "flags": {
      "type": "array",
      "description": "Free-form grouper flags (status codes / warnings the grouper itself emits — kept as raw strings since each tariff defines its own catalogue).",
      "items": { "type": "string" }
    },
    "grouperVersion": {
      "type": ["string", "null"],
      "description": "Version of the grouper that produced this result.",
      "examples": ["SwissDRG-15.0", "TARPSY-6.0", "ST-Reha-3.0"]
    },
    "grouperStatus": {
      "type": ["string", "null"],
      "description": "Grouper status / return code (per-tariff)."
    },
    "groupingSuccess": {
      "type": ["boolean", "null"],
      "description": "Whether grouping succeeded. False / null when grouping failed; see `grouperStatus` + `flags` + `warnings` for diagnostics."
    },
    "groupingError": {
      "type": ["string", "null"],
      "description": "Error message if grouping failed. Null when successful."
    },
    "groupedAt": {
      "$ref": "Timestamp.schema.json",
      "description": "When the grouper produced this result (server time). Independent of the session lifecycle timestamps in the envelope."
    }
  },
  "additionalProperties": false
}
