Направление работ: Back Код раздела: 2.2.1 Объекты и предложения > Площадки партнеров > Площадки связанные одним расписанием Статус: Готов Ответственный: Панкова Л.А. Версия: 0.1 Обновлено: 29.06.26
1. Назначение
Раздел описывает связанные площадки, которые представляют одно фактическое помещение и используют общее расписание занятости.
Система синхронизирует занятость календаря между такими площадками, чтобы бронирование или удаление события в одной площадке корректно отражалось на остальных площадках группы.
2. Ключевые понятия
Связанные площадки — несколько площадок, которые представляют одно фактическое помещение в одной или нескольких сферах и используют общее расписание занятости.
Предзаказ — служебная запись, которая закрывает время в календаре занятости связанной площадки при бронировании другой площадки из той же группы.
Процессинговый статус — статус события, при котором удаление связанных предзаказов блокируется.
3. Scope
Входит
S1 Связанные площадки: группа площадок, которые представляют одно фактическое помещение и используют общее расписание занятости.
S2 Процессы синхронизации расписания: создание бронирования, удаление события, блокировка удаления при процессинговом статусе, отмена события во внешнем синхронизированном календаре.
S3 Ограничение внешней синхронизации: правило подключения стороннего календаря для группы связанных площадок.
Не входит
-
Back: 2.2 Площадки партнеров
-
Back: 3.2 Процессинг заказов
-
Back: 6. Интеграции
-
WebLk: 4.2 Площадки
-
WebLk: 3.7 Интеграции
4. Требования
Правила и ограничения бизнеса на уровне данных, источник истины в системе
Открытые вопросы
BRD 2.2.1 v0.1 · репозиторий
booking-monorepo· commit83423b21· веткаmain· дата сверки 2026-07-15 Якорь:src/services/booking.partner-management(связь площадок,SharedRoom) +src/services/booking.orders(предзаказы = shared orders) +src/services/booking.integrations(внешние календари, оркестрация событий) +src/libs/booking.domain.abstractions(контракты,OrderStatus) +src/libs/booking.messaging.broker(inbox/дедупликация). Маршрутизация без сверки с гайдбуком — гайдбук раздела не передавался, поэтому отнесение вопроса к бизнесу или разработке основано только на характере расхождения. Адресаты: бизнесовые → Бизнес (Герман), программные/технические → Разработка (Владислав). Сводка: вопросов 15 — Open бизнесу 7 / Open разработке 7 / Закрывает PM 1. Требований подтверждено без вопросов: 0 из 5 (механизм создания предзаказов по BR2.2.1-2 реально работает, но с оговорками — см. OQ2.2.1-9, OQ2.2.1-10). Отрезано находок обратного прохода: 6 (перечислены под таблицей). Под особым вниманием (все требования раздела веса 1): OQ2.2.1-1 — «процессингового статуса» в коде не существует, ключевое исключение BR2.2.1-3/-4 не обеспечено ничем; OQ2.2.1-8 — при отмене заказа система не снимает, а пересоздаёт предзаказы в связанных площадках; OQ2.2.1-5 — инварианта «один календарь на группу» нет ни в коде, ни в БД.
| ID | Источник | Вопрос | Кому | Статус | Рекомендации | Итоговое решение |
|---|---|---|---|---|---|---|
| OQ2.2.1-1 | BR2.2.1-3, BR2.2.1-4 | Требование говорит: если одно из связанных событий находится в «процессинговом статусе», удаление связанных предзаказов блокируется — это ключевое исключение раздела. В коде: такого статуса не существует. У заказа ровно 8 статусов — PreReserv, Reserv, PayedHalf, PayedFull, Close, Cancel, CancelWithCollection, None; значения «Processing» среди них нет (src/libs/booking.domain.abstractions/.../Enums/Orders/OrderStatus.cs:6-47). Похожий по имени ProcessingOrderGroup — другая сущность: группировка заказов (абонементы) со своей таблицей и своими полями, к статусу заказа отношения не имеет (booking.orders/.../ProcessingGroupAggregate/ProcessingOrderGroup.cs:13; OrderGroupType.cs:3-7). Предзаказы при снятии архивируются безусловно — статус не читается вообще (UpdateSharedOrdersCommandHandler.cs:59-63; OrderAggregate.cs:408-429). Что отсутствие проверки не случайно, видно по контрасту: соседний метод Cancel() в том же агрегате статус проверяет (OrderAggregate.cs:444), а Archive() — нет. Итог: предзаказ будет снят независимо от того, оплачен он или подтверждён. | Бизнес (Герман) | Open | Нужно бизнес-определение: какие статусы заказа делают предзаказ защищённым от снятия. Вариант А (рекоменд.): зафиксировать перечень в BRD (вероятный кандидат — Reserv/PayedHalf/PayedFull/Close) и реализовать проверку (см. OQ2.2.1-11). Вариант Б: если блокировка фактически не нужна (предзаказ служебный и снимается всегда) — убрать условие «кроме процессингового статуса» из BR2.2.1-3/-4 и из раздела «Ключевые понятия». Сначала проверьте OQ2.2.1-7: возможно, искомое правило уже частично живёт в ветке внешней отмены. Пока вопрос открыт, BR2.2.1-3 и BR2.2.1-4 нереализуемы. | Основной процессинговый статсу - Reserv. PayFull & PayHalf относятся к оплате заказа и не присваиваются ему в рамках статуса. Используются только в проверках при отемене (штрафы). UpdateOrder сам по себе вообще не архивирует заказы. Архивация «копий» (shared-заказов) происходит отдельно — и только когда в основном заказе меняют площадку/комнату. Это логично: старая комната больше не нужна, копии с неё снимаются. Сейчас эта архивация выполняется вслепую, без проверки статуса копии. Даже если копия уже находится в «процессинговом» статусе (Reserv — то есть с ней уже идёт работа/оплата), она всё равно будет заархивирована. Требование же говорит: «если связанное событие в процессинге, удаление должно блокироваться». Обычно копии создаются как предзаказы (PreReserv), поэтому риск минимален. Но если вдруг копия по какой-то причине перешла в Reserv, текущая логика её молча уберёт, нарушив правило. Что делать: Нужно добавить защиту: перед архивацией проверять, нет ли среди копий заказов в статусе Reserv. Если есть — операцию блокировать целиком. Это закроет требование и исключит случайную потерю живого заказа. |
| OQ2.2.1-2 | BR2.2.1-1 | Требование говорит: связанные площадки представляют одно фактическое помещение — из чего следует, что группа взаимна: если A и B — одна комната, то и A знает про B, и B про A. В коде: связь хранится как направленная одиночная запись (room_id → shared_room_id) и не симметрична. При сохранении группы для площадки A пишутся только рёбра «из A»: удаляются строки WHERE room_id = A и вставляются новые, где room_id всегда равен редактируемой площадке (RoomRepository.cs:178-197; RoomMapper.cs:55-60). Обратное ребро «из B в A» не создаётся никогда. Чтение группы тоже возвращает только исходящие рёбра (RoomReadOnlyRepository.cs:935-936). Практическое следствие: если A связали с B, то запрос группы для B вернёт пустой список — группа «общая» лишь тогда, когда каждая площадка независимо перечислила всех остальных вручную. Границ у связи тоже нет: не проверяется, что площадки относятся к одному объекту или одной партнёрской зоне (UpdateRoomCommandHandler.cs:106-111) — связать можно любые две площадки, в том числе из разных зон. | Бизнес (Герман) | Open | Продуктовое решение «что такое одно помещение». Вариант А (рекоменд.): признать связь взаимной (хранить группу как множество либо писать оба ребра) и задать границу — например, одна партнёрская зона, при этом сферы допускаются разные, как и написано в BRD. Вариант Б: оставить связь направленной, но тогда честно описать в BRD, что взаимность и непротиворечивость группы — ручная ответственность того, кто настраивает площадки. От этого решения напрямую зависят OQ2.2.1-9 (полнота предзаказов) и OQ2.2.1-5 (что считать «группой» для правила про календарь). | Рекомендация А подходит, если “связанные площадки” действительно должны быть одной взаимной группой. Тогда лучше хранить группу как множество или писать оба ребра, плюс валидировать границу. Рекомендация Б подходит только если бизнес готов принять направленную ручную настройку, но тогда BRD надо честно сузить. |
| OQ2.2.1-3 | BR2.2.1-1 | Требование говорит: связанные площадки используют общее расписание занятости. В коде: «связанные площадки» — это только список идентификаторов (RoomAggregate.cs:136), никакого общего расписания у группы нет. Рабочие часы синхронизируются только между площадкой и её объектом (base): сужение часов объекта зажимает часы площадки, расширение — нет (RoomAggregate.cs:310, правила в комментарии :303-309). Внутри группы связанных площадок синхронизации часов нет вообще — в тестах синхронизации часов группа всегда пуста (PartnerZones.Tests/RoomAggregateSyncHoursTests.cs:46). Общая занятость достигается косвенно — через создание предзаказов на то же время (BR2.2.1-2), а не через общий режим работы. | Бизнес (Герман) | Open | Уточнить значение термина. Вариант А (рекоменд.): признать, что «общее расписание» = синхронизация занятости через предзаказы (как в коде), и явно зафиксировать это в BR2.2.1-1 / «Ключевых понятиях», чтобы не подразумевались общие рабочие часы. Вариант Б: если у группы должны быть и общие рабочие часы/режим работы — это отдельная незакрытая доработка в partner-management, её нужно ставить в план и описывать отдельным требованием. | Фактическое “общее расписание занятости” достигается не общими рабочими часами, а shared-предзаказами: при создании заказа создаются PreReserv-копии на связанных площадках в CreateSharedOrdersCommandHandler.cs. Синхронизации часов изначально не предполагалось, т.к. площадки могут работать назависимо друг от друга по часам. Например: площадка А работает с 10 до 15, площадка Б работает с 16 до 24 (при условии, что это одно помещение). У этих двух площадок могут быть совершенно разные часы работы, которые могут быть связаны с сферой, базой или просто желанием владельца. Едиснтвенное, что должно их связывать это как раз заказы. Потому что если, например, владелец действительно настроил площадки так, что они работают по один и ем же часа в разных сферах, то как раз по заказам и формируется общее закрытое время, чтобы не было овербукига |
| OQ2.2.1-4 | BR2.2.1-3, BR2.2.1-4 | Требование говорит: система удаляет связанные предзаказы / удаляет копии события. В коде: физического удаления нет нигде — в сервисе заказов вообще отсутствуют команды и методы удаления заказа (в Orders.Application/Commands нет ни одной папки delete/remove; в репозитории нет Delete/Remove, а единственный ExecuteDeleteAsync во всём сервисе относится к перестройке read-модели иерархии зон, не к заказам — PartnerZoneHierarchyRepository.cs:27). «Удаление» реализовано как мягкая архивация IsArchived = true (OrderAggregate.cs:408-429), после которой запись остаётся в базе. Побочный эффект: архивные предзаказы продолжают попадать в выборку копий — фильтра по IsArchived в ней нет (OrderRepository.cs:74). | Бизнес (Герман) | Open | Вариант А (рекоменд.): согласовать, что архивация предзаказа = снятие брони из календаря занятости (запись остаётся для аудита), и привести формулировку BRD к «архивируются». Вариант Б: если нужна именно очистка данных — добавить физическое удаление, но это противоречит общему подходу сервиса, где заказы не удаляются физически нигде. Уточните также, должно ли снятое время немедленно освобождаться в календаре занятости — это проверяется отдельно от флага архивности. | Бизнес изменил БРД. |
| OQ2.2.1-5 | BR2.2.1-5 | Требование говорит: для группы связанных площадок подключение и синхронизация со сторонним календарём доступны только через одну площадку. В коде: инварианта «один календарь на группу» нет ни в C#, ни в БД. Все проверки — на уровне отдельной площадки: «этот внешний календарь не привязан к другой площадке» (CalendarSyncRepository.cs:130-136 — сравнение идёт с одним roomId, списка комнат группы в сигнатуре нет) и «эта площадка ещё не интегрирована» (FinishGoogleCalendarRegistrationCommandHandler.cs:69-72). Группа связанных площадок при подключении календаря вообще не запрашивается: GetSharedRoomsAsync вызывается только в четырёх консьюмерах заказов и ни в одной команде подключения календаря. В базе уникален только calendar_context (= RoomId), индекс на room_id не уникальный, а google_calendar_id не индексирован вовсе (Integrations.Persistence/Migrations/20250824065018_Initial.cs:304-323). YClients — отдельная ветка команд со своими проверками (по зоне, площадке, StaffId), группу тоже не учитывает (BindRoomToYClientsCommandHandler.cs:41-109). Итог: к разным площадкам одной группы можно независимо подключить разные внешние календари. | Бизнес (Герман) | Open | Вариант А (рекоменд.): подтвердить правило и обеспечить его в integrations — при подключении календаря запрашивать группу связанных площадок и запрещать подключение, если у любой площадки группы календарь уже есть. Вариант Б: если ограничение фактически не требуется — смягчить BR2.2.1-5. Требование зависит от OQ2.2.1-2: пока связь несимметрична, «группа» определена неоднозначно и инвариант «на группу» невозможно сформулировать однозначно. Отдельно отметьте для разработки: даже существующая проверка «календарь занят другой площадкой» держится только на C#-коде без блокировки, поэтому при параллельном подключении возможна гонка. | Текущее поведение интеграций устроено по площадке, не по группе. Тут мы снова возвращаемся к вопросу связанности площадки, который я расписывала выше |
| OQ2.2.1-6 | BR2.2.1-2, BR2.2.1-3 | Требование говорит: (производное, в BRD не описано) состав группы связанных площадок может меняться задним числом — площадку добавляют или убирают из группы, когда брони на неё уже существуют. В коде: при изменении состава группы у существующих заказов не пересобирается ничего. Сервис интеграций на событие изменения площадки только сбрасывает кэш списка связанных площадок и на этом останавливается (RoomUpdatedImtegrationsComsumer.cs:26-32) — недостающие предзаказы для ранее сделанных броней не создаются, лишние не снимаются. В самом partner-management при обновлении площадки помечается «грязной» только она сама, связанные площадки — нет (UpdateRoomCommandHandler.cs:141). Итог: предзаказы синхронизируются только в момент бронирования/отмены, ретроспективное изменение группы оставляет рассинхрон занятости. | Бизнес (Герман) | Open | Вариант А: зафиксировать в BRD, что синхронизация работает только «вперёд», а состав группы меняется до появления броней — задокументировать как осознанное ограничение. Вариант Б (рекоменд., если важна корректность занятости): при изменении состава группы пересобирать предзаказы по активным броням за будущий период. Решение продуктовое: стоимость доработки против риска двойного бронирования одного физического помещения. | UpdateOrder сам родительский заказ не архивирует. В команде нет поля Archive: UpdateOrderCommand.cs:11. Единственное изменение статуса в UpdateOrder здесь: если asReserve = true, ставится OrderStatus.Reserv: UpdateOrderCommandHandler.cs:191, UpdateOrderCommandHandler.cs:193. Потом вызывается orderAggregate.Update(…), публикуется OrderUpdatedEvent: UpdateOrderCommandHandler.cs:207, UpdateOrderCommandHandler.cs:394. проблема “архивация без проверки статуса” технически есть, но в нормальном сценарии связанные копии создаются именно как PreReserv: |
| OQ2.2.1-7 | BR2.2.1-4 | Требование говорит: при отмене события во внешнем календаре система удаляет его копии в связанных площадках; копии удаляются, если не находятся в процессинговом статусе. В коде: статусный фильтр при внешней отмене есть, но применяется к основному заказу, а не к копиям: PreReserv → архивация; Reserv/Close → отмена; все остальные статусы — в том числе PayedHalf и PayedFull — отмена молча игнорируется, а маппинг события помечается как успешный (ProcessGoogleCalendarEventsCommandHandler.cs:211-237). То есть если событие удалили в Google, а заказ оплачен, в MusBooking он остаётся живым, и событие в Google обратно не восстанавливается (коррекция назад есть только в ветке PreReserv, :220-225). Сами копии при этом снимаются безусловно, без проверки их статусов (UpdateSharedOrdersCommandHandler.cs:59-63). Это ближайшее к «процессинговому статусу» место во всём коде — возможно, именно оно и имелось в виду в BRD. | Бизнес (Герман) | Open | Подтвердить: считается ли этот фильтр (оплаченные заказы защищены от внешней отмены) искомым «процессинговым статусом»? Если да — OQ2.2.1-1 закрывается ссылкой на него, перечень статусов переносится в BRD, а правило распространяется на копии (сейчас оно живёт только на родителе). Если нет — правило нужно определять с нуля. Отдельно решите продуктовое расхождение «в Google событие удалено, в MusBooking заказ жив»: восстанавливать событие обратно в Google (как в ветке PreReserv) или уведомлять партнёра о конфликте — сейчас не делается ни то, ни другое. | текущий механизм как раз такой: создаются технические PreReserv-копии, а проверка занятости исключает отмененные и архивные заказы: OrderRepository.cs:379, OrderRepository.cs:385. Тогда рекомендация: в BRD писать не “общее расписание”, а “общая занятость через технические предзаказы”. Если нужны именно общие рабочие часы, это отдельная доработка partner-management. |
| OQ2.2.1-8 | BR2.2.1-3 | Требование говорит: при удалении события в одной из связанных площадок система должна удалять связанные предзаказы в остальных. В коде при отмене заказа происходит обратное — предзаказы пересоздаются. Порядок в обработчике: сначала читается список существующих копий (UpdateSharedOrdersCommandHandler.cs:52), затем вызывается CreateOnRoomChange (:53), который при Archive = true и неархивированном родителе создаёт новый комплект живых копий в статусе PreReserv на то же время (:96, :113-137), и только потом архивируются старые (:55-74). Новые копии в цикл архивации не попадают — список прочитан раньше их создания. Отмена даёт ровно эту комбинацию: Cancel() запрещён для архивированных заказов (OrderAggregate.cs:449-452), значит родитель в момент отмены не архивирован, а консьюмер отмены передаёт archive: true безусловно (OrderCancelledIntegrationsConsumer.cs:55-62). Механизм задумывался под смену площадки, где флаг archive означает «RoomId изменился» (OrderUpdatedIntegrationsConsumer.cs:73), но ветка отмены переиспользует тот же флаг с другим смыслом. Итог: после отмены брони время в связанных площадках остаётся закрытым новыми предзаказами. При архивации заказа (родитель уже архивирован) эффекта нет — копии просто снимаются. | Разработка (Владислав) | Open | Вывод сделан чтением кода, на стенде не воспроизводился — первым шагом воспроизвести сценарий «отменить бронь в площадке группы → проверить занятость связанной площадки». Если подтверждается: разделить смыслы флага archive (снятие копий против переноса копий при смене площадки) — например, ввести отдельный флаг переноса или условие command.Archive && RoomChanged, вместо !parentOrder.ArchiveInfo.IsArchived. Смежный техдолг: создание предзаказа продублировано копипастой в двух местах (CreateSharedOrdersCommandHandler.cs:70-92 и UpdateSharedOrdersCommandHandler.cs:113-137) — при правке одного расходятся. Приоритет высокий: ломается ядро BR2.2.1-3 (вес 1). | удаление есть только для non-processing group/read-model, не для заказов, у нас во всем проекте единичне сущности вообще удаляются, в большинстве своем мы все архивируем “Удаление” связанных предзаказов фактически означает soft archive через IsArchived = true: OrderAggregate.cs:416. Это скорее вопрос формулировки BRD, а не разработки. Запись остается для аудита, но занятость освобождается, потому что ExistsAsync фильтрует IsArchived == false: OrderRepository.cs:387. Техдолг есть: выборка связанных копий не фильтрует архивные записи: OrderRepository.cs:73. Это не держит время занятым, но может давать лишнюю обработку. |
| OQ2.2.1-9 | BR2.2.1-2 | Требование говорит: при добавлении бронирования в одну из связанных площадок время закрывается в календарях остальных связанных площадок. В коде: механизм реализован и работает — на событие создания заказа сервис интеграций запрашивает связанные площадки и создаёт предзаказы (PreReserv, на то же время родителя, со ссылкой на родительский заказ, с комментарием «Копия из {RoomName}») во всех найденных площадках (OrderCreatedIntegrationsConsumer.cs:50-56; CreateSharedOrdersCommandHandler.cs:70-92). Но список площадок берётся только как исходящие связи бронируемой площадки — GetSharedRoomsAsync(order.RoomId) разворачивается в WHERE room_id = X (RoomReadOnlyRepository.cs:935-936). Из-за направленности связи (см. OQ2.2.1-2) бронирование в площадке B создаст предзаказы только в тех площадках, которые перечислены в списке самой B; если симметрия не настроена вручную, часть группы предзаказ не получит и время там останется свободным. | Тестирование | Open | Техническое следствие OQ2.2.1-2, самостоятельно не решается. После продуктового решения по симметрии: строить список связанных площадок для предзаказов по всей группе (в обе стороны связи), а не по исходящим рёбрам бронируемой площадки. Если по решению бизнеса связь остаётся направленной — явно задокументировать это ограничение в реализации и в BRD, поскольку «остальные связанные площадки» из требования и фактический список в коде тогда не совпадают. | Проверить реализацию на тестировании. |
| OQ2.2.1-10 | BR2.2.1-2 | Требование говорит: (контракт создания предзаказов) в остальных связанных площадках создаются предзаказы, закрывающие время. В коде: защиты от повторного создания нет — обработчик не проверяет, существуют ли уже копии этого родительского заказа, и каждый вызов создаёт заказы с новым Guid.NewGuid() (CreateSharedOrdersCommandHandler.cs:36-103, :72). Единственная защита — от бесконечной цепочки копий (:60-64). Идемпотентность держится только на inbox-дедупликации по паре (MessageId, endpoint) в окне 30 секунд (booking.messaging.broker/DependencyInjection.cs:219, :38), но при исключении в консьюмере inbox-запись переводится в Failed, а при ретрае сообщение обрабатывается заново целиком (:239-248, :274-277); вызов создания копий при этом идёт вне транзакции (OrderCreatedIntegrationsConsumer.cs:55 против BeginTransactionAsync на :64) и откатом не снимается. Любое падение после создания копий даёт при ретрае второй комплект. В БД дубли тоже ничем не предотвращены: уникального индекса по (shared_order_id, room_id) нет. Дополнительно: колонка shared_order_id не проиндексирована (OrderAggregateConfiguration.cs:23), хотя выборка копий идёт именно по ней (OrderRepository.cs:74), и внешнего ключа на родительский заказ нет — висячие ссылки возможны. | Разработка (Владислав) | Open | Вариант А (рекоменд.): добавить в обработчик проверку «для этого родительского заказа копии уже созданы» (или уникальный индекс по shared_order_id + room_id), перенести вызов создания копий внутрь транзакции консьюмера и добавить индекс на shared_order_id — закрывает дубли и ускоряет каскад при снятии. Вариант Б: положиться только на inbox-дедупликацию — приемлемо лишь при гарантированно ровно-однократной обработке, чего в текущей схеме с ретраем Failed нет. Дубли предзаказов видны партнёру как ложная занятость, поэтому вопрос стоит закрыть вместе с OQ2.2.1-8. | Как будто лучше не пытаться “перенести внутрь транзакции консьюмера” как основную защиту, потому что это другой сервис и другая БД/транзакция. Правильнее сделать CreateSharedOrders идемпотентной на стороне orders-service: перед созданием читать существующие копии по SharedOrderId, создавать только недостающие, добавить индекс по shared_order_id, и лучше уникальный частичный индекс на активную пару (shared_order_id, room_id) с учетом is_archived = false. Это закроет и retry, и повторный ручной вызов. |
| OQ2.2.1-11 | BR2.2.1-3, BR2.2.1-4 | Требование говорит: (реализация блокировки по процессинговому статусу) удаление предзаказов блокируется, если событие в процессинговом статусе. В коде: место, где эта блокировка должна жить, — цикл снятия копий: он перебирает все предзаказы родительского заказа и архивирует каждый при archive = true без единого условия (UpdateSharedOrdersCommandHandler.cs:55-63). Проверять статус нужно именно здесь. Рядом два смежных дефекта того же цикла: возвращаемое значение Archive() игнорируется — wasUpdated ставится безусловно (:61-62), поэтому уже архивированные копии снова попадают в список обновлённых и по ним повторно публикуются события (:76-80); а выборка копий не фильтрует архивные (OrderRepository.cs:74), из-за чего они перебираются снова и снова. | Разработка (Владислав) | Open | Реализация OQ2.2.1-1, до его закрытия не начинать: перечень «процессинговых» статусов задаёт бизнес. После этого — внедрить фильтр непосредственно в цикл снятия копий, исключая защищённые из архивации, и решить, у кого проверяется статус: у каждой копии, у родителя или у обоих (в BR2.2.1-3 сказано «одно из событий», что ближе к «у любого из группы»). Заодно использовать возврат Archive() для wasUpdated и отфильтровать архивные в выборке — это уберёт лишние события. | проблема разработки есть, но бизнес должен дать одно уточнение: “процессинговый” применяется к техническим копиям, родителю или любому событию группы. |
| OQ2.2.1-12 | BR2.2.1-1 | Требование говорит: (целостность группы) связанные площадки — устойчивая группа, представляющая одно помещение. В коде: целостность не обеспечена ни на одном уровне. В БД у таблицы shared_rooms нет ни внешних ключей, ни индексов, ни ограничений — таблица создаётся вызовом CreateTable<SharedRooms>(), единственное ограничение это составной первичный ключ из атрибутов модели (PartnerZones.Persistence/DbInitializer.cs:70; Rooms/Models/SharedRoom.cs:10-18); миграций и SQL-файлов в сервисе нет вовсе, [Association] даёт только навигацию для запросов, но не FK. Существование связываемой площадки не проверяется — входные Guid просто оборачиваются в RoomId (UpdateRoomCommandHandler.cs:106-111), валидаторов в сервисе нет ни одного, поэтому записать можно произвольный идентификатор. Конкурентной защиты нет: поле version у площадки существует (Rooms/Models/Room.cs:56), но нигде не инкрементируется и не участвует в условии обновления, а запись группы идёт по схеме «удалить все рёбра площадки и вставить заново» (RoomRepository.cs:178-197) — при параллельном редактировании изменения теряются (last-write-wins). Отдельно: задать связь при создании площадки нельзя, только через обновление, — проверка RecurringItemsException.ThrowIfRecurring в конструкторе агрегата всегда получает пустой список и никогда не срабатывает (RoomAggregate.cs:109, :91), то есть является мёртвым кодом. | Разработка (Владислав) | Open | Вариант А (рекоменд.): добавить проверку существования связываемых площадок при сохранении группы и оптимистичную блокировку по version (инкремент + предикат в условии обновления); рассмотреть FK на shared_rooms — устраняет битые ссылки и потерю изменений при параллельном редактировании. Вариант Б: оставить как есть, если связыванием управляет только доверенный UI и параллельные правки исключены организационно — рискованно, так как «битую» ссылку в группе ничто не обнаружит, а предзаказы по ней молча не создадутся. Мёртвую проверку в конструкторе либо удалить, либо задействовать, если состав группы будет задаваться при создании. | есть проблема на стороне разработки Минимально стоит добавить проверку существования всех SharedRoomId, индекс/ограничение на таблицу, и оптимистичную блокировку по version |
| OQ2.2.1-13 | BR2.2.1-2 | Требование говорит: (порядок обработки) предзаказы создаются при добавлении бронирования. В коде: копии в связанных площадках создаются до проверки того, что заказ не был архивирован: вызов создания стоит на OrderCreatedIntegrationsConsumer.cs:55-56, а проверка if (order.IsArchived) { ... return; } — только на :59-63, причём комментарий в коде на :58 («Если заказ был обновлен после нашего события, или заказ был еще и архивирован, то мы скипаем») явно предполагает обратный порядок. Итог: заказ, уже архивированный к моменту обработки события, всё равно получает предзаказы в связанных площадках, и они остаются жить. | Разработка (Владислав) | Open | Похоже на дефект порядка операций, а не на осознанное решение — комментарий противоречит коду. Рекоменд.: перенести проверку IsArchived выше вызова создания копий. Проверить заодно, не остались ли в проде «осиротевшие» предзаказы от архивированных родителей, созданные этим путём. Малая правка, закрывается вместе с OQ2.2.1-10. | нужно будет перенести одну из проверок выше, да, есть там такой косяк |
| OQ2.2.1-14 | BR2.2.1-2 | Требование говорит: (актуальность состава группы) время закрывается в календарях остальных связанных площадок — то есть по актуальному составу группы. В коде: список связанных площадок кэшируется на 30 минут по ключу sharedRooms:{roomId} (Integrations.Infrastructure/Services/PartnerZones/PartnerZonesService.cs:61-70; дефолтные настройки — Integrations.Presentation/Program.cs:231-232). При изменении состава группы сбрасывается только ключ изменённой площадки (RoomUpdatedImtegrationsComsumer.cs:28-29) — ключи остальных площадок группы остаются протухшими до 30 минут. При этом для данного ключа распределённый слой кэша пропускается (Program.cs:238-239), а backplane-уведомления по умолчанию выключены (Program.cs:243), то есть кэш фактически локальный для каждого пода и сброс, вероятно, не расходится на другие инстансы. Дополнительно fail-safe отдаёт протухшее значение ещё до 5 минут при недоступности partner-management (Program.cs:233-234). Итог: до получаса после изменения группы разные инстансы могут создавать предзаказы по устаревшему составу. | Разработка (Владислав) | Open | Оценить, приемлемо ли окно рассинхрона в 30 минут для занятости (связано с OQ2.2.1-6: если состав группы меняется только до появления броней, риск невелик). Если нет — вариант А (рекоменд.): при изменении группы сбрасывать ключи всех затронутых площадок (старый и новый состав) и включить backplane-уведомления для этого ключа, либо задать явный короткий TTL, как это уже сделано для заказов (OrderService.cs:42-44). Заметьте: для ключа заказов distributed-слой включён явно, а для sharedRooms — нет; вероятно, это недосмотр, а не решение. | Можно сделать явные cache options для sharedRooms с коротким TTL и distributed/backplane, плюс при RoomUpdatedEvent сбрасывал ключи текущей комнаты, старого состава и нового состава. Но полноценное исправление все равно зависит от решения по направленности связи: если группа должна быть взаимной, инвалидировать надо всю вычисленную группу, а не только исходящие ребра одной комнаты |
| OQ2.2.1-15 | BR2.2.1-2 | Требование говорит: в комментарии к BR2.2.1-2 — «В остальных связанных площадках создаются предзаказызакрывая тем самым время в календаре занятости у всех связанных площадок». Расхождение не с кодом, а внутри BRD: в тексте слиплись два слова («предзаказызакрывая»), пропущены пробел и запятая. Смысл восстанавливается однозначно и коду не противоречит — механизм именно так и работает (см. OQ2.2.1-9). | Закрывает PM | Open | Редакторская правка, согласования с бизнесом или разработкой не требует: «…создаются предзаказы, закрывая тем самым время в календаре занятости у всех связанных площадок». Внесена не была, так как контент BRD в этом документе сохраняется дословно. |
Находки обратного прохода, отрезанные как вне scope 2.2.1
Обход кода дал ещё 6 находок; они не вынесены в открытые вопросы, потому что относятся к соседним разделам (Back: 6. Интеграции, Back: 3.2 Процессинг заказов) или являются техдолгом без связи с требованиями раздела. Перечислены явно, чтобы отсечение не выглядело как «всё чисто»:
- YClients, недостижимая проверка: в
BindRoomToYClientsCommandHandler.cs:102-109объявлена переменнаяexistingRoomSyncByStaffId, но в условии используетсяexistingRoomSyncиз предыдущей проверки — ветка «этот StaffId уже привязан к другой площадке» фактически никогда не срабатывает. Похоже на копипасту. → Back: 6. - Обезличенный аудит предзаказов: gRPC-слой подставляет
AccountId.Emptyпри создании и обновлении копий (Orders.Infrastructure/Services/OrdersService.cs:107,:184), поэтомуUpdatedByу всех операций с предзаказами пуст — по аудиту не найти инициатора. → Back: 3.2. - Ложная маркировка
[Obsolete("unused")]на живых командахCreateSharedOrdersCommand.cs:8иUpdateSharedOrdersCommand.cs:8— код вызывается через gRPC из integrations. Вводит в заблуждение при оценке «можно ли трогать». Техдолг. - NRE вместо осмысленной ошибки: в
CreateSharedOrdersCommandHandler.cs:43-53parentOrderразыменовывается до null-проверки — при отсутствии родителя упадётNullReferenceExceptionвместоNotFoundException(в Update-обработчике порядок верный). Техдолг. - Дублирование логики создания предзаказа в двух местах (
CreateSharedOrdersCommandHandler.cs:70-92иUpdateSharedOrdersCommandHandler.cs:113-137) — упомянуто как контекст в OQ2.2.1-8, отдельным вопросом не выносилось. OrderArchivedIntegrationsConsumerне использует защиту от устаревших событий (AcceptAndCacheFromEventAsync), в отличие от консьюмеров создания, отмены и обновления, — данные берутся напрямую из сообщения без сверки с кэшем версий. → Back: 3.2.
Проверено и подтверждено (без вопросов)
Чтобы «Частично» не читалось как «ничего не работает» — вот что в коде подтверждено прямыми доказательствами:
- Предзаказы действительно создаются при бронировании в связанной площадке: статус
PreReserv, время родителя, ссылка на родительский заказ, комментарий «Копия из {RoomName}», бессрочный lifetime (CreateSharedOrdersCommandHandler.cs:70-92). Оговорки — в OQ2.2.1-9, OQ2.2.1-10. - Дискриминатор предзаказа однозначен: предзаказ определяется по
SharedOrderId != null, отдельного флага нет. Ранее существовавшая колонкаis_sharedдобавлена миграцией20250824170149_ISSharedOrder.cs:13-18и корректно удалена миграцией20250915133509_SharedOrderId.cs:14-22; мёртвой колонки в актуальном снапшоте не осталось. - Защита от бесконечной цепочки копий работает двумя контурами: пропуск площадки родителя родителя (
CreateSharedOrdersCommandHandler.cs:60-64) и эхо-защита по источникуSource.Webна брокере (OrderUpdatedIntegrationsConsumer.cs:48-53). - Одна площадка не может иметь два внешних календаря — обеспечено уникальным индексом на
calendar_context(= RoomId) в БД (Integrations.Persistence/Migrations/20250824065018_Initial.cs:304-308). Это площадочный, а не групповой инвариант — см. OQ2.2.1-5.
Статусы требований и маршрутизация вопросов — предложение агента по итогам сверки с кодом, а не решение. Требуется ручная валидация: колонка «Статус» проставлена на основании найденных доказательств
file:lineна commit83423b21; при изменении кода документ протухает и требует пересверки.