Consumer: CRM Fixation Approved
Назначение
Consumer слушает событие одобрения заявки на фиксацию клиента (переход Pending → FixedUnique/FixedNonUnique, fixation-request.md) и инкрементирует в
проекции AgentPerformanceEntity счётчик FixationsApprovedCount
агента-исполнителя — lifetime-источник, которого rewards раньше не потреблял (ADR-0061 п.5). Строка
проекции — композитный ключ (AgentUserId, CompanyId): AgentUserId = текущий участник-исполнитель
(ExecutorAgentUserId, = CommissionRecipientUserId на момент одобрения), CompanyId =
OriginAgencyCompanyId (иммутабельный снапшот агентства-основателя заявки, agent-performance.md).
Пожизненная личная сводка агента — агрегат строк по AgentUserId сквозь все агентства (на стороне
чтения), она не зависит от текущего членства: атрибуция OriginAgencyCompanyId/OriginAgentUserId
фиксации в crm иммутабельна (INV-FIX-7), проекция лишь считает факт одобрения текущим исполнителем. Право
на комиссию и её получатель (= текущий Executor, CommissionRecipientUserId) материализуются позже — при
завершении сделки (crm-deal-completed.md); этот consumer денег и начислений не
создаёт.
Идемпотентность — по идентификатору сообщения + бизнес-ключ FixationRequestId (first-seen): одна
одобренная фиксация увеличивает счётчик не более одного раза.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | rewards (фоновый обработчик) |
| Обрабатываемое событие | pin.crm.fixation-approved |
| Topic | pin.crm.fixation-approved.v1 |
| Partitions / key | key = FixationRequestId (3 партиции, ADR-0056) |
| ContractVersion | 1; при несовпадении версии — warn-and-proceed (rewards_contract_mismatch_total) |
| Consumer group | pin-rewards-crm-fixations |
| Producer | crm: одобрение фиксации (FixationRequest.Approve) |
| Эффект (событие → что меняется) | счётчик FixationsApprovedCount в строке (AgentUserId = ExecutorAgentUserId, CompanyId = OriginAgencyCompanyId) проекции AgentPerformanceEntity |
Контекст события
- Из метаданных события доступны: идентификатор сообщения (= Kafka key), время события,
Version, агрегат-источник (AggregateId = FixationRequestId,AggregateType = "FixationRequest"). - Корреляция сквозного трейса восстанавливается из метаданных события.
- Обработка — под системным актором вне tenant-контекста (platform-scope, ADR-0061 п.5).
- Ключ идемпотентности сообщения —
rewards:crm-fixation-approved:{MessageId}; бизнес-ключ —FixationRequestId(учтён-ли уже данный факт одобрения).
Входные данные / payload события
Контракт — ../../crm/events/fixation-approved.md. Читаемые поля:
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
FixationRequestId | Guid | Да | Бизнес-ключ идемпотентности (first-seen учёт факта) |
ExecutorAgentUserId | Guid | Да | Часть 1 ключа проекции (AgentUserId): текущий участник-исполнитель заявки на момент одобрения (= CommissionRecipientUserId); FixationsApprovedCount += 1 |
CommissionRecipientUserId | Guid | Да | Получатель комиссии (= ExecutorAgentUserId на момент одобрения, ADR-0071); равен ключу проекции — трассировка тождества «исполнитель = получатель» |
OriginAgencyCompanyId | Guid | Да | Часть 2 ключа проекции (CompanyId): иммутабельный снапшот агентства-основателя заявки (agent-performance.md — разрез строки, не tenant-фильтр) |
OriginAgentUserId | Guid | Да | Атрибуция агента-основателя; в проекцию не пишется (проекция считает факт одобрения текущим исполнителем) — только трассировка |
ApprovedAt | DateTimeOffset | Да | LastActivityAt = max(...), FirstActivityAt ??= ApprovedAt |
Uniqueness | string (enum FixationApprovalUniqueness) | Да | Не влияет на счётчик (учитываются оба вида одобрения); только трассировка |
Поля TenantId/InvestorUserId/ApprovedByUserId/OriginAgentUserId consumer-ом в проекцию не
пишутся (в строку идут только ключ (AgentUserId = ExecutorAgentUserId, CompanyId = OriginAgencyCompanyId)
и метрики активности). Событие авторизации DealId не несёт (сделка при одобрении не создаётся), поэтому
consumer его и не ожидает.
Алгоритм
- Контекст события. Восстановить метаданные события (идентификатор сообщения,
FixationRequestId); сверитьContractVersion— при несовпадении warn-and-proceed. Проверить идемпотентность по ключуrewards:crm-fixation-approved:{MessageId}— дубль →skipped-duplicate, пропуск (ack). - Проверка входа.
FixationRequestId != Guid.Empty;ExecutorAgentUserId != Guid.Empty;OriginAgencyCompanyId != Guid.Empty, иначеREWARDS_FIXATION_EVENT_INVALID→ DLQ. Unknown enumUniqueness— не ошибка (счётчик от вида одобрения не зависит), warning. Бизнес-идемпотентность: если фактFixationRequestIdуже учтён (бизнес-отметкаrewards:fixation-counted:{FixationRequestId}) → skip. - Что читаем. Строку проекции по композитному ключу
(AgentUserId = ExecutorAgentUserId, CompanyId = OriginAgencyCompanyId)(не tenant-фильтр —CompanyIdразрез строки, agent-performance.md); её отсутствие допустимо — тогда создаётся новый ряд с нулевыми метриками. - Бизнес-правила и переходы.
FixationsApprovedCount += 1;LastActivityAt = max(LastActivityAt, ApprovedAt);FirstActivityAt ??= ApprovedAt;Version += 1,UpdatedAt = now. Прочие счётчики/суммы не трогаются. Переходов состояний и доменных событий нет. - Что меняется. Строка проекции
AgentPerformanceEntityсоздаётся или обновляется. Конкурентная доставка — проигравший инстанс перечитывает; бизнес-ключFixationRequestIdисключает двойной инкремент. - Согласованность. Обновление проекции, отметка идемпотентности и бизнес-отметка факта фиксируются в одном согласованном изменении; offset подтверждается после фиксации.
- Результат. Успех → ack;
REWARDS_FIXATION_EVENT_INVALID→ DLQ + алерт; инфраструктурные сбои → retry. Исходящих событий проекция не порождает.
Диаграмма
Предусловия и постусловия
До обработки: топик pin.crm.fixation-approved.v1 доступен, consumer-group подключена; таблица
проекции AgentPerformanceEntity существует (ряд (ExecutorAgentUserId, OriginAgencyCompanyId) может
отсутствовать — создаётся lazy); событие несёт валидные ExecutorAgentUserId/OriginAgencyCompanyId
(гарантия producer-а crm, снапшот фиксации иммутабелен, INV-FIX-7).
После успешной обработки (гарантируется): относительно факта FixationRequestId счётчик
FixationsApprovedCount строки исполнителя увеличен ровно на 1 (при повторной доставке того же факта — без изменений);
LastActivityAt >= ApprovedAt; FirstActivityAt установлен, если был пуст; Version проекции увеличен,
UpdatedAt обновлён. Прочие метрики агента (DealsWonCount/суммы/членство) не затронуты. Offset
подтверждён; исходящих событий проекция не порождает.
Порядок, конкурентность и eventual-consistency
- Порядок доставки не важен. Инкремент идемпотентен по бизнес-ключу
FixationRequestId(first-seen), поэтому late/out-of-order доставка и реплей безопасны — переупорядочивание фиксаций одного агента не меняет итог. - Конкурентная запись одного ряда. Строку
(AgentUserId, CompanyId)параллельно пишут и другие consumer-ы (начисления поTenantId, членство поCompanyId— agent-performance.md). При одновременной записи по одной строке проигравший инстанс перечитывает её и переприменяет свою дельту; инкремент коммутативен, а бизнес-отметка факта исключает двойной учёт. Одновременная доставка двух разных одобренных фиксаций одного исполнителя даёт итог+2(оба факта учитываются по одному разу). - Окно eventual-consistency. Обновлённый счётчик становится виден в get-my-agent-performance.md в пределах лага consumer-group (обычно секунды; алерт при > 5 мин). До обработки агент видит прежнее значение — расхождение самокорректируется, ошибочного состояния не возникает.
Идемпотентность и replay
- Consumer-level: отметка по идентификатору сообщения.
- Business-level: факт одобрения
FixationRequestIdучитывается ровно один раз (бизнес-отметка) — защита от двойного инкремента при повторной доставке/реплее. - Replay: да. Реплей с offset 0 безопасен (учтённые фиксации пропускаются). Полный rebuild проекции —
вместе с другими источниками (runbook
../runbooks/rewards-agent-performance-rebuild.md, по конвенции).
Retry и DLQ
DLQ-топик группы: pin.rewards.crm-fixations-dlq.v1.
| Сбой | Код | Retry | DLQ | Ручное действие |
|---|---|---|---|---|
| Инфраструктурный (БД/Kafka недоступны) | — (exception) | Exponential backoff 1s→60s | Нет | Runbook: восстановить зависимость |
Нарушение контракта (ExecutorAgentUserId/OriginAgencyCompanyId/FixationRequestId пуст) | REWARDS_FIXATION_EVENT_INVALID | Нет | Да + алерт | Разбор с командой crm; replay из DLQ после фикса |
| Дубль/учтённый факт | — (skip) | — | Нет | Не требуется |
Наблюдаемость и безопасность
- Logs:
consumed/skipped-duplicate/fixation-counted/dead-letteredсMessageId,FixationRequestId,ExecutorAgentUserId,OriginAgencyCompanyId. PII отсутствует (событие не несёт ФИО/телефон — см. контрактcrm). - Metrics:
rewards_fixation_consume_duration_ms,rewards_fixation_counted_total,rewards_crm_fixations_lag_seconds,rewards_crm_fixations_dlq_total,rewards_contract_mismatch_total. - Traces: span приёма события → span обработки → span обращения к хранилищу; корреляция из метаданных события.
- Alerts: lag consumer-group > 5 мин; DLQ > 0.
- Безопасность: system-actor, platform-scope (проекция вне tenant); payload — доверенный внутренний
топик
crm, raw PII в нём отсутствует.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Projection: AgentPerformanceEntity
- API (1): GET /api/v1/my/agent-performance — Моя личная статистика (агент, user-centric)
- Use Cases (1): UC-RWD-006. Агент смотрит личную статистику за всю карьеру (user-centric)
- События (1): Event Contract: pin.crm.fixation-approved (FixationApprovedEvent)
- Консюмеры (2): Consumer: Agent Performance Projection, Consumer: Identity Agent Membership Changed
- index.md (1): Rewards Service
Связанные документы
- Проекция: agent-performance.md (
FixationsApprovedCount, источники). - Контракт события: ../../crm/events/fixation-approved.md; сущность-источник — ../../crm/domain/fixation-request.md (INV-FIX-7, снапшот иммутабелен).
- Соседние consumer-ы проекции: agent-performance-projection.md, agent-membership-changed.md; начисление комиссии по завершённой сделке — crm-deal-completed.md.
- Use-case: UC-RWD-006.
- Шаблон: consumer-template.md; ADR-0061 (п.5), ADR-0052, ADR-0056,
ADR-0071 (получатель
комиссии = Executor;
Origin*— иммутабельная атрибуция).