Event Contract: pin.referral.bond-closed (ReferralBondClosedKafkaEvent)
Версионируемый контракт интеграционного Kafka-события «реферальная связь закрыта». Consumer-документы
ссылаются сюда, не переописывая payload. Дом полей агрегата —
referral-bond.md; здесь перечислено только то, что реально входит в
событие. Пара к bond-created.md — второе produced-событие топика
pin.referral.bonds.v1.
Назначение
Фиксирует закрытие действующей ReferralBondEntity admin-коррекцией:
переход Active → Replaced (перепривязка, rebind-bond.md, правило R6)
или Active → Revoked (отзыв, revoke-bond.md). Оба — терминальные
состояния (INV-REF-B6); автоматическая перепривязка отключена (решение № 3 README.md) —
связь закрывается только вручную через админ-панель (ADR-0053). Даёт downstream-контекстам сигнал
закрытия без синхронного запроса в referral — rewards прекращает создавать новые начисления по
BondId, уже созданные сохраняются (consumer —
referral-bond.md; решение № 7: referral владеет фактом
связи, rewards — деньгами).
Metadata
| Поле | Значение |
|---|---|
| Event type | pin.referral.bond-closed |
| C# type | ReferralBondClosedKafkaEvent |
| Topic | pin.referral.bonds.v1 (общий с ReferralBondCreatedKafkaEvent — общий key = строгий порядок created → closed per-приглашённый) |
| Partitions / key | 6 partitions (~1 млн связей на горизонте, ADR-0056) / key = ReferredUserId |
| ContractVersion | 1 |
| Producer | referral: admin-rebind (rebind-bond.md, R6 → Replaced), admin-revoke (revoke-bond.md → Revoked) — админ-панель (ADR-0053) |
| Статус | draft |
Метаданные события: идентификатор сообщения (Guid v7, дедупликация), время, версия (= ContractVersion),
AggregateId = BondId, AggregateType = "ReferralBond". Корреляция трейса и actor admin-коррекции
восстанавливаются из метаданных. Публикуется атомарно со сменой состояния агрегата.
Payload ReferralBondClosedKafkaEvent
Snapshot-поля ссылаются на дом ReferralBondEntity (раздел «Доменные и
интеграционные события», enum ReferralBondStatus); здесь — только то, что реально в событии.
| Поле | Тип | Обязательность | Источник (поле агрегата) | Compatibility | Описание |
|---|---|---|---|---|---|
BondId | Guid | Да | ReferralBondEntity.Id | Immutable | Идентификатор закрываемой связи (= AggregateId); rewards останавливает начисления по нему. |
ReferredUserId | Guid | Да | ReferralBondEntity.ReferredUserId | Immutable | Приглашённый пользователь («кого привели»); = Kafka key (упорядочивает created → closed per-приглашённый). |
Status | string (enum ReferralBondStatus) | Да | ReferralBondEntity.Status | Additive only (новые значения) | Терминальный статус закрытия: Replaced (admin-rebind) или Revoked (admin-revoke). Дом enum — referral-bond.md. |
ReplacedByBondId | Guid? | Да при Status = Replaced (иначе null) | ReferralBondEntity.ReplacedByBondId | Immutable | Связь-замена (цепочка admin-коррекций); заполнен только при Replaced. null при Revoked. |
RevokeReason | string? | Да при Status = Revoked (иначе null) | ReferralBondEntity.RevokeReason | Additive only | Причина отзыва без PII (фрод / ошибка привязки / претензия), ≤ 512; заполнен только при Revoked. Правило PII — раздел «Безопасность». |
Version | long | Да | ReferralBondEntity.Version | Immutable semantics | Монотонная доменная версия связи после закрытия; guard потребителей (per-BondId). |
Вложенных моделей нет — все поля скалярные. ФИО/телефон участников, суммы вознаграждений и slug ссылки
в payload не входят (PII-минимизация и границы: денег в referral нет — зона rewards).
Когда публикуется
| Условие | Переход состояния | Согласованность |
|---|---|---|
| Admin-rebind (rebind-bond.md, правило R6): закрытие старой связи одновременно с созданием замены | Active → Replaced (INV-REF-B4; ReplacedByBondId заполнен) | замена старой связи + создание новой + журнал Rebind + оба события (closed + created) — в одном согласованном изменении |
| Admin-revoke (revoke-bond.md): отзыв связи | Active → Revoked (INV-REF-B5; RevokeReason обязателен) | отзыв + closed-событие — в одном согласованном изменении |
Только переход из Active; Replaced/Revoked — терминальны (INV-REF-B6, обратных переходов нет).
Восстановление связи — это новая связь admin-коррекцией (отдельное bond-created.md).
При rebind старая и новая связь делят один key ReferredUserId, поэтому потребитель получает closed
(остановка старого BondId) и created (запуск нового BondId) строго упорядоченно.
Потребители (consumers)
Сверено с referral-bond.md («Потребители pin.referral.bonds.v1») и
service-boundaries.md.
| Consumer-контекст | Эффект (событие → что меняется) | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
rewards | Помечает реплику связи закрытой (Status) — новые начисления по BondId не создаются; уже созданные начисления сохраняются (не отменяются и не пересчитываются; отзыв по фроду — только ручная отмена оператором ПИН). Consumer — referral-bond.md | идентификатор сообщения + BondId + Version-guard (per-BondId) | < 5 мин |
notifications (опц.) | Внутренний аудит-канал admin-коррекций связей; пользовательских СМС на закрытие связи нет | идентификатор сообщения (повтор — no-op) | < 5 мин |
crm/messenger этот контракт не потребляют: карточка лида и контакт не отзываются событием
закрытия связи (их lifecycle — собственные контексты).
Окно согласованности: закрытие фиксируется в referral атомарно (переход состояния + событие),
остановка новых начислений в rewards — в пределах SLA (< 5 мин). В интервале между коммитом и
обработкой rewards может создать ещё одно начисление по старому BondId — оно, как и все ранее
созданные, сохраняется (автоматической отмены по закрытию связи нет; отзыв по фроду — только явная
ручная отмена оператором ПИН, регламент —
referral-bond.md), referral его не откатывает. При rebind потребитель
получает closed (старый BondId) и created (новый) строго упорядоченно (общий key ReferredUserId) —
реплика rewards не окажется одновременно с двумя активными связями одного приглашённого.
Гарантии, на которые может опереться потребитель:
- На один
BondIdприходит не более одногоclosed:Replaced/Revokedтерминальны (INV-REF-B6), обратных переходов нет — событие можно трактовать как финальную остановку поBondId. - Конкурентные admin-коррекции одной связи сериализуются: закрытие проходит только из
Active, проигравшая операция получаетREFERRAL_BOND_INVALID_TRANSITIONи события не публикует — потребитель не увидит двухclosedпо одному переходу. - При rebind
closed(старыйBondId) иcreated(новый) делят ключReferredUserId— потребитель никогда не окажется с двумя активными связями одного приглашённого.
Версионирование и совместимость
- Версия — один
ContractVersion = 1; при несовпадении версии потребитель логирует warning и продолжает (upcasting/schema-migration нет). - Additive-only для minor: новые optional-поля с безопасным default — без bump.
- Breaking (удаление/переименование/смена типа) —
ContractVersion = 2+ новый топикpin.referral.bonds.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1(единая эволюция сReferralBondCreatedKafkaEvent— общий топик). - Unknown
Status→ warning + fail-closed:closed-событие по определению означает, что связь больше не активна, поэтому безопасное поведение — остановить начисления поBondId. - Порядок per-
ReferredUserIdгарантирован key-ом; потребители обязаны держатьVersion-guard per-BondId— событияcreatedиclosedодного топика упорядочены между собой.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
BondId, ReferredUserId, ReplacedByBondId, Version | Internal | include |
Status | Internal | include |
RevokeReason | Internal (free-text; PII запрещён политикой) | include с валидацией «без PII»; producer (админ-панель) не вносит ФИО/телефон/email — рекомендуется контролируемый справочник причин; в логах — короткая причина/код, при подозрении на PII маскируется |
Событие НЕ содержит secrets, tokens, PII (ФИО/телефон/email), сумм вознаграждений (деньги — зона
rewards), slug ссылки. Доступ к топику — m2m ACL Kafka (rewards, опц. notifications).
Trusted/untrusted boundary: RevokeReason — единственное free-text поле; источник — доверенная
админ-панель (ADR-0053), но валидация «без PII» обязательна на producer-стороне.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: ReferralAccrualEntity, Entity / Aggregate: ReferralRateRuleEntity
- API (3): POST /api/admin/referrals/bonds/{bondId}/rebind — коррекция связи (R6, Replace), POST /api/admin/referrals/bonds/{bondId}/revoke — Отзыв реферальной связи (adminRevokeBond), POST /api/v1/platform/referral-accruals/{accrualId}/cancel — Ручная отмена реферального начисления
- Use Cases (1): UC-RWD-009. Жизненный цикл реферального начисления (связь → начисление → гейт → выплата)
- События (1): Event Contract: pin.referral.bond-created (ReferralBondCreatedKafkaEvent)
- Консюмеры (2): Consumer: Referral Bond Sync (pin-rewards-referral-bonds), Consumer: Referral Events → Notifications (notifications-referral-events)
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- adr (1): ADR-0066 — удержание ПИН и реферальные ставки
- registries (1): Реестр событий PIN
- index.md (1): Rewards Service
Связанные документы
- Источник: агрегат referral-bond.md (enum
ReferralBondStatus, переходыActive → Replaced/Revoked, INV-REF-B4/B5/B6); producer-операции: rebind-bond.md (замена), revoke-bond.md (отзыв). - Соседний контракт топика: bond-created.md (
ReferralBondCreatedKafkaEvent— создание связи,Active) — пара produced-событийpin.referral.bonds.v1. - Правила коррекций: решение № 3 README.md (авто-rebind отключён), ADR-0053 (админ-панель).
- Потребители:
rewards— consumer referral-bond.md (закрытие реплики: новые начисления прекращаются, созданные — referral-accrual.md — сохраняются);docs/06-services/notifications/README.md.