diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md new file mode 100644 index 00000000..5caecb37 --- /dev/null +++ b/docs/contracts/program-solutions-v1/README.md @@ -0,0 +1,271 @@ +# C00 Контракт решения в программе + +Версия 1.0.0 от 23 октября 2026 года. Контракт фиксирует поведение для реализации BE01–BE06 и FE01–FE06. В BE01 реализованы модели и четыре операции контекста, создания, чтения и списка; остальные операции остаются задачами BE02–BE06. Новый режим выключен по умолчанию. Контракт описывает JSON на проводе в snake_case; преобразование в camelCase выполняет frontend adapter. + +Источники: ТЗ «Решение вместо проекта в программах PROCOLLAB» и действующие правила ветки dev, проверенные на backend b7d9f57 и frontend bc1f961b. Старые API и обычные проекты сохраняют свои контракты. + +- [OpenAPI 3.1](openapi.json) — URL, схемы, обязательность, статусы и примеры. +- [Независимые сценарии для моков](examples.json) — request и response по operation_id. +- [Проверка контракта](validate_contract.py) — проверка OpenAPI и примеров. +- [Продуктовое ТЗ](https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175). + +## Порядок разработки + +C00 фиксирует техническую основу. D00 v3 явно согласован пользователем 24 октября 2026 года в чате. BE01 и FE01 реализуются параллельно по C00. Для следующих задач сохраняется согласованный дизайн и порядок зависимостей ТЗ. + +## Решения C00 + +Одна основная сущность ProgramSolution одновременно представляет заявку команды и ее решение. Команда состоит из принадлежащих заявке записей участников. Самостоятельная Team, обязательный Project, отдельная Application поверх решения и процесс повторной сдачи не вводятся. + +Решение принадлежит одной программе. Формат формы — case_championship или hackathon. У программы одна общая форма; разные поля по кейсам не входят в v1. Кейс или задача представлены полем select с purpose:case, без новой сущности Case. Поля шаблона копируются в конфигурацию программы. + +Создание доступно по текущим правилам участника и дедлайна, включая существующие административные исключения. Повторное создание капитаном возвращает его заявку с HTTP 200; новое — 201. До первого включения все изменяющие endpoints проверяют технический режим программы. Наличие серверного кода при выключенном режиме не дает создать решение. + +## Модель хранения + +| Объект | Данные и связи | +| --- | --- | +| ProgramSolution | id, program FK, captain FK User, name, version bigint, status draft/submitted, submitted_at nullable, submission_snapshot JSON nullable, created_at, updated_at | +| ProgramSolutionMember | solution FK, user FK, role nullable, specialization nullable, joined_at | +| SolutionInvitation | solution FK, recipient FK User, role nullable, specialization nullable, motivational_letter nullable, status pending/accepted/declined, version bigint, created_at, updated_at | +| ProgramSolutionAnswer | solution FK, field FK PartnerProgramField, value JSON; одна запись на пару | +| Конфигурация формы программы | template nullable до настройки; определения PartnerProgramField расширяются type, purpose, position, validation; revision считается по всей конфигурации | +| Профиль участника программы | Существующая регистрация сохраняется. Текущая заявка определяется по принятому составу или капитанству, не только по старому profile.project | +| UserFile | Существующий файл с первичным ключом link. Новый File ID не изобретается | + +Капитан также имеет одну запись ProgramSolutionMember. При создании она добавляется в той же транзакции. При передаче капитанства меняется captain FK на уже принятого участника; прежний капитан остается участником. is_captain вычисляется сравнением user.id и captain_id. Дублировать капитана в численности нельзя. + +Ограничения БД: unique(solution,user) для участников; unique(solution,field) для ответов; частичный unique(solution,recipient) для pending приглашений; положительные версии; submitted_at и snapshot заполнены только в submitted. FK ссылаются на реальное решение без фиктивного Project. + +Инвариант «капитан входит в состав» и соответствие поля программе проверяются сервисом в транзакции. Их нельзя выразить обычным CHECK по строке. Создание дублей и запрет участия в двух командах активной программы обеспечиваются сериализацией через программу и повторными проверками всех поддерживаемых способов записи. Не объявляем динамическую exclusivity ограничением unique(user,program) навсегда. + +Физически новые приглашения хранятся в SolutionInvitation, а старые Invite не получают фиктивный project FK. Общая бизнес policy используется через адаптер владельца. Операции отзыва сохраняют действующую семантику удаления: pending приглашение удаляется; статус revoked не добавляется. accepted и declined остаются историческими строками. + +## Ревизии + +ProgramSolution.version начинается с 1 и увеличивается при каждом успешном изменении содержимого, состава или приглашений и при сдаче. Изменение разрешений из-за времени или роли и изменение формы организатором само по себе версию решения не повышают. + +SolutionInvitation.version начинается с 1 и повышается при редактировании и принятии либо отклонении. Отзыв удаляет строку. Ревизия приглашения не заменяет актуальные проверки состава. + +Form.revision — непрозрачный 64-символьный SHA-256 в hex от канонической конфигурации: template и определения в порядке position с ID, типом, назначением, подписью, подсказкой, обязательностью, options и validation. Используется существующий механизм хеша; одинаковая конфигурация дает одинаковую ревизию. Клиент сравнивает строки и не вычисляет хеш. Сам hash не является номером версии документа 1.0.0. + +- PATCH решения и submit требуют expected_version и expected_form_revision. +- Команды состава требуют expected_solution_version. +- Создание приглашения требует expected_solution_version. +- Редактирование приглашения требует expected_version приглашения и expected_solution_version. +- Accept и decline требуют только expected_version приглашения: получатель до вступления не должен читать закрытую заявку. +- DELETE приглашения передает обе ожидаемые версии в query. DELETE участника — expected_solution_version в query. +- PUT формы требует expected_revision. + +Состояние и права всегда повторно проверяются внутри транзакции. Ошибка ревизии — 409 version_conflict либо form_revision_conflict без записи. Клиент перечитывает объект, сохраняет локальные правки и предлагает повторить осмысленное действие. Автоматически повторять submit или принятие приглашения после конфликта нельзя. + +## Общие правила API + +API base URL берется из настроек окружения. Новые пути добавляются к тому же base URL, что /programs/ и /projects/. Отдельный /v1 в URL не добавляется; версия пакета контракта — 1.0.0. + +JWT Bearer и текущие правила аутентификации сохраняются. ID — положительные JSON integer не больше 2^53−1. Даты — ISO 8601 UTC с Z. Пагинация: limit от 1 до 100, default 20; offset от 0. Ответ: count, next, previous, results. + +Тело запроса строгое: неизвестные поля отклоняются; поля пользователя, роли авторизации, project_id, team_id и submitted не принимаются в командах, где они не определены. Клиенты должны терпимо читать дополнительные поля ответов при совместимом расширении контракта. + +Все несекретные публичные подписи имен полей берутся из определения формы. Поля ответа запроса могут возвращать только данные разрешенной заявки. Смена ID в URL не переносит объект в другую программу. + +## Endpoints + +| Метод и путь | Назначение | +| --- | --- | +| GET /programs/{program_id}/solution-context/ | Режим программы, сроки, доступность создания и текущая заявка | +| GET /programs/{program_id}/solutions/ | Доступный пользователю список решений | +| POST /programs/{program_id}/solutions/ | Создать решение с начальным названием либо вернуть существующее | +| GET /programs/{program_id}/solutions/{solution_id}/ | Полная разрешенная заявка: team, form, answers, submission, permissions, readiness | +| PATCH /programs/{program_id}/solutions/{solution_id}/ | Частично сохранить название и ответы черновика | +| POST /programs/{program_id}/solutions/{solution_id}/submit/ | Сдать сохраненную заявку | +| DELETE /programs/{program_id}/solutions/{solution_id}/members/{user_id}/ | Капитан удаляет участника | +| POST /programs/{program_id}/solutions/{solution_id}/leave/ | Участник выходит | +| POST /programs/{program_id}/solutions/{solution_id}/captain/ | Передача капитанства | +| GET и POST /programs/{program_id}/solutions/{solution_id}/invitations/ | Капитан читает приглашения заявки или создает приглашение | +| GET /solution-invitations/ | Входящие приглашения текущего пользователя | +| GET /solution-invitations/{invitation_id}/ | Безопасное представление для капитана и получателя | +| PATCH и DELETE /solution-invitations/{invitation_id}/ | Изменить или отозвать pending приглашение капитаном | +| POST /solution-invitations/{invitation_id}/accept/ | Получатель принимает приглашение | +| POST /solution-invitations/{invitation_id}/decline/ | Получатель отклоняет приглашение | +| GET /programs/{program_id}/solution-form/ | Текущая форма, доступная участнику программы | +| GET и PUT /programs/{program_id}/management/solution-form/ | Организатор читает или атомарно меняет форму | +| GET /solution-form-templates/ | Прототипы двух шаблонов | +| POST /files/ | Существующая multipart загрузка, ответ 201 {url} | + +Списки solutions используют search и submission=all/submitted/not_submitted. В неконкурсной программе submission, отличный от all, возвращает 400. Списки приглашений используют status=pending/accepted/declined, default pending. Общие списки и контекст сортируются детерминированно по created_at и ID согласно текущему интерфейсу, без случайного выбора первой заявки. + +Контекст существует и в legacy режиме. current_application — null либо ссылка с kind:solution и solution_id либо kind:legacy_project с project_id и program_link_id. Это discriminated union для навигации, а не новая сущность. Несогласованные несколько текущих заявок дают 409 ambiguous_application. + +blocked_reason в контексте: null, already_has_application, legacy_mode, form_not_configured, not_program_member, submission_closed или already_in_program_team. Порядок выбора: текущая заявка, legacy режим, права, закрытая подача, неготовая форма. Детали участия в чужой команде не раскрываются постороннему. + +## Чтение и полномочия + +Капитан и принятые участники читают заявку. Организатор, эксперт и прочие зрители получают доступ в пределах текущей политики программы, назначений и публикации работ. Это не дает прав изменения или сдачи за капитана. + +permissions в Solution вычисляются для текущего пользователя: can_edit, can_manage_team, can_invite, can_submit. У участника и эксперта все изменяющие флаги false. can_submit означает, что капитану разрешена команда сдачи по состоянию и срокам; неполнота полей отражается отдельно в readiness. Это позволяет нажать действие и увидеть причины неготовности. + +readiness содержит is_ready и issues. Для неконкурсных заявок и зрителей вне своей команды возвращается null. Для своей команды выводятся актуальные требования без раскрытия чужих персональных данных. Разрешение на действие не вычисляется на фронте по роли или часам браузера. + +Приглашение до принятия раскрывает program, безопасную ссылку solution, капитана, получателя и текст приглашения. Ответов формы, состава чужой команды, регистрационных анкет и оценок в нем нет. Входящий список содержит только приглашения текущему пользователю; список приглашений заявки доступен капитану. Административное чтение сохраняет действующий отдельный объем полномочий и не подразумевается этим endpoint. + +Если объект вне видимости вызывающего, возвращается 404. Если объект видим, но действие запрещено, — 403. Для новых API 401, 403 и 404 также имеют общий Error envelope. Старые endpoints не переводятся на него автоматически. + +## Команда и приглашения + +### Пользовательский путь из действующего проекта + +Заявка имеет две вкладки: «Решение» и «Команда». На первой — настроенные поля и материалы, на второй — строки принятого состава и отдельный список «Ожидают подтверждения». Это разделы одного ProgramSolution. Переключение сохраняет локальный ввод; общее сохранение и сдача относятся к обеим вкладкам. При ошибке сдачи открывается нужная вкладка. + +Источник текущего UI исправлен после предоставленных пользователем скриншотов: локальный ref origin/master, commit 8a064835, компоненты project-member-invite-modal, participant-picker, project-invite-role-input и project-team-step. Старый checkout f6f867c6 с inline формой по ссылке не является источником целевого сценария. Ссылка на профиль не вводится. + +«Пригласить участника» открывает модальное окно. В нем поиск по имени или фамилии от двух символов, нормализация пробелов, задержка 300 мс и отмена предыдущего запроса. Переиспользуется существующий GET `/auth/public-users/` с `user_type=1`, `fullname`, `partner_program=solution.program_id`, `limit=8`, `offset=0` через GetMembersUseCase и MemberHttpAdapter. program_link_id не подставляется вместо ID программы. Ответ использует действующую модель User и карточку имени, возраста, специальности и навыков; новый каталог API не вводится. + +Получатель выбирается из результата. Капитан, принятый участник и уже приглашенный человек недоступны с объяснением причины. Выбор заменяет список карточкой «Выбранный участник», которую можно очистить. В createInvitation передается существующий `recipient_user_id` выбранного пользователя; текст поиска не является выбранным ID. + +Роль обязательна в форме: после нормализации пробелов от 1 до 128 символов. Сохраняются 13 подсказок ProjectRoleSuggestions, их фильтрация и свободный ввод. Отправка активна только при валидных получателе и роли; pending блокирует повтор. Ошибка оставляет выбор и роль, успех закрывает окно и добавляет ожидающее приглашение. Отмена и новое открытие сбрасывают временную форму; закрытие прекращает обновление из старого запроса. + +В ожидающих приглашениях сохраняются видимые действия изменения роли и отзыва с подтверждением. Прежнее описание об отсутствии видимой кнопки редактирования относилось к старому checkout и исправлено. После сдачи применяются ограничения C00: редактирование роли и принятие запрещены, отзыв и отклонение доступны. Получатель использует существующие входящие приглашения и принять/отклонить, ссылка адаптируется к Solution. + +Схемы C00 для создания по recipient_user_id, изменения и отзыва уже поддерживают этот путь и не менялись. Nullable role на уровне API сохранен для совместимости с legacy моделью; UI требует заполненную нормализованную роль. Существующие проектные endpoints сохраняют работу; решения используют свой контракт. Ошибки показываются безопасным сообщением без raw backend body. Черновик заявки не отправляет приглашение автоматически. + +Принятый состав представлен team.members. accepted_count равен количеству уникальных членов, включая капитана. pending_count — уникальные pending получатели. reserved_count — количество уникальных пользователей в объединении принятого состава и pending получателей; при штатных данных равно сумме двух счетчиков. + +При создании и принятии приглашения проверяются регистрация, отсутствие в принятом составе, запрет второй команды активной конкурсной программы и максимум с резервами. При принятии свой резерв заменяется принятым участием и не увеличивает число занятых мест второй раз. + +На submit минимум и максимум проверяются по принятому составу, как сейчас. Не принявшие приглашение пользователи не закрывают минимум. Новая заявка допустима с одним капитаном. NULL отключает соответствующую границу. Источник границ — текущие параметры legacy_team_min_size и legacy_team_max_size через адаптер, без импорта параметров из независимого nextgen процесса. + +Exclusivity действует в конкурсной программе до datetime_finished. В другой программе и после завершения программы ложного конфликта быть не должно. При сосуществовании старых заявок и новых решений проверяются обе структуры; pending приглашения сами по себе не считаются принятым участием во второй команде. + +После submit состав и приглашения нельзя создавать, менять или принимать. Pending приглашения разрешено отклонить или отозвать, что изменяет служебную version и счетчики, но не снимок сданного принятого состава. Зафиксированная работа не размораживается. + +remove и transfer возвращают MutationReceipt с новой solution_version; клиент перечитывает доступное решение. Leave возвращает 204, поскольку вышедший пользователь может утратить право чтения. Accept возвращает обновленное приглашение, ссылку заявки и новую solution_version; decline возвращает application:null и solution_version:null. Удаление капитана и его выход запрещены до передачи капитанства. Прямого POST members для обхода приглашения нет. + +## Ответы и формы + +Ответ представлен как {field_id,value}. Тип берется из определения поля, а не из переданного клиентом type: + +| Тип | JSON value | +| --- | --- | +| text, textarea, url | string | +| select, radio | Точная строка из options | +| number | JSON number, включая 0 | +| checkbox | boolean, включая false | +| file | {url: URL существующего UserFile} | + +PATCH с value:null очищает ответ. Пустая строка и строка из пробелов также очищают текст, ссылку и выбор в черновике. В ответе GET очищенные поля отсутствуют в answers, не содержат null. Дубликат field_id в запросе отклоняется целиком. Сохраненный текст не меняется автоматически, кроме нормализации, предусмотренной текущим полем; проверка пустоты использует trim. + +Проверка типов, вариантов и ограничений выполняется при каждом непустом сохранении. Required применяется при submit. Число 0 и false удовлетворяют обязательности ответа; отдельное согласие с требованием true в v1 не добавляется. Системное название ограничено 256 символами, создается по текущему правилу начального названия и обязательно при сдаче. + +Типы: text, textarea, url, number, select, checkbox, file. radio сохраняется для существующих определений и переноса; новый конструктор не предлагает его создание. purpose:case допустим только для select и в единственном экземпляре; сервер сохраняет системное name=case. Подпись «Задача или трек» в хакатоне не меняет эту семантику. Системные назначения, кроме other, уникальны в форме. presentation требует file; repository, demo и video требуют url; run_instructions требует textarea. Любой поддерживаемый тип можно использовать с purpose:other. + +position — уникальные целые числа от 0; PUT нормализует порядок в последовательность. options хранятся точными строками, пустые и дубли после trim без учета регистра запрещены. Используемые значения нельзя переименовать или удалить. Select в сохраняемой конфигурации требует хотя бы один вариант; шаблон возвращает пустые options как заготовку для организатора. + +До первого PUT конфигурация может иметь template:null, fields:[], is_configured:false. Это блокирует создание новых решений с кодом form_not_configured. GET формы при этом допустим организатору и зарегистрированному участнику, а не генерирует автоматически выбранный шаблон. + +## Настройка организатором + +PUT management/solution-form полностью заменяет определения с expected_revision. id:null означает новое поле, пропуск существующего ID — удаление. Чужой ID и повтор одного ID отклоняются. Системное name не редактируется клиентом. + +После непустых ответов нельзя удалить поле или сменить его тип. Использованные варианты нельзя удалить или переименовать. Допустимо добавить поля, менять подпись, подсказку, порядок и required. Ограничения сужаются только если им соответствуют все сохраненные непустые ответы. Смена template после has_answers:true запрещена. Новая required настройка влияет только на несданные работы. + +В FormManagement fields_usage содержит возможность удаления и смены типа, а affected_draft_count — количество черновиков, которые после применения конфигурации не готовы по новым обязательным полям. При GET это количество по текущей конфигурации; после PUT — по сохраненной. Фронт не вычисляет счетчик по загруженной странице заявок. + +Начальные шаблоны копируются в конкретную программу. В КЧ требуются выбор, краткое описание и PDF, в хакатоне — выбор, краткое описание, описание реализации, репозиторий и инструкция запуска. Демо, видео и дополнительные материалы необязательны; в хакатоне также необязательна презентация. Иные поля добавляет организатор. Это стартовые требования, не отдельные классы решения. + +## Файлы + +POST /files/ принимает multipart file и возвращает ровно существующий {url}. На привязке file поля сервер ищет UserFile по link и проверяет право использования: собственная загрузка капитана либо файл, уже принадлежащий этой заявке, в том числе из переноса. Произвольная внешняя ссылка не становится file ответом. Внешние материалы добавляются через поле url. + +Один file ответ содержит один файл. Верхний предел привязки к решению в v1 — 100 MiB (104857600 bytes); поле может задать меньший предел. Существующий общий upload сохраняет свои ограничения для остальных потребителей. Фронт проверяет размер по определению поля до загрузки, а backend повторяет проверку при привязке UserFile. Для PDF в шаблоне КЧ — 20 MiB и extension pdf. Для дополнительных архивов задается отдельное поле; начальный предел 50 MiB. Проверяются фактический размер и разрешенный тип, а не только строка имени. Старые заведомо импортированные материалы не блокируют чтение истории. + +Снятие ответа не удаляет бинарный файл автоматически. Удаление UserFile, на который ссылается сданное решение, должно отклоняться защитой file_in_use в файловом сервисе. Действующие успешные upload ответы остаются прежними. Доступ к бинарным данным CDN остается в существующей модели; контракт не обещает новую выдачу подписанных ссылок. + +## Сдача + +Состояния хранения: draft и submitted. Статусы оценивания и публикации остаются производными действующего процесса. + +Последовательность фронта: + +1. Сохранить текущие ответы PATCH с обеими ожидаемыми ревизиями. +2. Получить новую Solution.version, не использовать старую версию до PATCH. +3. Выполнить текущее подтверждение сдачи. +4. POST submit с новой expected_version и actual expected_form_revision. +5. При успехе показать полученное представление submitted; при конфликте перечитать данные без автоматической повторной сдачи. + +PATCH и submit — две отдельные транзакции. Если submit отклонен, ранее успешно сохраненный черновик остается сохраненным; частично сданного состояния не возникает. + +Под блокировками повторяются проверки капитана, конкурсности, состояния, дедлайна, состава, выбора, всех required и ограничений. Успех записывает status, submitted_at, version и submission_snapshot атомарно. Снимок включает название, form definitions, form revision, answers, капитана и принятый состав. Pending приглашения в состав снимка не входят. + +Для draft GET возвращает текущую form и is_snapshot:false. Для submitted — form и answers из снимка с is_snapshot:true. Новый required, новая подпись или порядок не меняют сданный просмотр. Состояние таймера submission_open при этом вычисляется на момент GET и не является частью исторических требований. + +После сдачи повторный submit дает 409 already_submitted независимо от старой expected_version, после проверки доступа и роли. Не вводятся повторная сдача, отзыв сдачи или возврат в draft. При сетевом обрыве клиент делает GET и устанавливает фактический статус. + +Дедлайн: отдельная дата подачи либо дата окончания регистрации. Сравнение сохраняет текущую включенную границу: серверное now <= deadline. После закрытия подачи нельзя создавать и сдавать, но несданный черновик можно сохранять, как в действующем API. Конкурсность отдельно управляет submit: в неконкурсной программе status остается draft, submission.required:false, readiness:null, can_submit:false; работа сохраняется без искусственной сдачи. + +## Ошибки + +Новый Error содержит обязательные code, detail, errors и meta. errors — массив {path,code,field_id,detail}; field_id:null для системной ошибки. meta несет current_version или current_form_revision только при наличии права чтения. + +| HTTP | Коды | +| --- | --- | +| 400 | validation_error, invalid_answers, required_program_fields, case_required, case_unavailable, invalid_form | +| 401 | authentication_required | +| 403 | not_program_member, not_solution_captain, not_invite_recipient, permission_denied | +| 404 | not_found | +| 409 | solution_mode_unavailable, form_not_configured, version_conflict, form_revision_conflict, already_in_program_team, ambiguous_application | +| 409 | team_frozen, team_min_size, team_max_size, already_solution_member, duplicate_pending_invite, invite_already_processed | +| 409 | captain_cannot_leave, captain_cannot_be_removed, captain_already_selected, not_solution_member | +| 409 | submission_closed, already_submitted, not_competitive, field_in_use, template_locked | + +Это HTTP контракты новых endpoints. Существующие статусы старых /projects/, /invites/ и submit связи не меняются. Повторное действие с удаленным приглашением дает 404. Повтор accept/decline сохраненной обработанной строки — 409 invite_already_processed. + +Приоритет проверок: аутентификация → существование и видимость → право действия → финальное состояние → ожидаемые ревизии → валидация текущих бизнес правил и значений → запись. На уже сданной работе submit дает already_submitted, edit дает team_frozen; на уже обработанном приглашении accept/decline дают invite_already_processed. Внутри блока нарушений дедлайн проверяется перед составом и полями. Синтаксическая проверка запроса может выполняться до загрузки объекта. + +## Транзакции и совместимость + +Для новых writers порядок блокировок: PartnerProgram → ProgramSolution → затронутые SolutionInvitation в порядке ID → определения формы и профиль/записи участников в согласованном порядке. Config writer начинает с Program и блокирует затронутые решения в порядке ID перед определениями. Все writers работают в транзакции и повторяют проверки после блокировок. Фактический порядок внутренних таблиц фиксируется совместно в BE01–BE04; инверсия между endpoint запрещена. + +Legacy порядок Project → Program → program links не меняется молча. Новый writer не пытается получить блокировку старого Project, уже удерживая Program. Проверка старого состава выполняется чтением под общей сериализацией Program. Переход программы на единственный источник записи осуществляется отдельно в BE10 с остановкой старых writers и явной таблицей соответствий. + +После commit отправляются существующие уведомления и события; повторный submit или ошибка не создает их снова. Начисление оценок и выдача результата не входят в submit транзакцию. + +Не подменять старые поля partner_program/project/program_link_id новыми числовыми ID. Старый и новый ID могут совпасть по значению, поэтому тип ссылки всегда задан контекстом или kind. current_application для новой навигации берется из solution-context; старые endpoints остаются в legacy adapter. + +## Карта потребителей + +| Текущий участок | Изменение | Задача | +| --- | --- | --- | +| Project и PartnerProgramProject, profile.project | Владение данными переносится в ProgramSolution; профиль регистрации сохраняется | BE01, BE08, FE01 | +| Collaborator и projects.team_policy/team_service | Общая policy, новый владелец участников; счетчики без дубля капитана | BE02, FE04 | +| Invite и текущий inbox | Новые приглашения через SolutionInvitation и /solution-invitations/; старые /invites/ работают прежним способом | BE03, BE08, FE05 | +| PartnerProgramField и program field values | Definitions переиспользуются; ответы нового решения — typed JSON | BE04, BE05, FE02, FE03 | +| files.UserFile и /files/ | Текущий upload, проверка привязки и защита использования после сдачи | BE05, BE06, FE03 | +| Link-scoped submit | Новая команда submit с теми же правилами и снимком | BE06, FE06 | +| ProjectExpertAssignment, ProjectScore и результаты | Привязка к решению без смены критериев и формул | BE07, FE07 | +| Management, role widgets, case analytics, exports | Чтение решения; признак материала через purpose; нет двойного учета | BE08, BE09, FE08 | +| Уведомления и глубокие ссылки | Явная ссылка kind:solution с program_id и solution_id | BE08, FE05, FE08 | +| История программ | legacy adapter и таблица old link → new solution | BE10 | +| Чаты и вакансии программного Project | Не создаются автоматически для нового решения; сохраненный доступ к существующим связанным объектам проверяется до переноса | C00 карта, BE08, BE10 | + +Чаты и вакансии не являются зависимостью формы, приглашения и submit нового решения. Полный перенос их функциональности на новую сущность оформляется отдельно; существующие объекты старых программ не удаляются и не теряют маршрут. Если пилотной программе нужны эти функции, их готовность проверяется до выбора этой программы для REL01. + +Контракт оценивания, аналитики и массовых административных действий расширяется в BE07–BE09, без изменения C00 модели владения. Их точные payload не объявляются определенными схемой Solution. Это отдельные endpoints, не произвольные поля в answer JSON. + +## Параллельная разработка + +Backend реализует операции по operationId из openapi.json. Frontend использует те же схемы и examples.json; сценарии независимы, не описывают изменяемую in-memory базу. Сначала можно собрать FE01 и экранные моки FE02–FE06, пока идут BE01–BE06. + +Фикстуры содержат успешные и ошибочные состояния, включая неполный и готовый черновик, поданную работу, неконкурсный режим, закрытый дедлайн, приглашения, конфликты ревизий, 0 и false. Даты и ID фиктивные; моки с таймером используют собственные контролируемые часы, а не текущую дату компьютера. + +Изменение имен полей, required, статусов или формы ответа требует синхронного обновления этого описания, OpenAPI и примеров. Добавление совместимых endpoints или полей — minor версия; ломающее изменение согласуется до интеграции. Номер ревизии формы не используется как версия API. + +## Проверка пакета + +Для проверки нужны dev-зависимости jsonschema и openapi-spec-validator (`poetry install`). Они не нужны для обработки запросов продукта. + +```text +python validate_contract.py +``` + +Скрипт проверяет корректность OpenAPI, все локальные refs и схемы, примеры request/response, уникальность operationId, базовые инварианты состава и snapshot. Реальные ответы BE01 дополнительно проверяются JSON Schema в `partner_programs.tests.test_solution_foundation`. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json new file mode 100644 index 00000000..4d3a86d5 --- /dev/null +++ b/docs/contracts/program-solutions-v1/examples.json @@ -0,0 +1,4036 @@ +{ + "contract_version": "1.0.0", + "description": "Независимые примеры, не последовательность операций; фиктивные данные.", + "scenarios": [ + { + "name": "context_solution", + "operation_id": "getSolutionContext", + "status": 200, + "response": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + } + } + }, + { + "name": "context_legacy", + "operation_id": "getSolutionContext", + "status": 200, + "response": { + "program_id": 12, + "storage_mode": "legacy_project", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "legacy_project", + "program_id": 12, + "project_id": 55, + "program_link_id": 65, + "name": "Исторический проект" + } + } + }, + { + "name": "draft_incomplete", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "draft_ready", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 9, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "name": "submitted_snapshot", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "name": "noncompetitive", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 501, + "program_id": 14, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": null, + "max_size": null, + "is_frozen": false + }, + "form": { + "program_id": 14, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false + }, + "readiness": null + } + }, + { + "name": "deadline_closed_draft_editable", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": false, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "created_solo", + "operation_id": "createSolution", + "status": 201, + "request": {}, + "response": { + "id": 301, + "program_id": 12, + "version": 1, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "team", + "code": "team_min_size", + "field_id": null, + "detail": "Нужно не менее двух участников." + }, + { + "path": "answers", + "code": "required", + "field_id": 1001, + "detail": "Заполните поле." + }, + { + "path": "answers", + "code": "required", + "field_id": 1002, + "detail": "Заполните поле." + }, + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Заполните поле." + } + ] + } + } + }, + { + "name": "create_existing", + "operation_id": "createSolution", + "status": 200, + "request": {}, + "response": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "save_answers", + "operation_id": "updateSolution", + "status": 200, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answers": [ + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ] + }, + "response": { + "id": 301, + "program_id": 12, + "version": 9, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "name": "clear_answer", + "operation_id": "updateSolution", + "status": 200, + "request": { + "expected_version": 9, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "answers": [ + { + "field_id": 1003, + "value": null + } + ] + }, + "response": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "submit_success", + "operation_id": "submitSolution", + "status": 200, + "request": { + "expected_version": 9, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "response": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "name": "submit_required", + "operation_id": "submitSolution", + "status": 400, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "response": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + } + }, + { + "name": "submit_closed", + "operation_id": "submitSolution", + "status": 409, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "response": { + "code": "submission_closed", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + } + }, + { + "name": "submit_repeated", + "operation_id": "submitSolution", + "status": 409, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "response": { + "code": "already_submitted", + "detail": "Решение уже сдано.", + "errors": [], + "meta": {} + } + }, + { + "name": "not_captain", + "operation_id": "submitSolution", + "status": 403, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "response": { + "code": "not_solution_captain", + "detail": "Сдать решение может только капитан.", + "errors": [], + "meta": {} + } + }, + { + "name": "version_conflict", + "operation_id": "updateSolution", + "status": 409, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Новое название" + }, + "response": { + "code": "version_conflict", + "detail": "Заявка изменилась. Обновите данные.", + "errors": [], + "meta": { + "current_version": 10 + } + } + }, + { + "name": "form_conflict", + "operation_id": "updateSolution", + "status": 409, + "request": { + "expected_version": 8, + "expected_form_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "name": "Новое название" + }, + "response": { + "code": "form_revision_conflict", + "detail": "Форма изменилась. Обновите поля.", + "errors": [], + "meta": { + "current_form_revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + } + }, + { + "name": "create_invitation", + "operation_id": "createSolutionInvitation", + "status": 201, + "request": { + "expected_solution_version": 8, + "recipient_user_id": 9, + "role": "Аналитик" + }, + "response": { + "invitation": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + }, + { + "name": "duplicate_invitation", + "operation_id": "createSolutionInvitation", + "status": 409, + "request": { + "expected_solution_version": 8, + "recipient_user_id": 9 + }, + "response": { + "code": "duplicate_pending_invite", + "detail": "Приглашение не может быть отправлено.", + "errors": [], + "meta": {} + } + }, + { + "name": "max_team", + "operation_id": "createSolutionInvitation", + "status": 409, + "request": { + "expected_solution_version": 8, + "recipient_user_id": 9 + }, + "response": { + "code": "team_max_size", + "detail": "Приглашение не может быть отправлено.", + "errors": [], + "meta": {} + } + }, + { + "name": "inbox", + "operation_id": "listMySolutionInvitations", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": true, + "can_decline": true + } + } + ] + } + }, + { + "name": "accept_invitation", + "operation_id": "acceptSolutionInvitation", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "accepted", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "solution_version": 9 + } + }, + { + "name": "accept_other_team", + "operation_id": "acceptSolutionInvitation", + "status": 409, + "request": { + "expected_version": 1 + }, + "response": { + "code": "already_in_program_team", + "detail": "Вы уже состоите в команде программы.", + "errors": [], + "meta": {} + } + }, + { + "name": "decline_after_submit", + "operation_id": "declineSolutionInvitation", + "status": 200, + "request": { + "expected_version": 1 + }, + "response": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "declined", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": null, + "solution_version": null + } + }, + { + "name": "revoke_after_submit", + "operation_id": "revokeSolutionInvitation", + "status": 204 + }, + { + "name": "captain_transfer", + "operation_id": "transferCaptain", + "status": 200, + "request": { + "expected_solution_version": 8, + "new_captain_user_id": 8 + }, + "response": { + "solution_id": 301, + "solution_version": 9, + "updated_at": "2026-10-23T10:00:00Z" + } + }, + { + "name": "leave", + "operation_id": "leaveSolution", + "status": 204, + "request": { + "expected_solution_version": 8 + } + }, + { + "name": "remove_member", + "operation_id": "removeSolutionMember", + "status": 200, + "response": { + "solution_id": 301, + "solution_version": 9, + "updated_at": "2026-10-23T10:00:00Z" + } + }, + { + "name": "solution_list", + "operation_id": "listSolutions", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 301, + "program_id": 12, + "name": "Сервис для студентов", + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "accepted_count": 2, + "case_value": "Цифровой сервис", + "status": "draft", + "submitted_at": null + } + ] + } + }, + { + "name": "form", + "operation_id": "getSolutionForm", + "status": 200, + "response": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + } + }, + { + "name": "form_management", + "operation_id": "getManagedSolutionForm", + "status": 200, + "response": { + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "has_solutions": true, + "has_answers": true, + "affected_draft_count": 1, + "fields_usage": [ + { + "field_id": 1001, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1002, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1003, + "answers_count": 0, + "can_delete": true, + "can_change_type": true + }, + { + "field_id": 1004, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1005, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + } + ] + } + }, + { + "name": "templates", + "operation_id": "listSolutionFormTemplates", + "status": 200, + "response": { + "results": [ + { + "key": "case_championship", + "label": "Кейс-чемпионат", + "fields": [ + { + "id": null, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": null, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + }, + { + "key": "hackathon", + "label": "Хакатон", + "fields": [ + { + "id": null, + "type": "select", + "purpose": "case", + "label": "Задача или трек", + "help_text": "", + "required": true, + "position": 0, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Описание реализации", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": 10000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "run_instructions", + "label": "Инструкция запуска", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "url", + "purpose": "demo", + "label": "Демо", + "help_text": "", + "required": false, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": false, + "position": 6, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": null, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 7, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 8, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + } + ], + "max_file_size_bytes": 104857600, + "files_per_field": 1 + } + }, + { + "name": "upload", + "operation_id": "uploadExistingFile", + "status": 201, + "response": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + }, + { + "name": "hackathon_ready", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 401, + "program_id": 13, + "version": 3, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 13, + "revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 2001, + "type": "select", + "purpose": "case", + "label": "Задача или трек", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Городской сервис" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2003, + "type": "textarea", + "purpose": "other", + "label": "Описание реализации", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": 10000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2004, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2005, + "type": "textarea", + "purpose": "run_instructions", + "label": "Инструкция запуска", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2006, + "type": "url", + "purpose": "demo", + "label": "Демо", + "help_text": "", + "required": false, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2007, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": false, + "position": 6, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 2008, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 7, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2009, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 8, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + }, + "answers": [ + { + "field_id": 2001, + "value": "Городской сервис" + }, + { + "field_id": 2002, + "value": "Сервис обращений жителей." + }, + { + "field_id": 2003, + "value": "Реализован прототип обработки обращений и личный кабинет." + }, + { + "field_id": 2004, + "value": "https://example.test/team/repository" + }, + { + "field_id": 2005, + "value": "См. README репозитория." + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + { + "name": "solution_invitations", + "operation_id": "listSolutionInvitations", + "status": 200, + "response": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + } + ] + } + }, + { + "name": "invitation_detail", + "operation_id": "getSolutionInvitation", + "status": 200, + "response": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": true, + "can_decline": true + } + } + }, + { + "name": "edit_invitation", + "operation_id": "updateSolutionInvitation", + "status": 200, + "request": { + "expected_version": 1, + "expected_solution_version": 8, + "role": "Дизайнер" + }, + "response": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + }, + { + "name": "save_form", + "operation_id": "saveManagedSolutionForm", + "status": 200, + "request": { + "expected_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "response": { + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "has_solutions": true, + "has_answers": true, + "affected_draft_count": 1, + "fields_usage": [ + { + "field_id": 1001, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1002, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1003, + "answers_count": 0, + "can_delete": true, + "can_change_type": true + }, + { + "field_id": 1004, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1005, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + } + ] + } + }, + { + "name": "team_frozen", + "operation_id": "createSolutionInvitation", + "status": 409, + "request": { + "expected_solution_version": 10, + "recipient_user_id": 9 + }, + "response": { + "code": "team_frozen", + "detail": "Состав команды зафиксирован.", + "errors": [], + "meta": {} + } + }, + { + "name": "member_read_only", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + { + "name": "expert_snapshot", + "operation_id": "getSolution", + "status": 200, + "response": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": null + } + }, + { + "name": "context_empty", + "operation_id": "getSolutionContext", + "status": 200, + "response": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": true, + "blocked_reason": null, + "current_application": null + } + } + ] +} diff --git a/docs/contracts/program-solutions-v1/openapi.json b/docs/contracts/program-solutions-v1/openapi.json new file mode 100644 index 00000000..f61c50d2 --- /dev/null +++ b/docs/contracts/program-solutions-v1/openapi.json @@ -0,0 +1,7440 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "info": { + "title": "PROCOLLAB Program Solutions C00", + "version": "1.0.0", + "description": "Контракт для реализации и моков. Новые endpoints пока не реализованы. API base URL задается окружением. snake_case на проводе. Старые API не меняются." + }, + "servers": [ + { + "url": "/" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/programs/{program_id}/solution-context/": { + "get": { + "operationId": "getSolutionContext", + "summary": "Режим программы и текущая заявка", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Context" + }, + "examples": { + "context_solution": { + "value": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + } + } + }, + "context_legacy": { + "value": { + "program_id": 12, + "storage_mode": "legacy_project", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": false, + "blocked_reason": "already_has_application", + "current_application": { + "kind": "legacy_project", + "program_id": 12, + "project_id": 55, + "program_link_id": 65, + "name": "Исторический проект" + } + } + }, + "context_empty": { + "value": { + "program_id": 12, + "storage_mode": "solution", + "is_registered": true, + "is_competitive": true, + "submission_open": true, + "submission_deadline": "2026-10-30T18:00:00Z", + "can_create": true, + "blocked_reason": null, + "current_application": null + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/programs/{program_id}/solutions/": { + "get": { + "operationId": "listSolutions", + "summary": "Решения программы", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "submission", + "in": "query", + "required": false, + "schema": { + "enum": [ + "all", + "submitted", + "not_submitted" + ], + "default": "all" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SolutionPage" + }, + "examples": { + "solution_list": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 301, + "program_id": 12, + "name": "Сервис для студентов", + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "accepted_count": 2, + "case_value": "Цифровой сервис", + "status": "draft", + "submitted_at": null + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "createSolution", + "summary": "Создать или вернуть заявку капитана", + "description": "JSON {} создает заявку с начальным названием. Участие в другой команде дает 409.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Solution" + }, + "examples": { + "create_existing": { + "value": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + } + } + } + } + }, + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Solution" + }, + "examples": { + "created_solo": { + "value": { + "id": 301, + "program_id": 12, + "version": 1, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + } + ], + "accepted_count": 1, + "pending_count": 0, + "reserved_count": 1, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "team", + "code": "team_min_size", + "field_id": null, + "detail": "Нужно не менее двух участников." + }, + { + "path": "answers", + "code": "required", + "field_id": 1001, + "detail": "Заполните поле." + }, + { + "path": "answers", + "code": "required", + "field_id": 1002, + "detail": "Заполните поле." + }, + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Заполните поле." + } + ] + } + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSolution" + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/": { + "get": { + "operationId": "getSolution", + "summary": "Решение с командой формой и ответами", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Solution" + }, + "examples": { + "draft_incomplete": { + "value": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + "draft_ready": { + "value": { + "id": 301, + "program_id": 12, + "version": 9, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + "submitted_snapshot": { + "value": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + "noncompetitive": { + "value": { + "id": 501, + "program_id": 14, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": null, + "max_size": null, + "is_frozen": false + }, + "form": { + "program_id": 14, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": false, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false + }, + "readiness": null + } + }, + "deadline_closed_draft_editable": { + "value": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": false, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + "hackathon_ready": { + "value": { + "id": 401, + "program_id": 13, + "version": 3, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 13, + "revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "template": "hackathon", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 2001, + "type": "select", + "purpose": "case", + "label": "Задача или трек", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Городской сервис" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2003, + "type": "textarea", + "purpose": "other", + "label": "Описание реализации", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": 10000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2004, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2005, + "type": "textarea", + "purpose": "run_instructions", + "label": "Инструкция запуска", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2006, + "type": "url", + "purpose": "demo", + "label": "Демо", + "help_text": "", + "required": false, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2007, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": false, + "position": 6, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 2008, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 7, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 2009, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 8, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + }, + "answers": [ + { + "field_id": 2001, + "value": "Городской сервис" + }, + { + "field_id": 2002, + "value": "Сервис обращений жителей." + }, + { + "field_id": 2003, + "value": "Реализован прототип обработки обращений и личный кабинет." + }, + { + "field_id": 2004, + "value": "https://example.test/team/repository" + }, + { + "field_id": 2005, + "value": "См. README репозитория." + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + "member_read_only": { + "value": { + "id": 301, + "program_id": 12, + "version": 8, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + }, + "expert_snapshot": { + "value": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": null + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "updateSolution", + "summary": "Частично сохранить черновик", + "description": "null очищает ответ; пропуск оставляет прежний. Обе ревизии обязательны.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Solution" + }, + "examples": { + "save_answers": { + "value": { + "id": 301, + "program_id": 12, + "version": 9, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + }, + "clear_answer": { + "value": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": false + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + } + ], + "submission": { + "required": true, + "status": "draft", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": null + }, + "permissions": { + "can_edit": true, + "can_manage_team": true, + "can_invite": true, + "can_submit": true + }, + "readiness": { + "is_ready": false, + "issues": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ] + } + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "version_conflict": { + "value": { + "code": "version_conflict", + "detail": "Заявка изменилась. Обновите данные.", + "errors": [], + "meta": { + "current_version": 10 + } + } + }, + "form_conflict": { + "value": { + "code": "form_revision_conflict", + "detail": "Форма изменилась. Обновите поля.", + "errors": [], + "meta": { + "current_form_revision": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSolution" + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/submit/": { + "post": { + "operationId": "submitSolution", + "summary": "Сдать сохраненную заявку", + "description": "Сначала отдельный PATCH ответов, затем submit. Повторный submit: 409 already_submitted.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Solution" + }, + "examples": { + "submit_success": { + "value": { + "id": 301, + "program_id": 12, + "version": 10, + "name": "Сервис для студентов", + "captain_id": 7, + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:05:00Z", + "team": { + "members": [ + { + "user": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": true + }, + { + "user": { + "id": 8, + "first_name": "Анна", + "last_name": "Смирнова", + "avatar_url": null + }, + "role": null, + "specialization": null, + "joined_at": "2026-10-23T09:00:00Z", + "is_captain": false + } + ], + "accepted_count": 2, + "pending_count": 1, + "reserved_count": 3, + "min_size": 2, + "max_size": 4, + "is_frozen": true + }, + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": true, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "answers": [ + { + "field_id": 1001, + "value": "Цифровой сервис" + }, + { + "field_id": 1002, + "value": "Единый сервис поддержки студентов." + }, + { + "field_id": 1004, + "value": 0 + }, + { + "field_id": 1005, + "value": false + }, + { + "field_id": 1003, + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + ], + "submission": { + "required": true, + "status": "submitted", + "submission_open": true, + "deadline": "2026-10-30T18:00:00Z", + "submitted_at": "2026-10-23T10:05:00Z" + }, + "permissions": { + "can_edit": false, + "can_manage_team": false, + "can_invite": false, + "can_submit": false + }, + "readiness": { + "is_ready": true, + "issues": [] + } + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "submit_required": { + "value": { + "code": "required_program_fields", + "detail": "Заполните обязательные поля.", + "errors": [ + { + "path": "answers", + "code": "required", + "field_id": 1003, + "detail": "Загрузите презентацию." + } + ], + "meta": {} + } + } + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "not_captain": { + "value": { + "code": "not_solution_captain", + "detail": "Сдать решение может только капитан.", + "errors": [], + "meta": {} + } + } + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "submit_closed": { + "value": { + "code": "submission_closed", + "detail": "Срок сдачи завершен.", + "errors": [], + "meta": {} + } + }, + "submit_repeated": { + "value": { + "code": "already_submitted", + "detail": "Решение уже сдано.", + "errors": [], + "meta": {} + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Submit" + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/members/{user_id}/": { + "delete": { + "operationId": "removeSolutionMember", + "summary": "Удалить участника", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "user_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "expected_solution_version", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationReceipt" + }, + "examples": { + "remove_member": { + "value": { + "solution_id": 301, + "solution_version": 9, + "updated_at": "2026-10-23T10:00:00Z" + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/leave/": { + "post": { + "operationId": "leaveSolution", + "summary": "Выйти из заявки", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "204": { + "description": "Успех" + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamCommand" + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/captain/": { + "post": { + "operationId": "transferCaptain", + "summary": "Передать капитанство", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MutationReceipt" + }, + "examples": { + "captain_transfer": { + "value": { + "solution_id": 301, + "solution_version": 9, + "updated_at": "2026-10-23T10:00:00Z" + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaptainCommand" + } + } + } + } + } + }, + "/programs/{program_id}/solutions/{solution_id}/invitations/": { + "get": { + "operationId": "listSolutionInvitations", + "summary": "Приглашения заявки для капитана", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "enum": [ + "pending", + "accepted", + "declined" + ], + "default": "pending" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationPage" + }, + "examples": { + "solution_invitations": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "createSolutionInvitation", + "summary": "Пригласить участника", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "solution_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "201": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationCreated" + }, + "examples": { + "create_invitation": { + "value": { + "invitation": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "duplicate_invitation": { + "value": { + "code": "duplicate_pending_invite", + "detail": "Приглашение не может быть отправлено.", + "errors": [], + "meta": {} + } + }, + "max_team": { + "value": { + "code": "team_max_size", + "detail": "Приглашение не может быть отправлено.", + "errors": [], + "meta": {} + } + }, + "team_frozen": { + "value": { + "code": "team_frozen", + "detail": "Состав команды зафиксирован.", + "errors": [], + "meta": {} + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInvitation" + } + } + } + } + } + }, + "/solution-invitations/": { + "get": { + "operationId": "listMySolutionInvitations", + "summary": "Входящие приглашения текущего пользователя", + "description": "", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "enum": [ + "pending", + "accepted", + "declined" + ], + "default": "pending" + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationPage" + }, + "examples": { + "inbox": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": true, + "can_decline": true + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/solution-invitations/{invitation_id}/": { + "get": { + "operationId": "getSolutionInvitation", + "summary": "Безопасный просмотр приглашения", + "description": "Без ответов заявки и чужого списка участников.", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + }, + "examples": { + "invitation_detail": { + "value": { + "id": 701, + "version": 1, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": true, + "can_decline": true + } + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "updateSolutionInvitation", + "summary": "Изменить ожидающее приглашение", + "description": "", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationCreated" + }, + "examples": { + "edit_invitation": { + "value": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Дизайнер", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "pending", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": true, + "can_revoke": true, + "can_accept": false, + "can_decline": false + } + }, + "solution_version": 9 + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateInvitation" + } + } + } + } + }, + "delete": { + "operationId": "revokeSolutionInvitation", + "summary": "Отозвать приглашение", + "description": "Действующий DELETE lifecycle, отдельного revoked нет. Разрешено после сдачи.", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + { + "name": "expected_version", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "expected_solution_version", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + } + ], + "responses": { + "204": { + "description": "Успех" + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/solution-invitations/{invitation_id}/accept/": { + "post": { + "operationId": "acceptSolutionInvitation", + "summary": "Принять приглашение", + "description": "", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationActionResult" + }, + "examples": { + "accept_invitation": { + "value": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "accepted", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "solution_version": 9 + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "examples": { + "accept_other_team": { + "value": { + "code": "already_in_program_team", + "detail": "Вы уже состоите в команде программы.", + "errors": [], + "meta": {} + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationAction" + } + } + } + } + } + }, + "/solution-invitations/{invitation_id}/decline/": { + "post": { + "operationId": "declineSolutionInvitation", + "summary": "Отклонить приглашение", + "description": "Разрешено после сдачи.", + "parameters": [ + { + "name": "invitation_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationActionResult" + }, + "examples": { + "decline_after_submit": { + "value": { + "invitation": { + "id": 701, + "version": 2, + "program": { + "id": 12, + "name": "Кейс-чемпионат PROCOLLAB" + }, + "solution": { + "kind": "solution", + "program_id": 12, + "solution_id": 301, + "name": "Сервис для студентов" + }, + "captain": { + "id": 7, + "first_name": "Иван", + "last_name": "Петров", + "avatar_url": null + }, + "recipient": { + "id": 9, + "first_name": "Олег", + "last_name": "Иванов", + "avatar_url": null + }, + "role": "Аналитик", + "specialization": "Исследования", + "motivational_letter": "Приглашаем в команду.", + "status": "declined", + "created_at": "2026-10-23T09:00:00Z", + "updated_at": "2026-10-23T10:00:00Z", + "permissions": { + "can_edit": false, + "can_revoke": false, + "can_accept": false, + "can_decline": false + } + }, + "application": null, + "solution_version": null + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationAction" + } + } + } + } + } + }, + "/programs/{program_id}/solution-form/": { + "get": { + "operationId": "getSolutionForm", + "summary": "Актуальная форма программы", + "description": "Сданная работа использует form из своего snapshot.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form" + }, + "examples": { + "form": { + "value": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/programs/{program_id}/management/solution-form/": { + "get": { + "operationId": "getManagedSolutionForm", + "summary": "Форма и использование полей", + "description": "", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormManagement" + }, + "examples": { + "form_management": { + "value": { + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "has_solutions": true, + "has_answers": true, + "affected_draft_count": 1, + "fields_usage": [ + { + "field_id": 1001, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1002, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1003, + "answers_count": 0, + "can_delete": true, + "can_change_type": true + }, + { + "field_id": 1004, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1005, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "operationId": "saveManagedSolutionForm", + "summary": "Атомарно заменить форму", + "description": "id:null создает поле. Пропуск поля означает удаление, разрешенное только без ответов.", + "parameters": [ + { + "name": "program_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + ], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormManagement" + }, + "examples": { + "save_form": { + "value": { + "form": { + "program_id": 12, + "revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "template": "case_championship", + "is_configured": true, + "is_snapshot": false, + "fields": [ + { + "id": 1001, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [ + "Цифровой сервис", + "Операционная эффективность" + ], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1002, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1003, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": 1004, + "type": "number", + "purpose": "other", + "label": "Ожидаемый эффект", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": 0, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": 1005, + "type": "checkbox", + "purpose": "other", + "label": "Приложены расчеты", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + } + ] + }, + "has_solutions": true, + "has_answers": true, + "affected_draft_count": 1, + "fields_usage": [ + { + "field_id": 1001, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1002, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1003, + "answers_count": 0, + "can_delete": true, + "can_change_type": true + }, + { + "field_id": 1004, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + }, + { + "field_id": 1005, + "answers_count": 1, + "can_delete": false, + "can_change_type": false + } + ] + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormWrite" + } + } + } + } + } + }, + "/solution-form-templates/": { + "get": { + "operationId": "listSolutionFormTemplates", + "summary": "Стартовые шаблоны", + "description": "", + "parameters": [], + "responses": { + "200": { + "description": "Успех", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Templates" + }, + "examples": { + "templates": { + "value": { + "results": [ + { + "key": "case_championship", + "label": "Кейс-чемпионат", + "fields": [ + { + "id": null, + "type": "select", + "purpose": "case", + "label": "Кейс", + "help_text": "", + "required": true, + "position": 0, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": null, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + }, + { + "key": "hackathon", + "label": "Хакатон", + "fields": [ + { + "id": null, + "type": "select", + "purpose": "case", + "label": "Задача или трек", + "help_text": "", + "required": true, + "position": 0, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Краткое описание", + "help_text": "", + "required": true, + "position": 1, + "options": [], + "validation": { + "min_length": null, + "max_length": 3000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "other", + "label": "Описание реализации", + "help_text": "", + "required": true, + "position": 2, + "options": [], + "validation": { + "min_length": null, + "max_length": 10000, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "url", + "purpose": "repository", + "label": "Репозиторий", + "help_text": "", + "required": true, + "position": 3, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "textarea", + "purpose": "run_instructions", + "label": "Инструкция запуска", + "help_text": "", + "required": true, + "position": 4, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "url", + "purpose": "demo", + "label": "Демо", + "help_text": "", + "required": false, + "position": 5, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "presentation", + "label": "Презентация PDF", + "help_text": "", + "required": false, + "position": 6, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf" + ], + "max_file_size_bytes": 20971520 + } + }, + { + "id": null, + "type": "url", + "purpose": "video", + "label": "Видео", + "help_text": "", + "required": false, + "position": 7, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [], + "max_file_size_bytes": null + } + }, + { + "id": null, + "type": "file", + "purpose": "other", + "label": "Дополнительные материалы", + "help_text": "", + "required": false, + "position": 8, + "options": [], + "validation": { + "min_length": null, + "max_length": null, + "minimum": null, + "maximum": null, + "allowed_extensions": [ + "pdf", + "zip" + ], + "max_file_size_bytes": 52428800 + } + } + ] + } + ], + "max_file_size_bytes": 104857600, + "files_per_field": 1 + } + } + } + } + } + }, + "400": { + "description": "Контролируемая ошибка 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Контролируемая ошибка 401", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Контролируемая ошибка 403", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Контролируемая ошибка 404", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Контролируемая ошибка 409", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/files/": { + "post": { + "operationId": "uploadExistingFile", + "summary": "Существующая загрузка файла", + "description": "Существующий endpoint вне нового error envelope. Передать полученный URL в value:{url}.", + "x-existing-endpoint": true, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "file" + ], + "additionalProperties": false + } + } + } + }, + "responses": { + "201": { + "description": "Файл загружен", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResponse" + }, + "examples": { + "upload": { + "value": { + "url": "https://cdn.example.test/solutions/301/presentation.pdf" + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "avatar_url": { + "anyOf": [ + { + "type": "string", + "format": "uri", + "pattern": "^https?://" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "first_name", + "last_name", + "avatar_url" + ], + "additionalProperties": false + }, + "ProgramRef": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "SolutionRef": { + "type": "object", + "properties": { + "kind": { + "const": "solution" + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "kind", + "program_id", + "solution_id", + "name" + ], + "additionalProperties": false + }, + "LegacyRef": { + "type": "object", + "properties": { + "kind": { + "const": "legacy_project" + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "project_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program_link_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "kind", + "program_id", + "project_id", + "program_link_id", + "name" + ], + "additionalProperties": false + }, + "ApplicationRef": { + "oneOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "$ref": "#/components/schemas/LegacyRef" + } + ] + }, + "Issue": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "code": { + "type": "string" + }, + "field_id": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "detail": { + "type": "string" + } + }, + "required": [ + "path", + "code", + "field_id", + "detail" + ], + "additionalProperties": false + }, + "Error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + }, + "meta": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "code", + "detail", + "errors", + "meta" + ], + "additionalProperties": false + }, + "Context": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "storage_mode": { + "enum": [ + "legacy_project", + "solution" + ] + }, + "is_registered": { + "type": "boolean" + }, + "is_competitive": { + "type": "boolean" + }, + "submission_open": { + "type": "boolean" + }, + "submission_deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "can_create": { + "type": "boolean" + }, + "blocked_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "current_application": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationRef" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "program_id", + "storage_mode", + "is_registered", + "is_competitive", + "submission_open", + "submission_deadline", + "can_create", + "blocked_reason", + "current_application" + ], + "additionalProperties": false + }, + "Validation": { + "type": "object", + "properties": { + "min_length": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "max_length": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "minimum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "allowed_extensions": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]+$" + } + }, + "max_file_size_bytes": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 104857600 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "min_length", + "max_length", + "minimum", + "maximum", + "allowed_extensions", + "max_file_size_bytes" + ], + "additionalProperties": false + }, + "FieldType": { + "enum": [ + "text", + "textarea", + "url", + "number", + "select", + "checkbox", + "file", + "radio" + ] + }, + "Purpose": { + "enum": [ + "other", + "case", + "presentation", + "repository", + "demo", + "video", + "run_instructions" + ] + }, + "TemplateName": { + "enum": [ + "case_championship", + "hackathon" + ] + }, + "Field": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "type": { + "$ref": "#/components/schemas/FieldType" + }, + "purpose": { + "$ref": "#/components/schemas/Purpose" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "help_text": { + "type": "string", + "maxLength": 4000 + }, + "required": { + "type": "boolean" + }, + "position": { + "type": "integer", + "minimum": 0 + }, + "options": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "validation": { + "$ref": "#/components/schemas/Validation" + } + }, + "required": [ + "id", + "type", + "purpose", + "label", + "help_text", + "required", + "position", + "options", + "validation" + ], + "additionalProperties": false + }, + "FieldWrite": { + "type": "object", + "properties": { + "id": { + "anyOf": [ + { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "type": { + "$ref": "#/components/schemas/FieldType" + }, + "purpose": { + "$ref": "#/components/schemas/Purpose" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "help_text": { + "type": "string", + "maxLength": 4000 + }, + "required": { + "type": "boolean" + }, + "position": { + "type": "integer", + "minimum": 0 + }, + "options": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "validation": { + "$ref": "#/components/schemas/Validation" + } + }, + "required": [ + "id", + "type", + "purpose", + "label", + "help_text", + "required", + "position", + "options", + "validation" + ], + "additionalProperties": false + }, + "Form": { + "type": "object", + "properties": { + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "template": { + "anyOf": [ + { + "$ref": "#/components/schemas/TemplateName" + }, + { + "type": "null" + } + ] + }, + "is_configured": { + "type": "boolean" + }, + "is_snapshot": { + "type": "boolean" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Field" + } + } + }, + "required": [ + "program_id", + "revision", + "template", + "is_configured", + "is_snapshot", + "fields" + ], + "additionalProperties": false + }, + "FormWrite": { + "type": "object", + "properties": { + "expected_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "template": { + "$ref": "#/components/schemas/TemplateName" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldWrite" + } + } + }, + "required": [ + "expected_revision", + "template", + "fields" + ], + "additionalProperties": false + }, + "FormManagement": { + "type": "object", + "properties": { + "form": { + "$ref": "#/components/schemas/Form" + }, + "has_solutions": { + "type": "boolean" + }, + "has_answers": { + "type": "boolean" + }, + "affected_draft_count": { + "type": "integer", + "minimum": 0 + }, + "fields_usage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "answers_count": { + "type": "integer", + "minimum": 0 + }, + "can_delete": { + "type": "boolean" + }, + "can_change_type": { + "type": "boolean" + } + }, + "required": [ + "field_id", + "answers_count", + "can_delete", + "can_change_type" + ], + "additionalProperties": false + } + } + }, + "required": [ + "form", + "has_solutions", + "has_answers", + "affected_draft_count", + "fields_usage" + ], + "additionalProperties": false + }, + "Template": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/TemplateName" + }, + "label": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldWrite" + } + } + }, + "required": [ + "key", + "label", + "fields" + ], + "additionalProperties": false + }, + "Templates": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Template" + } + }, + "max_file_size_bytes": { + "const": 104857600 + }, + "files_per_field": { + "const": 1 + } + }, + "required": [ + "results", + "max_file_size_bytes", + "files_per_field" + ], + "additionalProperties": false + }, + "FileRef": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "AnswerValue": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/FileRef" + } + ] + }, + "Answer": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "value": { + "$ref": "#/components/schemas/AnswerValue" + } + }, + "required": [ + "field_id", + "value" + ], + "additionalProperties": false + }, + "AnswerPatch": { + "type": "object", + "properties": { + "field_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "value": { + "anyOf": [ + { + "$ref": "#/components/schemas/AnswerValue" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "field_id", + "value" + ], + "additionalProperties": false + }, + "Member": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/User" + }, + "role": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "joined_at": { + "type": "string", + "format": "date-time" + }, + "is_captain": { + "type": "boolean" + } + }, + "required": [ + "user", + "role", + "specialization", + "joined_at", + "is_captain" + ], + "additionalProperties": false + }, + "Team": { + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Member" + } + }, + "accepted_count": { + "type": "integer", + "minimum": 0 + }, + "pending_count": { + "type": "integer", + "minimum": 0 + }, + "reserved_count": { + "type": "integer", + "minimum": 0 + }, + "min_size": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + }, + "max_size": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + }, + "is_frozen": { + "type": "boolean" + } + }, + "required": [ + "members", + "accepted_count", + "pending_count", + "reserved_count", + "min_size", + "max_size", + "is_frozen" + ], + "additionalProperties": false + }, + "Permissions": { + "type": "object", + "properties": { + "can_edit": { + "type": "boolean" + }, + "can_manage_team": { + "type": "boolean" + }, + "can_invite": { + "type": "boolean" + }, + "can_submit": { + "type": "boolean" + } + }, + "required": [ + "can_edit", + "can_manage_team", + "can_invite", + "can_submit" + ], + "additionalProperties": false + }, + "Submission": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + }, + "status": { + "enum": [ + "draft", + "submitted" + ] + }, + "submission_open": { + "type": "boolean" + }, + "deadline": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "submitted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "required", + "status", + "submission_open", + "deadline", + "submitted_at" + ], + "additionalProperties": false + }, + "Readiness": { + "type": "object", + "properties": { + "is_ready": { + "type": "boolean" + }, + "issues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Issue" + } + } + }, + "required": [ + "is_ready", + "issues" + ], + "additionalProperties": false + }, + "Solution": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "version": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 256 + }, + "captain_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "team": { + "$ref": "#/components/schemas/Team" + }, + "form": { + "$ref": "#/components/schemas/Form" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } + }, + "submission": { + "$ref": "#/components/schemas/Submission" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "readiness": { + "anyOf": [ + { + "$ref": "#/components/schemas/Readiness" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "program_id", + "version", + "name", + "captain_id", + "created_at", + "updated_at", + "team", + "form", + "answers", + "submission", + "permissions", + "readiness" + ], + "additionalProperties": false + }, + "SolutionSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "program_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "captain": { + "$ref": "#/components/schemas/User" + }, + "accepted_count": { + "type": "integer", + "minimum": 0 + }, + "case_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": [ + "draft", + "submitted" + ] + }, + "submitted_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "program_id", + "name", + "captain", + "accepted_count", + "case_value", + "status", + "submitted_at" + ], + "additionalProperties": false + }, + "CreateSolution": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": [], + "additionalProperties": false + }, + "UpdateSolution": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AnswerPatch" + } + } + }, + "required": [ + "expected_version", + "expected_form_revision" + ], + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "answers" + ] + } + ] + }, + "Submit": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_form_revision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + } + }, + "required": [ + "expected_version", + "expected_form_revision" + ], + "additionalProperties": false + }, + "CaptainCommand": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "new_captain_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "expected_solution_version", + "new_captain_user_id" + ], + "additionalProperties": false + }, + "TeamCommand": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "expected_solution_version" + ], + "additionalProperties": false + }, + "InvitationPermissions": { + "type": "object", + "properties": { + "can_edit": { + "type": "boolean" + }, + "can_revoke": { + "type": "boolean" + }, + "can_accept": { + "type": "boolean" + }, + "can_decline": { + "type": "boolean" + } + }, + "required": [ + "can_edit", + "can_revoke", + "can_accept", + "can_decline" + ], + "additionalProperties": false + }, + "Invitation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "version": { + "type": "integer", + "minimum": 1 + }, + "program": { + "$ref": "#/components/schemas/ProgramRef" + }, + "solution": { + "$ref": "#/components/schemas/SolutionRef" + }, + "captain": { + "$ref": "#/components/schemas/User" + }, + "recipient": { + "$ref": "#/components/schemas/User" + }, + "role": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] + }, + "status": { + "enum": [ + "pending", + "accepted", + "declined" + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "permissions": { + "$ref": "#/components/schemas/InvitationPermissions" + } + }, + "required": [ + "id", + "version", + "program", + "solution", + "captain", + "recipient", + "role", + "specialization", + "motivational_letter", + "status", + "created_at", + "updated_at", + "permissions" + ], + "additionalProperties": false + }, + "CreateInvitation": { + "type": "object", + "properties": { + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "recipient_user_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "role": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expected_solution_version", + "recipient_user_id" + ], + "additionalProperties": false + }, + "UpdateInvitation": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + }, + "expected_solution_version": { + "type": "integer", + "minimum": 1 + }, + "role": { + "anyOf": [ + { + "type": "string", + "maxLength": 128 + }, + { + "type": "null" + } + ] + }, + "specialization": { + "anyOf": [ + { + "type": "string", + "maxLength": 100 + }, + { + "type": "null" + } + ] + }, + "motivational_letter": { + "anyOf": [ + { + "type": "string", + "maxLength": 4096 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "expected_version", + "expected_solution_version" + ], + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "role" + ] + }, + { + "required": [ + "specialization" + ] + }, + { + "required": [ + "motivational_letter" + ] + } + ] + }, + "InvitationAction": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "expected_version" + ], + "additionalProperties": false + }, + "InvitationCreated": { + "type": "object", + "properties": { + "invitation": { + "$ref": "#/components/schemas/Invitation" + }, + "solution_version": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "invitation", + "solution_version" + ], + "additionalProperties": false + }, + "InvitationActionResult": { + "type": "object", + "properties": { + "invitation": { + "$ref": "#/components/schemas/Invitation" + }, + "application": { + "anyOf": [ + { + "$ref": "#/components/schemas/SolutionRef" + }, + { + "type": "null" + } + ] + }, + "solution_version": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "invitation", + "application", + "solution_version" + ], + "additionalProperties": false + }, + "MutationReceipt": { + "type": "object", + "properties": { + "solution_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "solution_version": { + "type": "integer", + "minimum": 1 + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "solution_id", + "solution_version", + "updated_at" + ], + "additionalProperties": false + }, + "SolutionPage": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SolutionSummary" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "InvitationPage": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 + }, + "next": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "previous": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ], + "additionalProperties": false + }, + "UploadResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "SubmissionSnapshot": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "captain_id", + "members", + "form", + "answers", + "submitted_at" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 256 + }, + "captain_id": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Member" + } + }, + "form": { + "$ref": "#/components/schemas/Form" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Answer" + } + }, + "submitted_at": { + "type": "string", + "format": "date-time" + } + } + } + } + } +} diff --git a/docs/contracts/program-solutions-v1/validate_contract.py b/docs/contracts/program-solutions-v1/validate_contract.py new file mode 100644 index 00000000..3ed81f22 --- /dev/null +++ b/docs/contracts/program-solutions-v1/validate_contract.py @@ -0,0 +1,91 @@ +"""Validate the C00 document and independent mock scenarios, not a running API.""" +import json +from pathlib import Path + +from jsonschema import Draft202012Validator, FormatChecker +from openapi_spec_validator import validate_spec + +ROOT = Path(__file__).resolve().parent +spec = json.loads((ROOT / "openapi.json").read_text(encoding="utf-8")) +examples = json.loads((ROOT / "examples.json").read_text(encoding="utf-8")) +validate_spec(spec) +schemas = spec["components"]["schemas"] +for schema in schemas.values(): + Draft202012Validator.check_schema(schema) + + +def check(value, schema): + wrapper = {"components": spec["components"], **schema} + Draft202012Validator(wrapper, format_checker=FormatChecker()).validate(value) + + +operations = {} +for path, item in spec["paths"].items(): + for method, operation in item.items(): + key = operation["operationId"] + assert key not in operations, key + operations[key] = operation + +names = set() +covered = set() +for scenario in examples["scenarios"]: + name = scenario["name"] + assert name not in names, name + names.add(name) + operation = operations[scenario["operation_id"]] + covered.add(scenario["operation_id"]) + if "request" in scenario: + content = operation["requestBody"]["content"]["application/json"] + check(scenario["request"], content["schema"]) + response = operation["responses"][str(scenario["status"])] + if scenario["status"] == 204: + assert "response" not in scenario, name + else: + check(scenario["response"], response["content"]["application/json"]["schema"]) + payload = scenario.get("response", {}) + if "team" in payload: + team = payload["team"] + user_ids = [member["user"]["id"] for member in team["members"]] + assert len(user_ids) == len(set(user_ids)) == team["accepted_count"], name + captains = [m["user"]["id"] for m in team["members"] if m["is_captain"]] + assert captains == [payload["captain_id"]], name + assert ( + team["reserved_count"] == team["accepted_count"] + team["pending_count"] + ), name + fields = {field["id"]: field for field in payload["form"]["fields"]} + assert len(fields) == len(payload["form"]["fields"]), name + assert payload["form"]["program_id"] == payload["program_id"], name + ids = [a["field_id"] for a in payload["answers"]] + assert len(ids) == len(set(ids)), name + for answer in payload["answers"]: + field = fields[answer["field_id"]] + val = answer["value"] + field_type = field["type"] + assert ( + isinstance(val, str) + if field_type in ("text", "textarea", "url", "select", "radio") + else isinstance(val, bool) + if field_type == "checkbox" + else type(val) in (int, float) + if field_type == "number" + else isinstance(val, dict) and set(val) == {"url"} + ), name + if field_type in ("select", "radio"): + assert val in field["options"], name + if payload["submission"]["status"] == "submitted": + assert team["is_frozen"] and payload["form"]["is_snapshot"], name + assert payload["submission"]["submitted_at"] is not None, name + assert not any(payload["permissions"].values()), name + if payload["readiness"] and payload["readiness"]["is_ready"]: + answered = {a["field_id"] for a in payload["answers"]} + assert {f["id"] for f in fields.values() if f["required"]} <= answered, name + assert ( + team["min_size"] is None or team["accepted_count"] >= team["min_size"] + ), name +assert covered == set(operations), ( + "Operations missing examples", + sorted(set(operations) - covered), +) +print( # noqa: T201 (CLI report) + f"PASS: OpenAPI; {len(schemas)} schemas; {len(operations)} operations; {len(names)} scenarios" +) diff --git a/docs/product/program-solutions-be01.md b/docs/product/program-solutions-be01.md new file mode 100644 index 00000000..37717478 --- /dev/null +++ b/docs/product/program-solutions-be01.md @@ -0,0 +1,44 @@ +# BE01 — основа решения в программах + +Дизайн D00 v3 утвержден пользователем 24 октября 2026 года. ТЗ: https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175 . Контракт C00 1.0.0: `docs/contracts/program-solutions-v1/`. + +## План и границы + +Модели и аддитивная миграция: ProgramSolution с принадлежащими ему участниками, приглашениями и ответами; системные колонки формы; технический режим программы legacy_project по умолчанию. Старые данные не переносятся и не удаляются. + +API: контекст программы, идемпотентное создание, чтение и список по C00. Создание сериализуется блокировкой программы и проверяет существующую заявку через принятый состав и капитанство, включая legacy. Капитан и единственная запись состава создаются атомарно. Project, самостоятельная Team и новая регистрация не создаются. + +В BE01 нет PATCH/submit и команд состава/приглашений; permissions этих действий false. На этом этапе читать может своя команда и организатор/администратор программы. Политика эксперта и публикации подключается в BE07/BE08, до этого доступ не расширяется. Технический режим решения вручную включается только для тестовой новой программы после настройки формы; REL01 включает готовую функцию позже. + +FE01 выполняется независимо по тем же схемам: модели, адаптеры, use cases, маршруты, явное ветвление по storage_mode и моки. Полный редактор и приглашения относятся к FE03–FE06. + +Проверки: AC01/AC02/AC20/AC22; schema совместимость реальных HTTP ответов с C00; строгие запросы; отсутствие доступа из чужой программы; PostgreSQL миграции, уникальность и конкурентное создание; legacy regression. Публичные старые endpoints не меняются для legacy программ. В режиме solution старый POST создания проекта отклоняется. + +## Verification и review от 24 октября 2026 + +Checked by source: diff от `origin/dev` f77b6048. Изменения только в контуре partner_programs и dev-зависимостях проверки C00. Создание блокирует строку программы; повтор возвращает 200, новый объект 201. Новый и старый ID не взаимозаменяемы. Запросы строго проверяются, ответы новых API имеют envelope C00. Права регистрации повторяют project_apply (исключение для менеджера, без автоматического исключения staff). Видимость ограничена своей командой и организатором/администратором. Состав, приглашения и сдача не имеют изменяющих endpoints. + +Checked by tests: **147 tests PASS** на локальной изолированной PostgreSQL 18, Python 3.11, `procollab.settings_ci`: + +```text +python manage.py test partner_programs.tests.test_solution_foundation partner_programs.tests.test_project_apply partner_programs.tests.test_case_fields partner_programs.tests.test_field_values partner_programs.tests.test_field_value_mutation partner_programs.tests.test_management_project_form_api projects.tests.test_competitive_team_policy projects.tests.test_team_security invites.tests.test_invite_integrity invites.tests.test_invite_decisions --settings=procollab.settings_ci --noinput --keepdb +``` + +15 новых проверок включают реальные конкурентные транзакции с разными соединениями, ограничения БД, приоритет прав/дедлайна, закрытые команды, snapshot чтения, strict input и валидацию реальных HTTP ответов JSON Schema из C00. Остальные проверки подтверждают прежние формы, проекты, приглашения и team policy. Добавлены dev-зависимости jsonschema и openapi-spec-validator; `poetry check --lock` PASS, lock 2.0 сохранен. + +Checked by migration/checks: `manage.py check`, применение всех миграций на пустой локальной PostgreSQL, `makemigrations --check --dry-run` PASS. Миграция 0030 только добавляет структуры и варианты типов. `black --check` и `flake8` новых Python-файлов PASS. `validate_contract.py` PASS: 47 схем, 22 операции, 43 сценария. Это не означает реализацию всех операций C00: в BE01 доступны четыре операции контекста/создания/чтения/списка. + +Checked in browser совместно с FE01: реальная JWT авторизация вымышленного пользователя, создание решения без Project, повторный вход в тот же ID, чтение вкладок и переход из программы. Старый режим создает Project и открывает `/office/projects/1/edit?...&programLinkId=1`. Тестовая БД после этих сценариев содержит одно решение новой программы и один проект старой. + +Not checked: полный набор 693 старых тестов не завершился — Windows native PDF rendering сертификата завершился access violation; он не помечается PASS. PDF/сертификаты не затронуты. Полный suite остается проверкой Linux CI. Production, перенос данных BE10 и полный путь приглашений/сдачи QA01 не выполнялись. + +| AC в scope BE01 | Evidence | Verdict | +| --- | --- | --- | +| AC01: решение без Project/Team | Новый API, отсутствие Project/связи, атомарный captain member; PostgreSQL tests и реальный браузер FE01 | PASS | +| AC02: повтор и гонка без дубля | Repeat 200 после дедлайна, неизменное имя; два соединения дают один ID и одну запись состава | PASS | +| AC20: изоляция программ | Чужой ID дает 404, чужой менеджер не читает; current_application берет точный program/link | PASS | +| AC22: старый контур | 132 связанные regression tests и старый браузерный путь; default legacy_project | PASS в scope BE01 | + +Review: исправлены порядок ошибок прав/дедлайна по C00 и чтение сохраненного snapshot в деталях/списке. После исправлений 147 tests PASS. Оставшихся BLOCKER/MAJOR/MINOR в scope нет. Остальные AC относятся к BE02–BE10/FE02–FE08/QA01 и этим этапом не закрываются. + +Порядок внедрения: backend с миграцией и выключенным режимом → FE01. Не включать режим для участников до BE02–BE09, FE02–FE08 и QA01/REL01. Отключение режима не скрывает чтение уже созданного решения. Миграции со старого Project нет; merge и deployment выполняет пользователь. diff --git a/partner_programs/migrations/0030_program_solution_foundation.py b/partner_programs/migrations/0030_program_solution_foundation.py new file mode 100644 index 00000000..2057396b --- /dev/null +++ b/partner_programs/migrations/0030_program_solution_foundation.py @@ -0,0 +1,138 @@ +# Generated by Django 4.2.11 on 2026-10-24 12:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('partner_programs', '0029_programcertificateasset_programcertificateconfig_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ProgramSolution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Новое решение', max_length=256)), + ('version', models.PositiveBigIntegerField(default=1)), + ('status', models.CharField(choices=[('draft', 'Черновик'), ('submitted', 'Сдано')], default='draft', max_length=12)), + ('submitted_at', models.DateTimeField(blank=True, null=True)), + ('submission_snapshot', models.JSONField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('captain', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['created_at', 'pk'], + }, + ), + migrations.AddField( + model_name='partnerprogram', + name='solution_storage_mode', + field=models.CharField(choices=[('legacy_project', 'Проект'), ('solution', 'Решение')], default='legacy_project', max_length=20, verbose_name='Технический режим заявки'), + ), + migrations.AddField( + model_name='partnerprogram', + name='solution_template', + field=models.CharField(blank=True, choices=[('case_championship', 'Кейс-чемпионат'), ('hackathon', 'Хакатон')], max_length=24, null=True, verbose_name='Шаблон формы решения'), + ), + migrations.AddField( + model_name='partnerprogramfield', + name='position', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='partnerprogramfield', + name='purpose', + field=models.CharField(default='other', max_length=24), + ), + migrations.AddField( + model_name='partnerprogramfield', + name='validation', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='partnerprogramfield', + name='field_type', + field=models.CharField(choices=[('text', 'Однострочный текст'), ('textarea', 'Многострочный текст'), ('checkbox', 'Чекбокс'), ('select', 'Выпадающий список'), ('radio', 'Радио-кнопка'), ('file', 'Файл'), ('url', 'Ссылка'), ('number', 'Число')], max_length=20), + ), + migrations.CreateModel( + name='SolutionInvitation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(blank=True, max_length=128, null=True)), + ('specialization', models.CharField(blank=True, max_length=100, null=True)), + ('motivational_letter', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('pending', 'Ожидает ответа'), ('accepted', 'Принято'), ('declined', 'Отклонено')], default='pending', max_length=12)), + ('version', models.PositiveBigIntegerField(default=1)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='partner_programs.programsolution')), + ], + ), + migrations.CreateModel( + name='ProgramSolutionMember', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(blank=True, max_length=128, null=True)), + ('specialization', models.CharField(blank=True, max_length=100, null=True)), + ('joined_at', models.DateTimeField(auto_now_add=True)), + ('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='partner_programs.programsolution')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['joined_at', 'pk'], + }, + ), + migrations.CreateModel( + name='ProgramSolutionAnswer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.JSONField()), + ('field', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_answers', to='partner_programs.partnerprogramfield')), + ('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='partner_programs.programsolution')), + ], + ), + migrations.AddField( + model_name='programsolution', + name='program', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='partner_programs.partnerprogram'), + ), + migrations.AddConstraint( + model_name='solutioninvitation', + constraint=models.UniqueConstraint(condition=models.Q(('status', 'pending')), fields=('solution', 'recipient'), name='unique_pending_solution_invite'), + ), + migrations.AddConstraint( + model_name='solutioninvitation', + constraint=models.CheckConstraint(check=models.Q(('version__gte', 1)), name='solution_invite_version_positive'), + ), + migrations.AddConstraint( + model_name='solutioninvitation', + constraint=models.CheckConstraint(check=models.Q(('status__in', ['pending', 'accepted', 'declined'])), name='solution_invite_status_valid'), + ), + migrations.AddConstraint( + model_name='programsolutionmember', + constraint=models.UniqueConstraint(fields=('solution', 'user'), name='unique_solution_member'), + ), + migrations.AddConstraint( + model_name='programsolutionanswer', + constraint=models.UniqueConstraint(fields=('solution', 'field'), name='unique_solution_answer'), + ), + migrations.AddConstraint( + model_name='programsolution', + constraint=models.UniqueConstraint(fields=('program', 'captain'), name='unique_solution_program_captain'), + ), + migrations.AddConstraint( + model_name='programsolution', + constraint=models.CheckConstraint(check=models.Q(('version__gte', 1)), name='solution_version_positive'), + ), + migrations.AddConstraint( + model_name='programsolution', + constraint=models.CheckConstraint(check=models.Q(models.Q(('status', 'draft'), ('submission_snapshot__isnull', True), ('submitted_at__isnull', True)), models.Q(('status', 'submitted'), ('submission_snapshot__isnull', False), ('submitted_at__isnull', False)), _connector='OR'), name='solution_submission_consistent'), + ), + ] diff --git a/partner_programs/models.py b/partner_programs/models.py index e06c175c..d9ff7263 100644 --- a/partner_programs/models.py +++ b/partner_programs/models.py @@ -11,6 +11,12 @@ ProgramCertificateConfig, ) from partner_programs.constants import get_default_data_schema +from partner_programs.solution_models import ( # noqa: F401 + ProgramSolution, + ProgramSolutionMember, + SolutionInvitation, + ProgramSolutionAnswer, +) from projects.models import Project from partner_programs.attention_models import ( # noqa: F401 ProgramAttentionReminder, @@ -112,6 +118,19 @@ class PartnerProgram(models.Model): verbose_name="Конкурсная программа", help_text="Если включено, проекты участников подлежат сдаче на проверку", ) + solution_storage_mode = models.CharField( + max_length=20, + choices=[("legacy_project", "Проект"), ("solution", "Решение")], + default="legacy_project", + verbose_name="Технический режим заявки", + ) + solution_template = models.CharField( + max_length=24, + choices=[("case_championship", "Кейс-чемпионат"), ("hackathon", "Хакатон")], + null=True, + blank=True, + verbose_name="Шаблон формы решения", + ) legacy_team_min_size = models.PositiveIntegerField( null=True, blank=True, @@ -445,6 +464,11 @@ def clean(self): "Связь проекта с программой нельзя переназначить; создайте новую связь.", ) if previous is None: + if self.partner_program.solution_storage_mode == "solution": + raise TeamError( + "solution_mode_unavailable", + "В этой программе заявка создается как решение.", + ) team_policy.validate_project_binding(self.project, self.partner_program) if previous and previous.submitted and not self.submitted: raise TeamError( @@ -524,6 +548,8 @@ class PartnerProgramField(models.Model): ("select", "Выпадающий список"), ("radio", "Радио-кнопка"), ("file", "Файл"), + ("url", "Ссылка"), + ("number", "Число"), ] partner_program = models.ForeignKey( @@ -533,6 +559,9 @@ class PartnerProgramField(models.Model): label = models.CharField(max_length=256, verbose_name="Отображаемое название") field_type = models.CharField(max_length=20, choices=FIELD_TYPES) is_required = models.BooleanField(default=False) + purpose = models.CharField(max_length=24, default="other") + position = models.PositiveIntegerField(default=0) + validation = models.JSONField(default=dict, blank=True) help_text = models.TextField(blank=True, null=True) show_filter = models.BooleanField(default=False) options = models.TextField( diff --git a/partner_programs/services/project_apply.py b/partner_programs/services/project_apply.py index f7f0a192..31eb2bc3 100644 --- a/partner_programs/services/project_apply.py +++ b/partner_programs/services/project_apply.py @@ -92,6 +92,8 @@ def apply_project_to_program( from projects import team_policy program = PartnerProgram.objects.select_for_update().get(pk=program.pk) + if program.solution_storage_mode == "solution": + raise PermissionDenied("В этой программе заявка создается как решение.") require_can_apply_project_to_program(program=program, user=user) existing_link = ( diff --git a/partner_programs/solution_api.py b/partner_programs/solution_api.py new file mode 100644 index 00000000..e2a1adde --- /dev/null +++ b/partner_programs/solution_api.py @@ -0,0 +1,493 @@ +"""Основа контракта C00: контекст, создание и чтение. Команды BE02–BE06 закрыты.""" + +import hashlib +import json + +from django.db import transaction +from django.db.models import Q +from django.http import Http404 +from rest_framework import serializers +from rest_framework.exceptions import APIException, ValidationError +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from partner_programs.models import PartnerProgram, PartnerProgramUserProfile +from partner_programs.permissions import can_manage_program +from partner_programs.services.role_analytics import participant_project_links +from partner_programs.solution_models import ProgramSolution, ProgramSolutionMember + +MAX_ID = 2**53 - 1 +VALIDATION_DEFAULTS = dict( + min_length=None, + max_length=None, + minimum=None, + maximum=None, + allowed_extensions=[], + max_file_size_bytes=None, +) + + +class SolutionError(APIException): + def __init__(self, code, detail, status=409, *, errors=None, meta=None): + self.status_code = status + self.payload = dict( + code=code, detail=detail, errors=errors or [], meta=meta or {} + ) + super().__init__(detail=detail, code=code) + + +def issue(path, code, detail, field_id=None): + return dict(path=path, code=code, detail=detail, field_id=field_id) + + +def iso(value): + return value.isoformat().replace("+00:00", "Z") if value else None + + +def user_data(user): + return dict( + id=user.pk, + first_name=user.first_name, + last_name=user.last_name, + avatar_url=user.avatar or None, + ) + + +def program_for(pk, *, lock=False): + if not 0 < pk <= MAX_ID: + raise Http404 + query = PartnerProgram.objects.select_for_update() if lock else PartnerProgram.objects + try: + return query.get(pk=pk) + except PartnerProgram.DoesNotExist: + raise Http404 + + +def current_application(program, user): + solutions = list( + ProgramSolution.objects.filter(program=program) + .filter(Q(captain=user) | Q(members__user=user)) + .distinct() + .order_by("created_at", "pk")[:2] + ) + links = list( + participant_project_links(program, user) + .select_related("project") + .order_by("pk")[:2] + ) + if len(solutions) + len(links) > 1: + raise SolutionError( + "ambiguous_application", "Связи заявок требуют проверки организатором." + ) + if solutions: + obj = solutions[0] + return dict( + kind="solution", program_id=program.pk, solution_id=obj.pk, name=obj.name + ) + if links: + obj = links[0] + return dict( + kind="legacy_project", + program_id=program.pk, + project_id=obj.project_id, + program_link_id=obj.pk, + name=obj.project.name, + ) + return None + + +def form_data(program): + fields = [ + dict( + id=f.pk, + type=f.field_type, + purpose="case" if f.name == "case" else f.purpose, + label=f.label, + help_text=f.help_text or "", + required=f.is_required, + position=f.position, + options=f.get_options_list(), + validation={ + key: f.validation.get(key, default) + for key, default in VALIDATION_DEFAULTS.items() + }, + ) + for f in program.fields.order_by("position", "pk") + ] + definition = dict(template=program.solution_template, fields=fields) + revision = hashlib.sha256( + json.dumps( + definition, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode() + ).hexdigest() + configured = program.solution_template in ("case_championship", "hackathon") and any( + f["purpose"] == "case" + and f["type"] == "select" + and f["required"] + and f["options"] + for f in fields + ) + return dict( + program_id=program.pk, + revision=revision, + template=program.solution_template, + is_configured=bool(configured), + is_snapshot=False, + fields=fields, + ) + + +def context_data(program, user): + application = current_application(program, user) + registered = PartnerProgramUserProfile.objects.filter( + partner_program=program, user=user + ).exists() + reason = None + if application: + reason = "already_has_application" + elif program.solution_storage_mode != "solution": + reason = "legacy_mode" + elif not (registered or program.is_manager(user)): + reason = "not_program_member" + elif not program.is_project_submission_open(): + reason = "submission_closed" + elif not form_data(program)["is_configured"]: + reason = "form_not_configured" + return dict( + program_id=program.pk, + storage_mode=program.solution_storage_mode, + is_registered=registered, + is_competitive=program.is_competitive, + submission_open=program.is_project_submission_open(), + submission_deadline=iso(program.get_project_submission_deadline()), + can_create=reason is None, + blocked_reason=reason, + current_application=application, + ) + + +def visible_solutions(program, user): + query = ProgramSolution.objects.filter(program=program) + # Экспертные назначения и публикация подключаются в BE07/BE08; до этого закрыты. + if can_manage_program(user, program): + return query + return query.filter(Q(captain=user) | Q(members__user=user)).distinct() + + +def solution_data(solution, user): + program = solution.program + members = list(solution.members.select_related("user")) + accepted = {m.user_id for m in members} + pending = set( + solution.invitations.filter(status="pending").values_list( + "recipient_id", flat=True + ) + ) + own = solution.captain_id == user.pk or user.pk in accepted + submitted = solution.status == "submitted" + form = form_data(program) + answers = [ + dict(field_id=a.field_id, value=a.value) + for a in solution.answers.order_by("field_id") + ] + name, captain_id = solution.name, solution.captain_id + member_data = [ + dict( + user=user_data(m.user), + role=m.role, + specialization=m.specialization, + joined_at=iso(m.joined_at), + is_captain=m.user_id == captain_id, + ) + for m in members + ] + snapshot = solution.submission_snapshot + if submitted and snapshot: + name, captain_id = snapshot["name"], snapshot["captain_id"] + form, answers, member_data = ( + snapshot["form"], + snapshot["answers"], + snapshot["members"], + ) + accepted = {member["user"]["id"] for member in member_data} + problems = [] + if program.is_competitive and own: + if not name.strip(): + problems.append(issue("name", "required", "Укажите название решения.")) + if program.legacy_team_min_size and len(accepted) < program.legacy_team_min_size: + problems.append( + issue( + "team", "team_min_size", "В команде недостаточно принятых участников." + ) + ) + if program.legacy_team_max_size and len(accepted) > program.legacy_team_max_size: + problems.append( + issue("team", "team_max_size", "Превышен максимальный размер команды.") + ) + values = {a["field_id"]: a["value"] for a in answers} + for field in form["fields"]: + value = values.get(field["id"]) + if field["required"] and ( + value is None or isinstance(value, str) and not value.strip() + ): + problems.append( + issue( + f"answers.{field['id']}", + "required", + "Заполните обязательное поле.", + field["id"], + ) + ) + return dict( + id=solution.pk, + program_id=program.pk, + version=solution.version, + name=name, + captain_id=captain_id, + created_at=iso(solution.created_at), + updated_at=iso(solution.updated_at), + team=dict( + members=member_data, + accepted_count=len(accepted), + pending_count=len(pending), + reserved_count=len(accepted | pending), + min_size=program.legacy_team_min_size, + max_size=program.legacy_team_max_size, + is_frozen=submitted, + ), + form=form, + answers=answers, + submission=dict( + required=program.is_competitive, + status=solution.status, + submission_open=program.is_project_submission_open(), + deadline=iso(program.get_project_submission_deadline()), + submitted_at=iso(solution.submitted_at), + ), + # BE01 не публикует еще не реализованные команды BE02–BE06. + permissions=dict( + can_edit=False, can_manage_team=False, can_invite=False, can_submit=False + ), + readiness=dict(is_ready=not problems, issues=problems) + if program.is_competitive and own + else None, + ) + + +class StrictSerializer(serializers.Serializer): + def to_internal_value(self, data): + if isinstance(data, dict): + unknown = sorted(set(data) - set(self.fields)) + if unknown: + raise ValidationError({key: "Неизвестное поле." for key in unknown}) + return super().to_internal_value(data) + + +class NameField(serializers.CharField): + def to_internal_value(self, value): + if not isinstance(value, str): + self.fail("invalid") + return super().to_internal_value(value) + + +class CreateSolutionSerializer(StrictSerializer): + name = NameField(required=False, min_length=1, max_length=256, trim_whitespace=False) + + +class SolutionListQuery(StrictSerializer): + limit = serializers.IntegerField(min_value=1, max_value=100, default=20) + offset = serializers.IntegerField(min_value=0, default=0) + search = serializers.CharField(required=False, allow_blank=True) + submission = serializers.ChoiceField( + choices=["all", "submitted", "not_submitted"], default="all" + ) + + +@transaction.atomic +def create_solution(program_id, user, data): + program = program_for(program_id, lock=True) + if program.solution_storage_mode != "solution": + raise SolutionError( + "solution_mode_unavailable", "Программа использует заявки на основе проектов." + ) + current = current_application(program, user) + if current: + if current["kind"] == "solution": + existing = ProgramSolution.objects.get(pk=current["solution_id"]) + if existing.captain_id == user.pk: + return existing, False + raise SolutionError( + "already_in_program_team", "У вас уже есть заявка команды в этой программе." + ) + # Same registration/manager exception as legacy project_apply; C00 checks rights first. + if not ( + program.is_manager(user) + or PartnerProgramUserProfile.objects.filter( + partner_program=program, user=user + ).exists() + ): + raise SolutionError( + "not_program_member", + "Создание заявки доступно участникам программы.", + status=403, + ) + if not program.is_project_submission_open(): + raise SolutionError("submission_closed", "Срок создания заявок завершен.") + if not form_data(program)["is_configured"]: + raise SolutionError( + "form_not_configured", "Организатор еще не настроил форму решения." + ) + solution = ProgramSolution.objects.create(program=program, captain=user, **data) + ProgramSolutionMember.objects.create(solution=solution, user=user) + return solution, True + + +class SolutionAPIView(APIView): + permission_classes = [IsAuthenticated] + + def handle_exception(self, exc): + if isinstance(exc, SolutionError): + return Response(exc.payload, status=exc.status_code) + if isinstance(exc, Http404): + return Response( + dict( + code="not_found", + detail="Решение или программа не найдены.", + errors=[], + meta={}, + ), + status=404, + ) + if isinstance(exc, APIException): + errors = [] + if isinstance(exc, ValidationError) and isinstance(exc.detail, dict): + errors = [ + issue( + key, + "invalid", + str(value[0] if isinstance(value, list) else value), + ) + for key, value in exc.detail.items() + ] + code = ( + "validation_error" + if isinstance(exc, ValidationError) + else exc.default_code + ) + if exc.status_code == 401: + code = "authentication_required" + detail = ( + "Проверьте данные запроса." + if isinstance(exc, ValidationError) + else str(exc.detail) + ) + response = Response( + dict(code=code, detail=detail, errors=errors, meta={}), + status=exc.status_code, + ) + if exc.status_code == 401: + response["WWW-Authenticate"] = ( + self.get_authenticate_header(self.request) or "Bearer" + ) + return response + return super().handle_exception(exc) + + +class SolutionContextView(SolutionAPIView): + def get(self, request, pk): + return Response(context_data(program_for(pk), request.user)) + + +class SolutionListCreateView(SolutionAPIView): + def get(self, request, pk): + program = program_for(pk) + query = SolutionListQuery(data=request.query_params) + query.is_valid(raise_exception=True) + data = query.validated_data + if not program.is_competitive and data["submission"] != "all": + raise ValidationError({"submission": "Фильтр сдачи неприменим."}) + rows = ( + visible_solutions(program, request.user) + .select_related("captain") + .order_by("created_at", "pk") + ) + if data.get("search"): + rows = rows.filter(name__icontains=data["search"]) + if data["submission"] != "all": + rows = rows.filter( + status="submitted" if data["submission"] == "submitted" else "draft" + ) + offset, limit, total = data["offset"], data["limit"], rows.count() + result = [] + for row in rows[offset : offset + limit]: # noqa: E203 (black slice formatting) + case = row.answers.filter( + Q(field__purpose="case") | Q(field__name="case") + ).first() + name, captain = row.name, user_data(row.captain) + accepted_count = row.members.count() + case_value = case.value if case else None + if row.status == "submitted" and row.submission_snapshot: + snapshot = row.submission_snapshot + name = snapshot["name"] + accepted_count = len(snapshot["members"]) + captain = next(m["user"] for m in snapshot["members"] if m["is_captain"]) + case_ids = { + f["id"] for f in snapshot["form"]["fields"] if f["purpose"] == "case" + } + case_value = next( + ( + a["value"] + for a in snapshot["answers"] + if a["field_id"] in case_ids + ), + None, + ) + result.append( + dict( + id=row.pk, + program_id=program.pk, + name=name, + captain=captain, + accepted_count=accepted_count, + case_value=case_value, + status=row.status, + submitted_at=iso(row.submitted_at), + ) + ) + + def link(start): + params = request.query_params.copy() + params["offset"], params["limit"] = start, limit + return request.build_absolute_uri("?" + params.urlencode()) + + return Response( + dict( + count=total, + next=link(offset + limit) if offset + limit < total else None, + previous=link(max(0, offset - limit)) if offset > 0 else None, + results=result, + ) + ) + + def post(self, request, pk): + serializer = CreateSolutionSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj, created = create_solution(pk, request.user, serializer.validated_data) + return Response(solution_data(obj, request.user), status=201 if created else 200) + + +class SolutionDetailView(SolutionAPIView): + def get(self, request, pk, solution_id): + program = program_for(pk) + if not 0 < solution_id <= MAX_ID: + raise Http404 + try: + solution = ( + visible_solutions(program, request.user) + .select_related("program") + .get(pk=solution_id) + ) + except ProgramSolution.DoesNotExist: + raise Http404 + return Response(solution_data(solution, request.user)) diff --git a/partner_programs/solution_models.py b/partner_programs/solution_models.py new file mode 100644 index 00000000..fe50a3cb --- /dev/null +++ b/partner_programs/solution_models.py @@ -0,0 +1,126 @@ +"""Решение владеет составом, приглашениями и ответами без Project или Team.""" + +from django.conf import settings +from django.db import models + + +class ProgramSolution(models.Model): + program = models.ForeignKey( + "partner_programs.PartnerProgram", + on_delete=models.CASCADE, + related_name="solutions", + ) + captain = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + name = models.CharField(max_length=256, default="Новое решение") + version = models.PositiveBigIntegerField(default=1) + status = models.CharField( + max_length=12, + default="draft", + choices=[("draft", "Черновик"), ("submitted", "Сдано")], + ) + submitted_at = models.DateTimeField(null=True, blank=True) + submission_snapshot = models.JSONField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["created_at", "pk"] + constraints = [ + models.UniqueConstraint( + fields=["program", "captain"], + name="unique_solution_program_captain", + ), + models.CheckConstraint( + check=models.Q(version__gte=1), name="solution_version_positive" + ), + models.CheckConstraint( + check=( + models.Q( + status="draft", + submitted_at__isnull=True, + submission_snapshot__isnull=True, + ) + | models.Q( + status="submitted", + submitted_at__isnull=False, + submission_snapshot__isnull=False, + ) + ), + name="solution_submission_consistent", + ), + ] + + +class ProgramSolutionMember(models.Model): + solution = models.ForeignKey( + ProgramSolution, on_delete=models.CASCADE, related_name="members" + ) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + role = models.CharField(max_length=128, null=True, blank=True) + specialization = models.CharField(max_length=100, null=True, blank=True) + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["joined_at", "pk"] + constraints = [ + models.UniqueConstraint( + fields=["solution", "user"], name="unique_solution_member" + ) + ] + + +class SolutionInvitation(models.Model): + solution = models.ForeignKey( + ProgramSolution, on_delete=models.CASCADE, related_name="invitations" + ) + recipient = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + role = models.CharField(max_length=128, null=True, blank=True) + specialization = models.CharField(max_length=100, null=True, blank=True) + motivational_letter = models.TextField(null=True, blank=True) + status = models.CharField( + max_length=12, + default="pending", + choices=[ + ("pending", "Ожидает ответа"), + ("accepted", "Принято"), + ("declined", "Отклонено"), + ], + ) + version = models.PositiveBigIntegerField(default=1) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["solution", "recipient"], + condition=models.Q(status="pending"), + name="unique_pending_solution_invite", + ), + models.CheckConstraint( + check=models.Q(version__gte=1), name="solution_invite_version_positive" + ), + models.CheckConstraint( + check=models.Q(status__in=["pending", "accepted", "declined"]), + name="solution_invite_status_valid", + ), + ] + + +class ProgramSolutionAnswer(models.Model): + solution = models.ForeignKey( + ProgramSolution, on_delete=models.CASCADE, related_name="answers" + ) + field = models.ForeignKey( + "partner_programs.PartnerProgramField", + on_delete=models.PROTECT, + related_name="solution_answers", + ) + value = models.JSONField() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["solution", "field"], name="unique_solution_answer" + ) + ] diff --git a/partner_programs/tests/test_solution_foundation.py b/partner_programs/tests/test_solution_foundation.py new file mode 100644 index 00000000..163488aa --- /dev/null +++ b/partner_programs/tests/test_solution_foundation.py @@ -0,0 +1,397 @@ +import json +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from threading import Barrier + +from django.db import IntegrityError, close_old_connections, transaction +from django.core.exceptions import ValidationError as DjangoValidationError +from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature +from django.utils import timezone +from rest_framework.test import APIClient + +from partner_programs.models import PartnerProgramProject +from partner_programs.solution_api import create_solution, form_data, iso +from partner_programs.solution_models import ( + ProgramSolution, + ProgramSolutionAnswer, + ProgramSolutionMember, + SolutionInvitation, +) +from partner_programs.tests.helpers import ( + create_partner_program, + create_program_field, + create_program_member, + create_program_project, + create_project, + create_user, + project_apply_payload, +) +from projects.models import Project + + +def solution_program(**kwargs): + program = create_partner_program( + solution_storage_mode="solution", solution_template="hackathon", **kwargs + ) + create_program_field( + program, + name="case", + label="Задача", + field_type="select", + is_required=True, + show_filter=True, + options=["Задача 1"], + ) + return program + + +class SolutionFoundationTests(TestCase): + def setUp(self): + self.user = create_user() + self.program = solution_program(is_competitive=True) + create_program_member(self.program, user=self.user) + self.client = APIClient() + self.client.force_authenticate(self.user) + self.url = f"/programs/{self.program.pk}/solutions/" + + def create(self, **data): + return self.client.post(self.url, data, format="json") + + def context(self): + return self.client.get(f"/programs/{self.program.pk}/solution-context/") + + def test_create_and_read_match_frozen_contract_without_project(self): + response = self.create(name="Решение команды") + self.assertEqual(response.status_code, 201, response.data) + self.assertFalse(Project.objects.exists()) + self.assertFalse(PartnerProgramProject.objects.exists()) + obj = ProgramSolution.objects.get() + self.assertEqual(obj.members.get().user_id, self.user.pk) + self.assertEqual(obj.captain_id, self.user.pk) + self.assertEqual(response.data["team"]["accepted_count"], 1) + self.assertFalse(any(response.data["permissions"].values())) + self.assertEqual(response.data["answers"], []) + self.assertEqual(response.data["submission"]["status"], "draft") + self.assertEqual(self.client.get(f"{self.url}{obj.pk}/").data, response.data) + self.assertEqual( + self.context().data["current_application"]["solution_id"], obj.pk + ) + + # Validate actual wire responses, not just examples generated from the implementation. + from jsonschema import Draft202012Validator, FormatChecker + + spec = json.loads( + ( + Path(__file__).resolve().parents[2] + / "docs/contracts/program-solutions-v1/openapi.json" + ).read_text(encoding="utf-8") + ) + for schema, value in [ + ("Solution", response.data), + ("Context", self.context().data), + ("SolutionPage", self.client.get(self.url).data), + ]: + Draft202012Validator( + { + "components": spec["components"], + "$ref": f"#/components/schemas/{schema}", + }, + format_checker=FormatChecker(), + ).validate(value) + + def test_repeat_returns_same_solution_and_does_not_rename_even_after_deadline(self): + first = self.create(name="Первое") + self.program.datetime_project_submission_ends = ( + timezone.now() - timezone.timedelta(seconds=1) + ) + self.program.save() + repeat = self.create(name="Второе") + self.assertEqual(repeat.status_code, 200) + self.assertEqual(repeat.data["id"], first.data["id"]) + self.assertEqual(repeat.data["name"], "Первое") + self.assertEqual(ProgramSolution.objects.count(), 1) + self.assertEqual(ProgramSolutionMember.objects.count(), 1) + + def test_default_program_stays_legacy_and_old_api_still_works(self): + program = create_partner_program() + create_program_member(program, user=self.user) + self.assertEqual(program.solution_storage_mode, "legacy_project") + result = self.client.post(f"/programs/{program.pk}/solutions/", {}, format="json") + self.assertEqual(result.status_code, 409) + self.assertEqual(result.data["code"], "solution_mode_unavailable") + old = self.client.post( + f"/programs/{program.pk}/projects/apply/", + project_apply_payload(), + format="json", + ) + self.assertEqual(old.status_code, 201, old.data) + context = self.client.get(f"/programs/{program.pk}/solution-context/").data + self.assertEqual(context["current_application"]["kind"], "legacy_project") + self.assertEqual( + context["current_application"]["program_link_id"], old.data["program_link_id"] + ) + + def test_new_mode_cannot_create_project_through_old_endpoint(self): + result = self.client.post( + f"/programs/{self.program.pk}/projects/apply/", + project_apply_payload(), + format="json", + ) + self.assertEqual(result.status_code, 403, result.data) + self.assertFalse(Project.objects.exists()) + with self.assertRaises(DjangoValidationError): + PartnerProgramProject.objects.create( + partner_program=self.program, project=create_project(leader=self.user) + ) + + def test_registration_deadline_configuration_and_manager_exceptions(self): + outsider = create_user() + self.client.force_authenticate(outsider) + self.assertEqual(self.context().data["blocked_reason"], "not_program_member") + self.assertEqual(self.create().status_code, 403) + self.program.managers.add(outsider) + self.assertEqual(self.create().status_code, 201) + self.client.force_authenticate(self.user) + self.program.datetime_project_submission_ends = ( + timezone.now() - timezone.timedelta(seconds=1) + ) + self.program.save() + self.assertEqual(self.context().data["blocked_reason"], "submission_closed") + self.assertEqual(self.create().data["code"], "submission_closed") + self.program.datetime_project_submission_ends = None + self.program.solution_template = None + self.program.save() + self.assertEqual(self.context().data["blocked_reason"], "form_not_configured") + self.assertEqual(self.create().data["code"], "form_not_configured") + self.assertEqual(ProgramSolution.objects.count(), 1) + + def test_strict_request_and_identifier_validation(self): + for data in [ + {"project_id": 1}, + {"name": 1}, + {"name": None}, + {"name": ""}, + {"name": "x" * 257}, + [], + ]: + with self.subTest(data=data): + result = self.client.post(self.url, data, format="json") + self.assertEqual(result.status_code, 400, result.data) + self.assertEqual(result.data["code"], "validation_error") + for path in [ + "/programs/0/solutions/", + "/programs/9007199254740992/solutions/", + f"{self.url}9007199254740992/", + ]: + self.assertEqual(self.client.get(path).status_code, 404) + self.assertFalse(ProgramSolution.objects.exists()) + + def test_program_scoping_own_members_manager_and_outsider(self): + first = self.create().data["id"] + other_program = solution_program() + create_program_member(other_program, user=self.user) + other, _ = create_solution( + other_program.pk, self.user, {"name": "Другая программа"} + ) + self.assertNotEqual(first, other.pk) + self.assertEqual( + self.client.get( + f"/programs/{other_program.pk}/solutions/{first}/" + ).status_code, + 404, + ) + other_user = create_user() + self.client.force_authenticate(other_user) + self.assertEqual(self.client.get(self.url).data["count"], 0) + self.assertEqual(self.client.get(f"{self.url}{first}/").status_code, 404) + SolutionInvitation.objects.create(solution_id=first, recipient=other_user) + self.assertEqual(self.client.get(f"{self.url}{first}/").status_code, 404) + ProgramSolutionMember.objects.create(solution_id=first, user=other_user) + self.assertEqual(self.client.get(f"{self.url}{first}/").status_code, 200) + self.assertEqual(self.create().data["code"], "already_in_program_team") + self.assertEqual(self.context().data["current_application"]["solution_id"], first) + ProgramSolutionMember.objects.filter(user=other_user).delete() + other_program.managers.add(other_user) + self.assertEqual(self.client.get(f"{self.url}{first}/").status_code, 404) + self.program.managers.add(other_user) + self.assertEqual(self.client.get(f"{self.url}{first}/").status_code, 200) + + def test_legacy_reference_and_ambiguous_links_are_not_chosen_arbitrarily(self): + link = create_program_project( + self.program, project=create_project(leader=self.user) + ) + self.assertEqual( + self.context().data["current_application"]["program_link_id"], link.pk + ) + self.assertEqual(self.create().data["code"], "already_in_program_team") + create_program_project(self.program, project=create_project(leader=self.user)) + self.assertEqual(self.context().status_code, 409) + self.assertEqual(self.context().data["code"], "ambiguous_application") + self.assertFalse(ProgramSolution.objects.exists()) + + def test_collection_filter_pagination_and_noncompetitive_submission(self): + self.create(name="Alpha") + manager = create_user() + self.program.managers.add(manager) + create_solution(self.program.pk, manager, {"name": "Beta"}) + self.client.force_authenticate(manager) + page = self.client.get(self.url, {"limit": 1}).data + self.assertEqual(page["count"], 2) + self.assertIn("offset=1", page["next"]) + self.assertEqual(self.client.get(self.url, {"search": "Beta"}).data["count"], 1) + self.assertEqual( + self.client.get(self.url, {"submission": "submitted"}).data["count"], 0 + ) + for query in [ + {"limit": 0}, + {"limit": 101}, + {"offset": -1}, + {"unknown": 1}, + {"submission": "bad"}, + ]: + self.assertEqual(self.client.get(self.url, query).status_code, 400) + self.program.is_competitive = False + self.program.save() + self.assertEqual( + self.client.get(self.url, {"submission": "submitted"}).status_code, 400 + ) + self.assertIsNone( + self.client.get(f"{self.url}{ProgramSolution.objects.first().pk}/").data[ + "readiness" + ] + ) + + def test_schema_revision_tracks_configuration_and_read_survives_mode_rollback(self): + solution = self.create().data + revision = solution["form"]["revision"] + field = self.program.fields.get() + field.label = "Новая подпись" + field.save() + self.assertNotEqual(form_data(self.program)["revision"], revision) + self.program.solution_storage_mode = "legacy_project" + self.program.save() + self.assertEqual(self.client.get(f"{self.url}{solution['id']}/").status_code, 200) + self.assertEqual(self.create().status_code, 409) + + def test_read_and_collection_keep_stored_submitted_representation(self): + data = self.create(name="Сданное решение").data + field = self.program.fields.get() + stamp = timezone.now() + snapshot = dict( + name=data["name"], + captain_id=self.user.pk, + members=data["team"]["members"], + form={**data["form"], "is_snapshot": True}, + answers=[dict(field_id=field.pk, value="Задача 1")], + submitted_at=iso(stamp), + ) + ProgramSolution.objects.filter(pk=data["id"]).update( + status="submitted", + submitted_at=stamp, + submission_snapshot=snapshot, + name="Другое название", + ) + field.label = "Новая подпись" + field.save() + response = self.client.get(f"{self.url}{data['id']}/").data + self.assertEqual(response["name"], "Сданное решение") + self.assertEqual(response["form"], snapshot["form"]) + self.assertEqual(response["answers"], snapshot["answers"]) + self.assertTrue(response["team"]["is_frozen"]) + summary = self.client.get(self.url).data["results"][0] + self.assertEqual(summary["name"], "Сданное решение") + self.assertEqual(summary["case_value"], "Задача 1") + + def test_rights_precede_deadline_and_staff_does_not_gain_registration_exception(self): + outsider = create_user(is_staff=True) + self.client.force_authenticate(outsider) + self.program.datetime_project_submission_ends = ( + timezone.now() - timezone.timedelta(seconds=1) + ) + self.program.save() + self.assertEqual(self.context().data["blocked_reason"], "not_program_member") + result = self.create() + self.assertEqual(result.status_code, 403) + self.assertEqual(result.data["code"], "not_program_member") + + def test_foundation_constraints_and_no_team_mutation_endpoints(self): + solution_id = self.create().data["id"] + solution = ProgramSolution.objects.get(pk=solution_id) + invalid = [ + lambda: ProgramSolution.objects.create( + program=self.program, captain=self.user + ), + lambda: ProgramSolutionMember.objects.create( + solution=solution, user=self.user + ), + lambda: ProgramSolution.objects.filter(pk=solution.pk).update(version=0), + lambda: ProgramSolution.objects.filter(pk=solution.pk).update( + status="submitted" + ), + ] + invitation = SolutionInvitation.objects.create( + solution=solution, recipient=create_user() + ) + invalid.append( + lambda: SolutionInvitation.objects.create( + solution=solution, recipient=invitation.recipient + ) + ) + field = self.program.fields.get() + ProgramSolutionAnswer.objects.create(solution=solution, field=field, value=False) + invalid.append( + lambda: ProgramSolutionAnswer.objects.create( + solution=solution, field=field, value=0 + ) + ) + for action in invalid: + with self.subTest(action=action), self.assertRaises( + IntegrityError + ), transaction.atomic(): + action() + self.assertEqual( + self.client.patch( + f"{self.url}{solution_id}/", {"name": "X"}, format="json" + ).status_code, + 405, + ) + for suffix in ["members/", "invitations/", "submit/"]: + self.assertEqual( + self.client.post( + f"{self.url}{solution_id}/{suffix}", {}, format="json" + ).status_code, + 404, + ) + + def test_anonymous_api_uses_contract_error_envelope(self): + self.client.force_authenticate(None) + for url in [self.url, f"/programs/{self.program.pk}/solution-context/"]: + result = self.client.get(url) + self.assertEqual(result.status_code, 401) + self.assertEqual(set(result.data), {"code", "detail", "errors", "meta"}) + + +@skipUnlessDBFeature("has_select_for_update") +class SolutionCreationConcurrencyTests(TransactionTestCase): + def test_concurrent_create_serializes_on_program_and_returns_one_solution(self): + program = solution_program() + user = create_user() + create_program_member(program, user=user) + barrier = Barrier(2) + + def run(): + close_old_connections() + try: + barrier.wait(timeout=10) + obj, created = create_solution(program.pk, user, {"name": "Одновременно"}) + return obj.pk, created + finally: + close_old_connections() + + with ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(lambda _: run(), range(2))) + self.assertEqual(len({pk for pk, _ in results}), 1) + self.assertEqual(sorted(created for _, created in results), [False, True]) + self.assertEqual(ProgramSolution.objects.count(), 1) + self.assertEqual(ProgramSolutionMember.objects.count(), 1) + self.assertFalse(Project.objects.exists()) diff --git a/partner_programs/urls.py b/partner_programs/urls.py index 14193bc3..8fad64e2 100644 --- a/partner_programs/urls.py +++ b/partner_programs/urls.py @@ -1,4 +1,9 @@ from django.urls import path +from partner_programs.solution_api import ( + SolutionContextView, + SolutionDetailView, + SolutionListCreateView, +) from partner_programs.attention_views import AttentionActionView from partner_programs.report_views import ProgramReportView from partner_programs.registration_views import ( @@ -102,6 +107,9 @@ app_name = "partner_programs" urlpatterns = [ + path("/solution-context/", SolutionContextView.as_view()), + path("/solutions/", SolutionListCreateView.as_view()), + path("/solutions//", SolutionDetailView.as_view()), path("/management/certificates/", CertificateSettingsView.as_view()), path("/management/certificates/assets/", CertificateAssetView.as_view()), path( diff --git a/poetry.lock b/poetry.lock index d5c5f29c..2b4dae55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1622,6 +1622,58 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.25.0" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-path" +version = "0.3.4" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = "<4.0.0,>=3.8.0" +files = [ + {file = "jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8"}, + {file = "jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001"}, +] + +[package.dependencies] +pathable = ">=0.4.1,<0.5.0" +PyYAML = ">=5.1" +referencing = "<0.37.0" +requests = ">=2.31.0,<3.0.0" + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "kombu" version = "5.3.7" @@ -1654,6 +1706,79 @@ sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] +[[package]] +name = "lazy-object-proxy" +version = "1.13.0" +description = "A fast and thorough lazy object proxy." +optional = false +python-versions = ">=3.11" +files = [ + {file = "lazy_object_proxy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a12c27d18199a2e61609e2d08146f9ffccc081c0998c10f9feb97d243a7f7cf"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2b7f9f7b383145de190ee1df58e15e9f4a65b8e3eb417a250430e864b978ff92"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9f4840dcf082fde897e932a2f54ce8bae394ffd2583393df5f5189f2b3d03a"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db73d0d1b7140e62847b297d4db04d81cdec1e9645fb422f740f7f05154e1285"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:793478edf7ddc05de8cf9e20287505c06982eefebbab0df9d2383379d16c23a5"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bbf47d5c1aa84abe590c0606b82e93be127c6be611ee3ab335478bd29fe44fd7"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e3c8abad441bdfbf34406e92105acbf09d28d0f8bcf1c12f4e467a2ec2f95da"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:32710c3ac4ce4f45c4f0388adb9f1f7e2cab5a4d00a25e0f4e0821d036a5004b"}, + {file = "lazy_object_proxy-1.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:95abfa861ea4f09ecf17d9d1aa926f5b91217fdf318c07393bffbe7491b205fc"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:087eba19ea5ce02ea9eb1749d8fdcfb229eeeaf13abaebecf00d579d306d1fd9"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3df6b7db013fbbd00001a43af6b188b545442b4d125b5fddda79366108e02286"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bdc617c7b6f58aa5e59f514c7d619d7f60b52b343b77c9149d2fbfefc412f3a"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47d26b4c5a5ea2d3a1993a1c9509110beb030a2739ca69ebbe516fb04b6842b7"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6261a9c38f629c3566ab5caec3909f5c6f3333a711c7e39f6883715aa38c566"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:337b25ac5b0f5103c72343f74eea438ad3216b0a3cc77a3f6e4ec54c80d0256b"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:faa395d97edfafd3cd0cbd8855473c4673c7e3080fccf31f97a9bcfa7218d7ec"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:81437ad7883fe637620bc63a3df3e9974c9a63562e76d7e8a444fe724adccb5c"}, + {file = "lazy_object_proxy-1.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:da66d53e0c350e6e6876f2dc060b240dc06cc159ca7f279d59fe4ed48aec2ece"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7edc00d4792dba391667fcc5459b023b55190b9743735e7c1281855851c3d91b"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4046337019bd862440764930dffc47747a36b2e5d74346f0d404eadf1e3e6fa7"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903fc3eb8d87ff8fcb0b2f62e7d84e9808712aa085635612d2930b6ee2d8d4f5"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:168e1eb2773a187c40544653346622917152c5f93163effd80dcce848888f01e"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78a351da00aebe3ef8089f2c74bcaff8e851123419590fb0b86d8241a9ea3fc4"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea9e832c3d9112992eb1f6f16543ef555d2b51ff288c5a16c0741b9a9ca88984"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65a369a41028bd2ba53dd31da0f6ee97682b70bcf2bf7413cb69ba11c63e4fd4"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54fdf11db2733dfd8dd8177f79d8d9c80dd99a912c0b1a8906412306ad18ade2"}, + {file = "lazy_object_proxy-1.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:042284eea98fc08b9d347e43b7c258e6c862b4c03d5881d536bc61d48e6be88e"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:62612150e2b67d1cb26c2201d020773e9bab9c596d783d55ada42885a695f749"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad7630266b5775463f2a1806165c1d9dff07780cf1bba3c53f311e1d026e8921"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73f05b09e37b0eb0ca7d6e80e2b0689c49bedac8c5d69667c1ebb9f02a4ed5c7"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:91a725d3c64dfe13d365cf78c469c2a8f95884859379aeca89cb8d2ed291649a"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e28a75d33378141927c08f3eb291cc98d0a22776670ae330f8347f898cd3b175"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a785fe44b61fcbffc0e264fe6b735a909e0b5ef922f0e976e281bbe8477fb47a"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e321425f76451885c93083ecec0850b64de0f83bb94c113e930e8a0a630e45ab"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0e52094b7fff1f26f4e227b88be3f350817078e4bb79b4c80846fca0d15efae4"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:b2dd1c37d64cf9f3f4a2072971639408989b0803a3b7f05e75c1fe28d1a93c1f"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:348370676ea6d9ed08f9b74545c37e5be4cf009c45b9d50c6aff476fb7fcc7ec"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dee2fb3326cd81384d2b752c3f6888d7ed9eba2cdbf0583097b5168b8bb4a703"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b4c446cbc8e411b116c9ee9b475cb664b8bc5c3d912bcbf1bab5c785fc69a02"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e671e0e9f2793759f382a69a3853c8742474bc011865ed3595187340ac5956d"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a0155084a5dc8ce3e1a3d6e3f2fb4d84d5fc09e66ed84dc0e5f8a2697d421ea0"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2d07e0bde2738922a7117fccc1a82514aefbc9c07ee8ff0bb84a1c606dbb5ac8"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da9f1b8780678af93c4ddba46826166c53a25d8d71133fc818542bf7b481e033"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f21b4711e6744d46a4672497ca2db390cbdb03241d9b1654bbf38dbbc4db2a5e"}, + {file = "lazy_object_proxy-1.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de295ae25e5c1fd712088547f251c4b2976d2f60df28057b6c416e771a99842b"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:14fbc6debdf55fe6a801a6f562456c20056f1d3b0a191f64552ecc4f9bbdcc69"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5090a94e1f148b1323031342920ac4416d4702db93317e238724ea5252a0e52"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89331615366c05e30bf5c0821b4a0536b42117c9e83d2aef03d47c8beebcdf0c"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:90e841c41f8cdd380d38f4e64df824d92e8245ea8798bf6ee7533c941b3c2956"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:956757a130126c1c281c1f3cdb2856cef8ab933edf61bc7931c7947c91866af9"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:28d3de97c2dba54e2d1da3b3f2aef5c2bfab4a37520bd5ee275f7497914593ff"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c81002e6284f724cc1cb616951f254bb312b8fecc185de8ee205cd6288d370ad"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-win_amd64.whl", hash = "sha256:ca684bb99c88693f12df2f71d3753bec784810351b9053959ea4c17a453b9269"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315-win_arm64.whl", hash = "sha256:32fcf9849e1a27a1e8a79a8e593860661cc93e81b090283f911d823c8c52b092"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cedfa35e1d9f2b91599b5f635e36d04c9accb68e10c413cbdb7d02d4327d20fc"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:666d040e5f94c0d8420d5ce3e4e8926c1a51fbb26432a60dc61440c452c9b30c"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c842c320aadf7524b7361df692eea9a1d76ce533e35658188c4d51bc0dd7e85e"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7fd1cbcf0ac498aad0a3ff4142cc4daf70961017ac895c6b2ed92e8a128d5009"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1bdad8baf318ca048af58d70fa1274e31e20b0fa073fbe9fd4ad7b32a51a3e61"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:291263a1647524673146f10192caf27113a32c2e6d169e19c6a257f2876be343"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dd0c8ce17ec708cc81d541697d3746f2dcd8e45d039078aee980f865f2f5d41b"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-win_amd64.whl", hash = "sha256:68d6e346300c136dd4714a4772e592521beb1c0744c3f18f797854affeca96b5"}, + {file = "lazy_object_proxy-1.13.0-cp315-cp315t-win_arm64.whl", hash = "sha256:e97aa65f98cc4521f2e366a49dfb6a3c2315dbbf5d84e97d5d1e21b649de72fa"}, + {file = "lazy_object_proxy-1.13.0.tar.gz", hash = "sha256:ce34046cb021d69c72d05ddb83821ecfd4d2d0a5d43a93c5cd01c9e34779cf14"}, +] + [[package]] name = "loguru" version = "0.7.2" @@ -2032,6 +2157,39 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +description = "OpenAPI schema validation for Python" +optional = false +python-versions = "<4.0.0,>=3.8.0" +files = [ + {file = "openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3"}, + {file = "openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee"}, +] + +[package.dependencies] +jsonschema = ">=4.19.1,<5.0.0" +jsonschema-specifications = ">=2023.5.2" +rfc3339-validator = "*" + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +optional = false +python-versions = "<4.0.0,>=3.8.0" +files = [ + {file = "openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60"}, + {file = "openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734"}, +] + +[package.dependencies] +jsonschema = ">=4.18.0,<5.0.0" +jsonschema-path = ">=0.3.1,<0.4.0" +lazy-object-proxy = ">=1.7.1,<2.0.0" +openapi-schema-validator = ">=0.6.0,<0.7.0" + [[package]] name = "openpyxl" version = "3.1.2" @@ -2129,6 +2287,17 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pathable" +version = "0.4.4" +description = "Object-oriented paths" +optional = false +python-versions = "<4.0.0,>=3.7.0" +files = [ + {file = "pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2"}, + {file = "pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2"}, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -2613,6 +2782,22 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\ hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + [[package]] name = "requests" version = "2.31.0" @@ -2634,6 +2819,157 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rpds-py" +version = "2026.9.1" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.11" +files = [ + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2"}, + {file = "rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e"}, + {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391"}, + {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f"}, + {file = "rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af"}, + {file = "rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e"}, + {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc"}, + {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f"}, + {file = "rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33"}, + {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36"}, + {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9"}, + {file = "rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b"}, + {file = "rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35"}, + {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785"}, + {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c"}, + {file = "rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569"}, + {file = "rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297"}, + {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6"}, + {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15"}, + {file = "rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b"}, + {file = "rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32"}, + {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0"}, + {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37"}, + {file = "rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12"}, +] + [[package]] name = "service-identity" version = "24.1.0" @@ -3284,4 +3620,4 @@ test = ["pytest"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "05261773bbd83346d8b7927b9f3ae41997bbf5ca48e192bf20f7b0c0743243dc" +content-hash = "c7840ad88f2480a3efa2935f4e2a6932fc8105dc80023024248fc25c9c45da4b" diff --git a/pyproject.toml b/pyproject.toml index 3962224e..67e02594 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,3 +74,7 @@ phonenumbers = "^8.13.47" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "setuptools.build_meta" + +[tool.poetry.group.dev.dependencies] +jsonschema = "4.26.0" +openapi-spec-validator = "0.7.2"