Перейти к основному содержимому

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 typepin.identity.agent-membership-changed
Topicpin.identity.agent-membership-changed.v1
Partitions / key3 (агентов ≤ 20k, ADR-0056) / key = AgentUserId (порядок per-agent)
ContractVersion1
Produceridentity: 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Описание
MembershipIdGuidДаAgentMembershipEntity.IdImmutableИдентификатор записи членства (= AggregateId).
AgentUserIdGuidДаAgentMembershipEntity.AgentUserIdImmutableАгент (= Kafka key).
CompanyIdGuidДаAgentMembershipEntity.CompanyIdImmutableАгентство.
Statusstring (enum AgentMembershipStatus)ДаAgentMembershipEntity.StatusAdditive only (новые значения)Active (вступил) / Left (вышел).
JoinedAtDateTimeOffsetДаAgentMembershipEntity.JoinedAtImmutableМомент вступления.
LeftAtDateTimeOffset?НетAgentMembershipEntity.LeftAtAdditive onlyМомент выхода (при Status = Left); null для Active.
Reasonstring (enum LeaveReason)Нетвычисляется операцией выходаAdditive onlyОснование выхода (при Status = Left): SelfLeave (агент ушёл сам, endpoint leave) / ExcludedByAgency (руководитель/админ исключил, endpoint terminate-agent-membership). null для Active.
EffectiveAtDateTimeOffset?Нетвычисляется операцией выходаAdditive onlyМомент, с которого членство недействительно (при Status = Left); для отложенного увольнения ≠ времени события и ≠ LeftAt. null для Active.
InvitationIdGuid?НетAgentMembershipEntity.InvitationIdImmutableПриглашение-источник вступления; null при вступлении без приглашения.
VersionlongДаAgentMembershipEntity.VersionImmutable 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, роль agentPlatform); 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-seen AgentUserId, состояние досходится.
  • Порядок 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/EffectiveAt additive — старые консюмеры их игнорируют, ContractVersion не меняется (ADR-0072 п.2).
  • Порядок per-agent гарантирован key-ом (AgentUserId); потребители держат Version-guard.

Безопасность

ПолеКласс данныхПравило
MembershipId, AgentUserId, CompanyId, Status, Reason, EffectiveAt, JoinedAt, LeftAt, InvitationId, VersionInternalinclude

Событие НЕ содержит PII (телефон/ФИО/email), secrets, токенов, платёжных данных — только id-ссылки и метки времени.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Сущность: 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-каскад).