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

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
Topicpin.crm.fixation-approved.v1
Partitions / keykey = FixationRequestId (3 партиции, ADR-0056)
ContractVersion1; при несовпадении версии — warn-and-proceed (rewards_contract_mismatch_total)
Consumer grouppin-rewards-crm-fixations
Producercrm: одобрение фиксации (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ТипОбязательностьКак используется (маппинг)
FixationRequestIdGuidДаБизнес-ключ идемпотентности (first-seen учёт факта)
ExecutorAgentUserIdGuidДаЧасть 1 ключа проекции (AgentUserId): текущий участник-исполнитель заявки на момент одобрения (= CommissionRecipientUserId); FixationsApprovedCount += 1
CommissionRecipientUserIdGuidДаПолучатель комиссии (= ExecutorAgentUserId на момент одобрения, ADR-0071); равен ключу проекции — трассировка тождества «исполнитель = получатель»
OriginAgencyCompanyIdGuidДаЧасть 2 ключа проекции (CompanyId): иммутабельный снапшот агентства-основателя заявки (agent-performance.md — разрез строки, не tenant-фильтр)
OriginAgentUserIdGuidДаАтрибуция агента-основателя; в проекцию не пишется (проекция считает факт одобрения текущим исполнителем) — только трассировка
ApprovedAtDateTimeOffsetДаLastActivityAt = max(...), FirstActivityAt ??= ApprovedAt
Uniquenessstring (enum FixationApprovalUniqueness)ДаНе влияет на счётчик (учитываются оба вида одобрения); только трассировка

Поля TenantId/InvestorUserId/ApprovedByUserId/OriginAgentUserId consumer-ом в проекцию не пишутся (в строку идут только ключ (AgentUserId = ExecutorAgentUserId, CompanyId = OriginAgencyCompanyId) и метрики активности). Событие авторизации DealId не несёт (сделка при одобрении не создаётся), поэтому consumer его и не ожидает.

Алгоритм

  1. Контекст события. Восстановить метаданные события (идентификатор сообщения, FixationRequestId); сверить ContractVersion — при несовпадении warn-and-proceed. Проверить идемпотентность по ключу rewards:crm-fixation-approved:{MessageId} — дубль → skipped-duplicate, пропуск (ack).
  2. Проверка входа. FixationRequestId != Guid.Empty; ExecutorAgentUserId != Guid.Empty; OriginAgencyCompanyId != Guid.Empty, иначе REWARDS_FIXATION_EVENT_INVALID → DLQ. Unknown enum Uniqueness — не ошибка (счётчик от вида одобрения не зависит), warning. Бизнес-идемпотентность: если факт FixationRequestId уже учтён (бизнес-отметка rewards:fixation-counted:{FixationRequestId}) → skip.
  3. Что читаем. Строку проекции по композитному ключу (AgentUserId = ExecutorAgentUserId, CompanyId = OriginAgencyCompanyId) (не tenant-фильтр — CompanyId разрез строки, agent-performance.md); её отсутствие допустимо — тогда создаётся новый ряд с нулевыми метриками.
  4. Бизнес-правила и переходы. FixationsApprovedCount += 1; LastActivityAt = max(LastActivityAt, ApprovedAt); FirstActivityAt ??= ApprovedAt; Version += 1, UpdatedAt = now. Прочие счётчики/суммы не трогаются. Переходов состояний и доменных событий нет.
  5. Что меняется. Строка проекции AgentPerformanceEntity создаётся или обновляется. Конкурентная доставка — проигравший инстанс перечитывает; бизнес-ключ FixationRequestId исключает двойной инкремент.
  6. Согласованность. Обновление проекции, отметка идемпотентности и бизнес-отметка факта фиксируются в одном согласованном изменении; offset подтверждается после фиксации.
  7. Результат. Успех → 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, членство по CompanyIdagent-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.

СбойКодRetryDLQРучное действие
Инфраструктурный (БД/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 в нём отсутствует.

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

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

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