UC-CAL-001. Агент ведёт рабочий календарь
Назначение
Описывает базовый сценарий рабочего календаря: агент (или консультант ПИН) просматривает свои события за
период, создаёт события вручную (показ, консультация, встреча, задача), редактирует их, переносит и ведёт
по жизненному циклу «запланировано → в работе → завершено/отменено». Календарь показывает пересечения
событий мягко (признак конфликта, без блокировки), а события, порождённые записями инвесторов и бронями
CRM, защищены от прямого редактирования. Связан с контекстом calendar.
| Поле | Значение |
|---|
| ID | UC-CAL-001 |
| Контекст | calendar (см. docs/06-services/calendar/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|
| Роль | Агент (владелец календаря); те же операции доступны консультанту ПИН в своём календаре |
| Permissions / scope | calendar.events.view (просмотр), calendar.events.manage (создание/изменение/переходы статусов) — scope Organization |
| Tenant | TenantId = CompanyId агентства (для консультанта ПИН — компания ПИН), ADR-0052 |
| Record-level | Управляет только собственными событиями (OwnerUserId = актор); чтение чужих календарей — отдельный сценарий UC-CAL-009 |
Предусловие
- Пользователь авторизован, активен и состоит в компании (агентство / ПИН) с ролью, дающей
calendar.events.view / calendar.events.manage.
- Для привязки события к клиенту/сделке/объекту соответствующие идентификаторы известны агенту
(клиент из CRM, объект из каталога) — календарь хранит их как логические ссылки без синхронной проверки
в чужих контекстах.
Триггер
| Поле | Значение |
|---|
| Источник | UI button |
| Триггерящий элемент | ЛК агента → «Календарь» (просмотр, «Создать событие», карточка события) |
Основной Поток
- Агент открывает календарь; клиент запрашивает события за период (
../api/agent/list-events.md,
диапазон from/to — представление день/неделя/месяц собирает клиент, ADR-0064 п.2).
- Система возвращает события владельца за период:
Kind, Origin, Status, Priority,
StartAt/EndAt/TimeZone, заголовок/место, ссылки (ClientUserId/DealId/BookingId/
PropertyObjectId) и признак HasConflicts для каждого события, пересекающегося по времени с другим
Planned/InProgress-событием владельца (INV-CAL-6, calendar-event.md).
- Агент нажимает «Создать событие» и заполняет форму:
Kind, StartAt/EndAt (UTC), TimeZone (IANA),
Title?, Description?, Location?, MeetingLink?, Priority, ClientUserId?, DealId?/LeadId?,
PropertyObjectId?/ProjectId?, ReminderOffsetsMinutes (default {1440, 60}); клиент вызывает
../api/agent/create-event.md.
- Система проверяет вход: интервал корректен (INV-CAL-1:
EndAt > StartAt; точечное событие без EndAt —
только Task), матрица kind-обязательностей соблюдена (INV-CAL-5: MeetingLink — только
Consultation/Meeting; BookingDeadline вручную не создаётся — Origin = CrmBooking порождает только
consumer), TimeZone — валидный IANA-идентификатор, офсеты напоминаний в ограничениях (≤ 5, диапазон
5..40320, без дублей). Любое нарушение → отказ 400 (таблица ошибок).
- Система создаёт событие
Status = Planned, Origin = Manual, CreatedByUserId = актор; в том же
согласованном изменении материализует reminder-строки по офсетам (event-reminder.md,
офсеты с уже прошедшим DueAt — сразу Skipped) и публикует pin.calendar.event-created.v1.
- Агент ведёт событие по жизненному циклу (
../api/agent/start-event.md, complete-event.md,
cancel-event.md): Planned → InProgress (начал), Planned/InProgress → Completed (завершил),
Planned/InProgress → Cancelled (отменил — CancelReason обязателен). Каждый переход валидируется
матрицей статусов (INV-CAL-2) и публикует pin.calendar.event-status-changed.v1; при
отмене/завершении все Pending-напоминания переводятся в Skipped.
- Перенос (
../api/agent/reschedule-event.md): агент задаёт новый StartAt — длительность события
сохраняется (EndAt пересчитывается), статус не меняется (допустим для Planned/InProgress);
Pending-напоминания пересоздаются от нового StartAt; публикуется
pin.calendar.event-rescheduled.v1.
- Правка полей (
../api/agent/update-event.md): заголовок, описание, место, ссылка, приоритет, привязки,
офсеты напоминаний — с повторной проверкой INV-CAL-1/5; при изменении офсетов Pending-напоминания
пересоздаются.
- Агент видит обновлённый календарь; уведомления о переносах/статусах доставляет
notifications
(consumer ../../notifications/consumers/calendar-events.md).
Альтернативные Потоки
A1. Пересечение с существующим событием (soft-conflict)
- Новое или перенесённое событие пересекается по времени с другим
Planned/InProgress-событием
владельца. Создание/перенос не блокируется (INV-CAL-6): агент сам решает, как разрулить занятость.
- Обе стороны пересечения возвращаются в выборках с
HasConflicts = true; клиент подсвечивает конфликт.
Возвращается к основному потоку.
A2. Событие порождено записью инвестора
- Агент пытается изменить/перенести/отменить событие
Origin = Appointment прямыми операциями →
отказ CAL_EVENT_MANAGED_BY_APPOINTMENT (409, INV-CAL-3).
- Управлять таким событием можно только операциями записи (подтвердить/отклонить/отменить/зафиксировать
исход — UC-CAL-004, UC-CAL-005,
UC-CAL-006); каскад записи сам изменит событие.
A3. Событие — дедлайн брони CRM
- Агент пытается изменить событие
Origin = CrmBooking → отказ CAL_EVENT_MANAGED_BY_CRM_BOOKING
(409, INV-CAL-4). Дедлайны живут по booking-событиям CRM (UC-CAL-007).
A4. Операция над терминальным событием
- Любой переход/правка события в
Completed/Cancelled → отказ CAL_EVENT_ALREADY_TERMINAL (409,
INV-CAL-7). Клиент перечитывает событие и снимает управляющие кнопки.
A5. Конкурентная правка
- Два изменения одного события (агент с двух устройств; агент ↔ worker) — побеждает первая
зафиксированная мутация (оптимистическая блокировка по версии строки); проигравший получает 409,
перечитывает событие и повторяет действие уже на актуальном состоянии.
A6. Некорректный вход
EndAt ≤ StartAt или отсутствует у интервального Kind → CAL_EVENT_INVALID_INTERVAL;
нарушение матрицы kind-полей → CAL_EVENT_KIND_FIELDS_MISMATCH; невалидная таймзона →
CAL_EVENT_TIMEZONE_UNKNOWN; офсеты вне ограничений → CAL_EVENT_REMINDER_OFFSETS_INVALID;
отмена без причины → CAL_EVENT_CANCEL_REASON_REQUIRED. Агент исправляет форму и повторяет.
Постусловие
- Ручные события агента существуют в целевых статусах;
Origin = Manual, CreatedByUserId заполнен.
- Reminder-строки согласованы с актуальными
StartAt и офсетами каждого события (INV-REM-1).
- По каждому созданию/переносу/переходу опубликовано соответствующее событие
pin.calendar.* — история
изменений восстановима из потока событий.
- События
Origin = Appointment/CrmBooking прямыми операциями не изменены.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|
CAL_EVENT_NOT_FOUND | 404 | Событие не найдено или вне tenant актора | Вернуться к списку |
CAL_EVENT_ACCESS_DENIED | 403 | Актор — не владелец события | Скрыть управляющие действия |
CAL_EVENT_INVALID_INTERVAL | 400 | INV-CAL-1 (A6) | Подсветить поля времени |
CAL_EVENT_KIND_FIELDS_MISMATCH | 400 | INV-CAL-5 (A6) | Подсветить несовместимые поля |
CAL_EVENT_TIMEZONE_UNKNOWN | 400 | Невалидный IANA-идентификатор (A6) | Подсветить таймзону |
CAL_EVENT_REMINDER_OFFSETS_INVALID | 400 | Офсеты вне ограничений (A6) | Подсветить офсеты |
CAL_EVENT_CANCEL_REASON_REQUIRED | 400 | Отмена без причины (A6) | Запросить причину |
CAL_EVENT_INVALID_STATE | 409 | Переход вне матрицы статусов (INV-CAL-2) | Перечитать событие |
CAL_EVENT_ALREADY_TERMINAL | 409 | Операция над Completed/Cancelled (A4) | Перечитать событие |
CAL_EVENT_MANAGED_BY_APPOINTMENT | 409 | Прямая операция над событием записи (A2) | Открыть карточку записи |
CAL_EVENT_MANAGED_BY_CRM_BOOKING | 409 | Прямая операция над дедлайном брони (A3) | Открыть карточку брони |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|
CalendarEventCreatedEvent.v1 | pin.calendar.event-created.v1 | Создание события (шаг 5) | notifications |
CalendarEventRescheduledEvent.v1 | pin.calendar.event-rescheduled.v1 | Перенос (шаг 7) | notifications (CalendarEventRescheduled) |
CalendarEventStatusChangedEvent.v1 | pin.calendar.event-status-changed.v1 | Каждый переход статуса (шаг 6) | notifications |
Payload — без PII (Title/Description/Location в события не попадают); ключ партиционирования —
EventId (порядок изменений одного события гарантирован).
Acceptance Criteria
| # | Критерий | Проверка |
|---|
| AC1 | Создание Consultation с корректным интервалом и MeetingLink даёт событие Planned (Origin = Manual), reminder-строки по офсетам и event-created — атомарно | интеграционный тест создания |
| AC2 | start переводит Planned → InProgress (не Cancelled — фикс бага черновика, ADR-0064 п.2) и публикует event-status-changed | тест перехода |
| AC3 | Showing без EndAt → CAL_EVENT_INVALID_INTERVAL; Showing с MeetingLink → CAL_EVENT_KIND_FIELDS_MISMATCH | негативные тесты матрицы |
| AC4 | Перенос сохраняет длительность, пересоздаёт Pending-напоминания от нового StartAt и публикует event-rescheduled; Sent-строки не тронуты | тест переноса |
| AC5 | Пересекающиеся Planned-события создаются без блокировки и оба возвращаются с HasConflicts = true | тест выборки с пересечением |
| AC6 | Прямые операции над Origin = Appointment / CrmBooking → 409 (MANAGED_BY_APPOINTMENT / MANAGED_BY_CRM_BOOKING) | негативные тесты защиты |
| AC7 | Отмена без CancelReason → 400; отмена с причиной переводит Pending-напоминания в Skipped | тест отмены |
| AC8 | Повторный переход/правка терминального события → CAL_EVENT_ALREADY_TERMINAL, событий не публикуется | тест идемпотентности guard-ов |
| AC9 | Выборка list-events возвращает только события OwnerUserId = актор в диапазоне from/to | тест изоляции выборки |
Наблюдаемость
- Логи (без PII).
calendar-event-created / -rescheduled / -status-changed (eventId, tenantId,
ownerUserId, kind, origin, fromStatus?, toStatus?); Title/Description/Location в логи не
пишутся (могут содержать персональные данные клиента).
- Метрики.
calendar_events_total{kind,origin,status}; calendar_event_conflicts_returned_total
(объём soft-конфликтов); calendar_event_forbidden_ops_total{reason} (попытки править managed-события).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы
- Domain: calendar-event.md (поля, enum, INV-CAL-1..7, статус-машина),
event-reminder.md (материализация напоминаний).
- API:
../api/agent/list-events.md, get-event.md, create-event.md, update-event.md,
reschedule-event.md, start-event.md, complete-event.md, cancel-event.md.
- События:
../events/event-created.md, ../events/event-rescheduled.md, ../events/event-status-changed.md.
- ADR: 0064 контекст calendar (маппинг
черновика, фикс «start → InProgress», таймзоны).
- Связанные UC: UC-CAL-007 (автособытия дедлайнов),
UC-CAL-008 (доставка напоминаний),
UC-CAL-009 (календарь команды).