diff --git a/.prettierignore b/.prettierignore index c3dea04c8..195c990bb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,6 @@ /covarage/ /build/ *.scss + +# Frozen C00 source documents are copied byte-for-byte from the backend contract package. +/docs/contracts/program-solutions-v1/*.json diff --git a/docs/contracts/program-solutions-v1/README.md b/docs/contracts/program-solutions-v1/README.md new file mode 100644 index 000000000..91faad638 --- /dev/null +++ b/docs/contracts/program-solutions-v1/README.md @@ -0,0 +1,9 @@ + + +# C00 1.0.0 для FE01 + +Неизмененные `openapi.json` и `examples.json` из пакета C00 backend. JSON на проводе в snake_case. Источник требований: https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175 . Дизайн D00 v3 утвержден 24 октября 2026 года. + +`python tools/generate-solution-contract.py` генерирует 47 доменных типов и 43 независимых сценария в `domain/solution/`. После генерации выполнить scoped Prettier этих двух файлов. Фикстуры используются только тестами; production mock provider не включается. + +Изменение контракта требует синхронного изменения копий backend/frontend и повторной проверки wire boundary. Реализация FE01 ограничена контекстом, созданием и чтением; все сценарии C00 не означают реализацию будущих команд. diff --git a/docs/contracts/program-solutions-v1/examples.json b/docs/contracts/program-solutions-v1/examples.json new file mode 100644 index 000000000..4d3a86d5a --- /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 000000000..f61c50d2a --- /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/core/guards-models.md b/docs/core/guards-models.md index a22875804..ff920b665 100644 --- a/docs/core/guards-models.md +++ b/docs/core/guards-models.md @@ -81,11 +81,11 @@ projectRepository.getOne(projectId).pipe( `projects/core/src/lib/models/` — **набор интерфейсов и enum'ов общего назначения**. -| Файл | Экспорт | Что | +| Файл | Экспорт | Что | | ------------------------ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | --- | -| `error/error-code.ts` | `enum ErrorCode { NOT_FOUND = "404", SERVER_ERROR = "500" }` | Используется `ErrorService` для построения URL `/error/`. | -| `error/error-message.ts` | `enum ErrorMessage` | Все текстовые сообщения об ошибках на русском, сгруппированы по категориям: AUTH, VALIDATION, USER, рейтинг проектов. Полная таблица — ниже. | -| `http.model.ts` | `interface ApiError { detail: string }` | Типизация ошибок API: бэк отдаёт `{ detail: "..." }` для 4xx. | | +| `error/error-code.ts` | `enum ErrorCode { NOT_FOUND = "404", SERVER_ERROR = "500" }` | Используется `ErrorService` для построения URL `/error/`. | +| `error/error-message.ts` | `enum ErrorMessage` | Все текстовые сообщения об ошибках на русском, сгруппированы по категориям: AUTH, VALIDATION, USER, рейтинг проектов. Полная таблица — ниже. | +| `http.model.ts` | `interface ApiError { detail: string }` | Типизация ошибок API: бэк отдаёт `{ detail: "..." }` для 4xx. | | ### ErrorMessage — все значения diff --git a/docs/modules/auth.md b/docs/modules/auth.md index 158464a07..60337c728 100644 --- a/docs/modules/auth.md +++ b/docs/modules/auth.md @@ -126,9 +126,7 @@ type RegisterError = // results/password.result.ts type PasswordError = - | { kind: "server_error" } - | { kind: "invalid_token" } - | { kind: "unknown"; cause?: unknown }; + { kind: "server_error" } | { kind: "invalid_token" } | { kind: "unknown"; cause?: unknown }; ``` --- diff --git a/docs/product/program-solutions-fe01.md b/docs/product/program-solutions-fe01.md new file mode 100644 index 000000000..e370aef41 --- /dev/null +++ b/docs/product/program-solutions-fe01.md @@ -0,0 +1,48 @@ + + +# FE01 — контракт, маршруты и выбор режима + +Дизайн D00 v3 утвержден пользователем 24 октября 2026 года. ТЗ: https://chatgpt.com/space/page_9632e9ed74588191b6b3e4b397d55175 . BE01 и FE01 выполняются независимо по C00 1.0.0. + +План: сгенерировать полные доменные типы и 43 мока из C00; подключить port → HTTP adapter/repository → use cases; добавить явное получение solution-context перед созданием из программы; сохранить legacy ветку создания; добавить маршруты создания/просмотра/редактирования в контексте program ID. Данные ответа принадлежат активному маршруту; чужой/устаревший ответ не переносится в другую программу. Переход по discriminated union ApplicationRef готовит совместимые legacy ссылки для BE10. + +В FE01 реализуется оболочка чтения с двумя разделами утвержденного дизайна. Изменяющие действия зависят от серверных permissions и задач FE03–FE06; не показываются фиктивные сохранение, приглашение или сдача. API моделей не требует Project/Team ID. + +Проверки: соответствие моделей/моков C00, wire casing через действующий interceptor, создание/повторное открытие, legacy маршруты, прямые ссылки/invalid IDs, загрузка/ошибка/нет доступа, смена program ID; targeted tests, Angular build, архитектурный lint и доступная browser проверка. Новые режимы не включаются этим PR для существующих программ. + +## Verification и review от 24 октября 2026 + +Checked by source: diff от актуального `origin/dev` 2c4e91a1 (исходная база 5e22c54c). Доменные типы и независимые сценарии сгенерированы из неизмененного C00 1.0.0. Генератор не зависит от сети: `python tools/generate-solution-contract.py`, затем targeted Prettier. Полные определения приглашений/форм/команд готовы для следующих этапов; FE01 вызывает только контекст, создание и чтение. Adapter списка подготовлен. Project/Team ID в новом создании отсутствуют. + +Wire boundary: существующий CamelcaseInterceptor преобразует успешные ответы и тело запроса. Ошибки HttpErrorResponse преобразуются отдельно, включая `field_id` и ключи meta. Неизвестная ошибка показывает безопасный текст. Ветвление по storageMode; при ошибке контекста нет fallback создания проекта. Legacy reference содержит точный programLinkId. При смене программы подписка страницы отменяет предыдущий запрос; entry point игнорирует устаревший результат. + +Checked by tests: **44 tests PASS** на актуальном dev: 40 проверок FE01 и 4 проверки независимого исправления сертификата: + +```text +npm run test:ci -- projects/social_platform/src/app/api/solution/solution-contract.spec.ts projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts projects/social_platform/src/app/ui/routes/program/detail.routes.spec.ts projects/social_platform/src/app/ui/pages/program/detail/main/my-certificate.component.spec.ts +``` + +Проверены casing реальных HTTP requests/responses через interceptor, независимые C00 состояния, безопасные ID, error issues/meta, legacy/new маршруты, double click, ошибка контекста, точные ID, отмена старой программы, загрузка страницы, retry, отсутствие повторного POST и переключение вкладки без повторной загрузки. + +Checked by build: `npm run build:pr` PASS на штатной environment.ts. Существующие Sass deprecation и Angular template warnings остаются. Полный `npm run lint:ts` PASS (6 прежних warnings), scoped Stylelint нового SCSS PASS. Package/lock не менялись. + +Дополнительно полный `npm run test:ci -- --maxWorkers=2`: **2471 tests / 452 files PASS**. Генерация моделей и сценариев из C00 повторена с последующим scoped Prettier; итоговый diff двух generated файлов пустой. + +Форматирование: исходный `npm run format:check` на Windows сигнализировал о CRLF из `core.autocrlf=true` и 14 прежних стилевых расхождениях dev. В отдельном коммите `style: fix existing formatting that blocks dev checks` исправлены только эти 14 файлов (4 docs и 10 TS), без изменения поведения. Все 1885 первоначально отмеченных файлов затем проверены `prettier.check(source.replace(/\r\n/g, "\n"), resolvedConfig)` — **0 failures** при канонических LF. Неизмененные файлы не переписывались. Новые/измененные файлы проходят scoped Prettier. Общий SCSS autofix не запускался; локальный commit hook с таким действием заменен на ручные проверки в этой операции, без изменения конфигурации Git репозитория. + +Checked in browser: обычное Angular приложение на localhost → локальный Django BE01 → изолированная PostgreSQL 18, реальная JWT авторизация и вымышленные данные. Создание дает solution 1 без Project; повтор `/solutions/new` и кнопка программы открывают тот же ID. «Команда» — вторая вкладка той же заявки. GET несуществующего решения показывает not_found с retry; Enter запускает повтор. Старый режим создает проект и открывает прежний редактор с exact programLinkId. Временный локальный API URL восстановлен, в diff environment.ts отсутствует. + +Desktop 1280×720 и mobile 390×844 проверены визуально. На mobile scrollWidth=innerWidth=390, font-family Mont. Строки команды перестраиваются в один столбец, вкладки доступны ссылками и имеют aria-current. Семантические heading/main/nav, role=status/alert, native кнопка retry. [Desktop](program-solutions-fe01/team-desktop.jpg), [Mobile](program-solutions-fe01/team-mobile.jpg). + +| AC в scope FE01 | Evidence | Verdict | +| ----------------------------------- | ------------------------------------------------------------------------------------------ | ----------------- | +| AC01: новый путь без Project/Team | Контекст → create → detail; реальный browser и adapter tests | PASS | +| AC02: повтор открывает ту же заявку | currentApplication redirect без POST; browser и route tests; конкурентность проверена BE01 | PASS | +| AC20: изоляция программ | Родительский programId + solutionId, cancellation; test смены программы и backend scoping | PASS | +| AC22: прежний редактор | Legacy context/reference и старые тесты; реальный browser путь с programLinkId | PASS в scope FE01 | + +UI review: иерархия и два раздела согласованного D00 v3; нет конкурирующих или фиктивных изменяющих действий. Существующие Mont, цвета, Button foundation и кабинет; строки с разделителями, без дополнительных карточек. Данные могут переноситься по строкам, mobile порядок чтения совпадает с DOM. Полные формы и invitation modal не входят в FE01 и не считаются реализованными этим экраном. + +Review: исправлена нормализация meta ошибок под те же camelCase моки. BLOCKER/MAJOR/MINOR в scope не осталось. Not checked: screen reader, четыре роли в браузере, настоящие загрузки/приглашения/сдача; последние относятся к FE03–FE08 и QA01. Негативный доступ проверен server-side в BE01 и в тестах ошибки UI. Отдельный коммит форматирования проверен по diff: только переносы строк, отступы и эквивалентная запись TypeScript union. + +Внедрение: сначала BE01 с миграциями и default legacy_project, затем FE01. Новый режим для реальных участников не включать до REL01. Merge и deployment выполняет пользователь. diff --git a/docs/product/program-solutions-fe01/team-desktop.jpg b/docs/product/program-solutions-fe01/team-desktop.jpg new file mode 100644 index 000000000..8184617b6 Binary files /dev/null and b/docs/product/program-solutions-fe01/team-desktop.jpg differ diff --git a/docs/product/program-solutions-fe01/team-mobile.jpg b/docs/product/program-solutions-fe01/team-mobile.jpg new file mode 100644 index 000000000..88157b651 Binary files /dev/null and b/docs/product/program-solutions-fe01/team-mobile.jpg differ diff --git a/docs/social-platform/architecture.md b/docs/social-platform/architecture.md index 15d5ff503..e069943af 100644 --- a/docs/social-platform/architecture.md +++ b/docs/social-platform/architecture.md @@ -315,8 +315,7 @@ type AsyncState = ```ts type Result = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly error: E }; + { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E }; ``` Хелперы: `ok(value)`, `fail(error)`. diff --git a/docs/social-platform/shared.md b/docs/social-platform/shared.md index 1ccb78394..e27ee377c 100644 --- a/docs/social-platform/shared.md +++ b/docs/social-platform/shared.md @@ -60,8 +60,7 @@ readonly loading = computed(() => isLoading(this.course$())); ```ts type Result = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly error: E }; + { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E }; ``` `ok(value)` / `fail(error)` — фабрики. Use-case возвращает `Observable>` и **никогда не выбрасывает** — все ошибки представимы в `Result.fail`. diff --git a/projects/social_platform/src/app/api/paths/app-routes.ts b/projects/social_platform/src/app/api/paths/app-routes.ts index f3d49c7d2..6f954a2a0 100644 --- a/projects/social_platform/src/app/api/paths/app-routes.ts +++ b/projects/social_platform/src/app/api/paths/app-routes.ts @@ -42,6 +42,11 @@ export const AppRoutes = { }, program: { + solutionCreate: (programId: number | string) => `/office/program/${programId}/solutions/new`, + solution: (programId: number | string, solutionId: number | string) => + `/office/program/${programId}/solutions/${solutionId}`, + solutionEdit: (programId: number | string, solutionId: number | string) => + `/office/program/${programId}/solutions/${solutionId}/edit`, management: (programId: number | string, section: string = "participants") => `/office/program/${programId}/management/${section}`, root: () => "/office/program", diff --git a/projects/social_platform/src/app/api/profile/facades/edit/profile-form.service.ts b/projects/social_platform/src/app/api/profile/facades/edit/profile-form.service.ts index fa357e61d..efff0c4be 100644 --- a/projects/social_platform/src/app/api/profile/facades/edit/profile-form.service.ts +++ b/projects/social_platform/src/app/api/profile/facades/edit/profile-form.service.ts @@ -257,8 +257,7 @@ export class ProfileFormService { if ([2, 3, 4].includes(profile.personal.userType)) { this.typeSpecific?.addControl("preferredIndustries", this.fb.array([])); const role = profile.roles[this.userTypeMap[profile.personal.userType]] as - | { preferredIndustries?: string[] } - | undefined; + { preferredIndustries?: string[] } | undefined; role?.preferredIndustries?.forEach((industry: string) => this.addPreferredIndustry(industry), ); diff --git a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-drilldown.service.ts b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-drilldown.service.ts index 3879019d6..45e099da1 100644 --- a/projects/social_platform/src/app/api/program/facades/detail/program-analytics-drilldown.service.ts +++ b/projects/social_platform/src/app/api/program/facades/detail/program-analytics-drilldown.service.ts @@ -40,12 +40,7 @@ export type AnalyticsAttentionView = | "projects-awaiting-evaluation" | "projects-not-submitted"; export type AnalyticsDrilldownView = - | "case" - | "assignments" - | "scores" - | "delayed" - | "backlog" - | AnalyticsAttentionView; + "case" | "assignments" | "scores" | "delayed" | "backlog" | AnalyticsAttentionView; /** Живёт вместе с одной analytics-модалкой, не сохраняет manager-данные между открытиями. */ @Injectable() diff --git a/projects/social_platform/src/app/api/solution/solution-contract.spec.ts b/projects/social_platform/src/app/api/solution/solution-contract.spec.ts new file mode 100644 index 000000000..e134ed031 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-contract.spec.ts @@ -0,0 +1,145 @@ +/** @format */ + +import { + HTTP_INTERCEPTORS, + HttpErrorResponse, + provideHttpClient, + withInterceptorsFromDi, +} from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import snakecaseKeys from "snakecase-keys"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { ProgramSolution } from "@domain/solution/solution-contract.model"; +import { SolutionRepository } from "@infrastructure/repository/solution/solution.repository"; +import { SolutionHttpAdapter } from "@infrastructure/adapters/solution/solution-http.adapter"; +import { + CreateSolutionUseCase, + GetSolutionContextUseCase, + GetSolutionUseCase, +} from "./use-cases/solution.use-cases"; +import { solutionError } from "./solution-error"; +import { solutionApplicationRoute } from "./solution-navigation"; + +describe("C00 solution API boundary", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it.each([ + "draft_incomplete", + "submitted_snapshot", + "noncompetitive", + "hackathon_ready", + "member_read_only", + "expert_snapshot", + ])("reads independent %s fixture through the actual interceptor", async name => { + const scenario = solutionContractScenarios.find(s => s.name === name)!; + const expected = scenario.response as ProgramSolution; + const result = firstValueFrom( + TestBed.inject(GetSolutionUseCase).execute(expected.programId, expected.id), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + `https://local.test/programs/${expected.programId}/solutions/${expected.id}/`, + ); + request.flush(snakecaseKeys(expected, { deep: true })); + expect(await result).toEqual({ ok: true, value: expected }); + }); + + it("creates without Project/Team IDs and preserves snake_case wire data", async () => { + const result = firstValueFrom( + TestBed.inject(CreateSolutionUseCase).execute(12, { name: "Решение" }), + ); + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/programs/12/solutions/", + ); + expect(request.request.method).toBe("POST"); + expect(request.request.body).toEqual({ name: "Решение" }); + request.flush({ id: 88, program_id: 12, captain_id: 7 }); + expect(await result).toEqual({ ok: true, value: { id: 88, programId: 12, captainId: 7 } }); + }); + + it("reads explicit legacy context and serializes pagination/filter query", async () => { + const result = firstValueFrom(TestBed.inject(GetSolutionContextUseCase).execute(12)); + TestBed.inject(HttpTestingController) + .expectOne("https://local.test/programs/12/solution-context/") + .flush({ program_id: 12, storage_mode: "legacy_project" }); + expect(await result).toEqual({ + ok: true, + value: { programId: 12, storageMode: "legacy_project" }, + }); + TestBed.inject(SolutionHttpAdapter) + .list(12, { search: "Кейс", limit: 8, offset: 16, submission: "not_submitted" }) + .subscribe(); + const request = TestBed.inject(HttpTestingController).expectOne(r => + r.url.endsWith("/programs/12/solutions/"), + ); + expect(request.request.params.get("offset")).toBe("16"); + expect(request.request.params.get("submission")).toBe("not_submitted"); + request.flush({ count: 0, next: null, previous: null, results: [] }); + }); + + it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, NaN])( + "rejects invalid ID %s before HTTP", + async id => { + const result = await firstValueFrom(TestBed.inject(GetSolutionUseCase).execute(12, id)); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.code).toBe("invalid_id"); + TestBed.inject(HttpTestingController).expectNone(() => true); + }, + ); + + it("normalizes raw snake_case error issues separately from success bodies", () => { + const error = solutionError( + new HttpErrorResponse({ + status: 409, + error: { + code: "form_revision_conflict", + detail: "Форма обновлена", + errors: [{ path: "answers.8", code: "required", field_id: 8, detail: "Обязательно" }], + meta: { current_version: 2 }, + }, + }), + ); + expect(error.errors[0].fieldId).toBe(8); + expect(error.code).toBe("form_revision_conflict"); + expect(error.meta).toEqual({ currentVersion: 2 }); + expect( + solutionError(new HttpErrorResponse({ status: 500, error: "secret traceback" })).detail, + ).not.toContain("secret"); + }); + + it("keeps project ID, solution ID and program link ID distinct", () => { + expect( + solutionApplicationRoute({ + kind: "legacy_project", + programId: 12, + projectId: 77, + programLinkId: 901, + name: "Старое", + }), + ).toEqual({ + path: "/office/projects/77/edit", + queryParams: { editingStep: "additional", fromProgram: true, programLinkId: 901 }, + }); + expect( + solutionApplicationRoute({ kind: "solution", programId: 12, solutionId: 88, name: "Новое" }) + .path, + ).toBe("/office/program/12/solutions/88"); + }); +}); diff --git a/projects/social_platform/src/app/api/solution/solution-error.ts b/projects/social_platform/src/app/api/solution/solution-error.ts new file mode 100644 index 000000000..eb177d392 --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-error.ts @@ -0,0 +1,74 @@ +/** @format */ + +import { HttpErrorResponse } from "@angular/common/http"; +import camelcaseKeys from "camelcase-keys"; +import { SolutionApiError, SolutionIssue } from "@domain/solution/solution-contract.model"; + +export function solutionError(error: unknown): SolutionApiError { + if (error instanceof RangeError) + return { code: "invalid_id", detail: "Некорректный адрес решения.", errors: [], meta: {} }; + if (error instanceof HttpErrorResponse) { + const body = error.error as Record | null; + if ( + body && + typeof body === "object" && + typeof body["code"] === "string" && + typeof body["detail"] === "string" + ) { + const errors: SolutionIssue[] = Array.isArray(body["errors"]) + ? body["errors"].flatMap(item => { + if (!item || typeof item !== "object") return []; + const row = item as Record; + if ( + typeof row["path"] !== "string" || + typeof row["code"] !== "string" || + typeof row["detail"] !== "string" + ) + return []; + return [ + { + path: row["path"], + code: row["code"], + detail: row["detail"], + fieldId: typeof row["field_id"] === "number" ? row["field_id"] : null, + }, + ]; + }) + : []; + return { + code: body["code"], + detail: body["detail"], + errors, + meta: + body["meta"] && typeof body["meta"] === "object" && !Array.isArray(body["meta"]) + ? camelcaseKeys(body["meta"] as Record, { deep: true }) + : {}, + }; + } + const code = + error.status === 0 + ? "network" + : error.status === 401 + ? "unauthorized" + : error.status === 403 + ? "forbidden" + : error.status === 404 + ? "not_found" + : "server_error"; + const detail = + code === "unauthorized" + ? "Войдите в аккаунт снова." + : code === "forbidden" + ? "Нет доступа к решению." + : code === "not_found" + ? "Решение не найдено." + : "Не удалось загрузить решение. Попробуйте еще раз."; + return { code, detail, errors: [], meta: {} }; + } + return { + code: "unknown", + detail: "Не удалось выполнить действие. Попробуйте еще раз.", + errors: [], + meta: {}, + }; +} diff --git a/projects/social_platform/src/app/api/solution/solution-navigation.ts b/projects/social_platform/src/app/api/solution/solution-navigation.ts new file mode 100644 index 000000000..f357846ed --- /dev/null +++ b/projects/social_platform/src/app/api/solution/solution-navigation.ts @@ -0,0 +1,21 @@ +/** @format */ + +import { SolutionApplicationRef } from "@domain/solution/solution-contract.model"; +import { AppRoutes } from "@api/paths/app-routes"; + +/** Контекст передается явно; ID проекта, связи программы и решения не взаимозаменяемы. */ +export function solutionApplicationRoute(application: SolutionApplicationRef) { + return application.kind === "solution" + ? { + path: AppRoutes.program.solution(application.programId, application.solutionId), + queryParams: {}, + } + : { + path: AppRoutes.projects.edit(application.projectId), + queryParams: { + editingStep: "additional", + fromProgram: true, + programLinkId: application.programLinkId, + }, + }; +} diff --git a/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts b/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts new file mode 100644 index 000000000..45847098d --- /dev/null +++ b/projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts @@ -0,0 +1,39 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { catchError, defer, map, Observable, of } from "rxjs"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { CreateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { fail, ok } from "@domain/shared/result.type"; +import { solutionError } from "../solution-error"; + +function requestResult(request: () => Observable) { + return defer(request).pipe( + map(value => ok(value)), + catchError(error => of(fail(solutionError(error)))), + ); +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionContextUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number) { + return requestResult(() => this.repository.context(programId)); + } +} + +@Injectable({ providedIn: "root" }) +export class CreateSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, command: CreateSolutionCommand = {}) { + return requestResult(() => this.repository.create(programId, command)); + } +} + +@Injectable({ providedIn: "root" }) +export class GetSolutionUseCase { + private readonly repository = inject(SolutionRepositoryPort); + execute(programId: number, solutionId: number) { + return requestResult(() => this.repository.get(programId, solutionId)); + } +} diff --git a/projects/social_platform/src/app/domain/invite/results/invite.result.ts b/projects/social_platform/src/app/domain/invite/results/invite.result.ts index 7a0c774a0..a4b2077fa 100644 --- a/projects/social_platform/src/app/domain/invite/results/invite.result.ts +++ b/projects/social_platform/src/app/domain/invite/results/invite.result.ts @@ -5,8 +5,7 @@ import { Invite } from "@domain/invite/invite.model"; /** Ошибки операций с инвайтами */ export type InviteError = - | { kind: "unknown"; cause?: unknown } - | { kind: "get_invites_error"; cause?: unknown }; + { kind: "unknown"; cause?: unknown } | { kind: "get_invites_error"; cause?: unknown }; /** Результат операций, которые не возвращают данные (accept, reject, revoke). */ export type InviteResult = Result; diff --git a/projects/social_platform/src/app/domain/other/notification.model.ts b/projects/social_platform/src/app/domain/other/notification.model.ts index 3514959f7..f7daaf66f 100644 --- a/projects/social_platform/src/app/domain/other/notification.model.ts +++ b/projects/social_platform/src/app/domain/other/notification.model.ts @@ -5,9 +5,7 @@ export class Notification { id!: string; kind!: "expert_joined" | "registration_connection" | "program_attention"; category?: - | "participants-without-visit" - | "participants-without-team" - | "projects-awaiting-evaluation"; + "participants-without-visit" | "participants-without-team" | "projects-awaiting-evaluation"; text!: string; readAt!: string | null; programId!: number; diff --git a/projects/social_platform/src/app/domain/program/program-analytics.model.ts b/projects/social_platform/src/app/domain/program/program-analytics.model.ts index 83ddbf0e9..17502ffd4 100644 --- a/projects/social_platform/src/app/domain/program/program-analytics.model.ts +++ b/projects/social_platform/src/app/domain/program/program-analytics.model.ts @@ -105,10 +105,7 @@ export interface ProgramAnalyticsError { * pending/in_progress различаются наличием оценённых критериев, не длительностью ожидания. */ export type ProgramAnalyticsAssignmentStatus = - | "not_ready" - | "pending" - | "in_progress" - | "completed"; + "not_ready" | "pending" | "in_progress" | "completed"; /** pending включает все незавершённые назначения, в том числе not_ready. */ export type ProgramAnalyticsAssignmentScope = "all" | "completed" | "pending"; diff --git a/projects/social_platform/src/app/domain/program/program-attention-actions.model.ts b/projects/social_platform/src/app/domain/program/program-attention-actions.model.ts index 98128efe4..f75026257 100644 --- a/projects/social_platform/src/app/domain/program/program-attention-actions.model.ts +++ b/projects/social_platform/src/app/domain/program/program-attention-actions.model.ts @@ -1,8 +1,6 @@ /** @format */ export type ProgramAttentionCategory = - | "participants-without-visit" - | "participants-without-team" - | "projects-awaiting-evaluation"; + "participants-without-visit" | "participants-without-team" | "projects-awaiting-evaluation"; export interface ProgramAttentionSelection { mode: "selected" | "all"; diff --git a/projects/social_platform/src/app/domain/program/program-management.model.ts b/projects/social_platform/src/app/domain/program/program-management.model.ts index bff4d240d..4da8fcc87 100644 --- a/projects/social_platform/src/app/domain/program/program-management.model.ts +++ b/projects/social_platform/src/app/domain/program/program-management.model.ts @@ -14,13 +14,7 @@ export type ManagementSection = | "settings" | "evaluation"; export type SettingsSection = - | "basic" - | "dates" - | "materials" - | "project-form" - | "criteria" - | "organizers" - | "registration"; + "basic" | "dates" | "materials" | "project-form" | "criteria" | "organizers" | "registration"; export interface ManagementMaterial { id: number; title: string; @@ -67,11 +61,7 @@ export interface ManagementParticipant { registrationAnswers?: { label: string; value: string | string[] | number | null }[]; } export type ManagementEvaluationStatus = - | "not_submitted" - | "awaiting_evaluation" - | "partially_evaluated" - | "evaluated" - | "not_applicable"; + "not_submitted" | "awaiting_evaluation" | "partially_evaluated" | "evaluated" | "not_applicable"; export interface ManagementProject { id: number; projectId: number; diff --git a/projects/social_platform/src/app/domain/program/program-role-widget.model.ts b/projects/social_platform/src/app/domain/program/program-role-widget.model.ts index c59feb313..dbf3dff92 100644 --- a/projects/social_platform/src/app/domain/program/program-role-widget.model.ts +++ b/projects/social_platform/src/app/domain/program/program-role-widget.model.ts @@ -6,12 +6,7 @@ export type ProgramWidgetRole = "organizer" | "expert" | "participant"; export type ParticipantStage = - | "none" - | "not_submitted" - | "submitted" - | "review" - | "evaluated" - | "not_applicable"; + "none" | "not_submitted" | "submitted" | "review" | "evaluated" | "not_applicable"; export interface ParticipantWidget { participantProject: { id: number; name: string; programLinkId: number } | null; caseProvided: boolean; @@ -42,8 +37,4 @@ export type ProgramRoleWidget = WidgetContext & | { role: "expert"; expert: ExpertWidget } ); export type ProgramWidgetError = - | "network" - | "forbidden" - | "unauthorized" - | "not_found" - | "integrity"; + "network" | "forbidden" | "unauthorized" | "not_found" | "integrity"; diff --git a/projects/social_platform/src/app/domain/project/step.model.ts b/projects/social_platform/src/app/domain/project/step.model.ts index 52143d2d6..09878d34b 100644 --- a/projects/social_platform/src/app/domain/project/step.model.ts +++ b/projects/social_platform/src/app/domain/project/step.model.ts @@ -87,8 +87,4 @@ export interface WriteQuestion extends BaseStep { /** Объединенный тип всех возможных типов шагов */ export type StepType = - | InfoSlide - | ConnectQuestion - | SingleQuestion - | ExcludeQuestion - | WriteQuestion; + InfoSlide | ConnectQuestion | SingleQuestion | ExcludeQuestion | WriteQuestion; diff --git a/projects/social_platform/src/app/domain/shared/result.type.ts b/projects/social_platform/src/app/domain/shared/result.type.ts index 8fbad0db2..9d90cdc81 100644 --- a/projects/social_platform/src/app/domain/shared/result.type.ts +++ b/projects/social_platform/src/app/domain/shared/result.type.ts @@ -20,8 +20,7 @@ */ export type Result = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly error: E }; + { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: E }; /** Создаёт успешный Result */ export function ok(value: T): Result { diff --git a/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts new file mode 100644 index 000000000..da5e9f6e7 --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts @@ -0,0 +1,27 @@ +/** @format */ + +import { Observable } from "rxjs"; +import { + CreateSolutionCommand, + ProgramSolution, + SolutionContext, + SolutionPage, +} from "../solution-contract.model"; + +export abstract class SolutionRepositoryPort { + abstract context(programId: number): Observable; + abstract create(programId: number, command: CreateSolutionCommand): Observable; + abstract get(programId: number, solutionId: number): Observable; + abstract list(programId: number, query?: SolutionListQuery): Observable; +} + +export type SolutionListQuery = { + search?: string; + submission?: "all" | "submitted" | "not_submitted"; + limit?: number; + offset?: number; +}; + +export function validSolutionId(value: unknown): value is number { + return typeof value === "number" && Number.isSafeInteger(value) && value > 0; +} diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts new file mode 100644 index 000000000..4b1a90f52 --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts @@ -0,0 +1,3945 @@ +/** @format */ + +// Независимые сценарии C00; не последовательность запросов и не production mock provider. +export type SolutionContractScenario = { + name: string; + operationId: string; + status: number; + request?: unknown; + response?: unknown; +}; +export const solutionContractScenarios: SolutionContractScenario[] = [ + { + name: "context_solution", + operationId: "getSolutionContext", + status: 200, + response: { + programId: 12, + storageMode: "solution", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: "2026-10-30T18:00:00Z", + canCreate: false, + blockedReason: "already_has_application", + currentApplication: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + }, + }, + { + name: "context_legacy", + operationId: "getSolutionContext", + status: 200, + response: { + programId: 12, + storageMode: "legacy_project", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: "2026-10-30T18:00:00Z", + canCreate: false, + blockedReason: "already_has_application", + currentApplication: { + kind: "legacy_project", + programId: 12, + projectId: 55, + programLinkId: 65, + name: "Исторический проект", + }, + }, + }, + { + name: "draft_incomplete", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "draft_ready", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 9, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + }, + { + name: "submitted_snapshot", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + }, + { + name: "noncompetitive", + operationId: "getSolution", + status: 200, + response: { + id: 501, + programId: 14, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: null, + maxSize: null, + isFrozen: false, + }, + form: { + programId: 14, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: false, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: false, + }, + readiness: null, + }, + }, + { + name: "deadline_closed_draft_editable", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: false, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: false, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "created_solo", + operationId: "createSolution", + status: 201, + request: {}, + response: { + id: 301, + programId: 12, + version: 1, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + ], + acceptedCount: 1, + pendingCount: 0, + reservedCount: 1, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: false, + issues: [ + { + path: "team", + code: "team_min_size", + fieldId: null, + detail: "Нужно не менее двух участников.", + }, + { + path: "answers", + code: "required", + fieldId: 1001, + detail: "Заполните поле.", + }, + { + path: "answers", + code: "required", + fieldId: 1002, + detail: "Заполните поле.", + }, + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Заполните поле.", + }, + ], + }, + }, + }, + { + name: "create_existing", + operationId: "createSolution", + status: 200, + request: {}, + response: { + id: 301, + programId: 12, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "save_answers", + operationId: "updateSolution", + status: 200, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + answers: [ + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + }, + response: { + id: 301, + programId: 12, + version: 9, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + }, + { + name: "clear_answer", + operationId: "updateSolution", + status: 200, + request: { + expectedVersion: 9, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + answers: [ + { + fieldId: 1003, + value: null, + }, + ], + }, + response: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "submit_success", + operationId: "submitSolution", + status: 200, + request: { + expectedVersion: 9, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + response: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + }, + { + name: "submit_required", + operationId: "submitSolution", + status: 400, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + response: { + code: "required_program_fields", + detail: "Заполните обязательные поля.", + errors: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + meta: {}, + }, + }, + { + name: "submit_closed", + operationId: "submitSolution", + status: 409, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + response: { + code: "submission_closed", + detail: "Срок сдачи завершен.", + errors: [], + meta: {}, + }, + }, + { + name: "submit_repeated", + operationId: "submitSolution", + status: 409, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + response: { + code: "already_submitted", + detail: "Решение уже сдано.", + errors: [], + meta: {}, + }, + }, + { + name: "not_captain", + operationId: "submitSolution", + status: 403, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + response: { + code: "not_solution_captain", + detail: "Сдать решение может только капитан.", + errors: [], + meta: {}, + }, + }, + { + name: "version_conflict", + operationId: "updateSolution", + status: 409, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + name: "Новое название", + }, + response: { + code: "version_conflict", + detail: "Заявка изменилась. Обновите данные.", + errors: [], + meta: { + currentVersion: 10, + }, + }, + }, + { + name: "form_conflict", + operationId: "updateSolution", + status: 409, + request: { + expectedVersion: 8, + expectedFormRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + name: "Новое название", + }, + response: { + code: "form_revision_conflict", + detail: "Форма изменилась. Обновите поля.", + errors: [], + meta: { + currentFormRevision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }, + }, + }, + { + name: "create_invitation", + operationId: "createSolutionInvitation", + status: 201, + request: { + expectedSolutionVersion: 8, + recipientUserId: 9, + role: "Аналитик", + }, + response: { + invitation: { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + solutionVersion: 9, + }, + }, + { + name: "duplicate_invitation", + operationId: "createSolutionInvitation", + status: 409, + request: { + expectedSolutionVersion: 8, + recipientUserId: 9, + }, + response: { + code: "duplicate_pending_invite", + detail: "Приглашение не может быть отправлено.", + errors: [], + meta: {}, + }, + }, + { + name: "max_team", + operationId: "createSolutionInvitation", + status: 409, + request: { + expectedSolutionVersion: 8, + recipientUserId: 9, + }, + response: { + code: "team_max_size", + detail: "Приглашение не может быть отправлено.", + errors: [], + meta: {}, + }, + }, + { + name: "inbox", + operationId: "listMySolutionInvitations", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: false, + canRevoke: false, + canAccept: true, + canDecline: true, + }, + }, + ], + }, + }, + { + name: "accept_invitation", + operationId: "acceptSolutionInvitation", + status: 200, + request: { + expectedVersion: 1, + }, + response: { + invitation: { + id: 701, + version: 2, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "accepted", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: false, + canRevoke: false, + canAccept: false, + canDecline: false, + }, + }, + application: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + solutionVersion: 9, + }, + }, + { + name: "accept_other_team", + operationId: "acceptSolutionInvitation", + status: 409, + request: { + expectedVersion: 1, + }, + response: { + code: "already_in_program_team", + detail: "Вы уже состоите в команде программы.", + errors: [], + meta: {}, + }, + }, + { + name: "decline_after_submit", + operationId: "declineSolutionInvitation", + status: 200, + request: { + expectedVersion: 1, + }, + response: { + invitation: { + id: 701, + version: 2, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "declined", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: false, + canRevoke: false, + canAccept: false, + canDecline: false, + }, + }, + application: null, + solutionVersion: null, + }, + }, + { + name: "revoke_after_submit", + operationId: "revokeSolutionInvitation", + status: 204, + }, + { + name: "captain_transfer", + operationId: "transferCaptain", + status: 200, + request: { + expectedSolutionVersion: 8, + newCaptainUserId: 8, + }, + response: { + solutionId: 301, + solutionVersion: 9, + updatedAt: "2026-10-23T10:00:00Z", + }, + }, + { + name: "leave", + operationId: "leaveSolution", + status: 204, + request: { + expectedSolutionVersion: 8, + }, + }, + { + name: "remove_member", + operationId: "removeSolutionMember", + status: 200, + response: { + solutionId: 301, + solutionVersion: 9, + updatedAt: "2026-10-23T10:00:00Z", + }, + }, + { + name: "solution_list", + operationId: "listSolutions", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 301, + programId: 12, + name: "Сервис для студентов", + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + acceptedCount: 2, + caseValue: "Цифровой сервис", + status: "draft", + submittedAt: null, + }, + ], + }, + }, + { + name: "form", + operationId: "getSolutionForm", + status: 200, + response: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + }, + { + name: "form_management", + operationId: "getManagedSolutionForm", + status: 200, + response: { + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + hasSolutions: true, + hasAnswers: true, + affectedDraftCount: 1, + fieldsUsage: [ + { + fieldId: 1001, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1002, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1003, + answersCount: 0, + canDelete: true, + canChangeType: true, + }, + { + fieldId: 1004, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1005, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + ], + }, + }, + { + name: "templates", + operationId: "listSolutionFormTemplates", + status: 200, + response: { + results: [ + { + key: "case_championship", + label: "Кейс-чемпионат", + fields: [ + { + id: null, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: null, + type: "url", + purpose: "video", + label: "Видео", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "file", + purpose: "other", + label: "Дополнительные материалы", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf", "zip"], + maxFileSizeBytes: 52428800, + }, + }, + ], + }, + { + key: "hackathon", + label: "Хакатон", + fields: [ + { + id: null, + type: "select", + purpose: "case", + label: "Задача или трек", + helpText: "", + required: true, + position: 0, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "textarea", + purpose: "other", + label: "Описание реализации", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: 10000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "url", + purpose: "repository", + label: "Репозиторий", + helpText: "", + required: true, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "textarea", + purpose: "run_instructions", + label: "Инструкция запуска", + helpText: "", + required: true, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "url", + purpose: "demo", + label: "Демо", + helpText: "", + required: false, + position: 5, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: false, + position: 6, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: null, + type: "url", + purpose: "video", + label: "Видео", + helpText: "", + required: false, + position: 7, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: null, + type: "file", + purpose: "other", + label: "Дополнительные материалы", + helpText: "", + required: false, + position: 8, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf", "zip"], + maxFileSizeBytes: 52428800, + }, + }, + ], + }, + ], + maxFileSizeBytes: 104857600, + filesPerField: 1, + }, + }, + { + name: "upload", + operationId: "uploadExistingFile", + status: 201, + response: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + { + name: "hackathon_ready", + operationId: "getSolution", + status: 200, + response: { + id: 401, + programId: 13, + version: 3, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 13, + revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + template: "hackathon", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 2001, + type: "select", + purpose: "case", + label: "Задача или трек", + helpText: "", + required: true, + position: 0, + options: ["Городской сервис"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2003, + type: "textarea", + purpose: "other", + label: "Описание реализации", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: 10000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2004, + type: "url", + purpose: "repository", + label: "Репозиторий", + helpText: "", + required: true, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2005, + type: "textarea", + purpose: "run_instructions", + label: "Инструкция запуска", + helpText: "", + required: true, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2006, + type: "url", + purpose: "demo", + label: "Демо", + helpText: "", + required: false, + position: 5, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2007, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: false, + position: 6, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 2008, + type: "url", + purpose: "video", + label: "Видео", + helpText: "", + required: false, + position: 7, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 2009, + type: "file", + purpose: "other", + label: "Дополнительные материалы", + helpText: "", + required: false, + position: 8, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf", "zip"], + maxFileSizeBytes: 52428800, + }, + }, + ], + }, + answers: [ + { + fieldId: 2001, + value: "Городской сервис", + }, + { + fieldId: 2002, + value: "Сервис обращений жителей.", + }, + { + fieldId: 2003, + value: "Реализован прототип обработки обращений и личный кабинет.", + }, + { + fieldId: 2004, + value: "https://example.test/team/repository", + }, + { + fieldId: 2005, + value: "См. README репозитория.", + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: true, + canManageTeam: true, + canInvite: true, + canSubmit: true, + }, + readiness: { + isReady: true, + issues: [], + }, + }, + }, + { + name: "solution_invitations", + operationId: "listSolutionInvitations", + status: 200, + response: { + count: 1, + next: null, + previous: null, + results: [ + { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + ], + }, + }, + { + name: "invitation_detail", + operationId: "getSolutionInvitation", + status: 200, + response: { + id: 701, + version: 1, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Аналитик", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: false, + canRevoke: false, + canAccept: true, + canDecline: true, + }, + }, + }, + { + name: "edit_invitation", + operationId: "updateSolutionInvitation", + status: 200, + request: { + expectedVersion: 1, + expectedSolutionVersion: 8, + role: "Дизайнер", + }, + response: { + invitation: { + id: 701, + version: 2, + program: { + id: 12, + name: "Кейс-чемпионат PROCOLLAB", + }, + solution: { + kind: "solution", + programId: 12, + solutionId: 301, + name: "Сервис для студентов", + }, + captain: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + recipient: { + id: 9, + firstName: "Олег", + lastName: "Иванов", + avatarUrl: null, + }, + role: "Дизайнер", + specialization: "Исследования", + motivationalLetter: "Приглашаем в команду.", + status: "pending", + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + permissions: { + canEdit: true, + canRevoke: true, + canAccept: false, + canDecline: false, + }, + }, + solutionVersion: 9, + }, + }, + { + name: "save_form", + operationId: "saveManagedSolutionForm", + status: 200, + request: { + expectedRevision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + response: { + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + hasSolutions: true, + hasAnswers: true, + affectedDraftCount: 1, + fieldsUsage: [ + { + fieldId: 1001, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1002, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1003, + answersCount: 0, + canDelete: true, + canChangeType: true, + }, + { + fieldId: 1004, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + { + fieldId: 1005, + answersCount: 1, + canDelete: false, + canChangeType: false, + }, + ], + }, + }, + { + name: "team_frozen", + operationId: "createSolutionInvitation", + status: 409, + request: { + expectedSolutionVersion: 10, + recipientUserId: 9, + }, + response: { + code: "team_frozen", + detail: "Состав команды зафиксирован.", + errors: [], + meta: {}, + }, + }, + { + name: "member_read_only", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 8, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:00:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: false, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: false, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + ], + submission: { + required: true, + status: "draft", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: null, + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + }, + readiness: { + isReady: false, + issues: [ + { + path: "answers", + code: "required", + fieldId: 1003, + detail: "Загрузите презентацию.", + }, + ], + }, + }, + }, + { + name: "expert_snapshot", + operationId: "getSolution", + status: 200, + response: { + id: 301, + programId: 12, + version: 10, + name: "Сервис для студентов", + captainId: 7, + createdAt: "2026-10-23T09:00:00Z", + updatedAt: "2026-10-23T10:05:00Z", + team: { + members: [ + { + user: { + id: 7, + firstName: "Иван", + lastName: "Петров", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: true, + }, + { + user: { + id: 8, + firstName: "Анна", + lastName: "Смирнова", + avatarUrl: null, + }, + role: null, + specialization: null, + joinedAt: "2026-10-23T09:00:00Z", + isCaptain: false, + }, + ], + acceptedCount: 2, + pendingCount: 1, + reservedCount: 3, + minSize: 2, + maxSize: 4, + isFrozen: true, + }, + form: { + programId: 12, + revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + template: "case_championship", + isConfigured: true, + isSnapshot: true, + fields: [ + { + id: 1001, + type: "select", + purpose: "case", + label: "Кейс", + helpText: "", + required: true, + position: 0, + options: ["Цифровой сервис", "Операционная эффективность"], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1002, + type: "textarea", + purpose: "other", + label: "Краткое описание", + helpText: "", + required: true, + position: 1, + options: [], + validation: { + minLength: null, + maxLength: 3000, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1003, + type: "file", + purpose: "presentation", + label: "Презентация PDF", + helpText: "", + required: true, + position: 2, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: ["pdf"], + maxFileSizeBytes: 20971520, + }, + }, + { + id: 1004, + type: "number", + purpose: "other", + label: "Ожидаемый эффект", + helpText: "", + required: false, + position: 3, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: 0, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + { + id: 1005, + type: "checkbox", + purpose: "other", + label: "Приложены расчеты", + helpText: "", + required: false, + position: 4, + options: [], + validation: { + minLength: null, + maxLength: null, + minimum: null, + maximum: null, + allowedExtensions: [], + maxFileSizeBytes: null, + }, + }, + ], + }, + answers: [ + { + fieldId: 1001, + value: "Цифровой сервис", + }, + { + fieldId: 1002, + value: "Единый сервис поддержки студентов.", + }, + { + fieldId: 1004, + value: 0, + }, + { + fieldId: 1005, + value: false, + }, + { + fieldId: 1003, + value: { + url: "https://cdn.example.test/solutions/301/presentation.pdf", + }, + }, + ], + submission: { + required: true, + status: "submitted", + submissionOpen: true, + deadline: "2026-10-30T18:00:00Z", + submittedAt: "2026-10-23T10:05:00Z", + }, + permissions: { + canEdit: false, + canManageTeam: false, + canInvite: false, + canSubmit: false, + }, + readiness: null, + }, + }, + { + name: "context_empty", + operationId: "getSolutionContext", + status: 200, + response: { + programId: 12, + storageMode: "solution", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: "2026-10-30T18:00:00Z", + canCreate: true, + blockedReason: null, + currentApplication: null, + }, + }, +]; diff --git a/projects/social_platform/src/app/domain/solution/solution-contract.model.ts b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts new file mode 100644 index 000000000..1ce981309 --- /dev/null +++ b/projects/social_platform/src/app/domain/solution/solution-contract.model.ts @@ -0,0 +1,322 @@ +/** @format */ + +// Сгенерировано tools/generate-solution-contract.py из C00 1.0.0. +// camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются. + +export type SolutionUser = { + id: number; + firstName: string; + lastName: string; + avatarUrl: string | null; +}; + +export type SolutionProgramRef = { + id: number; + name: string; +}; + +export type SolutionRef = { + kind: "solution"; + programId: number; + solutionId: number; + name: string; +}; + +export type SolutionLegacyRef = { + kind: "legacy_project"; + programId: number; + projectId: number; + programLinkId: number; + name: string; +}; + +export type SolutionApplicationRef = SolutionRef | SolutionLegacyRef; + +export type SolutionIssue = { + path: string; + code: string; + fieldId: number | null; + detail: string; +}; + +export type SolutionApiError = { + code: string; + detail: string; + errors: Array; + meta: Record; +}; + +export type SolutionContext = { + programId: number; + storageMode: "legacy_project" | "solution"; + isRegistered: boolean; + isCompetitive: boolean; + submissionOpen: boolean; + submissionDeadline: string | null; + canCreate: boolean; + blockedReason: string | null; + currentApplication: SolutionApplicationRef | null; +}; + +export type SolutionValidation = { + minLength: number | null; + maxLength: number | null; + minimum: number | null; + maximum: number | null; + allowedExtensions: Array; + maxFileSizeBytes: number | null; +}; + +export type SolutionFieldType = + "text" | "textarea" | "url" | "number" | "select" | "checkbox" | "file" | "radio"; + +export type SolutionPurpose = + "other" | "case" | "presentation" | "repository" | "demo" | "video" | "run_instructions"; + +export type SolutionTemplateName = "case_championship" | "hackathon"; + +export type SolutionField = { + id: number; + type: SolutionFieldType; + purpose: SolutionPurpose; + label: string; + helpText: string; + required: boolean; + position: number; + options: Array; + validation: SolutionValidation; +}; + +export type SolutionFieldWrite = { + id: number | null; + type: SolutionFieldType; + purpose: SolutionPurpose; + label: string; + helpText: string; + required: boolean; + position: number; + options: Array; + validation: SolutionValidation; +}; + +export type SolutionForm = { + programId: number; + revision: string; + template: SolutionTemplateName | null; + isConfigured: boolean; + isSnapshot: boolean; + fields: Array; +}; + +export type SolutionFormWrite = { + expectedRevision: string; + template: SolutionTemplateName; + fields: Array; +}; + +export type SolutionFormManagement = { + form: SolutionForm; + hasSolutions: boolean; + hasAnswers: boolean; + affectedDraftCount: number; + fieldsUsage: Array<{ + fieldId: number; + answersCount: number; + canDelete: boolean; + canChangeType: boolean; + }>; +}; + +export type SolutionTemplate = { + key: SolutionTemplateName; + label: string; + fields: Array; +}; + +export type SolutionTemplates = { + results: Array; + maxFileSizeBytes: 104857600; + filesPerField: 1; +}; + +export type SolutionFileRef = { + url: string; +}; + +export type SolutionAnswerValue = string | number | boolean | SolutionFileRef; + +export type SolutionAnswer = { + fieldId: number; + value: SolutionAnswerValue; +}; + +export type SolutionAnswerPatch = { + fieldId: number; + value: SolutionAnswerValue | null; +}; + +export type SolutionMember = { + user: SolutionUser; + role: string | null; + specialization: string | null; + joinedAt: string; + isCaptain: boolean; +}; + +export type SolutionTeam = { + members: Array; + acceptedCount: number; + pendingCount: number; + reservedCount: number; + minSize: number | null; + maxSize: number | null; + isFrozen: boolean; +}; + +export type SolutionPermissions = { + canEdit: boolean; + canManageTeam: boolean; + canInvite: boolean; + canSubmit: boolean; +}; + +export type SolutionSubmission = { + required: boolean; + status: "draft" | "submitted"; + submissionOpen: boolean; + deadline: string | null; + submittedAt: string | null; +}; + +export type SolutionReadiness = { + isReady: boolean; + issues: Array; +}; + +export type ProgramSolution = { + id: number; + programId: number; + version: number; + name: string; + captainId: number; + createdAt: string; + updatedAt: string; + team: SolutionTeam; + form: SolutionForm; + answers: Array; + submission: SolutionSubmission; + permissions: SolutionPermissions; + readiness: SolutionReadiness | null; +}; + +export type SolutionSummary = { + id: number; + programId: number; + name: string; + captain: SolutionUser; + acceptedCount: number; + caseValue: string | null; + status: "draft" | "submitted"; + submittedAt: string | null; +}; + +export type CreateSolutionCommand = { + name?: string; +}; + +export type UpdateSolutionCommand = unknown | unknown; + +export type SubmitSolutionCommand = { + expectedVersion: number; + expectedFormRevision: string; +}; + +export type SolutionCaptainCommand = { + expectedSolutionVersion: number; + newCaptainUserId: number; +}; + +export type SolutionTeamCommand = { + expectedSolutionVersion: number; +}; + +export type SolutionInvitationPermissions = { + canEdit: boolean; + canRevoke: boolean; + canAccept: boolean; + canDecline: boolean; +}; + +export type SolutionInvitation = { + id: number; + version: number; + program: SolutionProgramRef; + solution: SolutionRef; + captain: SolutionUser; + recipient: SolutionUser; + role: string | null; + specialization: string | null; + motivationalLetter: string | null; + status: "pending" | "accepted" | "declined"; + createdAt: string; + updatedAt: string; + permissions: SolutionInvitationPermissions; +}; + +export type SolutionCreateInvitation = { + expectedSolutionVersion: number; + recipientUserId: number; + role?: string | null; + specialization?: string | null; + motivationalLetter?: string | null; +}; + +export type SolutionUpdateInvitation = unknown | unknown | unknown; + +export type SolutionInvitationAction = { + expectedVersion: number; +}; + +export type SolutionInvitationCreated = { + invitation: SolutionInvitation; + solutionVersion: number; +}; + +export type SolutionInvitationActionResult = { + invitation: SolutionInvitation; + application: SolutionRef | null; + solutionVersion: number | null; +}; + +export type SolutionMutationReceipt = { + solutionId: number; + solutionVersion: number; + updatedAt: string; +}; + +export type SolutionPage = { + count: number; + next: string | null; + previous: string | null; + results: Array; +}; + +export type SolutionInvitationPage = { + count: number; + next: string | null; + previous: string | null; + results: Array; +}; + +export type SolutionUploadResponse = { + url: string; +}; + +export type SolutionSubmissionSnapshot = { + name: string; + captainId: number; + members: Array; + form: SolutionForm; + answers: Array; + submittedAt: string; +}; diff --git a/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts new file mode 100644 index 000000000..3efb0303d --- /dev/null +++ b/projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts @@ -0,0 +1,48 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { HttpParams } from "@angular/common/http"; +import { ApiService } from "@corelib"; +import { + CreateSolutionCommand, + ProgramSolution, + SolutionContext, + SolutionPage, +} from "@domain/solution/solution-contract.model"; +import { + SolutionListQuery, + validSolutionId, +} from "@domain/solution/ports/solution.repository.port"; + +@Injectable({ providedIn: "root" }) +export class SolutionHttpAdapter { + private readonly api = inject(ApiService); + + private path(programId: number): string { + if (!validSolutionId(programId)) throw new RangeError("Invalid program ID"); + return `/programs/${programId}/solutions/`; + } + + context(programId: number) { + this.path(programId); + return this.api.get(`/programs/${programId}/solution-context/`); + } + + create(programId: number, command: CreateSolutionCommand) { + return this.api.post(this.path(programId), command); + } + + get(programId: number, solutionId: number) { + if (!validSolutionId(solutionId)) throw new RangeError("Invalid solution ID"); + return this.api.get(`${this.path(programId)}${solutionId}/`); + } + + list(programId: number, query: SolutionListQuery = {}) { + let params = new HttpParams(); + // Query string не проходит через CamelcaseInterceptor; имена уже из C00. + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) params = params.set(key, value); + } + return this.api.get(this.path(programId), params); + } +} diff --git a/projects/social_platform/src/app/infrastructure/di/program/program.providers.ts b/projects/social_platform/src/app/infrastructure/di/program/program.providers.ts index 4ac2d3a78..66ecbf706 100644 --- a/projects/social_platform/src/app/infrastructure/di/program/program.providers.ts +++ b/projects/social_platform/src/app/infrastructure/di/program/program.providers.ts @@ -3,7 +3,10 @@ import { Provider } from "@angular/core"; import { ProgramRepository } from "../../repository/program/program.repository"; import { ProgramRepositoryPort } from "@domain/program/ports/program.repository.port"; +import { SolutionRepositoryPort } from "@domain/solution/ports/solution.repository.port"; +import { SolutionRepository } from "../../repository/solution/solution.repository"; export const PROGRAM_PROVIDERS: Provider[] = [ { provide: ProgramRepositoryPort, useExisting: ProgramRepository }, + { provide: SolutionRepositoryPort, useExisting: SolutionRepository }, ]; diff --git a/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts new file mode 100644 index 000000000..62471c294 --- /dev/null +++ b/projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts @@ -0,0 +1,26 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { + SolutionRepositoryPort, + SolutionListQuery, +} from "@domain/solution/ports/solution.repository.port"; +import { CreateSolutionCommand } from "@domain/solution/solution-contract.model"; +import { SolutionHttpAdapter } from "../../adapters/solution/solution-http.adapter"; + +@Injectable({ providedIn: "root" }) +export class SolutionRepository implements SolutionRepositoryPort { + private readonly http = inject(SolutionHttpAdapter); + context(programId: number) { + return this.http.context(programId); + } + create(programId: number, command: CreateSolutionCommand) { + return this.http.create(programId, command); + } + get(programId: number, solutionId: number) { + return this.http.get(programId, solutionId); + } + list(programId: number, query?: SolutionListQuery) { + return this.http.list(programId, query); + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html new file mode 100644 index 000000000..99c036ace --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.html @@ -0,0 +1,74 @@ + + +
+ ← К программе + @if (loading()) { +

Заявка

+

Загружаем заявку…

+ } @else if (error(); as problem) { +

+ {{ problem.code === "not_found" ? "Заявка не найдена" : "Не удалось открыть заявку" }} +

+

{{ problem.detail }}

+ + } @else if (solution(); as item) { +
+
+

{{ item.name }}

+

+ {{ item.submission.status === "submitted" ? "Решение сдано" : "Черновик заявки" }} +

+
+ @if (item.submission.submittedAt) { +

Сдано {{ item.submission.submittedAt | date: "dd.MM.yyyy, HH:mm" }}

+ } +
+ + @if (teamTab()) { +
+

Команда заявки

+

Участники команды · {{ item.team.acceptedCount }}

+
    + @for (member of item.team.members; track member.user.id) { +
  • + {{ member.user.firstName }} {{ member.user.lastName + }}{{ + member.isCaptain ? "Капитан" : "Участник команды" + }} + {{ member.role || "Роль не указана" }} + В команде +
  • + } @empty { +
  • В команде пока нет участников.
  • + } +
+
+ } @else { +
+

Данные решения

+
+ @for (field of item.form.fields; track field.id) { +
+
{{ field.label }}{{ field.required ? " *" : "" }}
+
{{ answer(field.id) }}
+
+ } @empty { +

Организатор еще не настроил поля решения.

+ } +
+
+ } + } +
diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss new file mode 100644 index 000000000..9ea57f006 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss @@ -0,0 +1,121 @@ +/** @format */ + +@use "styles/ui-foundation"; + +:host { + display: block; + min-width: 0; +} + +.solution { + max-width: 864px; + padding: 0 20px; + margin: 28px auto 64px; + font-size: 14px; + color: var(--black); +} + +a { + color: var(--accent); + text-decoration: none; +} + +header { + display: flex; + gap: 20px; + align-items: start; + justify-content: space-between; + margin: 28px 0; +} + +h1 { + margin: 0; + font-size: 24px; + line-height: 1.4; + overflow-wrap: anywhere; +} + +h2 { + margin: 0 0 12px; + font-size: 18px; + font-weight: 500; +} + +p { + line-height: 1.5; +} + +.muted { + color: var(--grey-for-text); +} + +nav { + display: flex; + gap: 32px; + margin-bottom: 28px; + border-bottom: 1px solid var(--medium-grey-for-outline); +} + +nav a { + padding: 12px 4px; + color: var(--black); + border-bottom: 2px solid transparent; +} + +nav a[aria-current] { + color: var(--accent); + border-bottom-color: var(--accent); +} + +dl { + margin: 0; +} + +dl > div { + padding: 16px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} + +dt { + margin-bottom: 8px; +} + +dd { + margin: 0; + color: var(--grey-for-text); + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.members { + padding: 0; + list-style: none; +} + +.members li { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr; + gap: 24px; + padding: 20px 0; + border-bottom: 1px solid var(--medium-grey-for-outline); +} + +.name small { + display: block; + margin-top: 6px; +} + +.ui-button { + @include ui-foundation.button; +} + +@media (max-width: 600px) { + header { + flex-direction: column; + } + + .members li { + grid-template-columns: 1fr; + gap: 8px; + } +} diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts new file mode 100644 index 000000000..76cf0e167 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts @@ -0,0 +1,136 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { provideRouter, Router } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { Observable, of, Subject } from "rxjs"; +import { + CreateSolutionUseCase, + GetSolutionContextUseCase, + GetSolutionUseCase, +} from "@api/solution/use-cases/solution.use-cases"; +import { ProgramSolution, SolutionContext } from "@domain/solution/solution-contract.model"; +import { solutionContractScenarios } from "@domain/solution/solution-contract.fixture"; +import { fail, ok } from "@domain/shared/result.type"; +import { ProgramSolutionComponent } from "./solution.component"; + +const solution = solutionContractScenarios.find(s => s.name === "created_solo")! + .response as ProgramSolution; +const context: SolutionContext = { + programId: 12, + storageMode: "solution", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: null, + canCreate: true, + blockedReason: null, + currentApplication: null, +}; + +describe("Solution route foundation", () => { + const get = { execute: vi.fn() }; + const create = { execute: vi.fn() }; + const getContext = { execute: vi.fn() }; + beforeEach(() => { + get.execute.mockReset().mockReturnValue(of(ok(solution))); + create.execute.mockReset().mockReturnValue(of(ok(solution))); + getContext.execute.mockReset().mockReturnValue(of(ok(context))); + TestBed.configureTestingModule({ + providers: [ + provideRouter([ + { + path: "office/program/:programId", + children: [ + { path: "solutions/new", component: ProgramSolutionComponent }, + { path: "solutions/:solutionId/edit", component: ProgramSolutionComponent }, + { path: "solutions/:solutionId", component: ProgramSolutionComponent }, + ], + }, + ]), + { provide: GetSolutionUseCase, useValue: get }, + { provide: CreateSolutionUseCase, useValue: create }, + { provide: GetSolutionContextUseCase, useValue: getContext }, + ], + }); + }); + + it("loads the exact program/solution and switches tabs without a second request", async () => { + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/12/solutions/88/edit", ProgramSolutionComponent); + expect(get.execute).toHaveBeenCalledWith(12, 88); + expect(harness.routeNativeElement?.textContent).toContain(solution.name); + await TestBed.inject(Router).navigateByUrl("/office/program/12/solutions/88/edit?tab=team"); + harness.detectChanges(); + expect(harness.routeNativeElement?.textContent).toContain("Команда заявки"); + expect(get.execute).toHaveBeenCalledTimes(1); + expect(harness.routeNativeElement?.querySelector('a[aria-current="page"]')?.textContent).toBe( + "Команда", + ); + }); + + it("an existing current application redirects without posting", async () => { + getContext.execute.mockReturnValue( + of( + ok({ + ...context, + canCreate: false, + currentApplication: { + kind: "solution", + programId: 12, + solutionId: 88, + name: "Существующее", + }, + }), + ), + ); + const harness = await RouterTestingHarness.create(); + await harness.navigateByUrl("/office/program/12/solutions/new"); + await harness.fixture.whenStable(); + expect(TestBed.inject(Router).url).toBe("/office/program/12/solutions/88"); + expect(create.execute).not.toHaveBeenCalled(); + }); + + it("error state supports retry and does not render stale solution data", async () => { + get.execute.mockReturnValueOnce( + of(fail({ code: "not_found", detail: "Заявка не найдена.", errors: [], meta: {} })), + ); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + expect(harness.routeNativeElement?.querySelector('[role="alert"]')?.textContent).toContain( + "не найдена", + ); + component.reload(); + harness.detectChanges(); + expect(harness.routeNativeElement?.textContent).toContain(solution.name); + expect(get.execute).toHaveBeenCalledTimes(2); + }); + + it("cancels the previous program request when navigating to another program", async () => { + const previous = new Subject>>(); + let cancelled = false; + const request = new Observable>>(observer => { + const subscription = previous.subscribe(observer); + return () => { + cancelled = true; + subscription.unsubscribe(); + }; + }); + get.execute.mockImplementation((programId: number) => + programId === 12 ? request : of(ok({ ...solution, programId })), + ); + const harness = await RouterTestingHarness.create(); + const component = await harness.navigateByUrl( + "/office/program/12/solutions/88", + ProgramSolutionComponent, + ); + await harness.navigateByUrl("/office/program/13/solutions/99", ProgramSolutionComponent); + previous.next(ok(solution)); + expect(component.solution()?.programId).toBe(13); + expect(get.execute).toHaveBeenLastCalledWith(13, 99); + expect(cancelled).toBe(true); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts new file mode 100644 index 000000000..661989be0 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts @@ -0,0 +1,139 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { combineLatest, EMPTY, map, startWith, Subject, switchMap } from "rxjs"; +import { + GetSolutionContextUseCase, + CreateSolutionUseCase, + GetSolutionUseCase, +} from "@api/solution/use-cases/solution.use-cases"; +import { solutionApplicationRoute } from "@api/solution/solution-navigation"; +import { AppRoutes } from "@api/paths/app-routes"; +import { ProgramSolution, SolutionApiError } from "@domain/solution/solution-contract.model"; +import { validSolutionId } from "@domain/solution/ports/solution.repository.port"; +import { ButtonDirective } from "@uilib"; +import { DatePipe } from "@angular/common"; + +/** FE01: read surface and route foundation. Editing/team commands follow in FE03–FE06. */ +@Component({ + selector: "app-program-solution", + standalone: true, + imports: [RouterLink, ButtonDirective, DatePipe], + templateUrl: "./solution.component.html", + styleUrl: "./solution.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgramSolutionComponent { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly context = inject(GetSolutionContextUseCase); + private readonly create = inject(CreateSolutionUseCase); + private readonly get = inject(GetSolutionUseCase); + readonly solution = signal(null); + readonly loading = signal(true); + readonly error = signal(null); + readonly teamTab = signal(false); + readonly programId = signal(0); + readonly routes = AppRoutes; + private readonly retry = new Subject(); + + reload(): void { + this.retry.next(); + } + + constructor() { + this.route.queryParamMap + .pipe(takeUntilDestroyed()) + .subscribe(params => this.teamTab.set(params.get("tab") === "team")); + combineLatest([ + combineLatest(this.route.pathFromRoot.map(route => route.paramMap)), + this.retry.pipe(startWith(undefined)), + ]) + .pipe( + map(([params]) => ({ + programId: Number(params.find(p => p.has("programId"))?.get("programId")), + solutionId: params.find(p => p.has("solutionId"))?.get("solutionId"), + })), + switchMap(({ programId, solutionId }) => { + this.solution.set(null); + this.error.set(null); + this.loading.set(true); + this.programId.set(programId); + if ( + !validSolutionId(programId) || + (solutionId !== undefined && !validSolutionId(Number(solutionId))) + ) { + this.showError({ + code: "not_found", + detail: "Заявка не найдена.", + errors: [], + meta: {}, + }); + return EMPTY; + } + if (solutionId !== undefined) return this.get.execute(programId, Number(solutionId)); + return this.context.execute(programId).pipe( + switchMap(result => { + if (!result.ok) { + this.showError(result.error); + return EMPTY; + } + const context = result.value; + if (context.currentApplication) { + const destination = solutionApplicationRoute(context.currentApplication); + void this.router.navigate([destination.path], { + queryParams: destination.queryParams, + replaceUrl: true, + }); + } else if (context.storageMode === "legacy_project") { + void this.router.navigate([AppRoutes.program.detail(programId)], { + replaceUrl: true, + }); + } else if (context.canCreate) { + return this.create.execute(programId); + } else { + this.showError({ + code: context.blockedReason ?? "forbidden", + detail: + "Создание заявки сейчас недоступно. Проверьте регистрацию и сроки программы.", + errors: [], + meta: {}, + }); + } + return EMPTY; + }), + ); + }), + takeUntilDestroyed(), + ) + .subscribe(result => { + this.loading.set(false); + if (!result.ok) { + this.showError(result.error); + return; + } + this.solution.set(result.value); + if (!this.route.snapshot.paramMap.has("solutionId")) { + void this.router.navigate( + [AppRoutes.program.solution(this.programId(), result.value.id)], + { replaceUrl: true }, + ); + } + }); + } + + private showError(error: SolutionApiError): void { + this.error.set(error); + this.loading.set(false); + } + + answer(fieldId: number): string { + const value = this.solution()?.answers.find(answer => answer.fieldId === fieldId)?.value; + if (value === null || value === undefined || value === "") return "Не заполнено"; + if (typeof value === "boolean") return value ? "Да" : "Нет"; + // File metadata is displayed as text until the validated download flow in FE03. + return typeof value === "object" ? "Файл прикреплен" : String(value); + } +} diff --git a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts index 8e25bf556..2bd0a0e76 100644 --- a/projects/social_platform/src/app/ui/routes/program/detail.routes.ts +++ b/projects/social_platform/src/app/ui/routes/program/detail.routes.ts @@ -14,6 +14,21 @@ import { canLeaveManagement } from "./management-leave.guard"; /** Маршруты детальной страницы программы: информация, проекты и участники. */ export const PROGRAM_DETAIL_ROUTES: Routes = [ + { + path: "solutions/new", + loadComponent: () => + import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), + }, + { + path: "solutions/:solutionId/edit", + loadComponent: () => + import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), + }, + { + path: "solutions/:solutionId", + loadComponent: () => + import("@ui/pages/program/solution/solution.component").then(m => m.ProgramSolutionComponent), + }, { path: "management", pathMatch: "full", redirectTo: "management/participants" }, { path: "management/projects/:linkId/evaluation", diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts index 5c8d8887e..f759564da 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.spec.ts @@ -17,6 +17,20 @@ import { GetProjectUseCase } from "@api/project/use-cases/get-project.use-case"; import { PartnerProgramInfo, Project } from "@domain/project/project.model"; import { programLinkFields } from "@domain/project/program-link-fields.fixture"; import { ApplyToProgramResponse } from "@domain/program/results/apply-to-program"; +import { GetSolutionContextUseCase } from "@api/solution/use-cases/solution.use-cases"; +import { SolutionContext } from "@domain/solution/solution-contract.model"; + +const legacyContext: SolutionContext = { + programId: 12, + storageMode: "legacy_project", + isRegistered: true, + isCompetitive: true, + submissionOpen: true, + submissionDeadline: null, + canCreate: false, + blockedReason: "legacy_mode", + currentApplication: null, +}; type ApplyResult = Result< ApplyToProgramResponse, @@ -42,6 +56,10 @@ describe("DetailProgramInfoService", () => { { provide: ProjectFormService, useValue: { getForm: () => new FormGroup({}) } }, ProgramDetailMainUIInfoService, { provide: ApplyProjectToProgramUseCase, useValue: { execute: vi.fn() } }, + { + provide: GetSolutionContextUseCase, + useValue: { execute: vi.fn(() => of(ok(legacyContext))) }, + }, { provide: GetMyProjectsUseCase, useValue: { execute: vi.fn() } }, { provide: GetProjectUseCase, useValue: { execute: vi.fn() } }, { @@ -60,6 +78,60 @@ describe("DetailProgramInfoService", () => { return Object.assign(Program.default(), overrides); } + it("uses the explicit solution mode without creating a Project", () => { + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of(ok({ ...legacyContext, storageMode: "solution", canCreate: true, blockedReason: null })), + ); + service.addNewProject(12); + expect(router.navigate).toHaveBeenCalledWith(["/office/program/12/solutions/new"]); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + expect(TestBed.inject(GetProgramProjectAdditionalFieldsUseCase).execute).not.toHaveBeenCalled(); + }); + + it("opens an existing solution with its own program ID and never creates again", () => { + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of( + ok({ + ...legacyContext, + storageMode: "solution", + currentApplication: { kind: "solution", programId: 12, solutionId: 88, name: "Решение" }, + }), + ), + ); + service.addNewProject(12); + expect(router.navigate).toHaveBeenCalledWith(["/office/program/12/solutions/88"], { + queryParams: {}, + }); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + }); + + it("context failure does not silently fall back to legacy creation", () => { + vi.mocked(TestBed.inject(GetSolutionContextUseCase).execute).mockReturnValue( + of(fail({ code: "network", detail: "Нет сети", errors: [], meta: {} })), + ); + service.addNewProject(12); + expect(service.applicationPending()).toBe(false); + expect(service.isAssignProjectToProgramModalOpen()).toBe(true); + expect(service.assignProjectToProgramModalMessage()).toBe("Нет сети"); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it("ignores stale program context and guards double clicks", () => { + const response = new Subject>(); + const getContext = vi + .mocked(TestBed.inject(GetSolutionContextUseCase).execute) + .mockReturnValue(response); + service.addNewProject(12); + service.addNewProject(12); + expect(getContext).toHaveBeenCalledTimes(1); + programUI.program.set(program({ id: 99 })); + response.next(ok({ ...legacyContext, storageMode: "solution", canCreate: true })); + response.complete(); + expect(router.navigate).not.toHaveBeenCalled(); + expect(TestBed.inject(ApplyProjectToProgramUseCase).execute).not.toHaveBeenCalled(); + }); + it("omits only system case from apply, preserves generic defaults and navigates with exact link", () => { const fields = programLinkFields().fields; const file = { ...fields[2], id: 20, name: "attachment", fieldType: "file" as const }; diff --git a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts index 021ac9254..4d1970f56 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/services/program/detail-program-info.service.ts @@ -17,6 +17,8 @@ import { LoggerService } from "@core/lib/services/logger/logger.service"; import { AppRoutes } from "@api/paths/app-routes"; import { PROGRAM_CASE_FIELD_NAME } from "@domain/program/program-case-field.const"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { GetSolutionContextUseCase } from "@api/solution/use-cases/solution.use-cases"; +import { solutionApplicationRoute } from "@api/solution/solution-navigation"; @Injectable() export class DetailProgramInfoService { @@ -31,6 +33,7 @@ export class DetailProgramInfoService { private readonly programDetailMainUIInfoService = inject(ProgramDetailMainUIInfoService); private readonly applyProjectToProgramUseCase = inject(ApplyProjectToProgramUseCase); + private readonly getSolutionContext = inject(GetSolutionContextUseCase); private readonly getProgramProjectAdditionalFieldsUseCase = inject( GetProgramProjectAdditionalFieldsUseCase, ); @@ -61,6 +64,42 @@ export class DetailProgramInfoService { ); addNewProject(programId: number): void { + if (this.applicationPending() || this.application()?.submitted) return; + // Existing legacy references already carry the exact project and program-link IDs. + if (this.application()) { + this.openApplication(); + return; + } + this.applicationPending.set(true); + this.getSolutionContext + .execute(programId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.applicationPending.set(false); + if (this.programDetailMainUIInfoService.program()?.id !== programId) return; + if (!result.ok) { + this.assignProjectToProgramModalMessage.set(result.error.detail); + this.isAssignProjectToProgramModalOpen.set(true); + return; + } + const context = result.value; + if (context.currentApplication) { + const destination = solutionApplicationRoute(context.currentApplication); + void this.router.navigate([destination.path], { queryParams: destination.queryParams }); + } else if (context.storageMode === "legacy_project") { + this.addLegacyProject(programId); + } else if (context.canCreate) { + void this.router.navigate([AppRoutes.program.solutionCreate(programId)]); + } else { + this.assignProjectToProgramModalMessage.set( + "Создание заявки сейчас недоступно. Проверьте регистрацию и сроки программы.", + ); + this.isAssignProjectToProgramModalOpen.set(true); + } + }); + } + + private addLegacyProject(programId: number): void { if (this.applicationPending() || this.application()?.submitted) return; if (this.application()) { this.openApplication(); @@ -71,6 +110,7 @@ export class DetailProgramInfoService { .execute(programId) .pipe( switchMap(filtersResult => { + if (this.programDetailMainUIInfoService.program()?.id !== programId) return []; const fields = filtersResult.ok ? filtersResult.value.programFields : []; const newFieldsFormValues = fields .filter(field => field.fieldType !== "file" && field.name !== PROGRAM_CASE_FIELD_NAME) @@ -85,10 +125,10 @@ export class DetailProgramInfoService { ) .subscribe({ next: result => { + if (this.programDetailMainUIInfoService.program()?.id !== programId) return; if (!result.ok) { const error = result.error.cause as - | { status?: number; error?: { detail?: string } } - | undefined; + { status?: number; error?: { detail?: string } } | undefined; if (error?.status === 400) { this.isAssignProjectToProgramModalOpen.set(true); this.assignProjectToProgramModalMessage.set(error.error?.detail ?? null); diff --git a/tools/generate-solution-contract.py b/tools/generate-solution-contract.py new file mode 100644 index 000000000..fdb7a7f6e --- /dev/null +++ b/tools/generate-solution-contract.py @@ -0,0 +1,69 @@ +"""Генерация типов и независимых моков из зафиксированного C00 без сети.""" +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DOCS = ROOT / "docs/contracts/program-solutions-v1" +APP = ROOT / "projects/social_platform/src/app" +spec = json.loads((DOCS / "openapi.json").read_text(encoding="utf-8")) +examples = json.loads((DOCS / "examples.json").read_text(encoding="utf-8")) + + +def camel(value): + return re.sub(r"_([a-z])", lambda m: m[1].upper(), value) + + +def name(value): + special = {"Solution": "ProgramSolution", "Context": "SolutionContext", "Error": "SolutionApiError", + "CreateSolution": "CreateSolutionCommand", "UpdateSolution": "UpdateSolutionCommand", + "Submit": "SubmitSolutionCommand"} + return special.get(value, value if value.startswith("Solution") else "Solution" + value) + + +def ts(schema): + if "$ref" in schema: + return name(schema["$ref"].split("/")[-1]) + if "const" in schema: + return json.dumps(schema["const"], ensure_ascii=False) + if "enum" in schema: + return " | ".join(json.dumps(v, ensure_ascii=False) for v in schema["enum"]) + for key, separator in [("anyOf", " | "), ("oneOf", " | "), ("allOf", " & ")]: + if key in schema: + return separator.join("(" + ts(v) + ")" for v in schema[key]) + kind = schema.get("type") + if isinstance(kind, list): + return " | ".join(ts({**schema, "type": item}) for item in kind) + if kind == "array": + return "Array<" + ts(schema.get("items", {})) + ">" + if kind == "object": + props = schema.get("properties", {}) + if not props: + return "Record" + required = schema.get("required", []) + return "{\n" + "\n".join(" " + camel(k) + ("" if k in required else "?") + ": " + ts(v) + ";" for k, v in props.items()) + "\n}" + return {"string": "string", "integer": "number", "number": "number", "boolean": "boolean", "null": "null"}.get(kind, "unknown") + + +types = "// Сгенерировано tools/generate-solution-contract.py из C00 1.0.0.\n// camelCase соответствует CamelcaseInterceptor; значения ID и revision не преобразуются.\n\n" +types += "\n\n".join("export type " + name(k) + " = " + ts(v) + ";" for k, v in spec["components"]["schemas"].items()) + "\n" +model = APP / "domain/solution/solution-contract.model.ts" +model.parent.mkdir(parents=True, exist_ok=True) +model.write_text(types, encoding="utf-8") + + +def convert(obj): + if isinstance(obj, dict): + return {camel(k): convert(v) for k, v in obj.items()} + if isinstance(obj, list): + return [convert(v) for v in obj] + return obj + + +fixtures = APP / "domain/solution/solution-contract.fixture.ts" +fixtures.parent.mkdir(parents=True, exist_ok=True) +fixtures.write_text("// Независимые сценарии C00; не последовательность запросов и не production mock provider.\n" + "export type SolutionContractScenario = { name: string; operationId: string; status: number; request?: unknown; response?: unknown };\n" + "export const solutionContractScenarios: SolutionContractScenario[] = " + + json.dumps(convert(examples["scenarios"]), ensure_ascii=False, indent=2) + ";\n", encoding="utf-8") +print(f"Generated {len(spec['components']['schemas'])} types and {len(examples['scenarios'])} scenarios")