Периодический worker timeline-overdue переводит элементы таймлайна сделки, у которых наступил срок, из
InProgress в Overdue (иконка «просрочено», US-INV-09) и по каждому переходу публикует карточку в чат
сделки / push участникам. Реализует инвариант INV-TLE-4
(deal-timeline-event.md): статус Overdue ставит только этот worker —
по элементам со Status = InProgress и наступившим DueAt, идемпотентно. Это единственный писатель статуса
Overdue; штатные закрытия (InProgress → Done) делают команды CRM по парным событиям (например
DocumentRequested → DocumentUploaded). Дом мутируемой сущности —
deal-timeline-event.md; родитель — deal.md.
Как карточка «просрочено» попадает в чат/уведомления (кросс-сервис)
Через интеграционное событие DealTimelineOverdueEvent в outbox (шаг 6), консюмеры messenger/notifications — как у всех card-to-chat потоков (booking-expired.md). Additive-расширение реестра производимых событий README.
2
Распределённая блокировка
Нет (ADR-0056: без Redis). Мульти-pod безопасность — построчная блокировка SKIP LOCKED (непересекающиеся батчи), а не distributed lock.
3
Идемпотентность
Структурная — предикат Status = InProgress в выборке + доменный guard перехода; повтор/рестарт = no-op, отдельный idempotency-ключ не нужен.
нет (ADR-0056, без Redis); построчная блокировка SKIP LOCKED; при желании single-runner — опциональный Postgres advisory Pin:lock:job:scheduled:crm-timeline-overdue, не Redis
Актор — system (worker); cross-tenant scan разрешён явно (worker обслуживает все tenant'ы,
README → Background jobs). Публичного API/permission у job нет. Payload события PII не
несёт (только id/enum/timestamp), маскирование не требуется.
Контекст. Цикл worker-а стартует с отсечкой NowUtc (системные часы), BatchSize,
MaxBatchesPerCycle; actor = system, cross-tenant разрешён явно; correlationId = job-run id.
Идемпотентность структурная (шаг 5), не по ключу.
Проверка входа.BatchSize ∈ [1..1000], MaxBatchesPerCycle ∈ [1..200] → иначе ValidationError.
Существование сущностей by-id не применяется (батч-скан). Допустимость состояния — в предикате шага 3
(Status = InProgress).
Что читаем. Батч элементов таймлайна с Status = InProgress, заданным DueAt и DueAt < NowUtc,
порядок по DueAt asc, Id asc (стабильный ключ; выборка по partial-индексу «status = InProgress»,
deal-timeline-event.md → Индексы). Строки берутся под построчной
блокировкой SKIP LOCKED — параллельные pod'ы забирают непересекающиеся строки. Пустой батч → шаг 8
(no-op).
Бизнес-правила и переходы. Для каждой строки — доменный переход InProgress → Overdue (INV-TLE-4):
Status = Overdue (источник — переход), UpdatedAt = NowUtc; CompletedAt не трогается (остаётся
null). Контент события (Type, OccurredAt, Payload, DueAt, ActorUserId) иммутабелен
(INV-TLE-1, append-only). ActorRole/ActorUserId события просрочки — null (система).
Что меняется. Обновляются Status/UpdatedAt строк батча. Guard-предикат Status = InProgress (в
выборке шага 3 и в условии обновления) делает повторную обработку no-op (идемпотентность). Строки уже
под построчной блокировкой в этой записи — гонок значений нет.
Событие. На каждую переведённую строку — интеграционное DealTimelineOverdueEvent
(topic pin.crm.timeline-overdue.v1, ContractVersion = 1, AggregateType = "Deal") через
транзакционный outbox. Payload (источники — deal-timeline-event.md):
{ DealId: Guid, TimelineEventId: Guid, TenantId: Guid, Type: DealTimelineEventType (string enum), DueAt: DateTimeOffset, OverdueAt: DateTimeOffset } — без PII (ФИО/суммы/эскроу не входят; текст карточки
messenger рендерит по Type). Консюмеры: messenger (карточка «шаг просрочен» в чат сделки,
US-INV-16/11), notifications (push/email участникам, US-INV-11).
Согласованность. Мутации строк батча + запись outbox фиксируются атомарно в одной записи. Граница
— один батч; «checkpoint» — продвижение по DueAt, Id; следующий батч — новая запись и новый набор
заблокированных строк.
Результат.{ ProcessedCount, HasMore }. Worker повторяет батчи, пока HasMore и не превышен
MaxBatchesPerCycle, затем пишет метрики и завершает цикл. Пустой батч → ProcessedCount = 0, HasMore = false. Инфраструктурная ошибка → батч откатывается, строки разблокируются, следующий цикл
(через 5 мин) повторит (идемпотентно).
Предусловия цикла. Существуют элементы таймлайна со Status = InProgress, заданным DueAt и наступившим
сроком (DueAt < NowUtc); консюмеры messenger и notifications подписаны на pin.crm.timeline-overdue.v1.
Постусловия (после commit батча). Каждый обработанный элемент — Status = Overdue, UpdatedAt = NowUtc;
контент события (Type, OccurredAt, Payload, DueAt, ActorUserId) иммутабелен (INV-TLE-1),
CompletedAt остаётся null; по каждому переходу — запись DealTimelineOverdueEvent в outbox.
Цепочка следствий (асинхронно, eventual consistency).DealTimelineOverdueEvent после commit публикуется
outbox-publisher-ом и разветвляется на двух консюмеров:
messenger — карточка «шаг просрочен» в чат сделки (US-INV-16/11); текст рендерится по Type (payload PII
не несёт).
Во вкладке «Ход сделки» элемент получает иконку «просрочено» сразу после commit; карточка в чате сделки и
push/email участникам появляются позже — после обработки события консюмерами messenger/notifications
(окно лага консюмера).