From 5111e2d641453a29291080db0a1e77b44d0fb959 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 16:52:18 +0300 Subject: [PATCH 1/4] feat(programs): add solution contracts and routing foundation (FE01) --- .prettierignore | 3 + docs/contracts/program-solutions-v1/README.md | 9 + .../program-solutions-v1/examples.json | 4036 +++++++++ .../program-solutions-v1/openapi.json | 7440 +++++++++++++++++ docs/product/program-solutions-fe01.md | 44 + .../program-solutions-fe01/team-desktop.png | Bin 0 -> 28840 bytes .../program-solutions-fe01/team-mobile.png | Bin 0 -> 15833 bytes .../src/app/api/paths/app-routes.ts | 5 + .../api/solution/solution-contract.spec.ts | 145 + .../src/app/api/solution/solution-error.ts | 74 + .../app/api/solution/solution-navigation.ts | 21 + .../solution/use-cases/solution.use-cases.ts | 39 + .../ports/solution.repository.port.ts | 27 + .../solution/solution-contract.fixture.ts | 3945 +++++++++ .../solution/solution-contract.model.ts | 322 + .../solution/solution-http.adapter.ts | 48 + .../di/program/program.providers.ts | 3 + .../solution/solution.repository.ts | 26 + .../program/solution/solution.component.html | 74 + .../program/solution/solution.component.scss | 121 + .../solution/solution.component.spec.ts | 136 + .../program/solution/solution.component.ts | 139 + .../app/ui/routes/program/detail.routes.ts | 15 + .../detail-program-info.service.spec.ts | 72 + .../program/detail-program-info.service.ts | 44 +- tools/generate-solution-contract.py | 69 + 26 files changed, 16855 insertions(+), 2 deletions(-) create mode 100644 docs/contracts/program-solutions-v1/README.md create mode 100644 docs/contracts/program-solutions-v1/examples.json create mode 100644 docs/contracts/program-solutions-v1/openapi.json create mode 100644 docs/product/program-solutions-fe01.md create mode 100644 docs/product/program-solutions-fe01/team-desktop.png create mode 100644 docs/product/program-solutions-fe01/team-mobile.png create mode 100644 projects/social_platform/src/app/api/solution/solution-contract.spec.ts create mode 100644 projects/social_platform/src/app/api/solution/solution-error.ts create mode 100644 projects/social_platform/src/app/api/solution/solution-navigation.ts create mode 100644 projects/social_platform/src/app/api/solution/use-cases/solution.use-cases.ts create mode 100644 projects/social_platform/src/app/domain/solution/ports/solution.repository.port.ts create mode 100644 projects/social_platform/src/app/domain/solution/solution-contract.fixture.ts create mode 100644 projects/social_platform/src/app/domain/solution/solution-contract.model.ts create mode 100644 projects/social_platform/src/app/infrastructure/adapters/solution/solution-http.adapter.ts create mode 100644 projects/social_platform/src/app/infrastructure/repository/solution/solution.repository.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution.component.html create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution.component.scss create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/program/solution/solution.component.ts create mode 100644 tools/generate-solution-contract.py 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/product/program-solutions-fe01.md b/docs/product/program-solutions-fe01.md new file mode 100644 index 000000000..a1023c5a6 --- /dev/null +++ b/docs/product/program-solutions-fe01.md @@ -0,0 +1,44 @@ + + +# 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` 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: **40 tests PASS**: + +```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 +``` + +Проверены 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 остаются. Scoped ESLint всех новых/измененных TS и scoped Stylelint нового SCSS PASS. Массовый autofix и изменение package/lock не выполнялись. + +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.png), [Mobile](program-solutions-fe01/team-mobile.png). + +| 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: полный frontend suite, screen reader, четыре роли в браузере, настоящие загрузки/приглашения/сдача; последние относятся к FE03–FE08 и QA01. Негативный доступ проверен server-side в BE01 и в тестах ошибки UI. + +Внедрение: сначала BE01 с миграциями и default legacy_project, затем FE01. Новый режим для реальных участников не включать до REL01. Merge и deployment выполняет пользователь. diff --git a/docs/product/program-solutions-fe01/team-desktop.png b/docs/product/program-solutions-fe01/team-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..8184617b6eb7a6743689886c60d7860edefa7c60 GIT binary patch literal 28840 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 literal 0 HcmV?d00001 diff --git a/docs/product/program-solutions-fe01/team-mobile.png b/docs/product/program-solutions-fe01/team-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..88157b65199656abdd6a3c2550cd7bcdf9d08099 GIT binary patch literal 15833 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 literal 0 HcmV?d00001 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/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/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") From 2599c69d0d6acecc7cea5675f6366808ce82a8cc Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 16:56:13 +0300 Subject: [PATCH 2/4] docs: record FE01 verification on current dev --- docs/product/program-solutions-fe01.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/product/program-solutions-fe01.md b/docs/product/program-solutions-fe01.md index a1023c5a6..c1c1c21de 100644 --- a/docs/product/program-solutions-fe01.md +++ b/docs/product/program-solutions-fe01.md @@ -12,14 +12,14 @@ ## Verification и review от 24 октября 2026 -Checked by source: diff от `origin/dev` 5e22c54c. Доменные типы и независимые сценарии сгенерированы из неизмененного C00 1.0.0. Генератор не зависит от сети: `python tools/generate-solution-contract.py`, затем targeted Prettier. Полные определения приглашений/форм/команд готовы для следующих этапов; FE01 вызывает только контекст, создание и чтение. Adapter списка подготовлен. Project/Team ID в новом создании отсутствуют. +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: **40 tests PASS**: +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 +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 и переключение вкладки без повторной загрузки. From dfcc3a881ddc556398a3e96ac6971c37c1032459 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 17:08:10 +0300 Subject: [PATCH 3/4] style: fix existing formatting that blocks dev checks --- docs/core/guards-models.md | 8 ++++---- docs/modules/auth.md | 4 +--- docs/social-platform/architecture.md | 3 +-- docs/social-platform/shared.md | 3 +-- .../profile/facades/edit/profile-form.service.ts | 3 +-- .../detail/program-analytics-drilldown.service.ts | 7 +------ .../src/app/domain/invite/results/invite.result.ts | 3 +-- .../src/app/domain/other/notification.model.ts | 4 +--- .../app/domain/program/program-analytics.model.ts | 5 +---- .../program/program-attention-actions.model.ts | 4 +--- .../app/domain/program/program-management.model.ts | 14 ++------------ .../domain/program/program-role-widget.model.ts | 13 ++----------- .../src/app/domain/project/step.model.ts | 6 +----- .../src/app/domain/shared/result.type.ts | 3 +-- 14 files changed, 19 insertions(+), 61 deletions(-) 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/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/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/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 { From 9337546dbacbd3583e395fb2234e0271b9ac9c99 Mon Sep 17 00:00:00 2001 From: DarKylX Date: Sat, 24 Oct 2026 17:10:09 +0300 Subject: [PATCH 4/4] docs: record full FE01 verification and browser evidence --- docs/product/program-solutions-fe01.md | 10 +++++++--- .../{team-desktop.png => team-desktop.jpg} | Bin .../{team-mobile.png => team-mobile.jpg} | Bin 3 files changed, 7 insertions(+), 3 deletions(-) rename docs/product/program-solutions-fe01/{team-desktop.png => team-desktop.jpg} (100%) rename docs/product/program-solutions-fe01/{team-mobile.png => team-mobile.jpg} (100%) diff --git a/docs/product/program-solutions-fe01.md b/docs/product/program-solutions-fe01.md index c1c1c21de..e370aef41 100644 --- a/docs/product/program-solutions-fe01.md +++ b/docs/product/program-solutions-fe01.md @@ -24,11 +24,15 @@ npm run test:ci -- projects/social_platform/src/app/api/solution/solution-contra Проверены 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 остаются. Scoped ESLint всех новых/измененных TS и scoped Stylelint нового SCSS PASS. Массовый autofix и изменение package/lock не выполнялись. +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.png), [Mobile](program-solutions-fe01/team-mobile.png). +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 | | ----------------------------------- | ------------------------------------------------------------------------------------------ | ----------------- | @@ -39,6 +43,6 @@ Desktop 1280×720 и mobile 390×844 проверены визуально. На UI review: иерархия и два раздела согласованного D00 v3; нет конкурирующих или фиктивных изменяющих действий. Существующие Mont, цвета, Button foundation и кабинет; строки с разделителями, без дополнительных карточек. Данные могут переноситься по строкам, mobile порядок чтения совпадает с DOM. Полные формы и invitation modal не входят в FE01 и не считаются реализованными этим экраном. -Review: исправлена нормализация meta ошибок под те же camelCase моки. BLOCKER/MAJOR/MINOR в scope не осталось. Not checked: полный frontend suite, screen reader, четыре роли в браузере, настоящие загрузки/приглашения/сдача; последние относятся к FE03–FE08 и QA01. Негативный доступ проверен server-side в BE01 и в тестах ошибки UI. +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.png b/docs/product/program-solutions-fe01/team-desktop.jpg similarity index 100% rename from docs/product/program-solutions-fe01/team-desktop.png rename to docs/product/program-solutions-fe01/team-desktop.jpg diff --git a/docs/product/program-solutions-fe01/team-mobile.png b/docs/product/program-solutions-fe01/team-mobile.jpg similarity index 100% rename from docs/product/program-solutions-fe01/team-mobile.png rename to docs/product/program-solutions-fe01/team-mobile.jpg