{"openapi":"3.1.0","info":{"title":"AVINEX Thermo Box Picker API","version":"1.0.0","description":"REST API сервиса подбора термобоксов AVINEX.\n\n**Аутентификация:** заголовок `X-Api-Key` на каждый запрос (кроме `/api/health` и `/api/v1/openapi.json`).\n\n**Scopes:** `packing:compute` — Расчёт подбора; `packing:read` — Чтение планов и PDF; `catalog:read` — Каталог контейнеров\n\n**Температурные режимы в v1:** расчёт поддерживает `PLUS_15_25`, `PLUS_2_8`, `MINUS_25_15`."},"servers":[{"url":"https://tbp.avinex-doc.com","description":"Текущий хост"}],"tags":[{"name":"packing","description":"Расчёт и чтение планов подбора"},{"name":"catalog","description":"Справочники термоконтейнеров"},{"name":"system","description":"Служебные эндпоинты"}],"paths":{"/api/v1/packing/compute":{"post":{"tags":["packing"],"operationId":"computePackingPlan","summary":"Расчёт подбора термобоксов","description":"Принимает маршрут и грузы, выполняет термофильтр и 3D-укладку, сохраняет план и возвращает `planId` с полным результатом.","security":[{"ApiKeyAuth":["packing:compute"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackingPlanInput"},"example":{"orderRef":"LK-2026-00142","originCity":"Москва","destinationCity":"Хабаровск","sendDate":"2026-07-15","cargos":[{"id":"cargo-1","name":"Вакцина","lengthCm":30,"widthCm":20,"heightCm":15,"weightKg":2,"quantity":40,"temperatureMode":"PLUS_2_8","rules":{"allowTilt":false,"fragile":true,"allowedRotations":"FLOOR_90"}}],"options":{"algorithm":"first-fit-decreasing","searchProfile":"fast","maxContainers":10}}}}},"responses":{"200":{"description":"План рассчитан и сохранён","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputePackingResponse"}}}},"400":{"$ref":"#/components/responses/BadJson"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}}},"options":{"tags":["packing"],"summary":"CORS preflight","responses":{"204":{"description":"OK"}}}},"/api/v1/packing/plans/{id}":{"get":{"tags":["packing"],"operationId":"getPackingPlan","summary":"Получить сохранённый план","security":[{"ApiKeyAuth":["packing:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"planId из ответа compute (cuid)"}],"responses":{"200":{"description":"План найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PackingPlanRecord"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"options":{"tags":["packing"],"summary":"CORS preflight","responses":{"204":{"description":"OK"}}}},"/api/v1/packing/plans/{id}/render-pdf":{"post":{"tags":["packing"],"operationId":"renderPackingPlanPdf","summary":"PDF-отчёт по плану","description":"Генерирует PDF с раскладкой. Content-Type ответа: `application/pdf`.","security":[{"ApiKeyAuth":["packing:read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"planId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPdfRequest"}}}},"responses":{"200":{"description":"PDF-документ","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v1/catalog/containers":{"get":{"tags":["catalog"],"operationId":"listContainers","summary":"Каталог термоконтейнеров","description":"Список AV-контейнеров: габариты, объёмы, thermalProfiles (hold time по режимам).","security":[{"ApiKeyAuth":["catalog:read"]}],"responses":{"200":{"description":"Каталог контейнеров","content":{"application/json":{"schema":{"type":"object","required":["containers"],"properties":{"containers":{"type":"array","items":{"$ref":"#/components/schemas/CatalogContainer"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/ServerError"}}},"options":{"tags":["catalog"],"summary":"CORS preflight","responses":{"204":{"description":"OK"}}}},"/api/health":{"get":{"tags":["system"],"operationId":"healthCheck","summary":"Healthcheck","description":"Проверка доступности сервиса. API-ключ не требуется.","security":[],"responses":{"200":{"description":"Сервис доступен","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok"]}}}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"API-ключ формата `tbp_…`. Scopes: packing:compute, packing:read, catalog:read"}},"responses":{"BadJson":{"description":"Невалидный JSON в теле запроса","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"},"example":{"error":"Тело запроса не является валидным JSON"}}}},"Unauthorized":{"description":"Отсутствует или неверный X-Api-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"},"example":{"error":"auth_invalid"}}}},"Forbidden":{"description":"У ключа нет нужного scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"},"example":{"error":"scope_denied"}}}},"ValidationError":{"description":"Ошибка валидации полей","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorBody"}}}},"RateLimited":{"description":"Превышен rate limit compute-запросов","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitErrorBody"}}}},"NotFound":{"description":"Ресурс не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"},"example":{"error":"План не найден"}}}},"ServerError":{"description":"Внутренняя ошибка сервера","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"schemas":{"TemperatureModeCode":{"type":"string","enum":["PLUS_15_25","PLUS_2_25","PLUS_2_8","MINUS_25_15","MINUS_78","MINUS_196","NONE"],"description":"`PLUS_15_25` — +15...+25 °C; `PLUS_2_25` — +2...+25 °C; `PLUS_2_8` — +2...+8 °C; `MINUS_25_15` — -25...-15 °C; `MINUS_78` — -78 °C; `MINUS_196` — -196 °C; `NONE` — Без режима"},"CargoRules":{"type":"object","additionalProperties":false,"properties":{"allowTilt":{"type":"boolean","description":"Разрешить наклон при укладке"},"allowStackOnTop":{"type":"boolean","description":"Можно ставить другие места поверх (false — ничего сверху)"},"allowStackUnder":{"type":"boolean","description":"Можно ставить это место на другие (false — только на пол)"},"fragile":{"type":"boolean","description":"Хрупкое: ничего сверху, верхние слои"},"allowedRotations":{"type":"string","enum":["FLOOR_90","ALL"],"description":"`FLOOR_90` — Поворот в плоскости пола (90°); `ALL` — Все ориентации (6 граней)"},"loadingOrder":{"type":"string","enum":["OPTIMAL","MANUAL"],"description":"`OPTIMAL` — Оптимально; `MANUAL` — Вручную"},"unloadPriority":{"type":"integer","minimum":0,"description":"Очередь выгрузки: больше = выгрузить раньше (0 = обычный)"},"color":{"type":"string","description":"Цвет места в 3D-визуализации (hex)"}}},"PackingCargoItem":{"type":"object","required":["id","name","lengthCm","widthCm","heightCm","weightKg","quantity","temperatureMode"],"additionalProperties":false,"properties":{"id":{"type":"string","minLength":1,"description":"Уникальный ID позиции в рамках запроса"},"name":{"type":"string","minLength":1,"description":"Название груза для отчёта"},"lengthCm":{"type":"number","exclusiveMinimum":0,"description":"Длина одного места, см"},"widthCm":{"type":"number","exclusiveMinimum":0,"description":"Ширина одного места, см"},"heightCm":{"type":"number","exclusiveMinimum":0,"description":"Высота одного места, см"},"weightKg":{"type":"number","minimum":0,"description":"Вес одного места, кг"},"quantity":{"type":"integer","minimum":1,"description":"Количество одинаковых мест"},"temperatureMode":{"$ref":"#/components/schemas/TemperatureModeCode"},"includedInPacking":{"type":"boolean","description":"false — груз не участвует в упаковке (по умолчанию true)"},"rules":{"$ref":"#/components/schemas/CargoRules"}}},"PackingPlanOptions":{"type":"object","additionalProperties":false,"properties":{"preferredContainers":{"type":"array","items":{"type":"string"},"description":"Ограничить подбор slug-ами AV-контейнеров, напр. av-320"},"maxContainers":{"type":"integer","minimum":1,"maximum":200,"description":"Максимум контейнеров в плане"},"algorithm":{"type":"string","enum":["first-fit-decreasing","best-area-fit"],"description":"Алгоритм 3D-укладки"},"searchProfile":{"type":"string","enum":["fast","heavy"],"description":"fast — один тип контейнера; heavy — смешивание типов и alternatives"},"useMockThermal":{"type":"boolean","description":"Разрешить mock thermal/route данные (по умолчанию зависит от окружения)"},"skipThermalCapacity":{"type":"boolean","description":"Не фильтровать контейнеры по ёмкости °C·ч (термонагрузка всё равно считается для отчёта)"}}},"PackingPlanInput":{"type":"object","required":["originCity","destinationCity","cargos"],"additionalProperties":false,"properties":{"planTitle":{"type":"string","maxLength":200,"description":"Человекочитаемое название расчёта"},"orderRef":{"type":"string","maxLength":120,"description":"Внешний номер заявки (сохраняется как externalRef плана)"},"originCity":{"type":"string","minLength":1,"description":"Город отправления"},"destinationCity":{"type":"string","minLength":1,"description":"Город назначения"},"sendDate":{"type":"string","format":"date","description":"Дата отправления, ISO date"},"receiveDate":{"type":"string","format":"date","description":"Дата получения — альтернатива transitHours"},"transitHours":{"type":"number","exclusiveMinimum":0,"description":"Транзитное время, ч"},"cargos":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/PackingCargoItem"}},"options":{"$ref":"#/components/schemas/PackingPlanOptions"}}},"PackingErrorCode":{"type":"string","enum":["NO_CARGO","TOO_MANY_ITEMS","UNSUPPORTED_MODE","THERMAL_DATA_MISSING","TRANSIT_UNKNOWN","NO_ELIGIBLE_CONTAINERS","PACKING_FAILED","WEIGHT_EXCEEDED"],"description":"`NO_CARGO` — Не указаны грузы; `TOO_MANY_ITEMS` — Слишком много мест для одного расчёта; `UNSUPPORTED_MODE` — Температурный режим не поддерживается; `THERMAL_DATA_MISSING` — Нет данных по термонагрузке маршрута; `TRANSIT_UNKNOWN` — Неизвестно транзитное время маршрута; `NO_ELIGIBLE_CONTAINERS` — Ни один контейнер не выдерживает термонагрузку; `PACKING_FAILED` — Не удалось разместить весь груз; `WEIGHT_EXCEEDED` — Превышена грузоподъёмность контейнера"},"Rotation":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"integer","enum":[0,90]},"description":"Поворот по осям [X, Y, Z], каждая 0 или 90"},"ItemPlacement":{"type":"object","required":["cargoItemId","instanceIndex","containerIndex","positionMm","rotation","dimensionsMm","loadStep","color"],"properties":{"cargoItemId":{"type":"string"},"instanceIndex":{"type":"integer","minimum":0},"containerIndex":{"type":"integer","minimum":0},"positionMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"},"description":"Позиция в мм [X, Y, Z]"},"rotation":{"$ref":"#/components/schemas/Rotation"},"dimensionsMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"},"description":"Габариты места в мм"},"loadStep":{"type":"integer","minimum":1,"description":"Порядок погрузки в контейнере (1 = первым)"},"color":{"type":"string"}}},"ContainerMetric":{"type":"object","required":["used","max","percent"],"properties":{"used":{"type":"number"},"max":{"type":"number"},"percent":{"type":"number","minimum":0,"maximum":100}}},"ContainerAllocation":{"type":"object","required":["containerSlug","containerTitle","containerIndex","count","coolant","thermalProfileUsed","innerUsableMm","loadingSide","cargoWeightKg","tareWithCoolantKg","totalWeightKg","metrics"],"properties":{"containerSlug":{"type":"string","example":"av-320"},"containerTitle":{"type":"string","example":"AV-320"},"containerIndex":{"type":"integer","minimum":0},"count":{"type":"integer","minimum":1},"coolant":{"type":"string","enum":["BC","PCM"],"description":"BC — хладоэлемент BC; PCM — хладоэлемент PCM. Сухой лёд — отдельный профиль (режимы MINUS_78, MINUS_196, v2)."},"thermalProfileUsed":{"$ref":"#/components/schemas/ThermalProfileUsed"},"innerUsableMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"},"description":"Внутренние габариты под груз, мм"},"loadingSide":{"type":"string","enum":["TOP","FRONT","SIDE"],"description":"TOP — сверху; FRONT — с торца; SIDE — с бока"},"cargoWeightKg":{"type":"number"},"tareWithCoolantKg":{"type":"number"},"totalWeightKg":{"type":"number"},"metrics":{"$ref":"#/components/schemas/ContainerMetrics"}}},"ThermalProfileUsed":{"type":"object","required":["mode","holdHoursAtRefAmbient","thermalCapacityDegreeHours","requiredLoadDegreeHours"],"properties":{"mode":{"$ref":"#/components/schemas/TemperatureModeCode"},"holdHoursAtRefAmbient":{"type":"number"},"thermalCapacityDegreeHours":{"type":"number"},"requiredLoadDegreeHours":{"type":"number"},"requiredHoldHours":{"type":"number","deprecated":true}}},"ContainerMetrics":{"type":"object","properties":{"volumeUtilization":{"type":"number","minimum":0,"maximum":1},"lossRatio":{"type":"number","minimum":0,"maximum":1},"weightUtilization":{"type":"number","minimum":0,"maximum":1},"itemCount":{"type":"integer"},"volumeM3":{"$ref":"#/components/schemas/ContainerMetric"},"floorLengthCm":{"$ref":"#/components/schemas/ContainerMetric"},"widthCm":{"$ref":"#/components/schemas/ContainerMetric"},"heightCm":{"$ref":"#/components/schemas/ContainerMetric"},"floorAreaM2":{"$ref":"#/components/schemas/ContainerMetric"},"massKg":{"$ref":"#/components/schemas/ContainerMetric"},"centerOfGravityMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"}},"cogOffset":{"type":"number","minimum":0,"maximum":1},"cogHeightRatio":{"type":"number","minimum":0,"maximum":1},"rotationsUsed":{"type":"integer"}}},"UnplacedItem":{"type":"object","required":["cargoItemId","instanceIndex","reason"],"properties":{"cargoItemId":{"type":"string"},"instanceIndex":{"type":"integer"},"reason":{"type":"string"}}},"PlanAlternative":{"type":"object","required":["containerSlug","containerTitle","boxes","coolingVolumeL","outerVolumeL"],"properties":{"containerSlug":{"type":"string"},"containerTitle":{"type":"string"},"boxes":{"type":"integer"},"coolingVolumeL":{"type":"number"},"outerVolumeL":{"type":"number"}}},"CombinedModeReport":{"type":"object","required":["temperatureMode","feasible","transitHoursUsed","thermalFilter","requestedCount","placedCount"],"properties":{"temperatureMode":{"$ref":"#/components/schemas/TemperatureModeCode"},"feasible":{"type":"boolean"},"errorCode":{"$ref":"#/components/schemas/PackingErrorCode"},"message":{"type":"string"},"transitHoursUsed":{"type":"number"},"thermalFilter":{"$ref":"#/components/schemas/ThermalFilterResult"},"requestedCount":{"type":"integer"},"placedCount":{"type":"integer"}}},"ThermalFilterResult":{"type":"object","properties":{"requiredLoadDegreeHours":{"type":"number"},"rawLoadDegreeHours":{"type":"number"},"safetyMarginUsed":{"type":"number"},"routeSegmentsUsed":{"type":"array","items":{"$ref":"#/components/schemas/ResolvedRouteSegmentReport"}},"weatherUsed":{"type":"boolean"},"mapSource":{"type":"string","enum":["catalog","override","default-template"]},"eligibleContainerSlugs":{"type":"array","items":{"type":"string"}},"rejectedSlugs":{"type":"array","items":{"type":"object","required":["slug","reason"],"properties":{"slug":{"type":"string"},"reason":{"type":"string"}}}}}},"ResolvedRouteSegmentReport":{"type":"object","properties":{"order":{"type":"integer"},"label":{"type":"string"},"locationTypeKey":{"type":"string"},"cityKey":{"type":"string"},"hours":{"type":"number"},"ambientC":{"type":"number"},"ambientSource":{"type":"string","enum":["override","weather","seasonal","location-default"]},"loadDegreeHours":{"type":"number"}}},"CombinedPackingPlanResult":{"type":"object","required":["feasible","temperatureModes","modes","containers","placements","unplacedItems","summary"],"properties":{"feasible":{"type":"boolean"},"temperatureModes":{"type":"array","items":{"$ref":"#/components/schemas/TemperatureModeCode"}},"modes":{"type":"array","items":{"$ref":"#/components/schemas/CombinedModeReport"}},"containers":{"type":"array","items":{"$ref":"#/components/schemas/ContainerAllocation"}},"placements":{"type":"array","items":{"$ref":"#/components/schemas/ItemPlacement"}},"unplacedItems":{"type":"array","items":{"$ref":"#/components/schemas/UnplacedItem"}},"summary":{"$ref":"#/components/schemas/PackingSummary"},"alternatives":{"type":"array","items":{"$ref":"#/components/schemas/PlanAlternative"}}}},"PackingSummary":{"type":"object","required":["byContainerType","totalPlaced","totalRequested"],"properties":{"byContainerType":{"type":"object","additionalProperties":{"type":"integer"},"description":"slug → количество контейнеров"},"totalPlaced":{"type":"integer"},"totalRequested":{"type":"integer"}}},"ComputePackingResponse":{"allOf":[{"type":"object","required":["planId"],"properties":{"planId":{"type":"string","description":"ID сохранённого плана (cuid)"}}},{"$ref":"#/components/schemas/CombinedPackingPlanResult"}]},"PackingPlanRecord":{"type":"object","required":["planId","externalRef","source","createdAt","input","result"],"properties":{"planId":{"type":"string"},"externalRef":{"type":"string","nullable":true},"source":{"type":"string","enum":["API","UI"]},"createdAt":{"type":"string","format":"date-time"},"input":{"$ref":"#/components/schemas/PackingPlanInput"},"result":{"$ref":"#/components/schemas/CombinedPackingPlanResult"}}},"RenderPdfRequest":{"type":"object","properties":{"mode":{"type":"string","enum":["summary","full"],"default":"summary","description":"summary — краткий PDF; full — с 3D-схемами"}}},"CatalogContainer":{"type":"object","required":["slug","title","outerMm","innerWithCoolingMm","volumeWithCoolingL","maxPayloadKg","loadingSide","thermalProfiles"],"properties":{"slug":{"type":"string","example":"av-320"},"title":{"type":"string","example":"AV-320"},"outerMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"},"description":"Внешние габариты [длина, высота, ширина], мм"},"innerWithCoolingMm":{"type":"array","minItems":3,"maxItems":3,"items":{"type":"number"},"description":"Внутренние габариты с хладоэлементами, мм"},"volumeWithCoolingL":{"type":"number","description":"Полезный объём с хладоэлементами, л"},"maxPayloadKg":{"type":"number","nullable":true,"description":"Макс. масса груза, кг"},"loadingSide":{"type":"string","enum":["TOP","FRONT","SIDE"]},"thermalProfiles":{"type":"array","items":{"$ref":"#/components/schemas/CatalogThermalProfile"}}}},"CatalogThermalProfile":{"type":"object","required":["mode","coolant","holdHoursAtRefAmbient"],"properties":{"mode":{"$ref":"#/components/schemas/TemperatureModeCode"},"coolant":{"type":"string","enum":["BC","PCM"],"description":"BC или PCM — тип хладоэлемента в комплекте контейнера"},"holdHoursAtRefAmbient":{"type":"number","description":"Время удержания при опорной ambient-температуре, ч"}}},"ErrorBody":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"ValidationErrorBody":{"type":"object","required":["error","fieldErrors"],"properties":{"error":{"type":"string"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"RateLimitErrorBody":{"type":"object","required":["error","retryAfterSec"],"properties":{"error":{"type":"string","enum":["rate_limited"]},"retryAfterSec":{"type":"integer","minimum":1}}}}}}