partner_programs.services.application_team — транзакционный domain service
для индивидуальных и командных Application. Он отделяет бизнес-правила от
DRF views и не зависит от HTTP response.
Service предоставляет четыре публичные операции:
create_or_get_application();change_application_participation_mode();validate_team_invariants();submit_application().
Все ожидаемые отказы наследуются от ApplicationTeamServiceError и содержат
стабильные code, detail и field. Views преобразуют их в DRF
ValidationError, но сам service не импортирует DRF.
Основные коды: registration_required, application_deadline_passed,
participation_mode_not_allowed, participation_mode_undecided,
active_application_conflict, team_required, team_not_allowed,
team_has_other_members, captain_member_missing, captain_mismatch,
team_size_invalid, team_member_registration_missing и
application_not_editable.
create_or_get_application() выполняет в одной транзакции:
- блокирует строку
PartnerProgram; - проверяет
PartnerProgramUserProfileвладельца; - проверяет
datetime_application_endsбез fallback на legacy-дедлайны; - валидирует формат по Program policy;
- ищет активную собственную Application и accepted-членство в другой Team;
- создает individual/undecided draft либо team draft;
- для team атомарно создает
Teamи accepted captainTeamMember.
Повторный совместимый запрос возвращает существующую Application с
created=False. Отличающийся формат, form data, Project или Team name не
перезаписывает существующий draft скрытым образом и дает конфликт.
undecided допустим для draft и не создает Team. Старый API-запрос без
participation_mode преобразуется view в individual, поэтому существующий
frontend сохраняет прежний формат.
change_application_participation_mode() доступен только владельцу draft до
application deadline:
undecided → individualменяет только поле;undecided/individual → teamсоздает Team и капитана;individual → undecidedразрешен, пока Team отсутствует;team → individualудаляет Team только при наличии единственной записи accepted-капитана;team → undecidedзапрещен, чтобы не удалять состав неявно;- повторный
team → teamможет изменитьteam_name.
PATCH Application вызывает эту операцию до сохранения остальных полей в общей
транзакции. Serializer намеренно не записывает participation_mode и
team_name напрямую.
Активными остаются draft, submitted и approved. Для пользователя service
учитывает обе роли:
Application.user;- accepted
TeamMemberсвязанной активной Application той же Program.
Текущая Application исключается при change/submit. Терминальные Application и членство в другой Program не блокируют новую заявку.
Строка Program блокируется через select_for_update() как общая точка
сериализации, после чего блокируются найденные Application/TeamMember. Это
закрывает гонки между операциями, которые проходят через service. Существующая
partial unique constraint дополнительно защищает две собственные активные
Application.
Cross-table invariant нельзя выразить обычным UniqueConstraint. Прямые записи через admin/model и будущий Team API должны использовать тот же service или отдельную транзакционную операцию принятия участника.
validate_team_invariants() ничего не изменяет и проверяет:
- Team существует только для
participation_mode=team; Team.applicationи капитан согласованы с Application;- есть ровно один accepted captain member;
- все accepted-участники имеют Registration этой Program;
- accepted-состав находится между
team_min_sizeиteam_max_size.
Капитан входит в размер команды. invited, declined, removed и left
сохраняют историю, но не считаются участниками при submit.
submit_application() сохраняет текущий owner/staff access contract. Staff не
обходит Registration, Program policy, deadline или Team invariant.
Для draft операция проверяет Registration владельца, application deadline,
финальный формат, cross-table конфликты и Team invariant, затем атомарно
устанавливает submitted и submitted_at.
Повторный submit уже отправленной Application идемпотентен: он возвращает
текущую запись, не меняет submitted_at и не падает из-за дедлайна, который
истек после первой отправки.
Существующие routes и throttle scope не изменены:
POST /programs/<id>/applications/принимает необязательныеparticipation_modeи write-onlyteam_name;- отсутствие mode означает
individual; - response дополнен
participation_mode, но не раскрывает TeamMember; PATCH /applications/<id>/проводит mode/team name через service;POST /applications/<id>/submit/вызывает транзакционный submit service.
Публичного Team CRUD, управления участниками и TeamInvite в этом изменении нет.
Production-база с row-level locking сериализует service-операции одной Program.
SQLite не реализует полноценный select_for_update, поэтому автоматический
тест проверяет устойчивую последовательную конфликтную операцию без threading.
Service пока не управляет приглашениями/принятием участников и не блокирует прямое редактирование моделей через admin. Нет Team permissions для обычных members, передачи капитанства, returned Application и organizer review.