{"openapi":"3.1.0","info":{"title":"Совен — API платформы подбора роботизации","version":"0.1.0","description":"Контракт HTTP-API вертикального среза. Описывает то, что реализовано, а не то, что задумано: состав ручек сверяется с кодом тестом apps/web/src/app/api/openapi.test.ts, поэтому спецификация не может молча устареть.\n\nФормат JSON, а не YAML, намеренно: тест сверки читает файл без дополнительной зависимости, а добавлять парсер YAML ради документа дороже, чем потерять комментарии.\n\nОбщие правила ответов. Непригодный вход — 400. Отсутствующий объект — 404. Расчётный отказ (вход разобран, но посчитать его нечем) — 422 с машинно-читаемым кодом. Конфликт состояния и несогласованность хранилища — 409. Правка без названного исходного состояния — 428. Все ответы отдаются с cache-control: no-store: расчёт зависит от загруженного набора данных, и закэшированный ответ мог бы показать вердикт, которого уже нет."},"servers":[{"url":"/","description":"Приложение Next.js"}],"tags":[{"name":"health","description":"Проверка живости"},{"name":"projects","description":"Проекты и их ревизии"},{"name":"snapshots","description":"Неизменяемые снимки расчёта"},{"name":"object-import","description":"Импорт параметров объекта из XLSX и CSV"},{"name":"readiness","description":"Запросы готовности к роботизации"},{"name":"catalogue","description":"Каталог решений: поиск, отбор и сравнение"}],"paths":{"/api/health":{"get":{"tags":["health"],"summary":"Живость приложения и доступность базы","responses":{"200":{"description":"Приложение отвечает","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"description":"База недоступна: приложение отвечает, но расчёт на ней вести нельзя. Ответ намеренно не содержит строк подключения, хостов и паролей","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/projects":{"get":{"tags":["projects"],"summary":"Список сохранённых проектов","responses":{"200":{"description":"Проекты, от недавно изменённых к старым","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/ProjectListItem"}}},"required":["projects"]}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}},"post":{"tags":["projects"],"summary":"Создать проект и посчитать его","description":"Расчёт идёт до первой записи, поэтому отказ не оставляет следов в базе.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"}}}},"responses":{"201":{"description":"Проект создан и посчитан","content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"hash":{"$ref":"#/components/schemas/SnapshotHash"}},"required":["projectId","hash"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"422":{"$ref":"#/components/responses/CalculationRefusal"},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/projects/{projectId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"tags":["projects"],"summary":"Текущее состояние проекта","responses":{"200":{"description":"Проект и его текущий снимок","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StateConflict"},"503":{"$ref":"#/components/responses/StorageUnavailable"}}},"patch":{"tags":["projects"],"summary":"Правка проекта с пересчётом","description":"Полная замена входа проекта, а не частичное обновление: тело — весь `ProjectInput`, это не JSON Merge Patch (RFC 7396). Сервер не достраивает пропущенные поля из сохранённого проекта — иначе в хешируемый payload попало бы то, чего никто не проверял. Чтобы поменять одно поле, клиент берёт текущий вход (`GET`, `snapshot.input.project`) и отправляет его целиком вместе с правкой. Списки `overrides` и `assumedOfferPerformance` обязательны: `[]` — осознанная очистка, пропуск или `null` — `400`, а не молчаливое стирание сохранённого. `expectedHash` обязателен — правка без названного исходного состояния и есть незамеченная перезапись чужой правки.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ProjectInput"},{"type":"object","properties":{"expectedHash":{"$ref":"#/components/schemas/SnapshotHash"}},"required":["expectedHash","overrides","assumedOfferPerformance","assumedOfferCompatibility","assumedOfferCosts","assumedRaasTerms","assumedAnnualCosts"]}]}}}},"responses":{"200":{"description":"Сохранена новая ревизия либо вход совпал с текущим","content":{"application/json":{"schema":{"type":"object","properties":{"hash":{"$ref":"#/components/schemas/SnapshotHash"},"changed":{"type":"boolean"}},"required":["hash","changed"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Состояние разошлось: чужая правка успела раньше, сохранённый снимок прежней схемы либо состояние проекта несогласованно","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"$ref":"#/components/responses/CalculationRefusal"},"428":{"description":"Предусловие не выполнено: `expectedHash` не назван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}},"delete":{"tags":["projects"],"summary":"Удалить проект со всей историей расчётов","responses":{"204":{"description":"Проект удалён"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StateConflict"},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/projects/{projectId}/copy":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"tags":["projects"],"summary":"Скопировать проект вместе с текущим снимком","description":"Имя копии повторяет имя источника: переименование при копировании не поддержано, и попытка задать другое имя отвергается, а не игнорируется.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Копия создана","content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"}},"required":["projectId"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StateConflict"},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/projects/{projectId}/recalculate":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"tags":["projects"],"summary":"Пересчитать проект прежней схемы по сохранённому вводу","description":"Действие для проекта, снимок которого сохранён прежней версией модели. Вход берётся из сохранённой ревизии и разбирается текущим whitelist: не разобрался — отказ, а не достроенный догадкой расчёт. `expectedHash` обязателен и не обходится. Результат уходит новой ревизией и новым снимком; исторический снимок не меняется и остаётся читаемым по своему хешу.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"expectedHash":{"$ref":"#/components/schemas/SnapshotHash"}},"required":["expectedHash"]}}}},"responses":{"200":{"description":"Сохранена новая ревизия, посчитанная текущей моделью","content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"hash":{"$ref":"#/components/schemas/SnapshotHash"},"changed":{"type":"boolean"}},"required":["projectId","hash","changed"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Пересчёт невозможен: состояние разошлось с названным, сохранённый ввод не читается текущей моделью, проект уже посчитан ею либо состояние проекта несогласованно","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"$ref":"#/components/responses/CalculationRefusal"},"428":{"description":"Предусловие не выполнено: `expectedHash` не назван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/api/projects/{projectId}/object-import":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"tags":["object-import"],"summary":"Применить параметры объекта из файла к проекту","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"200":{"description":"Значения применены, проект пересчитан","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StateConflict"},"413":{"description":"Файл больше допустимого размера. Предел задан на границе, чтобы разбор не начинался вовсе","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"422":{"description":"Файл разобран, но значения применить нельзя: перечень замечаний по строкам","content":{"application/json":{"schema":{"type":"object"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/object-import/preview":{"post":{"tags":["object-import"],"summary":"Разобрать файл без записи","description":"Предпросмотр ничего не сохраняет: он отвечает на вопрос, что платформа прочитала в файле, до того как это станет ревизией проекта.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"200":{"description":"Разобранные значения и замечания","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"413":{"description":"Файл больше допустимого размера. Предел задан на границе, чтобы разбор не начинался вовсе","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}}}},"/api/projects/{projectId}/snapshots/{hash}/readiness":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"$ref":"#/components/parameters/Hash"}],"post":{"tags":["readiness"],"summary":"Отправить запрос готовности по снимку проекта","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"201":{"description":"Запрос сохранён. Токен возвращается здесь единственный раз: дальше хранится только его хеш","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"accessToken":{"type":"string","description":"Показывается один раз при создании"}},"required":["id","accessToken"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StateConflict"},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/readiness/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"get":{"tags":["readiness"],"summary":"Сохранённый запрос готовности","responses":{"200":{"description":"Запрос найден","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Запрос не найден либо токен не подошёл — эти случаи намеренно неразличимы","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}},"security":[{"readinessToken":[]}]}},"/api/snapshots/{hash}":{"parameters":[{"$ref":"#/components/parameters/Hash"}],"get":{"tags":["snapshots"],"summary":"Снимок расчёта по его хешу","description":"Снимок неизменяем и самодостаточен: он содержит весь вход расчёта и его результат. Снимок прежней схемы отдаётся как историческая запись, а не отвергается.","responses":{"200":{"description":"Снимок текущей схемы либо историческая запись","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Снимок сохранён прежней версией модели","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/snapshots/{hash}/pdf":{"parameters":[{"$ref":"#/components/parameters/Hash"}],"get":{"tags":["snapshots"],"summary":"Черновой PDF-отчёт по снимку","description":"Черновик печатается стандартным шрифтом PDF, а он кодирует только Latin-1: подписи английские, кириллица в данных транслитерируется.","responses":{"200":{"description":"PDF-файл","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Снимок сохранён прежней версией модели","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/snapshots/{hash}/csv":{"parameters":[{"$ref":"#/components/parameters/Hash"},{"name":"table","in":"query","required":true,"schema":{"type":"string","enum":["scenarios","sensitivity","comparison","trace"]},"description":"Какую расчётную таблицу выгрузить. Обязателен: выгрузка «какой-нибудь» таблицы неотличима от запрошенной, пока файл не откроют"}],"get":{"tags":["snapshots"],"summary":"Выгрузка расчётной таблицы снимка в CSV","description":"ТЗ 3.7.3: расчётные таблицы выгружаются рядом с отчётом PDF. По таблице на запрос — у каждой своя шапка, и сведённые в один файл они перестают быть таблицами.\n\nРазделитель — точка с запятой, файл начинается с BOM: цель выгрузки — открыть её в Excel, а по запятой русский Excel раскладывает строку в одну колонку.\n\nОтказ расчёта уходит в файл словами вместе с машинным кодом, а не пустой ячейкой: пустая читается как ноль.","responses":{"200":{"description":"CSV с запрошенной таблицей","content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Имя таблицы не указано или незнакомо","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Снимок сохранён прежней версией модели","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"503":{"$ref":"#/components/responses/StorageUnavailable"}}}},"/api/catalogue":{"get":{"tags":["catalogue"],"summary":"Отбор по каталогу решений","description":"Публичная ручка: каталог — это подбор, а не данные проекта.\n\nОтвечает на вопрос «что есть в каталоге», а не «подходит ли это объекту»: технический вердикт совместимости принадлежит расчёту проекта и здесь не выдаётся.\n\nНеизвестная характеристика не проходит диапазонный фильтр, но и не исчезает: число таких позиций возвращается отдельно, и они возвращаются в выдачу параметром `unknown`. Непригодное значение параметра — отказ 400, а не молча отброшенный фильтр: иначе ответ выглядел бы полноценной выдачей по другому запросу.","parameters":[{"name":"search","in":"query","required":false,"schema":{"type":"string"},"description":"Подстрока в модели, поставщике, категории и подписи конфигурации. Регистр и «ё» сворачиваются"},{"name":"provider","in":"query","required":false,"schema":{"type":"string"},"description":"Точное значение поставщика из каталога"},{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Точное значение категории продукта"},{"name":"industry","in":"query","required":false,"schema":{"type":"string"},"description":"Отрасль записи применимости; проверяется вместе с процессом по одной записи"},{"name":"process","in":"query","required":false,"schema":{"type":"string"},"description":"Процесс записи применимости"},{"name":"payloadMin","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Нижняя граница: грузоподъёмность, кг"},{"name":"payloadMax","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Верхняя граница: грузоподъёмность, кг"},{"name":"priceMin","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Нижняя граница: цена оборудования, **рубли**"},{"name":"priceMax","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Верхняя граница: цена оборудования, **рубли**"},{"name":"rateMin","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Нижняя граница: паспортная производительность, ед/час"},{"name":"rateMax","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Верхняя граница: паспортная производительность, ед/час"},{"name":"maturityMin","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Нижняя граница: уровень зрелости, 1–3"},{"name":"maturityMax","in":"query","required":false,"schema":{"type":"number","minimum":0},"description":"Верхняя граница: уровень зрелости, 1–3"},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["model","price","payload","rate","maturity"]},"description":"Поле сортировки; неизвестное имя отвергается"},{"name":"dir","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]},"description":"Направление сортировки. Неизвестные значения характеристики всегда в конце, в обе стороны"},{"name":"unknown","in":"query","required":false,"schema":{"type":"string","enum":["0","1"]},"description":"Показывать позиции с неизвестной характеристикой"},{"name":"compare","in":"query","required":false,"schema":{"type":"string"},"description":"Идентификаторы позиций для сравнения: через запятую либо повторяющимся параметром"}],"responses":{"200":{"description":"Отобранные позиции, значения фильтров из каталога и провенанс набора данных","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer","description":"Всего позиций в каталоге: знаменатель, без которого «найдено 3» ничего не значит"},"matched":{"type":"integer"},"unknownExcluded":{"type":"array","description":"Сколько позиций не прошло фильтр именно из-за неизвестной характеристики","items":{"type":"object","properties":{"field":{"type":"string"},"count":{"type":"integer"}}}},"rows":{"type":"array","items":{"type":"object"}},"comparison":{"type":"array","description":"Выбранные позиции с признаком происхождения: MANUAL означает добавление в обход текущего отбора","items":{"type":"object","properties":{"offerId":{"type":"string"},"origin":{"type":"string","enum":["FROM_FILTER","MANUAL"]}}}},"missingFromComparison":{"type":"array","description":"Запрошенные идентификаторы, которых в каталоге нет: молча они не выбрасываются","items":{"type":"string"}},"facets":{"type":"object","description":"Значения фильтров, которые реально есть в каталоге"},"sources":{"type":"array","items":{"type":"object"}}},"required":["total","matched","rows","facets","sources"]}}}},"400":{"$ref":"#/components/responses/InvalidInput"}}}}},"components":{"parameters":{"ProjectId":{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"Hash":{"name":"hash","in":"path","required":true,"schema":{"$ref":"#/components/schemas/SnapshotHash"}}},"responses":{"InvalidInput":{"description":"Вход непригоден: значение вне границ, нераспознанное поле либо битый JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"NotFound":{"description":"Объект не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"StateConflict":{"description":"Состояние разошлось либо сохранённое состояние несогласованно","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"CalculationRefusal":{"description":"Вход разобран, но посчитать его нечем: производная потребность вне возможностей модели, пак написан на другой сценарий либо ручная правка записана против другого значения. Расчёт идёт до первой записи, поэтому сохранённое не тронуто.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}},"StorageUnavailable":{"description":"Хранилище недоступно: база не отвечает, поэтому проекты и расчёты временно не читаются. Код STORAGE_UNAVAILABLE. Если связь оборвалась во время записи, её исход неизвестен: перед повтором перечитайте состояние. Текста запроса, имён таблиц, параметров и адреса сервера в ответе нет — техническая причина записана в журнал сервера. Ошибка самого запроса сюда не относится и остаётся 500.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CodedError"}}}}},"schemas":{"SnapshotHash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"sha256 канонической формы входа и результата вместе"},"CodedError":{"type":"object","properties":{"error":{"type":"string","description":"Сообщение для человека"},"code":{"type":"string","description":"Машинно-читаемая причина: по ней воспроизводят и ищут"}},"required":["error"]},"ProjectListItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"industry":{"type":"string"},"facilityType":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name"]},"ProjectInput":{"type":"object","description":"Вход расчёта: ровно то, что ввёл пользователь. Разбирается по whitelist — всё, что здесь не объявлено, отсеивается, а не «просто не мешает»: вход попадает в хешируемый payload снимка.","properties":{"name":{"type":"string"},"useCase":{"type":"string","description":"Ключ сценария; отрасль и процесс приходят из реестра, отдельных полей у проекта нет"},"facilityType":{"type":"string"},"facility":{"type":"object","description":"Параметры объекта. null означает «неизвестно» и никогда не становится нулём.","properties":{"aisleWidthMm":{"type":["number","null"]},"maxUnitPayloadKg":{"type":["number","null"]},"floorType":{"type":["string","null"]},"temperatureC":{"type":["number","null"]}}},"answers":{"type":"object","additionalProperties":{"type":"string","enum":["YES","NO","UNKNOWN","NOT_APPLICABLE"]}},"dailyFlowUnits":{"type":"number","exclusiveMinimum":0},"peakFactor":{"type":"number","exclusiveMinimum":0},"operatingHoursPerDay":{"type":"number","exclusiveMinimum":0,"maximum":24},"staffing":{"type":"object","description":"Штат процесса до внедрения: основа сценария текущего состояния","properties":{"totalHeadcount":{"type":"number","exclusiveMinimum":0},"monthlyGrossSalaryRub":{"type":"number","exclusiveMinimum":0},"payrollChargeFactor":{"type":"number","minimum":1,"maximum":3}},"required":["totalHeadcount","monthlyGrossSalaryRub","payrollChargeFactor"]},"roiHorizonYears":{"type":"integer","minimum":3,"maximum":15,"description":"Горизонт ROI. Верхняя граница 15: у части объектов датасета организатора горизонт задан диапазоном 5–15."},"overrides":{"type":"array","description":"Ручные правки входа по явному списку путей. Значение «до» сверяется с текущим: правка поверх другого состояния отвергается, а не применяется молча. При создании может отсутствовать; в правке обязателен, `[]` — очистка.","items":{"type":"object","properties":{"fieldPath":{"type":"string"},"previousValue":{"type":"number"},"newValue":{"type":"number"},"reason":{"type":"string"}},"required":["fieldPath","previousValue","newValue"]}},"assumedOfferPerformance":{"type":"array","description":"Производительность позиций каталога, названная пользователем там, где её нет в данных. Сохраняется как допущение проекта, а не как данные каталога; одна запись на позицию. При создании может отсутствовать; в правке обязателен, `[]` — очистка.","items":{"type":"object","properties":{"offerId":{"type":"string"},"nominalUnitsPerHour":{"type":"number","exclusiveMinimum":0,"maximum":100000},"reason":{"type":"string"}},"required":["offerId","nominalUnitsPerHour"]}},"simulationSteps":{"type":"integer","minimum":1},"assumedOfferCompatibility":{"type":"array","description":"Требования позиции в применении, названные пользователем там, где каталог их не называет: ширина проезда, предельная масса груза, допустимые покрытия пола, температурный диапазон. Восполняют только пробелы точной записи применимости этой позиции — известное каталогом не перебивается, изделие не правится, отсутствующая запись не создаётся. При создании может отсутствовать; в правке обязателен, `[]` — очистка.","items":{"type":"object","required":["offerId"],"properties":{"offerId":{"type":"string","description":"Позиция каталога, к которой относятся требования."},"minAisleWidthMm":{"type":"number","description":"Минимальная ширина проезда, мм."},"maxPayloadKg":{"type":"number","description":"Предельная масса единицы груза в применении, кг."},"floorTypes":{"type":"array","items":{"type":"string"},"description":"Допустимые покрытия пола. Пустой список не сохраняется: «не названо» — не «ни одно»."},"minTemperatureC":{"type":"number"},"maxTemperatureC":{"type":"number"},"reason":{"type":"string","description":"Зачем пользователь назвал эти значения: уходит в трассировку расчёта."}}}},"assumedOfferCosts":{"type":"array","description":"Денежные величины позиции, названные пользователем там, где их не называет пак: стоимость внедрения и интеграции, стоимость одной замены аккумуляторов. Восполняют пробел и не прибавляются к названному паком. При создании может отсутствовать; в правке обязателен, `[]` — очистка.","items":{"type":"object","required":["offerId"],"properties":{"offerId":{"type":"string"},"deploymentIntegrationCostRub":{"type":"number","description":"Доставка, пусконаладка, интеграция, обучение и прочие разовые работы, ₽."},"batteryReplacementCostRub":{"type":"number","description":"Стоимость одной замены аккумуляторов для всего рассчитанного парка, ₽."},"reason":{"type":"string"},"purchaseNonLaborRub":{"type":"number","description":"Годовые расходы после покупки этого робота без ФОТ, ₽/год. У позиции, а не у объекта: энергопотребление и обслуживание зависят от выбранного робота."}}}},"assumedRaasTerms":{"type":"array","description":"Условия аренды (RaaS), названные пользователем: аванс, годовая плата, срок и индексация предложения поставщика у конкретной позиции. Заменяют отраслевое допущение условий из допущений экономики — подтверждённое предложение не является предположением о рынке. Покрытие статей платой и правило продления берутся из допущений экономики. При создании может отсутствовать; в правке обязателен, `[]` — очистка.","items":{"type":"object","required":["offerId","upfrontRub","annualFeeRub","contractYears","annualIndexationPercent","upfrontIsMeaningfulInvestment"],"properties":{"offerId":{"type":"string"},"upfrontRub":{"type":"number","minimum":0,"description":"Разовый платёж при входе, ₽. Ноль допустим."},"annualFeeRub":{"type":"number","minimum":0,"description":"Платёж за год обслуживания, ₽/год."},"contractYears":{"type":"integer","minimum":1,"maximum":50,"description":"Срок договора, лет."},"annualIndexationPercent":{"type":"number","minimum":0,"maximum":100,"description":"Индексация платежа, % в год."},"upfrontIsMeaningfulInvestment":{"type":"boolean","description":"Считать ли аванс значимой инвестиционной базой: без этого ROI и окупаемость аренды не рассчитываются. Числового порога нет намеренно."},"reason":{"type":"string"}}}},"assumedAnnualCosts":{"type":"object","nullable":true,"description":"Годовые расходы текущего процесса без ФОТ, названные пользователем. Относятся к объекту: от выбора робота не зависят, поэтому одни на проект. Расходы после покупки, наоборот, лежат у позиции. `null` — не названы.","properties":{"baselineNonLaborRub":{"type":"number","description":"Годовые расходы текущего процесса без ФОТ, ₽/год."},"reason":{"type":"string"}}}},"required":["name","useCase","facilityType","facility","answers","dailyFlowUnits","peakFactor","operatingHoursPerDay","staffing","roiHorizonYears","simulationSteps"]}},"securitySchemes":{"readinessToken":{"type":"http","scheme":"bearer","description":"Токен выдаётся один раз при создании запроса готовности и хранится только в виде хеша. Неверный или отсутствующий токен даёт 404, а не 401: код статуса не должен раскрывать, существует ли запрос"}}}}