Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent)
Версионируемый контракт интеграционного события «членство агента в агентстве изменилось». Consumer-документы ссылаются сюда, не переописывая payload.
Назначение
Фиксирует переходы AgentMembershipEntity (ADR-0061): вступление агента в агентство (Active) и выход
(Left). Публичное: crm пересчитывает ростер агентов и user-centric метрики, rewards определяет
основания комиссий текущего периода, notifications уведомляет руководителя и агента. Историческая
привязка сделок/лидов/фиксаций не пересчитывается — снапшоты AgencyCompanyId/AgentUserId в crm
иммутабельны, back-fill запрещён (ADR-0061 п.4). Дом полей — entity: AgentMembershipEntity.
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.identity.agent-membership-changed |
| Topic | pin.identity.agent-membership-changed.v1 |
| Partitions / key | 3 (агентов ≤ 20k, ADR-0056) / key = AgentUserId (порядок per-agent) |
| ContractVersion | 1 |
| Producer | identity: accept-invitation / become-agent (join), leave-agency / become-investor (self-leave), terminate-agent-membership (exclude) |
| Статус | draft |
Событие публикуется атомарно с переходом членства: до фиксации перехода событие не отправляется,
после — доставляется гарантированно (at-least-once). Key = AgentUserId (а не MembershipId):
гарантирует порядок join/leave по агенту, чтобы потребители видели единственное активное членство
консистентно.
Payload AgentMembershipChangedKafkaEvent
Дом полей — entity: AgentMembershipEntity; здесь — только реально
присутствующее в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
MembershipId | Guid | Да | AgentMembershipEntity.Id | Immutable | Идентификатор записи членства (= AggregateId). |
AgentUserId | Guid | Да | AgentMembershipEntity.AgentUserId | Immutable | Агент (= Kafka key). |
CompanyId | Guid | Да | AgentMembershipEntity.CompanyId | Immutable | Агентство. |
Status | string (enum AgentMembershipStatus) | Да | AgentMembershipEntity.Status | Additive only (новые значения) | Active (вступил) / Left (вышел). |
JoinedAt | DateTimeOffset | Да | AgentMembershipEntity.JoinedAt | Immutable | Момент вступления. |
LeftAt | DateTimeOffset? | Нет | AgentMembershipEntity.LeftAt | Additive only | Момент выхода (при Status = Left); null для Active. |
Reason | string (enum LeaveReason) | Нет | вычисляется операцией выхода | Additive only | Основание выхода (при Status = Left): SelfLeave (агент ушёл сам, endpoint leave) / ExcludedByAgency (руководитель/админ исключил, endpoint terminate-agent-membership). null для Active. |
EffectiveAt | DateTimeOffset? | Нет | вычисляется операцией выхода | Additive only | Момент, с которого членство недействительно (при Status = Left); для отложенного увольнения ≠ времени события и ≠ LeftAt. null для Active. |
InvitationId | Guid? | Нет | AgentMembershipEntity.InvitationId | Immutable | Приглашение-источник вступления; null при вступлении без приглашения. |
Version | long | Да | AgentMembershipEntity.Version | Immutable semantics | Доменная версия; монотонный guard у потребителей. |
Вложенных моделей нет. Событие с Status = Active, Version = 1 ⇔ открытие нового членства.
Reason/EffectiveAt заполняются только при Status = Left (ADR-0072): различают добровольный уход и
принудительное исключение и фиксируют момент вступления выхода в силу. Нового значения Status под
увольнение не вводится — факт исключения переносится причиной, а не отдельным состоянием членства.
Enum LeaveReason
| Значение | Смысл | Источник (операция) |
|---|---|---|
SelfLeave | Агент вышел из агентства по собственной инициативе | leave-agency / become-investor |
ExcludedByAgency | Руководитель или админ агентства исключил агента | terminate-agent-membership (permission identity.users.manage) |
Additive-множество: новые значения добавляются без bump ContractVersion. Unknown LeaveReason у потребителя —
мягкая деградация: сохранить строку, warning, трактовать как «выход состоялся» (сам факт Status = Left
первичен), не выводить причину-специфичную логику.
Когда публикуется
| Условие | Переход состояния | Публикация |
|---|---|---|
| Вступление в агентство (accept-invitation / become-agent) | [*] → Active | атомарно с созданием членства; Reason/EffectiveAt = null |
| Добровольный выход (leave-agency; агент остаётся независимым) | Active → Left | та же (+ зеркало UserEntity.CompanyId → null, роль agent → Platform); Reason = SelfLeave |
| Возврат к инвестору (become-investor) | Active → Left | та же (+ снятие роли agent); Reason = SelfLeave |
| Принудительное исключение (terminate-agent-membership; актор — руководитель/админ агентства) | Active → Left | та же; Reason = ExcludedByAgency, EffectiveAt = момент вступления в силу (возможен отложенный) |
Независимый агент без агентства membership-события не порождает (нет записи членства) — его переход роли идёт через user-access-changed.
Потребители (consumers)
| Consumer-контекст | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
crm | Пересчёт ростера агентов агентства и user-centric метрик (агрегация по AgentUserId); при Status = Left (любой Reason) — offboarding-каскад: массовый soft-remove участий агента (Executor/Responsible/CoAgent/Assistant/Lawyer) в процессах этого агентства, публикация deal-members-changed / pin.crm.lead-participants-changed.v1; иммутабельная атрибуция OriginAgencyCompanyId/OriginAgentUserId и исторические снапшоты сделок/лидов/фиксаций не трогаются (ADR-0072 п.4) | MessageId + Version-guard | < 1 мин |
rewards | Основания комиссий текущего периода и AgentPerformance (user-centric метрики агента): при Left — прекращение начислений агенту от имени компании и перевод ЛК-истории в read-only (прошлые earned/paid — по иммутабельным Origin-снапшотам, не пересчитываются) | то же | < 5 мин |
notifications | Уведомления: агенту («вы вступили» / «вы вышли» при SelfLeave / «вы исключены из агентства» при ExcludedByAgency), руководителю («агент вступил/покинул/исключён») | то же | < 5 мин |
Порядок и согласованность
- Два топика из одного изменения.
accept-invitation/become-agentфиксируют переход членства и публикуют атомарно это событие (Active) и, при приёме приглашения, ещё company-invitation-changed (Accepted) — в разных топиках. Межтопиковый порядок доставки не гарантирован: потребитель, которому нужны оба факта (crm — ростер агентов + воронка найма), обрабатывает их независимо и идемпотентно; итоговое состояние сходится (eventual, окно — секунды). - Новый агент (investor → agent). Это событие может прийти раньше, чем зеркало
CompanyIdи рольagentиз user-upserted/user-access-changed (отдельные топики). Потребитель не трактует временное отсутствие роли/реплики как ошибку — при отсутствии реплики агента членство применяется на first-seenAgentUserId, состояние досходится. - Порядок per-agent и идемпотентность. Внутри топика ключ
AgentUserIdдаёт строгий порядок join/leave по агенту; потребитель держитVersion-guard и отбрасывает событие с меньшей версией (устаревший переход). Повторная доставка того же перехода (at-least-once) — идемпотентный no-op поMessageId.
Версионирование и совместимость
ContractVersion = 1; при несоответствии версии события consumer логирует warning и продолжает.- Additive-only: новые optional-поля с default — без bump. Breaking —
ContractVersion = 2+ топикpin.identity.agent-membership-changed.v2+ dual-publish + sunset.v1. - Unknown enum
Status— fallback: сохранить строку без изменений, warning; при неизвестном статусе не считать членство активным (fail closed для основания комиссий). - Unknown enum
LeaveReason— мягкая деградация: сохранить строку, warning, трактовать как состоявшийся выход (первичен самStatus = Left), пропустить причину-специфичную логику (нотификации/отчётность). ПоляReason/EffectiveAtadditive — старые консюмеры их игнорируют,ContractVersionне меняется (ADR-0072 п.2). - Порядок per-agent гарантирован key-ом (
AgentUserId); потребители держатVersion-guard.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
MembershipId, AgentUserId, CompanyId, Status, Reason, EffectiveAt, JoinedAt, LeftAt, InvitationId, Version | Internal | include |
Событие НЕ содержит PII (телефон/ФИО/email), secrets, токенов, платёжных данных — только id-ссылки и метки времени.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: AgentMembershipEntity, Entity / Projection: AgentPerformanceEntity
- API (6): Identity Agent API — реестр методов и общие правила области, POST /api/v1/companies/{companyId}/agents/{agentUserId}/terminate — Исключение агента из агентства, POST /api/v1/me/agency-membership/leave — Выход агента из агентства, POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство, POST /api/v1/me/become-agent — Инвестор становится агентом, POST /api/v1/me/become-investor — Агент возвращается к роли инвестора
- Use Cases (6): UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки, UC-IDN-009. Приглашение агента по телефону и его принятие, UC-IDN-010. Агент выходит из агентства, сохраняя метрики, UC-IDN-011. Инвестор становится агентом, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (1): Event Contract: pin.identity.company-invitation-changed (CompanyInvitationChangedKafkaEvent)
- Консюмеры (2): Consumer: CRM Agent Offboarding (CrmAgentOffboardingConsumer), Consumer: Identity Agent Membership Changed
- scenarios (1): UC-OVR-008. Агент выходит из агентства, сохраняя метрики
- index.md (2): Identity Service (Pin.Identity), Rewards Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность: agent-membership.md; соседнее событие: company-invitation-changed.md; переход прав — user-access-changed.md.
- Источники (операции): accept-invitation.md,
become-agent.md, leave-agency.md,
become-investor.md; принудительное исключение —
terminate-agent-membership(identity.users.manage, актор руководитель/админ агентства). - Каскад увольнения (reference only):
docs/06-services/crm/domain/deal.md,lead.md,fixation-request.md; события состава —deal-members-changed,pin.crm.lead-participants-changed.v1; статистика —crm/api/agent/get-statistics.md. - Решение: ADR-0061,
ADR-0070
(
SingleActivePerCompany), ADR-0072 (self-leave vs terminate,Reason/EffectiveAt, offboarding-каскад).