UC-CHS-001. Создание и публикация проекта застройщиком
Назначение
Менеджер застройщика заводит новый проект (ЖК или апарт-отель) в шахматке: заполняет карточку (тип,
класс, регион, локация, инфраструктура, финансирование, юрданные, ход строительства, видео, обложка) и
публикует его. После публикации (Status = Published) проект попадает в публичную ленту каталога и в
измерения статистики застройщика. Сценарий относится к контексту chessboard.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CHS-001 |
| Контекст | chessboard (см. docs/06-services/chessboard/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Девелопер (менеджер застройщика: внутренние роли «администратор проекта», «маркетолог») |
| Permissions / scope | chessboard.projects.manage; scope = организация / группа проектов (каталог и назначения — ../../identity/domain/permission.md) |
| Tenant | TenantId = 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 своей организации.
Триггер
| Поле | Значение |
|---|---|
| Источник | Действие пользователя в кабинете застройщика |
| Триггерящий элемент | Кабинет застройщика, страница «Проекты» → «Создать проект»; далее вкладки карточки → «Опубликовать» |
Основной Поток
Создание проекта
- Менеджер создаёт проект (
POST /developers/projects), заполняя обязательный минимум:Name,Type(ProjectType),Class(ProjectClass),ConstructionStatus; гео и валюта — ссылки на справочникhandbook:CountryId(по умолчанию — страна «Россия»),RegionId/CityId(можно указать позже, но обязательны к публикации — INV-PRJ-3),CurrencyId(по умолчанию — валюта «RUB»; RUB-only, ADR-0048). - Проверяется вход:
Name1..256; значенияType/Class/ConstructionStatus— из справочника reference-data.md;Stars1..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снимаются из реплики при записи. - Профиль застройщика по tenant-у актора должен существовать и быть не удалён; иначе —
CHESSBOARD_DEVELOPER_NOT_FOUND(INV-PRJ-2). - Создаётся проект:
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), дубликаты чистятся вручную. - Результат:
201 Created—Id,Status = Draft,Version.
Заполнение карточки
- Менеджер заполняет вкладки карточки — общие поля (
PUT /developers/projects/{id}) и пять jsonb-документов (PUT /developers/projects/{id}/{location|infrastructure|financing|legal-info|construction-progress}). Для каждой вкладки:- вход проверяется по схемам jsonb-моделей project.md: диапазоны
GeoPoint, ≤ 20TransportNodes, patternInn/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).
- вход проверяется по схемам jsonb-моделей project.md: диапазоны
Публикация
-
Менеджер нажимает «Опубликовать» (
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. - INV-PRJ-3: заполнены
-
Потребители (
catalog,dev-stats,identity,news) получаютpin.chessboard.project-upserted.v1; проект появляется в публичной ленте каталога, карточка показывает бейдж «Опубликован».
Альтернативные Потоки
A1. Публикация с незаполненными обязательными полями
- Guard INV-PRJ-3 не пройден →
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETEс перечнем незаполненных полей в деталях ошибки. - Фронт подсвечивает вкладки с пробелами. Возврат к заполнению (шаг 6 основного потока).
A2. Апарт-отель без отельера
Type in (ApartHotel, Hotel)при пустомHotelierId→CHESSBOARD_PROJECT_HOTELIER_REQUIRED(INV-PRJ-4).- Менеджер выбирает отельера из lookup-списка (
GET /developers/hoteliers/lookup, permissionchessboard.references.lookup) и повторяет публикацию.
A3. Несуществующий/архивный банк в финансировании
- Хотя бы один банк из
Financingне существует или архивирован →CHESSBOARD_PROJECT_BANK_NOT_FOUND(INV-PRJ-7); вкладка «Финансирование» не сохраняется. Возврат на шаг 6.
A4. Конкурентная правка (два менеджера)
- Переданная версия записи (
RowVersion) не совпала →409 Conflict; фронт перечитывает проект и предлагает повторить правку поверх свежей версии.
A5. Скрытие/архив опубликованного проекта
POST .../hide→Published → Hidden(проект и объекты недвижимости пропадают из public-проекций, данные целы);POST .../unhide— обратно (guard:Developer.Status = Published).POST .../archive→ guard INV-PRJ-8: архив запрещён при объектах недвижимости вReserved/Booked→CHESSBOARD_PROJECT_HAS_ACTIVE_HOLDS; при успехеStatus = ArchivedиDeletedAtпроставляются в одном согласованном изменении.
A6. Проект-реплика Talent
- Для проекта с
SyncSource = TalentChessboardправка реплика-полей (Name,Type,Class,ConstructionStatus, гео-поляCountryId/RegionId/RegionName/CityId/CityName,Stars,CoverImageFileId, address-частьLocation) →CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED(INV-PRJ-9). PIN-поля (описание, финансирование, юрданные, ход строительства, видео) и публикация — доступны. - Синк резолвит адресные строки 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_FOUND | 404 | Профиль застройщика не создан (INV-PRJ-2) | Показать «профиль компании ещё не активирован», обратиться к ПИН |
CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE | 422 | INV-PRJ-3: не заполнены Name/Description/Cover/Geo, не резолвлены RegionId/CityId либо Developer.Status != Published | Подсветить незаполненные поля из details |
CHESSBOARD_PROJECT_HOTELIER_REQUIRED | 422 | INV-PRJ-4 при публикации отеля | Открыть выбор отельера (lookup) |
CHESSBOARD_PROJECT_BANK_NOT_FOUND | 422 | INV-PRJ-7: id банка не существует/архивирован | Обновить lookup-список банков, поправить выбор |
CHESSBOARD_GEO_REF_UNKNOWN | 422 | INV-PRJ-10: ссылка справочника (CountryId/RegionId/CityId/CurrencyId) не найдена | Обновить lookup справочника, поправить выбор |
CHESSBOARD_GEO_REF_ARCHIVED | 422 | INV-PRJ-10: указана архивная запись справочника | Выбрать действующую запись справочника |
CHESSBOARD_CITY_REGION_MISMATCH | 422 | INV-PRJ-10: гео-цепочка город/регион/страна не сходится | Согласовать выбор города и региона |
CHESSBOARD_PROJECT_FIELD_SYNC_MANAGED | 409 | INV-PRJ-9: правка реплика-поля Talent-проекта | Показать «поле управляется синхронизацией Talent» |
CHESSBOARD_PROJECT_HAS_ACTIVE_HOLDS | 409 | INV-PRJ-8: архив при активных Резерв/Бронь | Показать список активных холдов, дождаться release |
CHESSBOARD_PROJECT_TENANT_MISMATCH | 403 | Мутация чужого проекта | Ошибка доступа; не ретраить |
| — (validation) | 400 | Форматы/диапазоны полей (шаг 2 валидации) | Подсветить поля по ошибке валидации |
| — (concurrency) | 409 | RowVersion устарел (A4) | Перечитать агрегат, повторить |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
ProjectCreatedEvent | Успешное создание (шаг 4) | in-proc (аудит, счётчик Developer.ProjectsPublishedCount при последующей публикации) |
ProjectStatusChangedEvent | Каждый переход Status (publish/hide/unhide/archive) | in-proc (пересчёт ProjectsPublishedCount застройщика) |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
ProjectUpsertedKafkaEvent.v1 | pin.chessboard.project-upserted.v1 | Создание, значимые обновления, каждый переход статуса (в той же транзакции) | catalog (лента/карточка), dev-stats (измерение Project), identity (scope «группа проектов/проект»), news (привязка постов) |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Draft-проект не виден в публичной ленте; Published — виден | интеграционный тест public query до/после publish |
| AC2 | Публикация без Description/Cover/Geo либо с нерезолвленными RegionId/CityId отклоняется кодом CHESSBOARD_PROJECT_PUBLISH_INCOMPLETE | unit-тест публикации по матрице заполненности |
| AC3 | Публикация отеля без HotelierId отклоняется (INV-PRJ-4); с отельером — проходит | unit-тест по Type |
| AC4 | Financing с несуществующим банком не сохраняется (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) |
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: создание проекта, обновление проекта, публикация проекта, снятие с публикации, поиск/список проектов, карточка проекта, lookup справочников банков/отельеров.
- Domain entity: ../domain/project.md (поля, инварианты INV-PRJ-1..10, stateDiagram), ../domain/developer.md, ../domain/bank.md, ../domain/hotelier.md, ../domain/reference-data.md, ../domain/handbook-replica.md (валидация ссылок справочника, INV-PRJ-10).
- Background: ../background/project-aggregates-recalc.md — денормализация цен/счётчиков.
- Связанные UC: UC-CHS-002 (здания и объекты недвижимости), UC-CHS-004 (публичный просмотр), UC-CHS-005 (синк Talent).
- Решения: ADR-0052 (tenancy), ADR-0056 (simplicity-first).