{
  "openapi": "3.0.3",
  "info": {
    "title": "Agrotest API",
    "version": "1.0.0",
    "description": "Read-only API системи Agrotest.\n\nAPI призначений **лише для читання**: створення, зміна, видалення та імпорт Shapefile через нього не виконуються.\n\n## Авторизація\n\nУсі методи, крім `/v1/health` і документації, вимагають заголовок:\n\n```http\nAuthorization: Bearer <api_key>\n```\n\nНатисніть **Authorize** угорі, вставте ключ — Swagger підставлятиме його в усі запити «Try it out».\n\nКлюч видає адміністратор системи (`php api/cli/key.php create`). Відкритий ключ показується **один раз** при створенні: у базі лежить лише його хеш, відновити ключ неможливо.\n\nКожен ключ привʼязаний до **одного** клієнта — його визначає сам токен. Параметр `client_id` не приймається (`422`): клієнта не можна ані вибрати, ані підмінити запитом. Обʼєкт чужого клієнта повертає `404`, так само як неіснуючий — відповідь не дає відрізнити «немає» від «не ваше».\n\n## Пагінація\n\nСписки приймають `page` (від 1) і `per_page` (до 100, типово 25). Некоректні значення дають `422`, а не мовчазний дефолт. Підсумки — у `meta`.\n\n## Rate limit\n\nТипово 60 запитів за хвилину на ключ. При перевищенні — `429` із заголовком `Retry-After`. Поточний стан ліміту — у заголовках `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.\n\n## Значення\n\n`null` означає «значення відоме як відсутнє». Відсутній показник **не** підмінюється нулем: справжній нуль повертається як `0`.",
    "contact": {
      "name": "Agrotest"
    },
    "license": {
      "name": "Proprietary — усі права належать Agrotest"
    }
  },
  "servers": [
    {
      "url": "/api",
      "description": "Поточний хост"
    },
    {
      "url": "https://TEST-HOST-TBD/api",
      "description": "Тестовий контур — адресу треба підтвердити (ТЗ §19, питання 13)"
    },
    {
      "url": "https://PROD-HOST-TBD/api",
      "description": "Production — адресу треба підтвердити (ТЗ §19, питання 13)"
    }
  ],
  "tags": [
    {
      "name": "Service",
      "description": "Стан сервісу"
    },
    {
      "name": "Account",
      "description": "Клієнт ключа"
    },
    {
      "name": "Regions",
      "description": "Області"
    },
    {
      "name": "Periods",
      "description": "Періоди/роки"
    },
    {
      "name": "Fields",
      "description": "Поля та набори аналізу"
    },
    {
      "name": "Geometry",
      "description": "GeoJSON контурів і зон"
    },
    {
      "name": "Soil Analysis",
      "description": "Результати аналізу ґрунту"
    },
    {
      "name": "Recommendations",
      "description": "Рекомендації та розрахунок добрив"
    },
    {
      "name": "Dictionaries",
      "description": "Довідники показників, добрив і одиниць"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "tags": [
          "Service"
        ],
        "summary": "Стан сервісу",
        "description": "Поверхневий статус. Працює без ключа й навмисно не розкриває версію PHP, параметри БД чи стан підключень.",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "Сервіс працює",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "ok",
                  "api_version": "1.0"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Хто я",
        "description": "Клієнт, до якого привʼязаний ключ, і метадані самого ключа.\n\nЗамінює колишній `/v1/clients`: клієнта визначає токен, тож список із одного елемента не має сенсу. Персональні дані клієнта (email, телефон тощо) і сам ключ не повертаються.",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "Клієнт ключа й метадані ключа",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "request_id": {
                          "$ref": "#/components/schemas/RequestId"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/regions": {
      "get": {
        "tags": [
          "Regions"
        ],
        "summary": "Області",
        "description": "Області, у яких клієнт має доступні поля.",
        "operationId": "listRegions",
        "parameters": [
          {
            "$ref": "#/components/parameters/PeriodIdQuery"
          },
          {
            "$ref": "#/components/parameters/HasFields"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Список",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Region"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/periods": {
      "get": {
        "tags": [
          "Periods"
        ],
        "summary": "Періоди",
        "description": "Періоди, за які в клієнта є дані.",
        "operationId": "listPeriods",
        "parameters": [
          {
            "$ref": "#/components/parameters/RegionIdQuery"
          },
          {
            "$ref": "#/components/parameters/HasFields"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Список",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Period"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields": {
      "get": {
        "tags": [
          "Fields"
        ],
        "summary": "Список полів",
        "description": "Поля клієнта. Поле без базового набору за фільтрований період у вибірку не входить (ТЗ API-005).",
        "operationId": "listFields",
        "parameters": [
          {
            "$ref": "#/components/parameters/RegionIdQuery"
          },
          {
            "$ref": "#/components/parameters/PeriodIdQuery"
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Пошук за назвою або номером поля.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Типово повертаються лише активні поля.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "inactive",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "number",
                "updated_at"
              ],
              "default": "name"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Список",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Field"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields/{field_id}": {
      "get": {
        "tags": [
          "Fields"
        ],
        "summary": "Картка поля",
        "description": "Картка поля з обраним набором.\n\nНабір визначається так: явний `dataset_id` → набір за `period_id` → єдиний набір поля. Якщо жодне правило не дає одного набору, `period`, `analysis_date` і `soil` повертаються як `null`, у `meta.dataset_selection` зʼявляється `ambiguous`, а перелік доступних наборів — у `datasets`. Набори НЕ обʼєднуються між собою.",
        "operationId": "getField",
        "parameters": [
          {
            "$ref": "#/components/parameters/FieldIdPath"
          },
          {
            "$ref": "#/components/parameters/PeriodIdQuery"
          },
          {
            "name": "dataset_id",
            "in": "query",
            "required": false,
            "description": "Конкретний набір. Мусить належати цьому полю, інакше 404.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Картка поля",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FieldCard"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "request_id": {
                          "$ref": "#/components/schemas/RequestId"
                        },
                        "dataset_selection": {
                          "type": "string",
                          "enum": [
                            "ambiguous"
                          ],
                          "description": "Присутнє, лише коли набір не вдалося визначити однозначно."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields/{field_id}/datasets": {
      "get": {
        "tags": [
          "Fields"
        ],
        "summary": "Набори поля",
        "description": "Базові набори аналізу поля.\n\nКонтурні й технічні шари самостійними наборами не є і в цьому переліку не зʼявляються (ТЗ API-007).",
        "operationId": "listFieldDatasets",
        "parameters": [
          {
            "$ref": "#/components/parameters/FieldIdPath"
          },
          {
            "$ref": "#/components/parameters/PeriodIdQuery"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Список",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Dataset"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields/{field_id}/geometry": {
      "get": {
        "tags": [
          "Geometry"
        ],
        "summary": "Геометрія поля (GeoJSON)",
        "description": "`FeatureCollection` у WGS84 (EPSG:4326), координати `[довгота, широта]`, кільця замкнені.\n\n**Шари.** `zones` — комірки опробування, по одному Feature на зону. `boundary` — контур поля одним Feature; додаткові кільця, що лежать усередині основного, стають дірами, тож геометрія коректно вирізає ставки й забудову. Кілька роздільних ділянок дають `MultiPolygon`. `grid` завжди порожній — окремого шару сітки в схемі немає.\n\n**Обсяг.** Зони пагінуються, стеля `per_page` тут 1000 (вища за звичайні 100), щоб поле вивантажувалось одним запитом. Контур не пагінується — це один обʼєкт.\n\n**Кешування.** Імпортований набір не змінюється, тож відповідь несе `ETag` і `Last-Modified`; повторний запит із `If-None-Match` дає `304`.",
        "operationId": "getFieldGeometry",
        "parameters": [
          {
            "$ref": "#/components/parameters/FieldIdPath"
          },
          {
            "name": "dataset_id",
            "in": "query",
            "required": true,
            "description": "Обовʼязковий: геометрія завжди належить конкретному набору.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "layer",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "boundary",
                "grid",
                "zones"
              ],
              "default": "all"
            }
          },
          {
            "name": "include_properties",
            "in": "query",
            "required": false,
            "description": "`false` віддає геометрію без атрибутів — помітно менший обсяг.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "precision",
            "in": "query",
            "required": false,
            "description": "Округлити координати до N знаків. Це спрощення геометрії, тож застосовується лише за явним запитом і на БД не впливає.",
            "schema": {
              "type": "integer",
              "minimum": 4,
              "maximum": 15
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Кількість зон на сторінку.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Геометрія набору",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "304": {
            "description": "Не змінилось від часу, вказаного в `If-None-Match`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields/{field_id}/soil-analysis": {
      "get": {
        "tags": [
          "Soil Analysis"
        ],
        "summary": "Аналіз ґрунту",
        "description": "Значення показників по зонах набору.\n\n**Три різні стани значення.** `0` — виміряний нуль, він нулем і лишається. `null` у `values` — показник міряли, але результату немає. Код у `meta.missing_indicators` — показник у цьому наборі не міряли зовсім (або його колонки немає в схемі даних).\n\n**Коди показників.** Параметр `indicators` приймає **лише публічні коди** з `/v1/dictionaries/indicators`. Внутрішнє імʼя колонки (`SOIL_PH__H`) не приймається: імена колонок — деталь імпорту, вона змінюється, і назовні не виходить. Невідомий код дає `422` із переліком нерозпізнаних.\n\nСирий набір із сотень колонок сховища не повертається за жодних умов.",
        "operationId": "getSoilAnalysis",
        "parameters": [
          {
            "$ref": "#/components/parameters/FieldIdPath"
          },
          {
            "name": "dataset_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "indicators",
            "in": "query",
            "required": false,
            "example": "ph_h2o,phosphorus,potassium",
            "description": "Публічні коди через кому. Без параметра — усі доступні показники аналізу.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_geometry",
            "in": "query",
            "required": false,
            "description": "Додати геометрію кожної зони. Піднімає стелю `per_page` до 1000.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Значення по зонах",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/SoilAnalysis"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "missing_indicators": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Показники, яких у цьому наборі немає. Порожній масив — усі запитані присутні."
                        },
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "total_zones": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "request_id": {
                          "$ref": "#/components/schemas/RequestId"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/dictionaries/indicators": {
      "get": {
        "tags": [
          "Dictionaries"
        ],
        "summary": "Показники",
        "description": "Реєстр публічних кодів показників зі шкалами.\n\nКод стабільний: він прив'язаний до показника, а не до імені колонки в сховищі й не до локалізованої назви, тож переживає і переімпорт даних, і переклад.",
        "operationId": "listIndicators",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "analysis",
                "recommendation",
                "technical",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Довідник",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Indicator"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/dictionaries/units": {
      "get": {
        "tags": [
          "Dictionaries"
        ],
        "summary": "Одиниці вимірювання",
        "description": "Канонічні коди одиниць. Набір фіксований і не залежить від даних.",
        "operationId": "listUnits",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Довідник",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Unit"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/fields/{field_id}/recommendations": {
      "get": {
        "tags": [
          "Recommendations"
        ],
        "summary": "Рекомендації по діючих речовинах",
        "description": "Дози по зонах набору.\n\n**Формули** (з ТЗ, власної агрономії API не вигадує):\n\n```\nдоза добрива (кг/га) = доза діючої речовини × коефіцієнт\nмаса на зону (кг)    = доза добрива × площа зони (га)\nразом (кг)           = Σ маса по зонах\nсередня (кг/га)      = разом (кг) / Σ площа\n```\n\nКоефіцієнт застосовується **множенням** і дорівнює 100 / відсоток діючої речовини в добриві. Без `fertilizer_id` він дорівнює 1, і відповідь містить дози діючої речовини.\n\n**Округлення — крок подання, не розрахунку.** Суми й середні рахуються на неокруглених значеннях засобами БД, округлюється лише те, що потрапляє у JSON.\n\n**Вибір речовини.** `nutrient_code` необовʼязковий: якщо в наборі рівно одна речовина з даними, вона береться автоматично. Якщо кілька — відповідь `409` з переліком доступних кодів, бо вибирати навмання API не буде.",
        "operationId": "getRecommendations",
        "parameters": [
          {
            "$ref": "#/components/parameters/FieldIdPath"
          },
          {
            "name": "dataset_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "nutrient_code",
            "in": "query",
            "required": false,
            "example": "p2o5",
            "description": "Публічний код діючої речовини з `/v1/dictionaries/indicators?type=recommendation`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fertilizer_id",
            "in": "query",
            "required": false,
            "description": "Добриво з `/v1/dictionaries/fertilizers`. Мусить нести обрану діючу речовину, інакше `422`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "include_geometry",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Дози по зонах і підсумки",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Recommendation"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "per_page": {
                          "type": "integer"
                        },
                        "total_zones": {
                          "type": "integer"
                        },
                        "total_pages": {
                          "type": "integer"
                        },
                        "warnings": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "ZONE_AREA_MISSING"
                            ]
                          },
                          "description": "Присутнє, коли підсумки неможливо порахувати через відсутні площі зон."
                        },
                        "zones_without_area": {
                          "type": "integer"
                        },
                        "request_id": {
                          "$ref": "#/components/schemas/RequestId"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/AmbiguousNutrient"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/dictionaries/fertilizers": {
      "get": {
        "tags": [
          "Dictionaries"
        ],
        "summary": "Добрива",
        "description": "Довідник добрив із коефіцієнтами перерахунку. Одне добриво може нести кілька діючих речовин — тоді воно зʼявиться у вибірці за кожною з них.",
        "operationId": "listFertilizers",
        "parameters": [
          {
            "name": "nutrient_code",
            "in": "query",
            "required": false,
            "example": "p2o5",
            "description": "Лишити тільки добрива, що несуть цю діючу речовину. Невідомий код дає `422`, а не порожній список.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "inactive",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          }
        ],
        "responses": {
          "200": {
            "description": "Довідник",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Fertilizer"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API-ключ у форматі `agt_<64 hex>`. Передається як `Authorization: Bearer <api_key>`."
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "description": "Номер сторінки, від 1.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "required": false,
        "description": "Розмір сторінки.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "RegionIdQuery": {
        "name": "region_id",
        "in": "query",
        "required": false,
        "description": "Фільтр за областю.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "PeriodIdQuery": {
        "name": "period_id",
        "in": "query",
        "required": false,
        "description": "Фільтр за періодом.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "HasFields": {
        "name": "has_fields",
        "in": "query",
        "required": false,
        "description": "Показувати лише записи, у яких є доступні поля з базовим набором. `false` не розширює доступ — лише перестає вимагати наявність набору.",
        "schema": {
          "type": "boolean",
          "default": true
        }
      },
      "FieldIdPath": {
        "name": "field_id",
        "in": "path",
        "required": true,
        "description": "ID поля.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "required": [
          "status",
          "api_version"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "api_version": {
            "type": "string",
            "example": "1.0"
          }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total",
          "total_pages",
          "request_id"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 25
          },
          "total": {
            "type": "integer",
            "example": 1
          },
          "total_pages": {
            "type": "integer",
            "example": 1
          },
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          }
        }
      },
      "PaginatedEnvelope": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "RequestId": {
        "type": "string",
        "description": "Ідентифікатор запиту. Дублюється заголовком `X-Request-Id`; вказуйте його у зверненнях у підтримку.",
        "example": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "request_id"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Стабільний машиночитний код помилки.",
                "enum": [
                  "BAD_REQUEST",
                  "UNAUTHORIZED",
                  "KEY_WITHOUT_SCOPE",
                  "NOT_FOUND",
                  "METHOD_NOT_ALLOWED",
                  "AMBIGUOUS_NUTRIENT",
                  "VALIDATION_ERROR",
                  "RATE_LIMITED",
                  "INTERNAL_ERROR"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ErrorDetail"
                }
              },
              "request_id": {
                "$ref": "#/components/schemas/RequestId"
              }
            }
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "required": [
          "field",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string",
            "example": "per_page"
          },
          "message": {
            "type": "string",
            "example": "Must be less than or equal to 100"
          }
        }
      },
      "Region": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 8
          },
          "name": {
            "type": "string",
            "example": "Житомирська область"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Plain text: HTML-розмітка й сутності з адмінки знімаються (ТЗ §7.7). Порожній опис — `null`."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          }
        }
      },
      "Period": {
        "type": "object",
        "required": [
          "id",
          "name",
          "year",
          "status"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 9
          },
          "name": {
            "type": "string",
            "example": "2024"
          },
          "year": {
            "type": "integer",
            "nullable": true,
            "description": "Заповнюється, лише коли назва періоду — це рівно чотирицифровий рік. Для «Усі роки», «2018-2024» і «2024 Літо» повертається `null`: це не рік, і вгадувати його означало б ототожнити різні набори даних."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          }
        }
      },
      "NamedRef": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Field": {
        "type": "object",
        "required": [
          "id",
          "client",
          "region",
          "name",
          "number",
          "area_ha",
          "status",
          "available_periods"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 304
          },
          "client": {
            "$ref": "#/components/schemas/NamedRef"
          },
          "region": {
            "$ref": "#/components/schemas/NamedRef"
          },
          "name": {
            "type": "string",
            "example": "Demo 1"
          },
          "number": {
            "type": "string",
            "nullable": true,
            "description": "`field_index`. У більшості полів не заповнений — тоді `null`."
          },
          "area_ha": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Площа в гектарах. Дістається з вільнотекстового поля, де крім площі трапляються крок сітки й культура («126,84 га. Кукурудза 9 т/га»), — береться лише провідне число. Якщо одиниця вказана й вона не гектари, повертається `null`, а не перерахунок."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "available_periods": {
            "type": "array",
            "description": "Періоди, за які в поля є базовий набір.",
            "items": {
              "$ref": "#/components/schemas/PeriodRef"
            }
          }
        }
      },
      "PeriodRef": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "SoilPart": {
        "type": "object",
        "required": [
          "name",
          "value",
          "unit"
        ],
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Пісок"
          },
          "value": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "example": 50.8
          },
          "unit": {
            "type": "string",
            "example": "%"
          }
        }
      },
      "Soil": {
        "type": "object",
        "required": [
          "name",
          "parts"
        ],
        "properties": {
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Легкосуглинковий"
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SoilPart"
            }
          }
        }
      },
      "FieldCardDataset": {
        "type": "object",
        "required": [
          "id",
          "period_id",
          "analysis_date",
          "is_default"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 931
          },
          "period_id": {
            "type": "integer",
            "example": 3
          },
          "analysis_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "is_default": {
            "type": "boolean",
            "description": "`true` у набору, який API обрав для цієї відповіді. Якщо жоден не обрано — набір неоднозначний, і дата-ендпоінти вимагають явний `dataset_id`."
          }
        }
      },
      "FieldCard": {
        "type": "object",
        "required": [
          "id",
          "name",
          "number",
          "area_ha",
          "status",
          "client",
          "region",
          "period",
          "crop",
          "analysis_date",
          "soil",
          "yield_forecast",
          "datasets"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "number": {
            "type": "string",
            "nullable": true
          },
          "area_ha": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Площа в гектарах. Дістається з вільнотекстового поля, де крім площі трапляються крок сітки й культура («126,84 га. Кукурудза 9 т/га»), — береться лише провідне число. Якщо одиниця вказана й вона не гектари, повертається `null`, а не перерахунок."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "client": {
            "$ref": "#/components/schemas/NamedRef"
          },
          "region": {
            "$ref": "#/components/schemas/NamedRef"
          },
          "period": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/FieldCardPeriod"
              }
            ],
            "description": "Період обраного набору. `null`, якщо набір не визначено однозначно."
          },
          "crop": {
            "type": "string",
            "nullable": true,
            "description": "Завжди `null`. Окремого атрибута культури в схемі немає, а вгадувати її з вільного тексту API не буде, доки замовник не підтвердить джерело (ТЗ §9.3, питання 4 розділу 19)."
          },
          "analysis_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "soil": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/Soil"
              }
            ]
          },
          "yield_forecast": {
            "type": "string",
            "nullable": true,
            "description": "Вільний текст прогнозу, як його ввів оператор. Не число: у базі трапляються довільні записи."
          },
          "datasets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldCardDataset"
            }
          }
        }
      },
      "Dataset": {
        "type": "object",
        "required": [
          "id",
          "field_id",
          "period_id",
          "analysis_date",
          "has_boundary",
          "has_zones",
          "has_soil_analysis",
          "has_recommendations"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 931
          },
          "field_id": {
            "type": "integer",
            "example": 304
          },
          "period_id": {
            "type": "integer",
            "example": 3
          },
          "analysis_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "has_boundary": {
            "type": "boolean",
            "description": "У набору є контурний шар."
          },
          "has_zones": {
            "type": "boolean",
            "description": "У набору є зони (полігони)."
          },
          "has_soil_analysis": {
            "type": "boolean",
            "description": "У наборі є заповнені показники аналізу ґрунту."
          },
          "has_recommendations": {
            "type": "boolean",
            "description": "У наборі є заповнені показники рекомендацій."
          }
        }
      },
      "Position": {
        "type": "array",
        "description": "Пара `[довгота, широта]` у WGS84 — порядок за RFC 7946.",
        "minItems": 2,
        "maxItems": 2,
        "items": {
          "type": "number",
          "format": "double"
        },
        "example": [
          30.51231,
          50.32111
        ]
      },
      "LinearRing": {
        "type": "array",
        "description": "Замкнене кільце: перша й остання точки збігаються. Зовнішнє обходиться проти годинникової стрілки, діри — за нею.",
        "minItems": 4,
        "items": {
          "$ref": "#/components/schemas/Position"
        }
      },
      "PolygonGeometry": {
        "type": "object",
        "required": [
          "type",
          "coordinates"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Polygon"
            ]
          },
          "coordinates": {
            "type": "array",
            "description": "Перше кільце — зовнішнє, решта — діри.",
            "items": {
              "$ref": "#/components/schemas/LinearRing"
            }
          }
        }
      },
      "MultiPolygonGeometry": {
        "type": "object",
        "required": [
          "type",
          "coordinates"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "MultiPolygon"
            ]
          },
          "coordinates": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/LinearRing"
              }
            }
          }
        }
      },
      "ZoneProperties": {
        "type": "object",
        "required": [
          "zone_id",
          "layer",
          "area_ha"
        ],
        "properties": {
          "zone_id": {
            "type": "integer",
            "example": 12001
          },
          "layer": {
            "type": "string",
            "enum": [
              "zone"
            ]
          },
          "area_ha": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "Площа зони в гектарах із показника `AREA_SAMPL`. Одиницю підтверджено вимірюванням: на 600 зонах медіана відношення до площі, порахованої з самої геометрії, дорівнює 1.0022, і 94% зон вкладаються в ±2%."
          }
        }
      },
      "BoundaryProperties": {
        "type": "object",
        "required": [
          "layer",
          "rings"
        ],
        "properties": {
          "layer": {
            "type": "string",
            "enum": [
              "boundary"
            ]
          },
          "rings": {
            "type": "integer",
            "description": "Скільки вихідних кілець зібрано в цю геометрію (зовнішні + діри)."
          }
        }
      },
      "GeometryFeature": {
        "type": "object",
        "required": [
          "type",
          "id",
          "geometry",
          "properties"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "Feature"
            ]
          },
          "id": {
            "type": "string",
            "description": "Стабільний у межах набору: `zone_<id>` або `boundary_<dataset_id>`.",
            "example": "zone_12001"
          },
          "geometry": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PolygonGeometry"
              },
              {
                "$ref": "#/components/schemas/MultiPolygonGeometry"
              }
            ]
          },
          "properties": {
            "type": "object",
            "nullable": true,
            "description": "`null`, якщо запит зроблено з `include_properties=false`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ZoneProperties"
              },
              {
                "$ref": "#/components/schemas/BoundaryProperties"
              }
            ]
          }
        }
      },
      "GeometryMeta": {
        "type": "object",
        "required": [
          "crs",
          "grid_available",
          "layer"
        ],
        "properties": {
          "crs": {
            "type": "string",
            "enum": [
              "EPSG:4326"
            ]
          },
          "grid_available": {
            "type": "boolean",
            "description": "Завжди `false`: окремого шару сітки в цій схемі немає — зони нарізані кроком опробування, тобто сітка і зони це один шар."
          },
          "layer": {
            "type": "string",
            "enum": [
              "all",
              "boundary",
              "grid",
              "zones"
            ]
          },
          "boundary_available": {
            "type": "boolean",
            "description": "Чи імпортовано контур для цього набору."
          },
          "boundary_nesting": {
            "type": "string",
            "enum": [
              "skipped"
            ],
            "description": "Присутнє, коли кілець у контурі надто багато й вкладеність (діри) не аналізувалась — кільця віддано окремими частинами."
          },
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total_zones": {
            "type": "integer"
          },
          "total_pages": {
            "type": "integer"
          },
          "request_id": {
            "$ref": "#/components/schemas/RequestId"
          }
        }
      },
      "FeatureCollection": {
        "type": "object",
        "required": [
          "type",
          "field_id",
          "dataset_id",
          "features",
          "meta"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "FeatureCollection"
            ]
          },
          "field_id": {
            "type": "integer"
          },
          "dataset_id": {
            "type": "integer"
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GeometryFeature"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/GeometryMeta"
          }
        }
      },
      "IndicatorValue": {
        "type": "object",
        "required": [
          "value",
          "unit"
        ],
        "properties": {
          "value": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "`null` означає «міряли, результату немає». Справжній нуль повертається як `0` і нулем лишається."
          },
          "unit": {
            "type": "string",
            "example": "ppm",
            "description": "Символ одиниці («pH», «ppm», «%»). Машиночитний код — у `/v1/dictionaries/indicators`."
          }
        }
      },
      "AnalysisZone": {
        "type": "object",
        "required": [
          "zone_id",
          "area_ha",
          "values"
        ],
        "properties": {
          "zone_id": {
            "type": "integer",
            "example": 12001
          },
          "area_ha": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "values": {
            "type": "object",
            "description": "Ключ — публічний код показника.",
            "additionalProperties": {
              "$ref": "#/components/schemas/IndicatorValue"
            }
          },
          "geometry": {
            "type": "object",
            "nullable": true,
            "description": "Присутнє лише при `include_geometry=true`.",
            "allOf": [
              {
                "$ref": "#/components/schemas/PolygonGeometry"
              }
            ]
          }
        }
      },
      "SoilAnalysis": {
        "type": "object",
        "required": [
          "field_id",
          "dataset_id",
          "analysis_date",
          "zones"
        ],
        "properties": {
          "field_id": {
            "type": "integer"
          },
          "dataset_id": {
            "type": "integer"
          },
          "analysis_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "zones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalysisZone"
            }
          }
        }
      },
      "ScaleRange": {
        "type": "object",
        "required": [
          "from",
          "to",
          "color",
          "label",
          "name"
        ],
        "properties": {
          "from": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "to": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "color": {
            "type": "string",
            "nullable": true,
            "example": "#ff0000",
            "description": "CSS-hex; невалідне значення з довідника повертається як `null`."
          },
          "label": {
            "type": "string",
            "nullable": true,
            "example": "<5.4",
            "description": "Текст діапазону."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Сильнокисла",
            "description": "Агрономічна класифікація діапазону."
          }
        }
      },
      "Indicator": {
        "type": "object",
        "required": [
          "id",
          "code",
          "name",
          "type",
          "unit",
          "decimals",
          "color",
          "available",
          "scale"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "ID показника в довіднику кабінету."
          },
          "code": {
            "type": "string",
            "example": "ph_h2o",
            "description": "Стабільний публічний код — саме він приймається в параметрі `indicators`."
          },
          "name": {
            "type": "string",
            "example": "pH H₂O",
            "description": "Plain text: індекси віддаються юнікодом (`P₂O₅`), а не тегами."
          },
          "type": {
            "type": "string",
            "enum": [
              "analysis",
              "recommendation",
              "technical"
            ]
          },
          "unit": {
            "type": "object",
            "nullable": true,
            "required": [
              "code",
              "symbol"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "ph"
              },
              "symbol": {
                "type": "string",
                "example": "pH"
              }
            }
          },
          "decimals": {
            "type": "integer",
            "nullable": true,
            "description": "Рекомендована точність подання."
          },
          "color": {
            "type": "string",
            "nullable": true
          },
          "available": {
            "type": "boolean",
            "description": "Чи існує відповідна колонка в поточній схемі даних. `false` означає, що значень не буде за жодним набором."
          },
          "scale": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScaleRange"
            }
          }
        }
      },
      "Unit": {
        "type": "object",
        "required": [
          "code",
          "symbol",
          "name"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "kg_ha"
          },
          "symbol": {
            "type": "string",
            "example": "kg/ha"
          },
          "name": {
            "type": "string",
            "example": "Кілограм на гектар"
          }
        }
      },
      "NutrientRef": {
        "type": "object",
        "required": [
          "code",
          "name",
          "unit"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "p2o5"
          },
          "name": {
            "type": "string",
            "example": "P₂O₅"
          },
          "unit": {
            "type": "string",
            "example": "kg/ha"
          }
        }
      },
      "FertilizerRef": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "conversion_coefficient",
          "unit"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 9
          },
          "name": {
            "type": "string",
            "example": "Аммофос 12:52:0"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "conversion_coefficient": {
            "type": "number",
            "format": "double",
            "example": 1.92,
            "description": "Множник переходу від дози діючої речовини до дози добрива. Дорівнює 100 / відсоток діючої речовини: Аммофос 12:52:0 → 100/52 ≈ 1.92."
          },
          "unit": {
            "type": "string",
            "example": "kg/ha"
          }
        }
      },
      "RecommendationZone": {
        "type": "object",
        "required": [
          "zone_id",
          "area_ha",
          "nutrient_rate",
          "fertilizer_rate",
          "amount_kg"
        ],
        "properties": {
          "zone_id": {
            "type": "integer"
          },
          "area_ha": {
            "type": "number",
            "format": "float",
            "nullable": true,
            "description": "`null`, якщо площу зони не заповнено. Нуль у сховищі трактується як «не заповнено»: зони нульової площі не буває."
          },
          "nutrient_rate": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Доза діючої речовини."
          },
          "fertilizer_rate": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Доза добрива = доза діючої речовини × коефіцієнт. Без `fertilizer_id` збігається з `nutrient_rate`."
          },
          "amount_kg": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Маса на зону = доза добрива × площа. `null`, якщо площі немає."
          },
          "geometry": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PolygonGeometry"
              }
            ],
            "description": "Присутнє лише при `include_geometry=true`."
          }
        }
      },
      "RecommendationSummary": {
        "type": "object",
        "required": [
          "min_rate",
          "max_rate",
          "average_rate",
          "total_area_ha",
          "total_amount_kg",
          "total_amount_t",
          "rate_unit"
        ],
        "properties": {
          "min_rate": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "max_rate": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "average_rate": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "description": "Зважена за площею середня: загальна маса / загальна площа. Не середнє арифметичне доз — велика зона важить більше за дрібну."
          },
          "total_area_ha": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "total_amount_kg": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "total_amount_t": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "rate_unit": {
            "type": "string",
            "example": "kg/ha"
          }
        },
        "description": "Підсумки рахуються по **всьому набору**, а не по сторінці. Якщо бодай в одній зоні немає площі, `total_area_ha`, `total_amount_*` і `average_rate` дорівнюють `null`, а в `meta.warnings` зʼявляється `ZONE_AREA_MISSING`: часткова сума виглядала б як повна."
      },
      "Recommendation": {
        "type": "object",
        "required": [
          "field_id",
          "dataset_id",
          "nutrient",
          "fertilizer",
          "zones",
          "summary"
        ],
        "properties": {
          "field_id": {
            "type": "integer"
          },
          "dataset_id": {
            "type": "integer"
          },
          "nutrient": {
            "$ref": "#/components/schemas/NutrientRef"
          },
          "fertilizer": {
            "type": "object",
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/FertilizerRef"
              }
            ],
            "description": "`null`, якщо `fertilizer_id` не передано: тоді коефіцієнт дорівнює 1."
          },
          "zones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecommendationZone"
            }
          },
          "summary": {
            "$ref": "#/components/schemas/RecommendationSummary"
          }
        }
      },
      "Fertilizer": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "status",
          "nutrients"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "example": 9
          },
          "name": {
            "type": "string",
            "example": "Аммофос 12:52:0"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Plain text: формули віддаються юнікодом."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "nutrients": {
            "type": "array",
            "description": "Діючі речовини добрива з коефіцієнтами. Одне добриво може нести кілька.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "conversion_coefficient",
                "unit"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "example": "p2o5"
                },
                "conversion_coefficient": {
                  "type": "number",
                  "format": "double",
                  "example": 1.92
                },
                "unit": {
                  "type": "string",
                  "example": "kg/ha"
                }
              }
            }
          }
        }
      },
      "FieldCardPeriod": {
        "type": "object",
        "required": [
          "id",
          "name",
          "year"
        ],
        "description": "Період обраного набору. Легке посилання: без `status`, який має сенс лише в довіднику періодів.",
        "properties": {
          "id": {
            "type": "integer",
            "example": 9
          },
          "name": {
            "type": "string",
            "example": "2024"
          },
          "year": {
            "type": "integer",
            "nullable": true,
            "description": "Заповнюється, лише коли назва періоду — це рівно чотирицифровий рік."
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "client",
          "key"
        ],
        "properties": {
          "client": {
            "type": "object",
            "required": [
              "id",
              "name",
              "status"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "example": 12
              },
              "name": {
                "type": "string",
                "example": "Демо кабінет"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "inactive"
                ]
              }
            }
          },
          "key": {
            "type": "object",
            "required": [
              "name",
              "expires_at"
            ],
            "description": "Метадані ключа, яким зроблено запит. Сам ключ не розкривається.",
            "properties": {
              "name": {
                "type": "string",
                "example": "Інтеграція «Нива»",
                "description": "Назва інтеграції, задана при видачі ключа."
              },
              "expires_at": {
                "type": "string",
                "format": "date",
                "nullable": true,
                "description": "Строк дії ключа або null (безстроковий)."
              }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Ключа немає, він невірний, відкликаний або протермінований. Відповідь навмисно однакова для всіх чотирьох випадків.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "UNAUTHORIZED",
                "message": "Valid API key is required",
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Ключ валідний, але не привʼязаний до жодного клієнта — це помилка налаштування ключа.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "KEY_WITHOUT_SCOPE",
                "message": "API key is not linked to any client",
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Значення параметрів некоректні.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "VALIDATION_ERROR",
                "message": "Invalid request parameters",
                "details": [
                  {
                    "field": "per_page",
                    "message": "Must be less than or equal to 100"
                  }
                ],
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Перевищено ліміт запитів.",
        "headers": {
          "Retry-After": {
            "description": "Через скільки секунд можна повторити.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Ліміт запитів у вікні.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Скільки запитів лишилось у поточному вікні.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix-час, коли вікно оновиться.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "RATE_LIMITED",
                "message": "Rate limit exceeded",
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Внутрішня помилка. Технічні деталі лишаються в журналі сервера; для розбору передайте `request_id`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "INTERNAL_ERROR",
                "message": "Internal server error",
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Обʼєкт не існує або недоступний ключу — відповідь однакова, щоб не підтверджувати існування чужих ID.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "NOT_FOUND",
                "message": "Resource not found",
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      },
      "AmbiguousNutrient": {
        "description": "У наборі кілька діючих речовин — потрібен явний `nutrient_code`. Доступні коди перелічені в `details`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "AMBIGUOUS_NUTRIENT",
                "message": "Dataset contains several nutrients, specify nutrient_code",
                "details": [
                  {
                    "field": "nutrient_code",
                    "message": "Available: p2o5"
                  }
                ],
                "request_id": "req_01J8ZK3M4N5P6Q7R8S9T0V1W2X"
              }
            }
          }
        }
      }
    }
  }
}
