CrewsForge — Шаг 7: Операционный контур
Статус: черновик на подтверждение · Шаг 7 из 9 Основание: ФТ раздел 8 (очереди, SLA, окна, уведомления), Q-1…Q-4, вопрос 10.3.8 (консьерж); событийный контур шага 1; эффекты переходов шага 3
Четыре консьюмера platform-worker получают здесь свои декларативные таблицы. Принцип: вся операционная механика — данные, не код: маппинг «событие → задача», SLA-сроки, политика напоминаний живут в конфиг-таблицах, которые можно менять без переписывания консьюмеров.
1. Каталог доменных событий (routing keys)
Заголовок раздела «1. Каталог доменных событий (routing keys)»Публикуются только исполнителем машин (шаг 3), формат {subject}.status.changed с именем перехода в payload; плюс несколько неструктурных событий.
project.status.changed milestone.status.changed hold.status.changedcontract.status.changed dispute.status.changed termination.status.changedoffer.status.changed plan-version.status.changed payout-account.status.changedtimer.fired webhook.external-dispute concierge.escalatedPayload-минимум: { subjectId, projectId?, transition, from, to, actorType, transitionId, occurredAt }. Денежных сумм в payload нет (Q-3 распространяется на шину: события читают все консьюмеры, суммы обогащаются адресно на этапе построения DTO/уведомления).
2. task-generator: маппинг «переход → задача» (материализация Q-4)
Заголовок раздела «2. task-generator: маппинг «переход → задача» (материализация Q-4)»Единственный писатель QueueItem. Декларативная таблица (в коде — константа-массив, покрыта тестом полноты):
| Переход-триггер | QueueItemType | Роль | SLA (dueAt) |
|---|---|---|---|
project: SPEC_READY → TEAM_MATCHING | TEAM_MATCHING | ADMIN | 2 дня |
проект создан с belowBudgetGate = true и запросил мэтчинг | BUDGET_GATE_REVIEW | ADMIN | 24 ч |
contracting: PLAN_DRAFTING → PLAN_INTERNAL_REVIEW (C01) | PRICE_REVIEW | ADMIN | 24 ч |
contracting: → KYC_PENDING (C03) | KYC_DOCUMENTS | ADMIN | 3 дня |
plan-version: SENT → REJECTED (C06) | PLAN_MEDIATION | ADMIN | 24 ч |
milestone: reject оператора (M02/M12, из журнала) | PLAN_MEDIATION / ACCEPTANCE_REVIEW | ADMIN | 24 ч |
milestone: PLANNED создан после старта (M10, R-2) | PLAN_APPROVAL | OPERATOR | 24 ч |
milestone: SUBMITTED → REJECTED (M07) | ACCEPTANCE_REVIEW | ADMIN | 24 ч |
milestone: → FOUNDER_APPROVED (M06/M10) | SUBMISSION_VERIFICATION | OPERATOR | 4 рабочих часа |
milestone: → VERIFIED (M11) | SETTLEMENT_EXECUTION | ADMIN | 24 ч |
timer.fired: FOUNDER_SILENCE (M08) | FOUNDER_SILENCE_DECISION | ADMIN | 24 ч |
dispute: → UNDER_ARBITRATION (D03) | DISPUTE_ARBITRATION | ARBITER | 5 дней |
dispute: → RESOLVED (D04/D06) | SETTLEMENT_EXECUTION | ADMIN | 24 ч |
termination: → UNDER_REVIEW (T03) | TERMINATION_ASSESSMENT | ADMIN | 24 ч |
termination: EXECUTED → расчёты готовы | SETTLEMENT_EXECUTION | ADMIN | 24 ч |
payout-account: → RESTRICTED | ESCALATED_INQUIRY | ADMIN | 8 ч |
webhook.external-dispute (chargeback, шаг 5.6) | ESCALATED_INQUIRY (high) | ADMIN | 8 ч |
contract: конверт истёк | ESCALATED_INQUIRY | ADMIN | 24 ч |
| заявка команды на онбординг | TEAM_ONBOARDING | ADMIN | 3 дня |
concierge.escalated (см. §5) | ESCALATED_INQUIRY | ADMIN | 8 ч |
| консьерж-обращение создано | CONCIERGE_FIRST_LINE | OPERATOR | 8 ч |
| ночная сверка billing: расхождение (шаг 5.7) | ESCALATED_INQUIRY (high) | ADMIN | 8 ч |
Правила консьюмера:
- Дедуп открытых задач: частичный уникальный индекс
UNIQUE (type, subject_type, subject_id) WHERE status IN ('OPEN','IN_PROGRESS')— повторное событие (at-least-once доставка) не плодит дубликат; попытка вставки при существующей открытой задаче — no-op. Дельта к схеме шага 2. - Закрытие задач — тоже по событиям: задача закрывается переходом, ради которого создана (пример:
SUBMISSION_VERIFICATIONзакрывается M11/M12 по тому же subjectId). Маппинг закрытия — вторая колонка той же таблицы в коде. «Осиротевшие» открытые задачи по субъектам в терминальных статусах отлавливает ночная проверка → это дефект Q-4, алерт. - Тест полноты Q-4: каждый переход шага 3, в чьих эффектах заявлена задача, обязан присутствовать в таблице маппинга — unit-тест сверяет две декларации. «Нужен администратор, но задача никуда не приземлилась» становится красным CI, а не продовым инцидентом.
3. timer-scheduler: окна и напоминания
Заголовок раздела «3. timer-scheduler: окна и напоминания»Хранение — таблица Timer (шаг 2), тик — cron worker’а раз в минуту: WHERE fires_at <= now AND fired_at IS NULL AND cancelled_at IS NULL → пометить fired → опубликовать timer.fired (через outbox — атомарность сохраняется).
| Окно / таймер | Взводится | Длительность (конфиг) | Отменяется | По истечении |
|---|---|---|---|---|
| FOUNDER_SILENCE | M05 (сдача этапа) | 5 рабочих дней | M06 / M07 | задача FOUNDER_SILENCE_DECISION — не автовыплата (8.3) |
| DISPUTE_POSITION_WINDOW | D02 | 5 календарных дней | обе позиции поданы | D03 по имеющимся материалам; неответ — в журнал (S-2, L-5) |
| TERMINATION_OBJECTION_WINDOW | T01 | 5 календарных дней | T02 / T04 | T03; неответ — в журнал с меткой (X-6) |
| ENVELOPE_EXPIRY | отправка конверта | 14 дней | подписание/отзыв | задача администратору, перевыпуск |
| REMINDER (генерируемые) | вместе с родительским окном | см. политику ниже | вместе с родительским | уведомление-напоминание (8.4) |
| SLA_TARGET | создание QueueItem | = dueAt задачи | закрытие задачи | пометка overdueMarkedAt (Q-2) + уведомление роли |
| MILESTONE_DEADLINE | M03 (этап FUNDED, если dueAt задан) | до dueAt | пауза R-3 / спор / расторжение | уведомление обеим сторонам; автоперехода нет — просрочка = информация к разбору, не санкция |
Политика напоминаний (единая, конфигом): для окон ≥ 3 дней — напоминания адресату действия на 50% и 80% длительности; для конвертов — на 7-й и 12-й день. Напоминание — уведомление с requiresAction = true, новых задач не создаёт.
Рабочий календарь платформы («4 рабочих часа», «5 рабочих дней»): конфиг platform-calendar — таймзона платформы (UTC для беты), рабочие часы (10:00–18:00 UTC), рабочие дни (пн–пт), без учёта праздников на бете (упрощение, помечено). Расчёт dueAt/firesAt для «рабочих» величин — функция календаря в shared-либе; календарные величины — прямое сложение.
4. notification-dispatcher: адресность и каналы
Заголовок раздела «4. notification-dispatcher: адресность и каналы»Триггер — те же события смены состояния (8.4). Декларативная таблица адресности: (событие, переход) → [{ адресат: роль-в-контексте, requiresAction, шаблон }], где адресат-в-контексте: founder, team.admins, team.submitter, team.signer, platform.role(X).
Правила:
- Каналы: in-app (
Notification) всегда; email — дляrequiresAction = trueи терминальных событий (оплата, решение спора, расторжение); email идёт через существующую либу email-sender. Digest и push — вне скоупа беты. - F-1-фильтр на выходе (шаг 4.4): события контрактинга в стадиях
PLAN_DRAFTING/PLAN_INTERNAL_REVIEWне имеют адресатаfounderв таблице — проверяется тем же тестом полноты, что и Q-4. - T-1: уведомление «вторая сторона подала позицию» содержит только факт подачи, без содержимого.
- Асинхронные денежные состояния (7.3): отдельные шаблоны для
hold.PENDING_IN(«платёж в обработке, до 3–5 дней») иRELEASING(«выплата в пути») — формулировки не употребляют «оплачено/выплачено» до терминальных состояний. Слово «escrow» в шаблонах запрещено линтером строк (7.6) — тем же, что проверяет договорные шаблоны.
5. Консьерж (закрытие вопроса 10.3.8)
Заголовок раздела «5. Консьерж (закрытие вопроса 10.3.8)»Решение: консьерж — платформенный тред обращений, первая линия — оператор, эскалация — задача администратору.
Модель (дельта к схеме шага 2):
model ConciergeThread { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid openedByUserId String @map("opened_by_user_id") @db.Uuid side DealSide? // null = обращение вне контекста сделки projectId String? @map("project_id") @db.Uuid // опциональная привязка к проекту subject String @db.VarChar(255) status ConciergeStatus @default(OPEN) // OPEN | ESCALATED | RESOLVED | CLOSED createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") messages ConciergeMessage[] @@index([status, createdAt])}
model ConciergeMessage { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid threadId String @map("thread_id") @db.Uuid authorUserId String @map("author_user_id") @db.Uuid authorRole String @map("author_role") @db.VarChar(32) // FOUNDER | TEAM | OPERATOR | ADMIN body String @db.Text createdAt DateTime @default(now()) @map("created_at") thread ConciergeThread @relation(fields: [threadId], references: [id], onDelete: Restrict)}Механика: создание треда → задача CONCIERGE_FIRST_LINE оператору (SLA 8 ч). Оператор отвечает в треде; эскалация — третья операторская операция рядом с approve/reject, и она консистентна O-2 по духу: не вердикт, а передача (status → ESCALATED, событие concierge.escalated, задача ESCALATED_INQUIRY администратору, SLA 8 ч). Ответ администратора возвращает тред оператору или закрывает. Денежные вопросы в тредах оператор по регламенту эскалирует не отвечая — содержимое треда пишет пользователь, оградить оператора от чтения сумм в свободном тексте технически нельзя, но отвечать по денежным вопросам оператор не уполномочен (регламент + аудит тредов администратором). O-1 касается системных денежных объектов — они в консьерже не отображаются.
Walrider в консьерже не участвует (бета): первая линия человеческая, AI-ассист оператору — пост-бета кандидат.
6. Наблюдаемость контура
Заголовок раздела «6. Наблюдаемость контура»- Метрики per-очередь: глубина, возраст старейшей открытой задачи, % overdue — прямые SQL-агрегаты по QueueItem, витрина в admin-api (администратору; операторская витрина — без денежных агрегатов, там их и нет).
- Время ожидания команды после принятой работы (метрика satisfaction supply-стороны из 8.2) =
PAID.occurredAt − FOUNDER_APPROVED.occurredAtиз журнала переходов — считается запросом, ничего дополнительно не пишем. - Мёртвые буквы:
ai.dlxуже мониторится; добавляются DLX для консьюмеров worker’а — сообщение, упавшее 5 раз, уходит в DLX + алерт (иначе тихая потеря = нарушение Q-4).
7. На подтверждение
Заголовок раздела «7. На подтверждение»- Дизайн консьержа (§5): тред + оператор первой линией + эскалация как третья операторская операция.
- Рабочий календарь платформы: UTC, 10:00–18:00, пн–пт, без праздников на бете.
- Политика напоминаний 50%/80% и отсутствие автосанкций за просрочку дедлайна этапа (просрочка — информация к разбору).
- Дедуп открытых задач частичным уникальным индексом + тест полноты Q-4 как CI-гейт (две декларации сверяются автоматически).