Направление работ: Back Код раздела: 2.5 Объекты и предложения > Дополнительные позиции Статус: Готов Ответственный: Панкова Л.А. Версия: 0.1 Обновлено: 22.07.26


1. Назначение

Дополнительные позиции расширяют состав заказа товарами и услугами партнера и могут увеличивать его итоговую стоимость.

Система хранит параметры позиции, определяет область ее доступности, рассчитывает доступный остаток и применяет правила валидации при сохранении.

2. Ключевые понятия

Дополнительная позиция — товар или дополнительная услуга партнера, которую можно привязать к заказу.

Тип позиции — классификация дополнительной позиции как оборудования или услуги; услуга-позиция не является услугой объекта.

Группа позиций — значение системного справочника, объединяющее дополнительные позиции по назначению.

Доступный остаток — количество позиции, доступное для привязки к заказу с учетом позиций, уже связанных с заказами на тех же объектах или площадках.

3. Scope

Входит

S1 Позиции: дополнительные элементы состава заказа и их связь с заказом.

S2 Параметры и данные: описательные и классификационные данные, доступность по объектам, площадкам, услугам и источникам заказа, количество и остатки, стоимость и тип расчета, архивация.

S3 Валидация: запрет сохранения позиций с полностью идентичными параметрами.

Не входит

4. Требования

Правила и ограничения бизнеса на уровне данных, источник истины в системе.

IDТребованиеКомментарийТип требованияВесСтатус
S1: BR2.5-1Дополнительная позиция может быть привязана к заказу как дополнительный элемент его составаПозиция может увеличивать итоговую стоимость заказа. Формирование состава раскрывается в Back 3.1 Состав заказа, расчет стоимости — в Back 4.2 Стоимость заказа и позиций.Бизнес1Реализовано
OQ2.5-16
S2: BR2.5-2Карточка дополнительной позиции должна хранить описательные данныеСостав данных:
* название;
* фотография в формате PNG или JPG (логика хранения раскрывается в Back 9.6.1 Загруженные файлы);
* текстовое описание.
Системное1Частично
OQ2.5-13
S2: BR2.5-3Для дополнительной позиции должны задаваться тип и группаДопустимые типы:
* оборудование;
* услуга, не являющаяся услугой объекта.
Группа выбирается из справочника Back 8.2.4 Типы фильтров, позиций, шаблонов и расходов.
Бизнес1Реализовано
OQ2.5-17
S2: BR2.5-4Дополнительная позиция должна быть связана минимум с одним объектом партнераСписок объектов формируется из Back 2.1 Объекты партнеров.Бизнес1Не реализовано
OQ2.5-6
S2: BR2.5-5Дополнительная позиция может быть ограничена списком площадок или услуг выбранного объектаЕсли список не задан, позиция доступна для всех площадок и услуг выбранного объекта. Логика связанных сущностей раскрывается в Back 2.2 Площадки партнеров и Back 2.3 Услуги партнеров.Бизнес1Частично
OQ2.5-7
S2: BR2.5-6Для дополнительной позиции должно задаваться общее количество, привязанное к объектам и площадкам, а система должна рассчитывать доступный остаток на выбранное времяОстаток уменьшают позиции в заказах, время которых пересекается с выбранным периодом, кроме заказов в статусе «Отменено». Если на выбранное время занято все количество позиции, позиция недоступна для привязки к заказу.Системное1Частично
OQ2.5-8, OQ2.5-9, OQ2.5-18
S2: BR2.5-7Стоимость дополнительной позиции после привязки к заказу должна рассчитываться по заданным стоимости и типу расчетаСтоимость может составлять 0 рублей. Допустимые типы расчета:
* за заказ — стоимость рассчитывается за весь заказ;
* за часы — стоимость рассчитывается по продолжительности заказа в часах с округлением в большую сторону.
Детальная логика расчета раскрывается в Back 4.2 Стоимость заказа и позиций.
Системное1Частично
OQ2.5-4, OQ2.5-5
S2: BR2.5-8Доступность дополнительной позиции для привязки к заказу должна ограничиваться выбранными источниками заказаИсточники выбираются из справочника Back 8.2.2 Источники заказа.Бизнес1Не реализовано
OQ2.5-12
S2: BR2.5-9Архивирование должно деактивировать дополнительную позициюСистемное2Частично
OQ2.5-10
S3: BR2.5-10Система должна отклонять сохранение позиции, если ее название, объект и цена совпадают с параметрами существующей позицииСистемное2Не реализовано
OQ2.5-11

5. Открытые вопросы

Сверка с кодом: BRD 2.5 v0.1 · репозиторий booking-monorepo · commit 83423b21 / main · 26.07.26 · якорь src/services/booking.positions Сводка: 15 вопросов (Open 12 · Подтверждено 3) · строки OQ2.5-1…3 — вопросы аналитика к гайдбуку, оставлены без изменений Маршрутизация без сверки с гайдбуком (гайдбук не передавался).

IDИсточникВопросКомуСтатусРекомендацииИтоговое решение
OQ2.5-1ГайдбукПодтвердить модель доступного остатка: количество задается общее для позиции или отдельно по объекту/площадке, а также какие статусы заказов и пересечения по времени уменьшают остаток.ПродуктOpenКоличество задается общее для позиции и привязывается к объектам и площадкам. Остаток на выбранное время уменьшают все пересекающиеся по времени заказы, кроме заказов в статусе «Отменено». Если все количество занято, позиция недоступна для заказа.
OQ2.5-2ГайдбукПодтвердить источник параметра «Количество часов» для почасового расчета: он задается при привязке позиции к заказу или определяется из продолжительности заказа.ПродуктOpenКоличество часов определяется из продолжительности заказа.
OQ2.5-3ГайдбукПодтвердить, какие параметры участвуют в проверке полного совпадения позиций и в какой области выполняется сравнение: в пределах партнера, партнерской зоны или всей системы.ПродуктOpenСовпадение позиции проверяется по названию, объекту и цене.
OQ2.5-4Код BR2.5-7Требование говорит: стоимость дополнительной позиции может составлять 0 рублей. В коде сохранение позиции с нулевой ценой запрещено: при создании и при любом изменении настройки позиции проверяются, и цена, равная нулю или меньше нуля, отклоняется с ошибкой. Завести бесплатную позицию (например, подарочную услугу или оборудование «в подарок» к заказу) нельзя. \ Settings.cs:21 (проверка «цена не ноль и не отрицательная»), вызывается из PositionAggregate.cs:94 и :161БизнесOpen(А) Привести код к BRD (рекоменд.): разрешить нулевую цену — заменить проверку «не ноль и не отрицательная» на «не отрицательная», как уже сделано для количества и коэффициента в той же строке кода. Требование о бесплатных позициях выглядит осмысленным: партнёры дарят позиции в составе заказа. (Б) Привести BRD к коду: убрать из BR2.5-7 фразу про 0 рублей, если бесплатных позиций быть не должно.
OQ2.5-5Код BR2.5-7Требование говорит: при почасовом расчёте стоимость считается по продолжительности заказа в часах с округлением в большую сторону. В коде почасовой расчёт устроен иначе, и расхождений три. Первое: у позиции хранится не признак «за заказ / за часы», а числовой «коэффициент часов»: если он равен нулю — цена берётся за весь заказ, иначе — за блоки длиной в коэффициент (коэффициент 2 = блок из двух часов). Второе: продолжительность заказа усекается до целых часов вниз: заказ на 1 час 45 минут считается как 1 час, а заказ короче часа даёт нулевую длительность — и тогда позиция вообще помечается недоступной с причиной «неверная длительность». Третье: число блоков округляется к ближайшему по банковскому правилу, а не вверх: при коэффициенте 2 заказ на 5 часов даёт 2 блока вместо 3 (round(2,5) = 2). В сумме короткие и «половинчатые» заказы систематически считаются дешевле, чем требует BRD, а заказ короче часа не может получить позицию вовсе. \ CalculatePositionsCommandHandler.cs:63 (усечение длительности), :91-97 (отказ при нулевой длительности), :176-184 (блоки и округление); поле — Settings.cs:15БизнесOpen(А) Привести код к BRD (рекоменд.): считать продолжительность без потери минут и округлять итог вверх; коэффициент часов либо задокументировать как отдельный параметр, либо свести к 1. (Б) Признать целевой модель «коэффициент часов» — тогда дополнить BR2.5-7 описанием блоков и правилом округления. Независимо от выбора отдельно решите судьбу заказов короче часа: сейчас они не просто дешевле — позиция к ним не привязывается, и это ближе к дефекту, чем к продуктовому решению.
OQ2.5-6Код BR2.5-4Требование говорит: дополнительная позиция должна быть связана минимум с одним объектом партнёра. В коде проверки нет: если при создании не передать ни объектов, ни площадок, служба иерархии возвращает пустой список областей действия, и позиция всё равно создаётся. Последствие серьёзнее пропущенной валидации: партнёрская зона позиции вычисляется как первая зона из списка её областей, поэтому у позиции с пустым списком зона оказывается пустой — такая позиция не попадёт ни в один список кабинета (все выборки фильтруют по зоне) и останется висеть в базе. \ PartnerZoneScopeService.cs:36-39 (пустой список), CreatePositionCommandHandler.cs:82-97 (создание без проверки), PositionAggregate.cs:35-38 (зона = первая из областей)РазработкаOpenЗавести тикет в Bitrix: при создании и обновлении позиции отклонять пустой список объектов/площадок с понятной ошибкой. Продуктового выбора нет: BR2.5-4 однозначен. Заодно проверить, есть ли в базе позиции с пустой партнёрской зоной, — их придётся чинить отдельно.
OQ2.5-7Код BR2.5-5Требование говорит: позиция может быть ограничена списком площадок или услуг выбранного объекта. В коде ограничение по услугам отсутствует полностью: область действия позиции состоит только из объектов и площадок, услуг в ней нет ни как поля, ни как проверки при расчёте. Часть требования про площадки работает, включая правило «список не задан → доступна для всего объекта»: при расчёте позиция считается подходящей, если совпал объект или площадка. \ CalculatePositionsCommandHandler.cs:80-82; состав области — CreatePositionCommand.cs:26-28 (только Rooms и Bases)БизнесOpen(А) Подтвердить требование (рекоменд., если позиции действительно должны привязываться к конкретным услугам объекта) — тогда это доработка: добавить услуги в область действия позиции и в проверку при расчёте. (Б) Снять услуги из BR2.5-5, если на практике достаточно объекта и площадки. Вопрос стоит решить до правок в клиентах: кабинет партнёра и виджет показывают позиции по той же области.
OQ2.5-8Код BR2.5-6Требование говорит: если на выбранное время занято всё количество позиции, позиция недоступна для привязки к заказу. В коде это зависит от того, каким путём идёт клиент. При расчёте (предпросмотр состава заказа) нехватка не блокирует: система считает цену, возвращает предупреждение «Недостаточно доступного количества. Рассчитана цена без проверки доступности» и продолжает — в комментарии кода это названо сознательным повторением поведения старой системы. Жёсткий запрет срабатывает только на шаге резервирования: там позиция не бронируется и весь пакет откатывается. \ CalculatePositionsCommandHandler.cs:130-147 (предупреждение вместо отказа), PositionAggregate.cs:274-276 и ReservePositionsBulkCommandHandler.cs:84-109 (отказ и откат)БизнесOpen(А) Подтвердить текущее поведение (рекоменд.): расчёт показывает цену и предупреждение, отказ — на резервировании; тогда дополнить BR2.5-6 фразой о том, что недоступность проявляется при сохранении заказа, а не при расчёте. (Б) Ужесточить расчёт до отказа — но тогда клиенты (кабинет, виджет, мобильное) перестанут показывать цену недоступной позиции, это заметное изменение UX. Проверьте, что клиенты вообще показывают приходящее предупреждение: если нет, пользователь увидит доступную позицию и получит ошибку только при сохранении.
OQ2.5-9Код BR2.5-6Проблема: занятое количество считается по двум разным правилам. При бронировании из подсчёта исключаются архивированные брони и интервал считается полуоткрытым (заказ, который начинается ровно в момент окончания предыдущего, не считается пересечением). При расчёте архивированные брони не исключаются — учитываются все записи. Одна и та же ситуация даёт разный ответ: расчёт покажет «недостаточно», а резервирование пройдёт успешно. \ PositionAggregate.cs:268-274 (фильтр по архивности при бронировании) против CalculatePositionsCommandHandler.cs:117-128 (без фильтра)РазработкаOpenЗавести тикет в Bitrix: свести подсчёт занятого количества к одному правилу — вынести его в доменный метод и вызывать из обоих путей. Продуктового выбора нет: расхождение внутреннее. Нужно только уточнить у разработки, когда бронь становится архивированной, — если этого не происходит никогда, расхождение сейчас не проявляется, но останется миной.
OQ2.5-10Код BR2.5-9Требование говорит: архивирование должно деактивировать дополнительную позицию. В коде архивирование выключает позицию только в списках: все запросы кабинета и виджета отдают архивные записи лишь по явному флагу «показывать архив». Но путь заказа архивность не проверяет: выборка позиций по идентификаторам, которой пользуются и расчёт стоимости, и резервирование, фильтра по архивности не имеет. Зная идентификатор, архивную позицию можно рассчитать и привязать к заказу. Косвенный признак того, что так не задумывалось: при резервировании отсутствующая позиция возвращает текст «Позиция не существует или архивирована», хотя проверки архивности рядом нет. \ PositionRepository.cs:66-77 (выборка без фильтра), потребители — CalculatePositionsCommandHandler.cs:51 и ReservePositionsBulkCommandHandler.cs:48; сообщение — ReservePositionsBulkCommandHandler.cs:65РазработкаOpenЗавести тикет в Bitrix: отсекать архивные позиции на пути заказа — либо фильтром в выборке по идентификаторам, либо явной проверкой в расчёте и резервировании с понятной ошибкой. Продуктового выбора нет: BR2.5-9 однозначен. Перед правкой стоит проверить, не опирается ли на текущее поведение перерасчёт старых заказов, где позиция уже архивирована, — иначе такие заказы перестанут пересчитываться.
OQ2.5-11Код BR2.5-10Требование говорит: система должна отклонять сохранение позиции, если её название, объект и цена совпадают с существующей позицией (область сравнения подтверждена ответом на OQ2.5-3). В коде такой проверки нет нигде: ни при создании, ни при обновлении сравнения с другими позициями не выполняется, а в базе нет ни одного уникального индекса или ограничения — у сервиса позиций вообще нет папки миграций, схема создаётся из моделей. Полные дубли создаются свободно. \ CreatePositionCommandHandler.cs:39-101 и UpdatePositionCommandHandler.cs (сравнения нет), Positions.Persistence/ApplicationDbContext.cs:29-62 (только ключи и связи, ни одного уникального индекса)РазработкаOpenЗавести тикет в Bitrix: реализовать проверку дубля по названию, объекту и цене при создании и обновлении. Бизнес-ответ уже есть в OQ2.5-3, продуктового выбора не осталось. Уточнить у разработки два момента: (1) регистр и пробелы в названии при сравнении, (2) нужен ли уникальный индекс в базе — без него параллельные запросы всё равно создадут дубль.
OQ2.5-12Код BR2.5-8Требование говорит: доступность позиции для привязки к заказу должна ограничиваться выбранными источниками заказа (сайт, виджет, мобильное приложение и т. п.). В коде у позиции нет ни поля источников, ни фильтра по ним — ни при создании, ни при расчёте, ни при выдаче списков. Слово «источник» в сервисе встречается только как источник вызова API: шлюз определяет, откуда пришёл запрос, и по умолчанию подставляет «Веб», но с доступностью позиции это никак не связано. \ CreatePositionCommand.cs:25-38 (поля источников нет), RpcProxyMiddleware.cs:71-96 и :152-183 (источник вызова, не ограничение)БизнесOpen(А) Подтвердить требование (рекоменд., если партнёры действительно должны прятать позиции от отдельных каналов продаж) — тогда это доработка: хранить список источников у позиции и фильтровать выдачу и расчёт. (Б) Снять BR2.5-8, если ограничение по каналам не нужно. Требование помечено весом 1, а в коде отсутствует целиком, — стоит решить до планирования работ по позициям.
OQ2.5-13Код BR2.5-2Требование говорит: карточка позиции хранит название, фотографию в формате PNG или JPG и текстовое описание. В коде название и описание хранятся у позиции, а фотография — нет: изображения лежат в сервисе ресурсов и запрашиваются по идентификатору позиции с типом файла «иконка» (Ico), то есть отдельного типа «фото позиции» не заведено. Ограничение формата PNG/JPG в сервисе позиций не проверяется — если оно есть, то в сервисе ресурсов (Back 9.6.1, вне этой сверки). Отдельно: название не проверяется на пустоту — позицию можно сохранить с пустым именем. \ ImageService.cs:26-33 (тип Ico), Info.cs:14-18 (нет проверок), PositionAggregate.cs:92РазработкаOpenЗавести тикет в Bitrix: добавить проверку непустого названия позиции. По фотографии — вопрос к разработке на уточнение: тип файла «иконка» для фото позиции выбран осознанно или это заимствование из соседнего сценария; проверка PNG/JPG должна быть подтверждена при сверке Back 9.6.1, здесь её ставить некорректно.
OQ2.5-14Код вне BRDЧто нашли: у позиции есть два ограничения доступности, которых нет в BRD, — период (даты «с» и «по») и время суток («с» и «по»). Оба задаются при создании, хранятся в настройках позиции и проверяются при расчёте: если заказ выходит за период или за окно времени, позиция помечается недоступной с причиной «неподходящая дата» / «неподходящее время». При пустом периоде подставляются минимальная и максимальная даты, то есть ограничение снимается. \ CreatePositionCommandHandler.cs:62-92 (значения по умолчанию), Settings.cs:16-17 (хранение), CalculatePositionsCommandHandler.cs:85-97 (проверка)БизнесOpen(А) Признать поведение целевым (рекоменд.) и внести в BRD 2.5 требование о периоде и времени доступности позиции — сейчас существенная часть модели данных не описана, и при следующей сверке она снова всплывёт как расхождение. (Б) Если ограничения не нужны — ставить задачу на их удаление, но это повлияет на уже заведённые позиции. Отдельно отметьте для разработки: проверка времени написана несимметрично (для «по» есть особый случай нулевого значения, для «с» — нет), логику стоит перечитать при любом решении.
OQ2.5-15Код вне BRDПроблема: в домене позиций объявлена проверка «дата брони попадает в период доступности позиции», но она не вызывается ни из одного места сервиса — мёртвый код. Фактически период при бронировании не проверяется вовсе: проверка периода есть только в расчёте (см. OQ2.5-14), а резервирование её не повторяет, поэтому вызовом резервирования напрямую можно забронировать позицию вне её периода доступности. \ PositionInUseValidationHelper.cs:15-25 (объявлена, вызовов нет)РазработкаOpenЗавести тикет в Bitrix: либо задействовать проверку при бронировании, либо удалить её как мёртвый код. Выбор зависит от ответа на OQ2.5-14: если период доступности — целевое правило, проверку нужно включить в резервирование, а не только в расчёт.
OQ2.5-16Код BR2.5-1Что подтверждено: дополнительная позиция привязывается к заказу как отдельный элемент состава — у позиции есть записи «занято под заказ» со ссылкой на заказ, количеством и периодом; привязка выполняется пакетно на заказ, в одной транзакции, с откатом всего пакета при любой неудаче; стоимость позиций рассчитывается отдельной операцией и возвращается по каждой позиции. \ PositionInUse.cs, ReservePositionsBulkCommandHandler.cs:52-119, CalculatePositionsCommandHandler.cs:29-171ПодтвержденоРасхождения нет.
OQ2.5-17Код BR2.5-3Что подтверждено: тип позиции ограничен двумя значениями — оборудование и услуга (Equipment, Service), группа позиции хранится ссылкой на справочник и поддерживается в актуальном состоянии подписками на изменение и архивацию группы. \ PositionType.cs:9-13, PositionAggregate.cs:52, PositionGroupUpdatedPositionsConsumer.cs, PositionGroupArchivedPositionsConsumer.csПодтвержденоРасхождения нет.
OQ2.5-18Код BR2.5-6Что подтверждено: отменённые заказы не уменьшают остаток — на событие отмены заказа сервис позиций удаляет все записи «занято под заказ» по этому заказу, то же самое делается при архивации заказа и при неудачном создании заказа. Количество задаётся общим для позиции, а остаток считается вычитанием пересекающихся по времени броней, как и описано в BRD. \ OrderCancelledPositionsConsumer.cs:22-33, OrderArchivedPositionsConsumer.cs, OrderCreationFailedPositionConsumer.cs, PositionAggregate.cs:230-233ПодтвержденоРасхождения нет.

0 items under this folder.