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

UC-CHS-001. Создание и публикация проекта застройщиком

Назначение

Менеджер застройщика заводит новый проект (ЖК или апарт-отель) в шахматке: заполняет карточку (тип, класс, регион, локация, инфраструктура, финансирование, юрданные, ход строительства, видео, обложка) и публикует его. После публикации (Status = Published) проект попадает в публичную ленту каталога и в измерения статистики застройщика. Сценарий относится к контексту chessboard.

Metadata

ПолеЗначение
IDUC-CHS-001
Контекстchessboard (см. docs/06-services/chessboard/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольДевелопер (менеджер застройщика: внутренние роли «администратор проекта», «маркетолог»)
Permissions / scopechessboard.projects.manage; scope = организация / группа проектов (каталог и назначения — ../../identity/domain/permission.md)
TenantTenantId = CompanyId застройщика = DeveloperEntity.Id (ADR-0052); мутации ограничены своим tenant-ом
Record-levelНе применяется — границы задаёт tenant + scope «группа проектов/проект»

Предусловие

  • DeveloperEntity застройщика существует (создан по компании Type = Developer из identity либо синком Talent — ../domain/developer.md).
  • Для шага публикации: Developer.Status = Published (INV-PRJ-3); для типов ApartHotel/Hotel отельер заведён в справочнике (../domain/hotelier.md).
  • Актор аутентифицирован, имеет chessboard.projects.manage в scope своей организации.

Триггер

ПолеЗначение
ИсточникДействие пользователя в кабинете застройщика
Триггерящий элементКабинет застройщика, страница «Проекты» → «Создать проект»; далее вкладки карточки → «Опубликовать»

Основной Поток

Создание проекта

  1. Менеджер создаёт проект (POST /developers/projects), заполняя обязательный минимум: Name, Type (ProjectType), Class (ProjectClass), ConstructionStatus; гео и валюта — ссылки на справочник handbook: CountryId (по умолчанию — страна «Россия»), RegionId/CityId (можно указать позже, но обязательны к публикации — INV-PRJ-3), CurrencyId (по умолчанию — валюта «RUB»; RUB-only, ADR-0048).
  2. Проверяется вход: Name 1..256; значения Type/Class/ConstructionStatus — из справочника reference-data.md; Stars 1..5 только при Type in (ApartHotel, Hotel). Нарушение формата/диапазона → ошибка валидации. Справочные ссылки (CountryId/RegionId/CityId/CurrencyId) проверяются по локальной реплике handbook-replica.md (INV-PRJ-10): неизвестная → CHESSBOARD_GEO_REF_UNKNOWN, архивная → CHESSBOARD_GEO_REF_ARCHIVED, рассогласованная гео-цепочка город/регион/страна → CHESSBOARD_CITY_REGION_MISMATCH. Display-имена RegionName/CityName снимаются из реплики при записи.
  3. Профиль застройщика по tenant-у актора должен существовать и быть не удалён; иначе — CHESSBOARD_DEVELOPER_NOT_FOUND (INV-PRJ-2).
  4. Создаётся проект: Status = Draft, SyncSource = Pin, TenantId = DeveloperId (INV-PRJ-1); пять typed-jsonb документов Location/Infrastructure/Financing/LegalInfo/ConstructionProgress инициализируются пустыми. В одном согласованном изменении фиксируется факт создания (ProjectCreatedEvent) и публикуется ProjectUpsertedKafkaEvent (топик pin.chessboard.project-upserted.v1, key = Id; снапшот — project.md). Повторный вызов создаёт второй Draft — осознанное упрощение (ADR-0056), дубликаты чистятся вручную.
  5. Результат: 201 CreatedId, Status = Draft, Version.

Заполнение карточки

  1. Менеджер заполняет вкладки карточки — общие поля (PUT /developers/projects/{id}) и пять jsonb-документов (PUT /developers/projects/{id}/{location|infrastructure|financing|legal-info|construction-progress}). Для каждой вкладки:
    • вход проверяется по схемам jsonb-моделей project.md: диапазоны GeoPoint, ≤ 20 TransportNodes, pattern Inn/Ogrn, формат Period = YYYY-MM и т.д.; для Financing каждый банк из EscrowBankIds ∪ MortgageBankIds ∪ {ProjectFinancingBankId} должен существовать и не быть архивным, иначе CHESSBOARD_PROJECT_BANK_NOT_FOUND (INV-PRJ-7);
    • у проекта-реплики Talent (SyncSource = TalentChessboard) правка реплика-полей запрещена — CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED (INV-PRJ-9); меняются только PIN-поля;
    • клиент передаёт версию записи (RowVersion) для контроля конкурентного доступа: устаревшая версия → 409 Conflict (правка не затирает чужую);
    • при значимых изменениях публикуется ProjectUpsertedKafkaEvent (перечень значимых полей — project.md).

Публикация

  1. Менеджер нажимает «Опубликовать» (POST /developers/projects/{id}/publish). Допустимые исходные статусы — Draft или Hidden (stateDiagram project.md). Проверяются guard-ы:

    • INV-PRJ-3: заполнены Name, Description, CoverImageFileId, Location.Geo, резолвлены ссылки справочника RegionId и CityId и застройщик опубликован (Developer.Status = Published); иначе — CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE с перечнем незаполненных полей в деталях ошибки;
    • INV-PRJ-4: для Type in (ApartHotel, Hotel) задан HotelierId; иначе — CHESSBOARD_PROJECT_HOTELIER_REQUIRED.

    Переход Draft → Published (или Hidden → Published); фиксируется ProjectStatusChangedEvent(From, To, actor) и публикуется ProjectUpsertedKafkaEvent со снапшотом Status = Published.

  2. Потребители (catalog, dev-stats, identity, news) получают pin.chessboard.project-upserted.v1; проект появляется в публичной ленте каталога, карточка показывает бейдж «Опубликован».

Альтернативные Потоки

A1. Публикация с незаполненными обязательными полями

  1. Guard INV-PRJ-3 не пройден → CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE с перечнем незаполненных полей в деталях ошибки.
  2. Фронт подсвечивает вкладки с пробелами. Возврат к заполнению (шаг 6 основного потока).

A2. Апарт-отель без отельера

  1. Type in (ApartHotel, Hotel) при пустом HotelierIdCHESSBOARD_PROJECT_HOTELIER_REQUIRED (INV-PRJ-4).
  2. Менеджер выбирает отельера из lookup-списка (GET /developers/hoteliers/lookup, permission chessboard.references.lookup) и повторяет публикацию.

A3. Несуществующий/архивный банк в финансировании

  1. Хотя бы один банк из Financing не существует или архивирован → CHESSBOARD_PROJECT_BANK_NOT_FOUND (INV-PRJ-7); вкладка «Финансирование» не сохраняется. Возврат на шаг 6.

A4. Конкурентная правка (два менеджера)

  1. Переданная версия записи (RowVersion) не совпала → 409 Conflict; фронт перечитывает проект и предлагает повторить правку поверх свежей версии.

A5. Скрытие/архив опубликованного проекта

  1. POST .../hidePublished → Hidden (проект и объекты недвижимости пропадают из public-проекций, данные целы); POST .../unhide — обратно (guard: Developer.Status = Published).
  2. POST .../archive → guard INV-PRJ-8: архив запрещён при объектах недвижимости в Reserved/BookedCHESSBOARD_PROJECT_HAS_ACTIVE_HOLDS; при успехе Status = Archived и DeletedAt проставляются в одном согласованном изменении.

A6. Проект-реплика Talent

  1. Для проекта с SyncSource = TalentChessboard правка реплика-полей (Name, Type, Class, ConstructionStatus, гео-поля CountryId/RegionId/RegionName/CityId/CityName, Stars, CoverImageFileId, address-часть Location) → CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED (INV-PRJ-9). PIN-поля (описание, финансирование, юрданные, ход строительства, видео) и публикация — доступны.
  2. Синк резолвит адресные строки Talent в ссылки справочника name-match-ем по реплике (../consumers/talent-chessboard-sync.md); нерезолвленная ссылка (RegionId/CityId = null + warn-метрика) не блокирует применение синка, но блокирует публикацию проекта (INV-PRJ-3) до ручного разрешения конфликта синка администратором платформы.

Постусловие

  • ProjectEntity существует: Status = Published, TenantId = DeveloperId, ссылки справочника RegionId/CityId резолвлены и display-имена RegionName/CityName сняты из реплики, пять typed-jsonb документов заполнены, Version инкрементирован на каждую мутацию.
  • Доставлен pin.chessboard.project-upserted.v1; catalog показывает проект в ленте, dev-stats завёл измерение Project, identity может назначать scope «проект».
  • Price/PropertyObjectsTotalCount/PropertyObjectsFreeCount пока null/0 — заполнятся job-ом project-aggregates-recalc после добавления объектов недвижимости (UC-CHS-002).

Интеграционная последовательность и согласованность

Публикация и появление в ленте — асинхронны

Команда publish в одном согласованном изменении фиксирует переход Draft/Hidden → Published и кладёт project-upserted в outbox. Менеджер сразу видит бейдж «Опубликован» в кабинете (source of truth — chessboard), а публичная лента catalog догоняет в пределах SLA (< 1 мин). Промежуточно проект уже опубликован в системе, но ещё не виден в публичной ленте — это ожидаемое окно eventual-consistency, не ошибка.

Порядок потребления

catalog/dev-stats/identity/news получают одно и то же событие; каждый держит монотонный Version-guard по ProjectId — повтор или переупорядоченное старое событие отбрасывается. identity заводит scope-объект «проект» из этого же события: назначение record-level прав на новый проект возможно после доставки (окно < 5 мин), до этого scope на проект недоступен.

Недоступность потребителя

Если consumer временно недоступен, событие не теряется (outbox → at-least-once) и догоняет после восстановления; ядовитое сообщение уходит в DLQ потребителя (алерт), расхождение закрывается ресинком из chessboard. Отказ доставки события не откатывает уже зафиксированную публикацию — согласованность восстанавливается вперёд (eventual), а не откатом.

Агрегаты проекта появляются позже

Price/PropertyObjectsTotalCount/PropertyObjectsFreeCount заполняются job-ом project-aggregates-recalc только после добавления объектов недвижимости (UC-CHS-002); до этого карточка показывает проект без диапазона цен — это корректное промежуточное состояние.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
CHESSBOARD_DEVELOPER_NOT_FOUND404Профиль застройщика не создан (INV-PRJ-2)Показать «профиль компании ещё не активирован», обратиться к ПИН
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE422INV-PRJ-3: не заполнены Name/Description/Cover/Geo, не резолвлены RegionId/CityId либо Developer.Status != PublishedПодсветить незаполненные поля из details
CHESSBOARD_PROJECT_HOTELIER_REQUIRED422INV-PRJ-4 при публикации отеляОткрыть выбор отельера (lookup)
CHESSBOARD_PROJECT_BANK_NOT_FOUND422INV-PRJ-7: id банка не существует/архивированОбновить lookup-список банков, поправить выбор
CHESSBOARD_GEO_REF_UNKNOWN422INV-PRJ-10: ссылка справочника (CountryId/RegionId/CityId/CurrencyId) не найденаОбновить lookup справочника, поправить выбор
CHESSBOARD_GEO_REF_ARCHIVED422INV-PRJ-10: указана архивная запись справочникаВыбрать действующую запись справочника
CHESSBOARD_CITY_REGION_MISMATCH422INV-PRJ-10: гео-цепочка город/регион/страна не сходитсяСогласовать выбор города и региона
CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED409INV-PRJ-9: правка реплика-поля Talent-проектаПоказать «поле управляется синхронизацией Talent»
CHESSBOARD_PROJECT_HAS_ACTIVE_HOLDS409INV-PRJ-8: архив при активных Резерв/БроньПоказать список активных холдов, дождаться release
CHESSBOARD_PROJECT_TENANT_MISMATCH403Мутация чужого проектаОшибка доступа; не ретраить
— (validation)400Форматы/диапазоны полей (шаг 2 валидации)Подсветить поля по ошибке валидации
— (concurrency)409RowVersion устарел (A4)Перечитать агрегат, повторить

Доменные События

СобытиеКогда поднимаетсяSubscribers
ProjectCreatedEventУспешное создание (шаг 4)in-proc (аудит, счётчик Developer.ProjectsPublishedCount при последующей публикации)
ProjectStatusChangedEventКаждый переход Status (publish/hide/unhide/archive)in-proc (пересчёт ProjectsPublishedCount застройщика)

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
ProjectUpsertedKafkaEvent.v1pin.chessboard.project-upserted.v1Создание, значимые обновления, каждый переход статуса (в той же транзакции)catalog (лента/карточка), dev-stats (измерение Project), identity (scope «группа проектов/проект»), news (привязка постов)

Acceptance Criteria

#КритерийПроверка
AC1Draft-проект не виден в публичной ленте; Published — виденинтеграционный тест public query до/после publish
AC2Публикация без Description/Cover/Geo либо с нерезолвленными RegionId/CityId отклоняется кодом CHESSBOARD_PROJECT_PUBLISH_INCOMPLETEunit-тест публикации по матрице заполненности
AC3Публикация отеля без HotelierId отклоняется (INV-PRJ-4); с отельером — проходитunit-тест по Type
AC4Financing с несуществующим банком не сохраняется (INV-PRJ-7)тест с фейковым id банка
AC5Менеджер компании A не может создать/править проект компании Bтест tenant-изоляции
AC6Каждая мутация публикует project-upserted в той же транзакцииoutbox-тест: rollback команды → события нет
AC7Правка реплика-поля Talent-проекта отклоняется, PIN-поля правятсятест INV-PRJ-9
AC8Конкурентная правка даёт 409 без потери данныхтест двух параллельных правок с одним RowVersion
AC9Ссылка на несуществующую/архивную запись справочника и рассогласованная гео-цепочка отклоняются кодами INV-PRJ-10тест валидации по реплике (unknown / archived / city-region mismatch)

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

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

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