Направление работ: Back Код раздела: 2.5 Объекты и предложения > Дополнительные позиции Статус: Готов Ответственный: Панкова Л.А. Версия: 0.1 Обновлено: 22.07.26
1. Назначение
Дополнительные позиции расширяют состав заказа товарами и услугами партнера и могут увеличивать его итоговую стоимость.
Система хранит параметры позиции, определяет область ее доступности, рассчитывает доступный остаток и применяет правила валидации при сохранении.
2. Ключевые понятия
Дополнительная позиция — товар или дополнительная услуга партнера, которую можно привязать к заказу.
Тип позиции — классификация дополнительной позиции как оборудования или услуги; услуга-позиция не является услугой объекта.
Группа позиций — значение системного справочника, объединяющее дополнительные позиции по назначению.
Доступный остаток — количество позиции, доступное для привязки к заказу с учетом позиций, уже связанных с заказами на тех же объектах или площадках.
3. Scope
Входит
S1 Позиции: дополнительные элементы состава заказа и их связь с заказом.
S2 Параметры и данные: описательные и классификационные данные, доступность по объектам, площадкам, услугам и источникам заказа, количество и остатки, стоимость и тип расчета, архивация.
S3 Валидация: запрет сохранения позиций с полностью идентичными параметрами.
Не входит
- Back: 2.1 Объекты партнеров — управление объектами, на которых доступны позиции.
- Back: 2.2 Площадки партнеров — управление площадками объектов.
- Back: 2.3 Услуги партнеров — управление услугами объектов.
- Back: 3.1 Состав заказа — формирование состава заказа и привязка его элементов.
- Back: 4.2 Стоимость заказа и позиций — алгоритмы расчета стоимости.
- Back: 8.2.2 Источники заказа — управление справочником источников.
- Back: 8.2.4 Типы фильтров, позиций, шаблонов и расходов — управление справочником групп позиций.
- Back: 9.6.1 Загруженные файлы — загрузка и хранение фотографий.
- WebLk: 4.5 Позиции — управление позициями в кабинете партнера.
- Mobile: 2.5 Дополнительные позиции к заказу — клиентский сценарий выбора позиций.
- Widget: 5.1 Дополнительные позиции к заказу — клиентский сценарий выбора позиций.
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· commit83423b21/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 | — | Подтверждено | Расхождения нет. |