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

CrewsForge — Шаг 7: Операционный контур

Статус: черновик на подтверждение · Шаг 7 из 9 Основание: ФТ раздел 8 (очереди, SLA, окна, уведомления), Q-1…Q-4, вопрос 10.3.8 (консьерж); событийный контур шага 1; эффекты переходов шага 3

Четыре консьюмера platform-worker получают здесь свои декларативные таблицы. Принцип: вся операционная механика — данные, не код: маппинг «событие → задача», SLA-сроки, политика напоминаний живут в конфиг-таблицах, которые можно менять без переписывания консьюмеров.


Публикуются только исполнителем машин (шаг 3), формат {subject}.status.changed с именем перехода в payload; плюс несколько неструктурных событий.

project.status.changed milestone.status.changed hold.status.changed
contract.status.changed dispute.status.changed termination.status.changed
offer.status.changed plan-version.status.changed payout-account.status.changed
timer.fired webhook.external-dispute concierge.escalated

Payload-минимум: { 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_MATCHINGTEAM_MATCHINGADMIN2 дня
проект создан с belowBudgetGate = true и запросил мэтчингBUDGET_GATE_REVIEWADMIN24 ч
contracting: PLAN_DRAFTING → PLAN_INTERNAL_REVIEW (C01)PRICE_REVIEWADMIN24 ч
contracting: → KYC_PENDING (C03)KYC_DOCUMENTSADMIN3 дня
plan-version: SENT → REJECTED (C06)PLAN_MEDIATIONADMIN24 ч
milestone: reject оператора (M02/M12, из журнала)PLAN_MEDIATION / ACCEPTANCE_REVIEWADMIN24 ч
milestone: PLANNED создан после старта (M10, R-2)PLAN_APPROVALOPERATOR24 ч
milestone: SUBMITTED → REJECTED (M07)ACCEPTANCE_REVIEWADMIN24 ч
milestone: → FOUNDER_APPROVED (M06/M10)SUBMISSION_VERIFICATIONOPERATOR4 рабочих часа
milestone: → VERIFIED (M11)SETTLEMENT_EXECUTIONADMIN24 ч
timer.fired: FOUNDER_SILENCE (M08)FOUNDER_SILENCE_DECISIONADMIN24 ч
dispute: → UNDER_ARBITRATION (D03)DISPUTE_ARBITRATIONARBITER5 дней
dispute: → RESOLVED (D04/D06)SETTLEMENT_EXECUTIONADMIN24 ч
termination: → UNDER_REVIEW (T03)TERMINATION_ASSESSMENTADMIN24 ч
termination: EXECUTED → расчёты готовыSETTLEMENT_EXECUTIONADMIN24 ч
payout-account: → RESTRICTEDESCALATED_INQUIRYADMIN8 ч
webhook.external-dispute (chargeback, шаг 5.6)ESCALATED_INQUIRY (high)ADMIN8 ч
contract: конверт истёкESCALATED_INQUIRYADMIN24 ч
заявка команды на онбордингTEAM_ONBOARDINGADMIN3 дня
concierge.escalated (см. §5)ESCALATED_INQUIRYADMIN8 ч
консьерж-обращение созданоCONCIERGE_FIRST_LINEOPERATOR8 ч
ночная сверка billing: расхождение (шаг 5.7)ESCALATED_INQUIRY (high)ADMIN8 ч

Правила консьюмера:

  • Дедуп открытых задач: частичный уникальный индекс 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, а не продовым инцидентом.

Хранение — таблица Timer (шаг 2), тик — cron worker’а раз в минуту: WHERE fires_at <= now AND fired_at IS NULL AND cancelled_at IS NULL → пометить fired → опубликовать timer.fired (через outbox — атомарность сохраняется).

Окно / таймерВзводитсяДлительность (конфиг)ОтменяетсяПо истечении
FOUNDER_SILENCEM05 (сдача этапа)5 рабочих днейM06 / M07задача FOUNDER_SILENCE_DECISIONне автовыплата (8.3)
DISPUTE_POSITION_WINDOWD025 календарных днейобе позиции поданыD03 по имеющимся материалам; неответ — в журнал (S-2, L-5)
TERMINATION_OBJECTION_WINDOWT015 календарных днейT02 / T04T03; неответ — в журнал с меткой (X-6)
ENVELOPE_EXPIRYотправка конверта14 днейподписание/отзывзадача администратору, перевыпуск
REMINDER (генерируемые)вместе с родительским окномсм. политику нижевместе с родительскимуведомление-напоминание (8.4)
SLA_TARGETсоздание QueueItem= dueAt задачизакрытие задачипометка overdueMarkedAt (Q-2) + уведомление роли
MILESTONE_DEADLINEM03 (этап 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-либе; календарные величины — прямое сложение.

Триггер — те же события смены состояния (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) — тем же, что проверяет договорные шаблоны.

Решение: консьерж — платформенный тред обращений, первая линия — оператор, эскалация — задача администратору.

Модель (дельта к схеме шага 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-ассист оператору — пост-бета кандидат.

  • Метрики per-очередь: глубина, возраст старейшей открытой задачи, % overdue — прямые SQL-агрегаты по QueueItem, витрина в admin-api (администратору; операторская витрина — без денежных агрегатов, там их и нет).
  • Время ожидания команды после принятой работы (метрика satisfaction supply-стороны из 8.2) = PAID.occurredAt − FOUNDER_APPROVED.occurredAt из журнала переходов — считается запросом, ничего дополнительно не пишем.
  • Мёртвые буквы: ai.dlx уже мониторится; добавляются DLX для консьюмеров worker’а — сообщение, упавшее 5 раз, уходит в DLX + алерт (иначе тихая потеря = нарушение Q-4).
  1. Дизайн консьержа (§5): тред + оператор первой линией + эскалация как третья операторская операция.
  2. Рабочий календарь платформы: UTC, 10:00–18:00, пн–пт, без праздников на бете.
  3. Политика напоминаний 50%/80% и отсутствие автосанкций за просрочку дедлайна этапа (просрочка — информация к разбору).
  4. Дедуп открытых задач частичным уникальным индексом + тест полноты Q-4 как CI-гейт (две декларации сверяются автоматически).