|
| 1 | +# Application Team Model |
| 2 | + |
| 3 | +## Назначение |
| 4 | + |
| 5 | +Базовый слой командных заявок разделяет три понятия: |
| 6 | + |
| 7 | +- `Application` — заявка на одну партнерскую программу; |
| 8 | +- `Team` — команда, собранная только для этой заявки; |
| 9 | +- `TeamMember` — членство пользователя в команде заявки. |
| 10 | + |
| 11 | +Публичного Team API и атомарного service создания команды пока нет. Модели |
| 12 | +подготавливают структуру данных для следующих PR, не меняя текущий |
| 13 | +индивидуальный API flow. |
| 14 | + |
| 15 | +## Participation mode |
| 16 | + |
| 17 | +В `Application` добавлено поле `participation_mode`: |
| 18 | + |
| 19 | +| Значение | Смысл | |
| 20 | +|---|---| |
| 21 | +| `undecided` | Формат участия еще не выбран | |
| 22 | +| `individual` | Индивидуальная заявка | |
| 23 | +| `team` | Командная заявка | |
| 24 | + |
| 25 | +Runtime default временно равен `individual`: существующие API и frontend не |
| 26 | +передают новое поле, поэтому все текущие и исторические Application продолжают |
| 27 | +работать как индивидуальные. `undecided` должен стать default одновременно с |
| 28 | +wizard и запретом submit заявки без выбранного формата. |
| 29 | + |
| 30 | +Миграция `0020_team_application_participation_mode_teammember_and_more` |
| 31 | +добавляет поле с default `individual`; Django применяет это значение ко всем |
| 32 | +существующим строкам Application. |
| 33 | + |
| 34 | +## Team |
| 35 | + |
| 36 | +`Team` содержит: |
| 37 | + |
| 38 | +- one-to-one `application` с `related_name="team"`; |
| 39 | +- необязательное на стадии черновика `name` длиной до 255 символов; |
| 40 | +- `captain`; |
| 41 | +- `created_at` и `updated_at`. |
| 42 | + |
| 43 | +Team относится к Application, потому что состав команды может различаться в |
| 44 | +разных активностях даже при использовании одного Project. Существующий |
| 45 | +`projects.Collaborator` описывает постоянного участника Project и намеренно не |
| 46 | +переиспользуется как TeamMember. |
| 47 | + |
| 48 | +В первом MVP `Team.captain` обязан совпадать с `Application.user`. |
| 49 | +`Team.status`, invite code, token и настройки размера команды не добавлены: |
| 50 | +редактируемость будущего Team flow должна выводиться из `Application.status`. |
| 51 | + |
| 52 | +## TeamMember |
| 53 | + |
| 54 | +`TeamMember` содержит: |
| 55 | + |
| 56 | +- `team` и `user`; |
| 57 | +- роль `captain` или `member`; |
| 58 | +- статус `invited`, `accepted`, `declined`, `removed` или `left`; |
| 59 | +- nullable `invited_by`; |
| 60 | +- nullable `joined_at`; |
| 61 | +- `created_at` и `updated_at`. |
| 62 | + |
| 63 | +Для первоначального captain member `invited_by` остается `null`: капитан не |
| 64 | +принимает собственное приглашение, а создается как владелец команды. Статус |
| 65 | +`invited` для обычных участников пока является только модельной заготовкой; |
| 66 | +механизм TeamInvite в этом PR отсутствует. |
| 67 | + |
| 68 | +При первом сохранении статуса `accepted` модель автоматически заполняет |
| 69 | +`joined_at`, если дата не передана. При переходе в `removed` или `left` дата не |
| 70 | +очищается и сохраняет момент фактического присоединения. |
| 71 | + |
| 72 | +## Database constraints |
| 73 | + |
| 74 | +На уровне БД обеспечены: |
| 75 | + |
| 76 | +- одна Team на Application через `OneToOneField`; |
| 77 | +- уникальность `TeamMember(team, user)`; |
| 78 | +- не более одного `accepted` TeamMember с ролью `captain` в Team; |
| 79 | +- check: роль `captain` допустима только со статусом `accepted`. |
| 80 | + |
| 81 | +Простым constraint одной таблицы нельзя надежно обеспечить: |
| 82 | + |
| 83 | +- участие пользователя в нескольких Team разных Application одной Program; |
| 84 | +- конфликт TeamMember с индивидуальной Application той же Program; |
| 85 | +- Registration всех членов команды; |
| 86 | +- размер команды; |
| 87 | +- блокировку состава после submit. |
| 88 | + |
| 89 | +Эти правила проходят через Application, Team и TeamMember, поэтому требуют |
| 90 | +отдельного транзакционного domain service с блокировкой строк. |
| 91 | + |
| 92 | +## Model validation |
| 93 | + |
| 94 | +`Team.clean()` проверяет: |
| 95 | + |
| 96 | +- `Application.participation_mode == team`; |
| 97 | +- `Team.captain == Application.user`. |
| 98 | + |
| 99 | +Это запрещает Team для `individual` и `undecided` Application. |
| 100 | + |
| 101 | +`TeamMember.clean()` проверяет: |
| 102 | + |
| 103 | +- captain member имеет статус `accepted`; |
| 104 | +- его `user` совпадает с `Team.captain`; |
| 105 | +- для первого принятия заполнен `joined_at`. |
| 106 | + |
| 107 | +Наличие captain TeamMember намеренно не проверяется при первом `Team.save()`. |
| 108 | +Такая проверка создала бы цикл: Team должна быть сохранена до создания |
| 109 | +TeamMember, а TeamMember уже требует сохраненную Team. |
| 110 | + |
| 111 | +## Текущий технический порядок создания |
| 112 | + |
| 113 | +До появления публичного API техническая последовательность выглядит так: |
| 114 | + |
| 115 | +1. создать или обновить Application с `participation_mode=team`; |
| 116 | +2. создать Team с captain, совпадающим с `Application.user`; |
| 117 | +3. создать accepted TeamMember с `role=captain` и тем же пользователем. |
| 118 | + |
| 119 | +Клиентам нельзя использовать эту последовательность напрямую. Сейчас нет |
| 120 | +публичных serializers/views/URLs и транзакционного service, который откатит |
| 121 | +частично созданную команду. |
| 122 | + |
| 123 | +## Вне текущего MVP |
| 124 | + |
| 125 | +Следующие PR должны добавить: |
| 126 | + |
| 127 | +- атомарный creation/invariant service; |
| 128 | +- проверку Registration и одной активной заявки на Program; |
| 129 | +- Team permissions и публичный Team API; |
| 130 | +- блокировку состава по Application status; |
| 131 | +- TeamInvite, accept/decline/revoke/expire; |
| 132 | +- email и внутренние уведомления; |
| 133 | +- program policy по формату и размеру команды; |
| 134 | +- frontend wizard и вкладку команды. |
| 135 | + |
| 136 | +Project, `projects.Collaborator`, legacy `invites.Invite`, Submission и |
| 137 | +существующие Application endpoints этим слоем данных не изменяются. |
0 commit comments