# 14. Отчёт этапа 4: повышение и жизненный цикл **Дата:** 2026-10-27. **Статус:** этап 4 выполнен; этап 5 (UX и контент курса) — следующий. ## 1. Повышение ### Алгоритм План строится по активным группам и студентам: - Активная группа с курсом меньше предела (`БО` — 4, `МО` — 2) переводится на следующий курс: меняются `course` и имя по шаблону `поток-курсномерстепень`. - Активная группа на последнем курсе получает статус `GRADUATED`. - Активные студенты выпускаемой группы получают `GRADUATED`. - Студенты в академическом отпуске переводятся в параллель следующего набора того же курса: `enrollmentYear = год повышения - (курс - 1)`. Если такой группы нет, она создаётся в плане; при применении — в БД. - Студенты в статусах `EXPELLED`, `GRADUATED`, `ARCHIVED` в план не попадают. - Группы без курса/степени/потока/номера и студенты без зачисления пропускаются с предупреждением; план при этом не блокируется. Порядок применения: сначала группы (от старших курсов), затем студенты. Каждый элемент хранит `before`/`after` в JSONB. Перед применением все `before` сверяются с текущим состоянием; при расхождении вся транзакция отменяется с `409`, частичного применения нет. ### API Все эндпоинты требуют роль `ADMIN`: | Метод | Путь | Назначение | |---|---|---| | `GET` | `/api/v1/promotion` | список запусков | | `GET` | `/api/v1/promotion/{id}` | план/результат с элементами | | `POST` | `/api/v1/promotion/dry-run` | построить план; тело `{ "year": 2026 }` необязательно | | `POST` | `/api/v1/promotion/{id}/apply` | применить запланированный dry-run | | `POST` | `/api/v1/promotion/{id}/rollback` | откатить завершённое применение | Правила: - `year` — 2000–2100; без года берётся текущий. - Повторный dry-run за год возвращает существующий план в статусе `PLANNED`. - Применять можно только `DRY_RUN` в статусе `PLANNED`. Запуск переходит в `APPLY/RUNNING`, затем `COMPLETED`. - Один успешный `APPLY` на год защищён частичным уникальным индексом `promotion_runs(year) WHERE mode = 'APPLY' AND status IN ('RUNNING','COMPLETED')`; конкурентное применение даёт `409`. - Повторное применение уже завершённого запуска идемпотентно возвращает результат и не создаёт новых записей истории. - Откат доступен только для `COMPLETED`. Перед откатом проверяется, что текущее состояние совпадает с `after`; ручные изменения дают `409`. Восстанавливаются курс, имя и статус групп, статусы/группы студентов, удаляются созданные планом пустые целевые группы. История не стирается: добавляются записи `RETURN` с причиной «Откат повышения». ### Планировщик - `PromotionScheduler` строит dry-run 31 августа в `00:00` по `Europe/Moscow` (`cron = "0 0 0 31 8 *"`). - При старте сервиса выполняется догон: если сегодня 31 августа или позже и за год ещё нет запусков, dry-run строится сразу. - Догон идемпотентен: advisory lock не даёт двум инстансам создать два плана. - Планировщик включён по умолчанию; отключается `lms.promotion.scheduler-enabled=false` (используется в тестах). - Автоматически применяется только dry-run. Применение — всегда вручную админом после проверки плана. ## 2. Жизненный цикл студента Эндпоинты требуют роль `ADMIN`: | Метод | Путь | Тело | Результат | |---|---|---|---| | `POST` | `/api/v1/students/{id}/academic-leave` | `reason?` | перевод в параллель следующего набора, статус `ACADEMIC_LEAVE` | | `POST` | `/api/v1/students/{id}/return` | `groupId`, `reason?` | возврат в указанную активную группу, статус `ACTIVE` | | `POST` | `/api/v1/students/{id}/expel` | `reason?` | статус `EXPELLED`, зачисление сохраняется | | `POST` | `/api/v1/students/{id}/archive` | — | статус студента `ARCHIVED`, аккаунт `ARCHIVED` | | `POST` | `/api/v1/students/{id}/restore` | `groupId`, `reason?` | восстановление из архива/отчисления в активную группу | Правила: - Академотпуск доступен только `ACTIVE`; группа должна иметь заполненные поток, курс, номер и степень. Целевая параллель ищется по когорте и создаётся при отсутствии. - Возврат доступен только из `ACADEMIC_LEAVE`; целевая группа должна быть активной. - Отчислить нельзя уже отчисленного, выпущенного или архивного студента. Зачисление остаётся для истории, вход закрывает статус. - Архив закрывает и студента, и аккаунт (`archived_at`); зачисление сохраняется. Восстановление возвращает аккаунт в `ACTIVE` и переводит студента в выбранную группу. - Каждое действие пишет `membership_history` с типом, причиной и актором. ## 3. Архив группы, удаление и вход - `POST /api/v1/groups/{id}/archive` переводит группу в `ARCHIVED`, не меняя статусы студентов. - Вход студента блокируется, если его текущая группа `ARCHIVED` или `GRADUATED` (`AuthService.ensureGroupOpen`). Студент без группы может войти, если его собственный статус и аккаунт активны. - Удаление пользователя требует параметр `confirm`, совпадающий с логином или ФИО студента (trim, точное совпадение). Иначе — `400`, данные не удаляются. В админской форме появилось обязательное поле подтверждения. - «Убрать из группы» осталось отдельным действием: оно удаляет зачисление, но не является отчислением. Для отчисления с историей используется «Отчислить». ## 4. UI - `/admin/promotion` — список запусков, построение dry-run за выбранный год. - `/admin/promotion/{id}` — сводка, предупреждения, таблица элементов «было/стало», кнопки «Применить план» и «Откатить» по статусу. - На странице группы: - «Академ» — для активного студента; - «В архив» — для активного студента; - «Отчислить» — для активного студента; - «Вернуть в группу» — для `ACADEMIC_LEAVE`; - «Восстановить в группу» — для `ARCHIVED`/`EXPELLED`; - «Убрать из группы» — отдельно от отчисления. - «В архив» для группы — на странице группы; кнопка скрыта для уже архивной группы. - В навигацию админа добавлен пункт «Повышение». - Шаблоны и представления покрыты тестами, включая подписи статусов, счётчики и состояния before/after. ## 5. Тесты Добавлены наборы: - `PromotionServiceIntegrationTest` (6): dry-run не меняет данные, применение переводит группы/выпускает студентов/двигает академ, повторное применение идемпотентно, откат восстанавливает данные и удаляет созданную группу, конфликт ручных изменений, guard даты и дедупликация планировщика. - `StudentLifecycleIntegrationTest` (11): академотпуск с созданием и переиспользованием параллели, возврат, отчисление с блокировкой входа, вход из архивной группы, активной группы и без группы, восстановление после отчисления и архива, архив группы без смены статусов студентов, подтверждение удаления. - `AdminStage4TemplateTest` (5) и `PromotionDtoTest` (2): журнал повышения, кнопки apply/rollback, lifecycle-кнопки по статусам, поле подтверждения удаления, JSON-контракты. - `AuthServiceLoginBackoffTest` обновлён под новую зависимость проверки группы. По результатам последнего прогона (2026-10-27) перечисленные наборы прошли с `0 failures / 0 errors`. ## 6. Известные пробелы и forward-only - **Только ручное применение.** Планировщик создаёт только dry-run; автоматического применения и уведомлений нет. - **Один план на год.** Повторный dry-run за год возвращает уже сохранённый `PLANNED`-план; пересборки или удаления плана в UI нет. После завершённого применения новый dry-run за тот же год запрещён до отката. - **Откат целиком.** Поэлементного отката нет; при конфликте откатывается весь запуск. После ручных изменений сначала нужно вернуть данные к состоянию `after`. - **История не удаляется.** Откат добавляет `RETURN`, а не стирает исходные записи; журнал остаётся аудиторским следом. - **Групповой архив односторонний.** Отдельного endpoint/UI для возврата группы из архива нет; студенты архивной группы не входят. - **Lifecycle из ведомости.** Возврат и восстановление в UI выполняются в текущую группу; выбор другой группы доступен только через API. Массовых операций по студентам нет. - **JWT и блокировка.** Вход закрывается при следующей аутентификации; уже выданный JWT действует до истечения (до 30 дней). Это остаточный риск, зафиксированный в плане; аудит и hardening — этап 6. - **Нет новых миграций.** Используются таблицы `promotion_runs`, `promotion_run_items`, `membership_history`, `accounts.status` из миграций `V7`–`V9` этапа 1. JSONB пишется через `JdbcTemplate` из-за несовместимости Jackson 2/3 в Hibernate.