Разработка большой фичи с нуля часто начинается обманчиво легко. Заказчик объяснил идею, команда обсудила экраны, разработчики примерно представляют архитектуру. Можно приступать. Но стоит перейти от общего замысла к конкретному поведению, как появляются вопросы, на которые у каждого участника свой ответ.
Возьмём веб-систему бронирования общего оборудования. Сотрудник выбирает проектор и время, подтверждает бронь, при необходимости отменяет её. Звучит как несколько форм и CRUD. Иллюзия простоты исчезает, когда два человека одновременно бронируют один проектор, временный резерв зависает навсегда, а отменённая заявка продолжает занимать время в календаре.
Разберём, как пройти путь от такой идеи до реализации через LLM: сначала сделать правила видимыми, затем согласовать модель и контракты, после этого передавать агенту небольшие задачи.
Первый способ справиться с неопределённостью, это больше разговаривать. На встрече всё выглядит понятным: «резервируем на некоторое время», «не допускаем двойной брони», «после отмены освобождаем». На следующем обсуждении выясняется, что один участник понимал под резервом десять минут, второй — весь выбранный интервал, а третий считал, что занятость проверяется только при подтверждении.
Команда снова собирается, уточняет термины, договаривается. Так можно потратить часы и дни: разговоры создают ощущение прогресса. Проблема начинается, когда результат не превращается в описание, которое можно проверить целиком.
Другая крайность — быстрее написать код, чтобы было что обсуждать. Здесь тоже есть понятная логика: работающий экран конкретнее разговора. Однако предположения уже становятся таблицами, обработчиками, клиентскими моделями и тестами. Когда выясняется, что временный резерв должен блокировать оборудование сразу, менять приходится несколько связанных частей. Чем больше реализации построено на неверном предположении, тем дороже исправление и шире область возможных ошибок.

*Рисунок 1.Точность описания растёт вместе с трудоёмкостью его подготовки и изменения. *
Код наиболее однозначно задаёт исполняемые инструкции. Но можно реализовать неправильное поведение.
Между разговором и реализацией есть удобная область: модель данных, диаграммы поведения и контракты интерфейсов. Они уже вынуждают ответить на неудобные вопросы, но изменить переход состояния или связи сущностей обычно проще, чем переделать затронутую ими функциональность. В этой статье именно такой подход будем называть Spec driven development: реализация опирается на согласованное описание решения.

Рисунок 2. Основное проектное внимание приходится на середину маршрута. LLM помогает на всех этапах; человек проверяет смысл и принимает решения.
Для примера договоримся, что резерв живёт десять минут, после чего его нельзя подтвердить. Подтверждённая бронь и действующий резерв занимают выбранный интервал. Отмена освобождает его. Оплату, выдачу и возврат оборудования оставим за границами фичи.
Схема задаёт порядок уточнения, а не запрет возвращаться назад. Обнаруженный в Sequence конфликт может потребовать изменить требование. Тогда обновляем связанные артефакты до реализации зависимого шага. Также не нужно описывать весь будущий продукт: сначала проходим маршрут для одного законченного сценария.
Начинаем с людей, которым нужна фича. Какая проблема возникает сейчас? Кто бронирует оборудование? Что пользователь должен получить после подтверждения? Какие ситуации считаются недопустимыми? Здесь полезны конкретные истории: «два сотрудника пришли за одним проектором», «человек открыл форму и ушёл, оборудование осталось занятым».
В нашем примере нужно выяснить момент блокировки, срок резерва, смысл отмены и поведение при конкуренции запросов. Отдельно уточняем границы: бронируем конкретный проектор с инвентарным номером, а не произвольную единицу из категории.
LLM может заранее составить вопросы, сгруппировать противоречивые пожелания, предложить пограничные ситуации. Но ответ «пользователь, вероятно, ожидает десять минут» остаётся предположением модели. Выбор срока принадлежит людям, отвечающим за процесс.
Полезный результат встречи — согласованные решения и короткий список вопросов с ответственными. Если вопрос влияет на данные или публичный интерфейс, его нужно разрешить до проектирования соответствующей части. Другую, независимую часть работы можно продолжать.
После встречи нужен компактный конспект. Полная расшифровка сохраняет разговор, но заставляет следующего читателя заново извлекать из него решения. В заметке должны быть видны принятый вариант, его причина и оставшаяся неопределённость.
Например:
Решение: резерв блокирует оборудование на десять минут с момента создания. Причина: пользователь должен успеть проверить параметры перед подтверждением. Истечение определяется сервером. Вопрос: можно ли продлевать резерв? Решение для первой версии: продление не поддерживается.
Такой текст уже отличает согласованный срок от случайно прозвучавшей идеи.
LLM удобно поручить черновик конспекта и список противоречий между встречами. Участники проверяют, что модель не превратила вопрос в решение и не потеряла исключение. На этом этапе особенно легко получить аккуратный, убедительный текст, который приписывает команде несуществующую договорённость.
Теперь формулируем правила так, чтобы два исполнителя получили одинаковый результат. Вместо «не допускаем двойную бронь» записываем, какие заявки занимают ресурс и что считать пересечением.
| Правило | Согласованное поведение | | — | — | | BR-01 | Интервал имеет вид [startsAt, endsAt): начало включено, конец исключён; начало раньше конца. | | BR-02 | При создании устанавливается HELD, срок резерва — десять минут по серверному времени. | | BR-03 | Оборудование блокируют CONFIRMED и HELD, у которого now < holdExpiresAt. | | BR-04 | Проверка пересечения и создание резерва составляют одну атомарную операцию. | | BR-05 | При now >= holdExpiresAt резерв истёк; подтвердить его нельзя. | | BR-06 | Подтверждение и отмена доступны только владельцу бронирования. |
Идентификаторы помогают связывать решения с диаграммами и сценариями. Агенту можно поручить найти правило без примера или неоднозначное слово. Финальный смысл утверждает человек. Подробные переходы и обработку повторных действий зафиксируем дальше, когда рассмотрим жизненный цикл бронирования.
Хорошее требование выдерживает вопрос «как мы поймём, что оно выполнено?». «Система корректно обрабатывает одновременные запросы» такого ответа не даёт. «Из двух пересекающихся заявок на свободное оборудование успешно создаётся ровно одна» уже задаёт наблюдаемую проверку.
В модели достаточно трёх сущностей: пользователь, единица оборудования и бронирование. Каждое бронирование принадлежит одному пользователю и относится к одной единице. У пользователя и оборудования может быть много бронирований.

Рисунок 3. startsAt и endsAt описывают использование оборудования, holdExpiresAt — срок принятия решения пользователем.
ERD показывает структуру и кратность связей. Правило запрета пересечений нужно записать рядом: обычного внешнего ключа для него недостаточно.
LLM может подготовить текстовую схему и проверить имена полей относительно требований.
Сразу полезно отделить хранимые данные от публичного ответа. Серверу нужен владелец записи, но возвращать клиенту внешнюю идентичность пользователя для подтверждения своей брони незачем. И наоборот, интерфейсу необходимо знать срок резерва, чтобы объяснить оставшееся время. Такой разбор предотвращает привычное копирование таблицы целиком в API и помогает сформулировать будущую схему ответа без привязки к используемому ORM.
Use Case помогает проверить, что проект решает пользовательскую задачу. Для сотрудника выделим четыре цели: посмотреть доступность, создать временный резерв, подтвердить и отменить бронирование.

Рисунок 4. Линии связывают участника с целями; они не задают последовательность HTTP-вызовов.
Не стоит превращать этот рисунок в перечень всех методов сервиса. Сохранить запись в базе — действие реализации, а сотруднику нужен результат бронирования. Аутентификацию принимаем как предусловие рассматриваемых действий.
LLM легко строит такую схему по списку целей. Польза ревью состоит в проверке границ: не появилась ли у пользователя возможность подтвердить чужую заявку, не пропала ли отмена, не добавилось ли управление оборудованием, которое пока не входит в задачу. Общую нотацию этих и следующих поведенческих диаграмм определяет UML.
Следующий вопрос — где заканчивается наша ответственность. На Context-диаграмме есть сотрудник, система бронирования и внешний провайдер идентификации. Мы используем существующие учётные записи, а правила резервирования принадлежат создаваемой системе.

Рисунок 5. Контекст системы: пользователь, целевая система и внешняя зависимость.
Context удобно обсуждать с людьми, которым не нужны подробности серверного кода. Они могут заметить отсутствующую внешнюю зависимость или неверно очерченную ответственность. Например, если оборудование на самом деле выдаёт отдельная служба, её участие станет новым требованием.
Раскроем систему до трёх частей: веб-интерфейс, API бронирования и реляционная база. Интерфейс показывает данные и отправляет команды. API проверяет пользователя и правила. База хранит состояние и участвует в обеспечении атомарности.

Рисунок 6. Контейнер C4 — приложение или хранилище данных
На схеме указываем протоколы и сохраняем названия для будущей Sequence. LLM помогает собрать и отрисовать архитектуру. Для выбранного поведения здесь достаточно одного серверного приложения. Назначение этого уровня подробнее описано в документации C4 Container.
У бронирования четыре состояния: временный резерв HELD, подтверждённая бронь CONFIRMED, отмена CANCELLED и истечение EXPIRED. Из временного резерва можно перейти в любое из трёх остальных состояний. Подтверждённую бронь можно отменить. Отмена и истечение завершают жизненный цикл в пределах нашего примера.

Рисунок 7. Условия переходов важнее самого списка статусов. Ровно на границе срока подтверждение уже недопустимо.
Для подготовки диаграммы достаточно короткого задания:
По правилам BR-02, BR-05 и BR-07 версии equipment-reservation-v1
построй State Machine в PlantUML. Покажи условия переходов.
Повторные действия опиши отдельно. Не добавляй новых состояний.
Если правило неоднозначно, перечисли вопросы перед диаграммой.
Текстовую диаграмму легко править через LLM и хранить рядом с документами. Синтаксис и рендер автоматизируются; проверка смысла остаётся задачей ревью. Описание State Diagram в PlantUML.
Теперь проследим создание резерва: веб-интерфейс отправляет параметры, API проверяет их, обращается к базе, создаёт запись и возвращает результат. Названия участников берём из Container, полей — из ERD, условий — из требований.

Рисунок 8. Проверка и запись защищены одной транзакцией и сериализацией операций по оборудованию.
LLM полезно попросить найти сообщения без обработчика, разрывы транзакции и альтернативные ветки. На ревью проверяем также момент ответа: 201 отправляется после успешного завершения транзакции.
Sequence отвечает на вопрос «кто с кем взаимодействует», Activity — «какие решения принимаются». В алгоритме создания резерва проверяем входные данные, затем в защищённой операции определяем блокирующие брони и ищем пересечение.

Рисунок 9. Истёкший HELD исключается из блокирующих броней до проверки пересечения.
Условия удобно записать без привязки к языку программирования:
overlap = existing.startsAt < requested.endsAt
and requested.startsAt < existing.endsAt
blocks = status == CONFIRMED
or (status == HELD and now < holdExpiresAt)
LLM может развернуть правила в Activity и предложить примеры для каждой ветки. При ревью важно сверить порядок действий с Sequence. Красивые картинки, описывающие разные алгоритмы, только увеличат неопределённость. Одна и та же атомарная область должна сохраняться в обоих представлениях.
Когда поведение прояснилось, фиксируем HTTP-контракт. В учебном документе есть создание резерва, подтверждение и отмена. Просмотр оборудования и весь процесс входа в него не включены. Используем OpenAPI 3.0.3, как в исходной презентации; это выбор формата примера, а не рекомендация считать его последней версией.
Для создания нужны equipmentId, startsAt и endsAt. Владелец и срок резерва назначаются сервером. Ни userId, ни желаемый статус клиент не передаёт. Короткий фрагмент операции выглядит так:
/api/v1/reservations:
post:
operationId: createReservation
security:
— bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: ’#/components/schemas/CreateReservation’
responses:
’201′:
description: Временный резерв создан
content:
application/json:
schema:
$ref: ’#/components/schemas/Reservation’
’409′:
$ref: ’#/components/responses/Conflict’
Это фрагмент; определения схем и остальные ответы находятся в полном учебном OpenAPI. Успешные подтверждение и отмена возвращают 200. Неверные параметры — 400, отсутствие действительной аутентификации — 401. Для неизвестной или чужой брони используется 404; конфликт времени или состояния — 409 с различимым кодом причины.
Контракт позволяет независимо обсуждать фронтенд, сервер и тесты. LLM может подготовить клиентские типы и обработчики, сверить примеры со схемами. Однако описание Bearer не реализует проверку токена, а корректный JSON не доказывает отсутствие двойной брони. Эти обязанности остаются в правилах и реализации. Назначение и структуру HTTP-контрактов определяет спецификация OpenAPI.
К этому моменту появились несколько связанных представлений. Теперь собираем документ, через который разработчик или агент может пройти от цели до конкретного решения: границы, термины, правила, модель, диаграммы, контракт, принятые ограничения и версия.
Первоначальные требования объясняли, чего мы хотим. Итоговая Markdown-спецификация фиксирует согласованное устройство выбранного сценария. Не нужно вручную переписывать в неё весь OpenAPI: достаточно ссылок и объяснения решений, которые нельзя вывести из структуры запроса.
В приложениях есть спецификация примера версии equipment-reservation-v1. Это обозначение редакции учебного комплекта, не ссылка на существующий Git-тег. В ней, например, явно разделены сохранённый статус и фактическое истечение по серверному времени.
LLM помогает проверить согласованность: все ли состояния существуют в модели, соответствует ли ответ ветке алгоритма, нет ли правила без сценария.
Перед кодом задаём конкретные исходные данные, действие и ожидаемый результат. Например, в терминах Gherkin:
# language: ru
Функциональность: Резервирование оборудования
Сценарий: Подтверждение ровно на границе срока
Допустим мой резерв находится в состоянии HELD
И его срок истекает в «2026-11-02T09:10:00Z»
И серверное время равно «2026-11-02T09:10:00Z»
Когда я подтверждаю резерв
Тогда API возвращает статус 409
И код ошибки равен «RESERVATION_EXPIRED»
И резерв не блокирует оборудование
Нужны также успешное создание, пересечение, соседние интервалы, отмена, повторные команды и доступ к чужой брони. Для конкуренции фиксируем результат: из двух одновременных пересекающихся запросов к свободному оборудованию один получает 201, другой — 409; блокирующая запись остаётся одна. Победитель заранее не определён.
Сценарии хранятся в отдельном файле. Пока у них нет шаговых определений и приложения, это согласованные примеры, а не выполненные автоматические тесты. Их автоматизацию поручаем LLM при реализации соответствующего шага.
Теперь можно передавать реализацию агенту. Большую фичу разбиваем так, чтобы результат каждого задания можно было проверить отдельно: модель и переходы; атомарное резервирование; подтверждение и отмена; HTTP API; веб-интерфейс; итоговая проверка пользовательских сценариев. Тесты добавляются на каждом шаге, а не откладываются до последнего.

Рисунок 10. Человек проверяет результат текущего шага перед переходом к следующему. Изменение бизнес-правил возвращается на согласование проекта.
Например, задание для этапа резервирования:
Реализуй атомарное создание временного резерва.
Основа: equipment-reservation-v1 — reservation-spec.md,
openapi.yaml, 03-erd.puml, 08-sequence.puml, 09-activity.puml
и сценарии reservations.feature с тегом @create.
Все материалы находятся в assets/.
Проверь согласованность входных материалов.
Соблюдай выбранную сериализацию операций по оборудованию.
Автоматизируй создание, конфликт, соседние интервалы,
истечение блокировки и два конкурентных запроса.
Не меняй контракт и ожидания сценариев ради зелёных тестов.
Представь изменения, результаты проверок и открытые вопросы.
После этого дождись ревью; следующий этап не начинай.
Это задание для будущего проекта: приложенный комплект не содержит реализации сервиса. В реальном репозитории к нему добавляются технология, команды проверки и актуальная ревизия материалов.
Человеку остаётся проверить соответствие правилу и разумность инженерного решения. Для доменной логики полезны быстрые тесты с управляемым временем. Конкуренцию и блокировки проверяют на выбранной базе: имитация хранилища не подтвердит её транзакционные гарантии. Затем проверяются HTTP-контракт и пользовательский путь.
Если ошибся код, исправляем код. Если обнаружен пробел проекта, сначала согласуем изменение схем, правил и сценариев. Ревью небольшого шага помогает локализовать проблему до того, как поверх неё вырастет следующая часть фичи.
В большой разработке полезно максимально сосредоточиться там, где решение уже достаточно точное, а менять его ещё сравнительно просто: на модели данных, UML-диаграммах, границах C4 и контрактах. Разговоры дают материал для этих решений. Код воплощает их. Markdown связывает, сценарии делают ожидания проверяемыми.
LLM снижает порог подготовки схем: описываем правило обычным языком, получаем текст диаграммы, просматриваем результат, исправляем. Тратить время приходится прежде всего на смысл — какой переход разрешён, кто отвечает за операцию, что считается конфликтом. Именно эти решения особенно дорого восстанавливать из уже написанного кода.
Когда схема, данные и контракт согласованы, у агента меньше причин угадывать поведение. Реализацию можно последовательно отдавать небольшими задачами, оставляя человеку принятие решений и ревью. Это не делает ошибки пренебрежимо малыми автоматически, но позволяет раньше обнаруживать расхождения и ограничивать объём переделок.
Для работы с примером сохраните содержимое приложений в файлы с указанными именами в каталоге assets/. Текстовые исходники диаграмм включены в приложение со спецификацией.
openapi: 3.0.3
info:
title: Бронирование оборудования — учебный HTTP-контракт
version: 1.0.0
description: ’Редакция комплекта: equipment-reservation-v1. Охват: создание, подтверждение
и отмена. Просмотр доступности, вход, эксплуатационные ошибки и выдача оборудования
не описаны. Бизнес-правила и модель находятся в reservation-spec.md. Это проект
контракта, сервис не реализован.’
paths:
/api/v1/reservations:
post:
operationId: createReservation
summary: Создать временный резерв
description: ’BR-01—BR-04. Атомарно проверить пересечение и создать HELD на
10 минут. Владелец берётся из проверенной идентичности. Повторный POST не
имеет специального ключа идемпотентности: при действующем первом резерве пересекающийся
повтор получит 409 EQUIPMENT_BUSY.’
security:
— bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: ’#/components/schemas/CreateReservation’
example:
equipmentId: eq-042
startsAt: ’2026-11-02T10:00:00Z’
endsAt: ’2026-11-02T11:00:00Z’
responses:
’201′:
description: Временный резерв создан
content:
application/json:
schema:
$ref: ’#/components/schemas/Reservation’
example:
id: res-001
equipmentId: eq-042
startsAt: ’2026-11-02T10:00:00Z’
endsAt: ’2026-11-02T11:00:00Z’
status: HELD
holdExpiresAt: ’2026-11-02T09:10:00Z’
’400′: &id001
$ref: ’#/components/responses/BadRequest’
’401′: &id002
$ref: ’#/components/responses/Unauthorized’
’404′: &id003
$ref: ’#/components/responses/NotFound’
’409′: &id004
$ref: ’#/components/responses/Conflict’
/api/v1/reservations/{reservationId}/confirm:
post:
operationId: confirmReservation
summary: Подтвердить собственный резерв
description: BR-05—BR-07. HELD подтверждается только при now < holdExpiresAt.
CONFIRMED возвращается без изменения с 200. EXPIRED, включая HELD с истёкшим
сроком, возвращает 409 RESERVATION_EXPIRED. CANCELLED возвращает 409 INVALID_STATE.
security:
— bearerAuth: []
parameters:
— name: reservationId
in: path
required: true
schema: &id005
type: string
minLength: 1
maxLength: 80
pattern: ^[A-Za-z0-9_-]+$
responses:
’200′:
description: Операция выполнена
content:
application/json:
schema:
$ref: ’#/components/schemas/Reservation’
example:
id: res-001
equipmentId: eq-042
startsAt: ’2026-11-02T10:00:00Z’
endsAt: ’2026-11-02T11:00:00Z’
status: CONFIRMED
holdExpiresAt: ’2026-11-02T09:10:00Z’
’400′: *id001
’401′: *id002
’404′: *id003
’409′: *id004
/api/v1/reservations/{reservationId}/cancel:
post:
operationId: cancelReservation
summary: Отменить собственное бронирование
description: BR-05—BR-07. Действующий HELD и CONFIRMED переходят в CANCELLED.
Повторная отмена CANCELLED возвращает 200 без изменения. EXPIRED, включая
HELD с истёкшим сроком, возвращает 409 RESERVATION_EXPIRED.
security:
— bearerAuth: []
parameters:
— name: reservationId
in: path
required: true
schema: *id005
responses:
’200′:
description: Операция выполнена
content:
application/json:
schema:
$ref: ’#/components/schemas/Reservation’
example:
id: res-001
equipmentId: eq-042
startsAt: ’2026-11-02T10:00:00Z’
endsAt: ’2026-11-02T11:00:00Z’
status: CANCELLED
holdExpiresAt: ’2026-11-02T09:10:00Z’
’400′: *id001
’401′: *id002
’404′: *id003
’409′: *id004
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: API проверяет подпись, издателя, аудиторию и срок токена. Локальный
пользователь определяется по паре issuer/subject; userId из запроса не принимается.
schemas:
CreateReservation:
type: object
additionalProperties: false
required:
— equipmentId
— startsAt
— endsAt
description: ’BR-01: startsAt < endsAt. Сравнение дат и проверка пересечения
выполняются сервером.’
properties:
equipmentId: *id005
startsAt: &id006
type: string
format: date-time
pattern: Z$
description: Время UTC; строка RFC 3339 с суффиксом Z.
endsAt: *id006
Reservation:
type: object
additionalProperties: false
required:
— id
— equipmentId
— startsAt
— endsAt
— status
— holdExpiresAt
properties:
id: *id005
equipmentId: *id005
startsAt: *id006
endsAt: *id006
status:
type: string
enum:
— HELD
— CONFIRMED
— CANCELLED
— EXPIRED
description: ’Эффективное состояние: HELD с now >= holdExpiresAt считается
EXPIRED.’
holdExpiresAt:
type: string
format: date-time
pattern: Z$
description: Исходный срок временного резерва; после CONFIRMED или CANCELLED
сохраняется для истории и больше не ограничивает эти состояния.
Error:
type: object
additionalProperties: false
required:
— code
— message
properties:
code:
type: string
enum:
— INVALID_REQUEST
— UNAUTHENTICATED
— EQUIPMENT_NOT_FOUND
— RESERVATION_NOT_FOUND
— EQUIPMENT_BUSY
— RESERVATION_EXPIRED
— INVALID_STATE
message:
type: string
description: Пояснение для человека; клиент ветвится по code.
responses:
BadRequest:
description: Некорректные параметры, лишние поля или startsAt >= endsAt.
content:
application/json:
schema:
$ref: ’#/components/schemas/Error’
example:
code: INVALID_REQUEST
message: Некорректные параметры, лишние поля или startsAt >= endsAt.
Unauthorized:
description: Отсутствует действительная аутентификация.
content:
application/json:
schema:
$ref: ’#/components/schemas/Error’
example:
code: UNAUTHENTICATED
message: Отсутствует действительная аутентификация.
headers:
WWW-Authenticate:
schema:
type: string
example: Bearer
description: Схема аутентификации.
NotFound:
description: Оборудование не найдено; бронирование отсутствует или принадлежит
другому пользователю.
content:
application/json:
schema:
$ref: ’#/components/schemas/Error’
example:
code: RESERVATION_NOT_FOUND
message: Оборудование не найдено; бронирование отсутствует или принадлежит
другому пользователю.
Conflict:
description: Конфликт интервала либо недопустимое действие над состоянием.
content:
application/json:
schema:
$ref: ’#/components/schemas/Error’
examples:
EQUIPMENT_BUSY:
value:
code: EQUIPMENT_BUSY
message: Интервал занят.
RESERVATION_EXPIRED:
value:
code: RESERVATION_EXPIRED
message: Срок резерва истёк.
INVALID_STATE:
value:
code: INVALID_STATE
message: Действие недопустимо в текущем состоянии.
Редакция: equipment-reservation-v1. Статус: согласованный внутри статьи учебный проект. Это не реализованный сервис и не Git-тег. Все материалы данного комплекта относятся к этой редакции.
Сотрудник резервирует конкретную единицу оборудования, подтверждает или отменяет собственное бронирование. Рассматриваются конфликты интервалов, срок резерва и конкуренция команд. Оплата, продление резерва, выдача, возврат, управление оборудованием и регистрация пользователей вне охвата.
Просмотр доступности входит в пользовательский сценарий, но его HTTP-контракт в приложенном OpenAPI не описан. Доступность на экране справочная: гарантия появляется только после успешного создания резерва. Учебный проект не устанавливает производственные SLO, лимиты запросов и политику обработки инфраструктурных сбоев.
| ID | Правило | | — | — | | BR-01 | Интервал [startsAt, endsAt); startsAt < endsAt. Пересечение: a.startsAt < b.endsAt && b.startsAt < a.endsAt. Соседние интервалы разрешены. В контракте все даты передаются в UTC с Z. Дополнительных ограничений относительно текущей даты учебный контракт не вводит. | | BR-02 | При успешном создании состояние HELD, createdAt = now, holdExpiresAt = now + 10 минут. Владельца и время назначает сервер. | | BR-03 | Одинаковое оборудование блокируется пересекающимися CONFIRMED и HELD при now < holdExpiresAt. CANCELLED и истёкшие резервы не блокируют. | | BR-04 | Проверка конфликта и создание атомарны. Все изменения броней одного оборудования сериализуются блокировкой его строки. Проверки читают актуальное состояние после получения блокировки. | | BR-05 | HELD при now >= holdExpiresAt имеет эффективное состояние EXPIRED, даже если сохранённое поле ещё HELD. Подтверждение и отмена возвращают 409 RESERVATION_EXPIRED. Истёкшая запись не занимает интервал. | | BR-06 | API проверяет токен и сопоставляет (issuer, subject) локальному пользователю. Подтверждать и отменять можно только собственные бронирования. Отсутствие действительной аутентификации — 401; неизвестная и чужая бронь — одинаковый 404 RESERVATION_NOT_FOUND. | | BR-07 | Действующий HELD подтверждается в CONFIRMED или отменяется в CANCELLED. CONFIRMED отменяется в CANCELLED. Повтор confirm для CONFIRMED и cancel для CANCELLED дают 200 без изменения. Confirm для CANCELLED — 409 INVALID_STATE. Восстановление из CANCELLED и EXPIRED не поддерживается. |
Порядок обработки: аутентификация → проверка формата → поиск ресурса и проверка владельца → защищённая проверка состояния и изменение. Для чужой брони ошибки состояния не раскрываются. Команды confirm/cancel не имеют тела запроса. Повторный create не получает идемпотентность команд confirm/cancel: при действующем первом резерве повтор на пересекающийся интервал вернёт 409 EQUIPMENT_BUSY.
— User: id (PK), authIssuer, authSubject; пара (authIssuer, authSubject) уникальна. — Equipment: id (PK), inventoryNumber (уникален), name. — Reservation: id (PK), userId (FK), equipmentId (FK), startsAt, endsAt, createdAt, holdExpiresAt, status. — Все перечисленные поля обязательны. Каждая бронь относится ровно к одному пользователю и оборудованию; у каждого из них может быть от нуля до многих броней. — status: HELD, CONFIRMED, CANCELLED, EXPIRED. Срок резерва сохраняется для истории после подтверждения или отмены и не ограничивает эти состояния. — Публичный ответ не содержит userId, внешнюю идентичность и createdAt; схема хранения и HTTP DTO различаются.
Use Case · C4 Context · C4 Container.
Сотрудник использует веб-интерфейс. Веб-интерфейс обращается к API бронирования по HTTPS/JSON с Bearer-токеном. Внешний провайдер идентификации обслуживает вход; API получает метаданные и ключи для проверки токена. API обращается по SQL к базе бронирований. Локальная проверка владельца остаётся обязанностью API. C4 описывает архитектуру отдельно от UML.
State Machine · Sequence · Activity.
Операция создания:
Проверить аутентификацию, формат и обязательные поля, сравнение начала и конца; найти оборудование.
Начать транзакцию и получить блокировку строки оборудования. Удаление оборудования в учебном сценарии не предусмотрено; в полном продукте существование проверяется и внутри транзакции.
Получить актуальное серверное время now после ожидания блокировки.
Прочитать актуальные брони этого оборудования. СУБД и уровень изоляции должны обеспечивать видимость результатов ранее завершившейся операции после получения блокировки; устаревший снимок недопустим.
Проверить BR-03. При конфликте откатить транзакцию, освободить блокировку и вернуть 409 EQUIPMENT_BUSY.
Создать HELD по BR-02. Зафиксировать транзакцию и только после успешного commit вернуть 201 с бронированием.
Confirm/cancel используют ту же сериализацию по оборудованию; после блокировки заново читают запись и проверяют эффективное состояние. Время и состояние оцениваются внутри защищённой операции. Фоновая материализация EXPIRED необязательна для правильности и также не должна перезаписывать конкурентное подтверждение.
Тайм-ауты, дедлоки и потеря соединения требуют отдельной политики полного продукта. Автоматический повтор create после неизвестного результата не считается безопасным решением по умолчанию. Пример описывает нормальное завершение транзакций и бизнес-конфликты.
OpenAPI 3.0.3, версия документа 1.0.0.
| Операция | Успех | Бизнес-конфликты | | — | — | — | | POST /api/v1/reservations | 201, HELD | 409 EQUIPMENT_BUSY | | POST /api/v1/reservations/{reservationId}/confirm | 200, CONFIRMED | 409 RESERVATION_EXPIRED / INVALID_STATE | | POST /api/v1/reservations/{reservationId}/cancel | 200, CANCELLED | 409 RESERVATION_EXPIRED |
Общие ошибки: 400 INVALID_REQUEST, 401 UNAUTHENTICATED, 404 EQUIPMENT_NOT_FOUND при создании и 404 RESERVATION_NOT_FOUND для неизвестной/чужой брони. Ошибка содержит code и message; логика клиента использует code.
Пример создания: серверное время 2026-11-02T09:00:00Z, оборудование eq-042, интервал 10:00—11:00Z той же даты. Результат: HELD, срок 09:10:00Z. Ровно в 09:10:00Z неподтверждённый резерв больше не блокирует интервал.
BDD на Gherkin. Сценарии имеют теги правил и этапов. Время контролируется тестовым стендом, даты примеров не зависят от фактического дня запуска.
| Проверка | Правила | | — | — | | Создание и назначение срока | BR-01, BR-02 | | Пересечение и соседние интервалы | BR-01, BR-03 | | Параллельные заявки | BR-03, BR-04 | | Истечение и задержка фонового обновления | BR-03, BR-05 | | Подтверждение, отмена, повтор команд | BR-05, BR-07 | | Неавторизованный запрос, чужая и неизвестная бронь | BR-06 |
Модель, функции пересечения и переходы; быстрые проверки с управляемым временем.
Хранилище и атомарное резервирование; сценарии @create, включая конкуренцию на реальной выбранной СУБД.
Подтверждение и отмена; сценарии @confirm, @cancel, проверка владельца и повторов.
HTTP-адаптеры по OpenAPI; проверка форматов и статусов. При необходимости проверки бизнес-слоя на предыдущих шагах работают без HTTP-адаптера.
Веб-интерфейс: свободное время, команды, понятное отображение конфликтов и срока. До этого шага нужно отдельно согласовать API чтения, отсутствующее в учебном документе.
Итоговая проверка пользовательского пути и согласованности артефактов.
После каждого шага — проверки и ревью человеком. Изменение требований предваряет изменение зависимых контрактов, сценариев и кода. Наличие BDD-файла не означает, что тесты выполнены: сервис и шаговые определения в этот комплект не входят.
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
hide circle
hide methods
entity «User\nПользователь» as U {
* id : identifier <<PK>>
—
* authIssuer : string
* authSubject : string
}
entity «Equipment\nЕдиница оборудования» as E {
* id : identifier <<PK>>
—
* inventoryNumber : string <<unique>>
* name : string
}
entity «Reservation\nБронирование» as R {
* id : identifier <<PK>>
—
* userId : identifier <<FK>>
* equipmentId : identifier <<FK>>
* startsAt : UTC timestamp
* endsAt : UTC timestamp
* holdExpiresAt : UTC timestamp
* createdAt : UTC timestamp
* status : enum
}
U ||-down-o{ R : владеет
E ||-down-o{ R : бронируется
U -[hidden]right- E
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
actor «Сотрудник» as U
rectangle «Система бронирования» {
usecase «Посмотреть\nдоступность» as View
usecase «Создать\nвременный резерв» as Hold
usecase «Подтвердить\nсвоё бронирование» as Confirm
usecase «Отменить\nсвоё бронирование» as Cancel
View -[hidden]down- Hold
Hold -[hidden]down- Confirm
Confirm -[hidden]down- Cancel
}
U -right- View
U -right- Hold
U -right- Confirm
U -right- Cancel
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
actor «Сотрудник\n[Person]» as U
rectangle «Система бронирования\n[Software System]\n\nДоступность, резервы,\nподтверждение и отмена» as S
rectangle «Провайдер идентификации\n[External Software System]\n\nУчётные записи\nи выпуск токенов» as I #E8E9ED
U -down-> S : Бронирует оборудование
S -down-> I : Использует внешнюю идентичность
U -right-> I : Входит в учётную запись
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
actor «Сотрудник\n[Person]» as U
rectangle «Система\nбронирования» {
rectangle «Веб-интерфейс\n[Container: браузерное приложение]\n\nВыбор времени и команды» as W
rectangle «API бронирования\n[Container: серверное приложение]\n\nПроверка пользователя и правил» as A
database «База бронирований\n[Container: реляционная БД]\n\nДанные, транзакции, блокировки\n» as D
W -down-> A : Команды / ответы\n[HTTPS / JSON]\nBearer-токен
A -down-> D : Чтение и изменение\n[SQL]
}
rectangle «Провайдер\nидентификации\n[External System]» as I #E8E9ED
U -down-> W : Использует
W -right-> I : Вход\n[OIDC / HTTPS]
A -right-> I : Ключи\nпроверки\n[HTTPS]
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
state «Временный резерв\nHELD» as H
state «Подтверждённая бронь\nCONFIRMED» as C
state «Отмена\nCANCELLED» as X
state «Истечение\nEXPIRED» as E
[*] —> H : создать
H -down-> C : подтвердить\n[now <\nholdExpiresAt]
H -right-> E : срок истёк\n[now >=\nholdExpiresAt]
H -left-> X : отменить\n[now <\nholdExpiresAt]
C -left-> X : отменить
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
hide footbox
skinparam maxMessageSize 350
participant «Веб-интерфейс» as W
participant «API бронирования» as A
database «База бронирований» as D
note over W,D
Предусловие: аутентификация и параметры проверены;
оборудование существует. Показана операция создания.
end note
W -> A : POST /api/v1/reservations
activate A
group Одна транзакция / сериализация по оборудованию
A -> D : BEGIN; заблокировать строку Equipment
D —> A : Блокировка получена
A -> A : now = серверное время
A -> D : Прочитать блокирующие брони\nи проверить пересечение
D —> A : Результат проверки
alt Есть пересечение
A -> D : ROLLBACK; освободить блокировку
D —> A : Транзакция завершена
A —> W : 409 EQUIPMENT_BUSY
else Интервал свободен
A -> D : INSERT Reservation: HELD\nholdExpiresAt = now + 10 мин
A -> D : COMMIT; освободить блокировку
D —> A : Изменение сохранено
A —> W : 201 Reservation
end
end
deactivate A
@enduml
@startuml
!pragma layout smetana
skinparam backgroundColor #F7F6F2
skinparam defaultFontName DejaVu Sans
skinparam defaultFontSize 23
skinparam shadowing false
skinparam roundcorner 12
skinparam ArrowColor #315DF5
skinparam ArrowThickness 2
skinparam LineThickness 2
skinparam defaultFontColor #182020
skinparam classBackgroundColor #E9EEFF
skinparam classBorderColor #315DF5
skinparam classAttributeFontSize 22
skinparam classFontSize 25
skinparam rectangleBackgroundColor #E9EEFF
skinparam rectangleBorderColor #315DF5
skinparam rectangleFontSize 23
skinparam packageBackgroundColor #F7F6F2
skinparam packageBorderColor #9BAACD
skinparam stateBackgroundColor #E9EEFF
skinparam stateBorderColor #315DF5
skinparam stateFontSize 24
skinparam usecaseBackgroundColor #E9EEFF
skinparam usecaseBorderColor #315DF5
skinparam activityBackgroundColor #E9EEFF
skinparam activityBorderColor #315DF5
skinparam activityDiamondBackgroundColor #FFFFFF
skinparam activityDiamondBorderColor #315DF5
skinparam participantBackgroundColor #E9EEFF
skinparam participantBorderColor #315DF5
skinparam sequenceLifeLineBorderColor #9BAACD
skinparam sequenceGroupBorderColor #315DF5
skinparam sequenceGroupBackgroundColor #F7F6F2
skinparam noteBackgroundColor #FFFFFF
skinparam noteBorderColor #9BAACD
skinparam nodesep 55
skinparam ranksep 65
start
:Аутентифицированный запрос;
if (Параметры корректны?) then (нет)
:400 INVALID_REQUEST;
stop
else (да)
endif
if (Оборудование существует?) then (нет)
:404 EQUIPMENT_NOT_FOUND;
stop
else (да)
endif
partition «Атомарная операция» {
:Начать транзакцию;\nзаблокировать строку Equipment;
:Получить серверное now;
:CONFIRMED блокирует без проверки срока;\nHELD — только при now < holdExpiresAt;
if (Есть пересечение\nинтервалов?) then (да)
:Откатить транзакцию;\nосвободить блокировку;
:409 EQUIPMENT_BUSY;
stop
else (нет)
:Создать HELD;\nholdExpiresAt = now + 10 мин;
:Зафиксировать транзакцию;\nосвободить блокировку;
endif
}
:201 Reservation;
stop
@enduml
# language: ru
@equipment-reservation-v1
Функциональность: Резервирование оборудования
Учебные ожидания; реализация сервиса и шагов отсутствует.
Все времена UTC, часы сервера управляются тестовым стендом.
Предыстория:
Допустим существует оборудование «eq-042»
И я аутентифицирован как пользователь «user-a»
И серверное время равно «2026-11-02T09:00:00Z»
И у оборудования нет бронирований
@create @BR-01 @BR-02
Сценарий: Создание свободного резерва
Когда я создаю резерв «eq-042» с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
Тогда API возвращает статус 201
И состояние бронирования равно «HELD»
И срок резерва равен «2026-11-02T09:10:00Z»
И владельцем сохранённой записи является «user-a»
@create @BR-01 @BR-03
Сценарий: Пересечение с подтверждённой бронью
Допустим оборудование занято подтверждённой бронью с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
Когда я создаю резерв «eq-042» с «2026-11-02T10:30:00Z» до «2026-11-02T11:30:00Z»
Тогда API возвращает статус 409
И код ошибки равен «EQUIPMENT_BUSY»
И новое бронирование не создаётся
@create @BR-01 @BR-03
Сценарий: Соседние интервалы не пересекаются
Допустим оборудование занято подтверждённой бронью с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
Когда я создаю резерв «eq-042» с «2026-11-02T11:00:00Z» до «2026-11-02T12:00:00Z»
Тогда API возвращает статус 201
@create @BR-01
Структура сценария: Некорректный интервал
Когда я создаю резерв «eq-042» с «<начало>» до «<конец>»
Тогда API возвращает статус 400
И код ошибки равен «INVALID_REQUEST»
И новое бронирование не создаётся
Примеры:
| начало | конец |
| 2026-11-02T10:00:00Z | 2026-11-02T10:00:00Z |
| 2026-11-02T11:00:00Z | 2026-11-02T10:00:00Z |
@create @BR-03 @BR-05
Структура сценария: Срок резерва влияет на доступность без фонового обновления
Допустим сохранён резерв HELD на «eq-042» с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
И его срок истекает в «2026-11-02T09:10:00Z»
И фоновый обработчик не обновлял статус
И серверное время равно «<время>»
Когда я создаю резерв «eq-042» с «2026-11-02T10:30:00Z» до «2026-11-02T11:30:00Z»
Тогда API возвращает статус <статус>
Примеры:
| время | статус |
| 2026-11-02T09:09:59Z | 409 |
| 2026-11-02T09:10:00Z | 201 |
| 2026-11-02T09:10:01Z | 201 |
@create @BR-03 @BR-04
Сценарий: Два конкурентных запроса на свободный интервал
Когда пользователи «user-a» и «user-b» одновременно резервируют «eq-042» с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
Тогда один запрос получает статус 201
И другой запрос получает статус 409 с кодом «EQUIPMENT_BUSY»
И для интервала существует ровно одно блокирующее бронирование
И личность победителя не фиксируется ожиданием
@confirm @BR-05 @BR-07
Сценарий: Подтверждение до истечения
Допустим мой резерв находится в состоянии HELD
И его срок истекает в «2026-11-02T09:10:00Z»
И серверное время равно «2026-11-02T09:09:59Z»
Когда я подтверждаю резерв
Тогда API возвращает статус 200
И состояние бронирования равно «CONFIRMED»
И бронирование продолжает блокировать интервал после «2026-11-02T09:10:00Z»
@confirm @BR-05
Сценарий: Подтверждение ровно на границе срока
Допустим мой резерв находится в состоянии HELD
И его срок истекает в «2026-11-02T09:10:00Z»
И серверное время равно «2026-11-02T09:10:00Z»
Когда я подтверждаю резерв
Тогда API возвращает статус 409
И код ошибки равен «RESERVATION_EXPIRED»
И резерв не блокирует оборудование
@cancel @BR-03 @BR-07
Структура сценария: Отмена освобождает интервал
Допустим моё бронирование «eq-042» с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z» имеет состояние «<состояние>»
И его срок резерва равен «2026-11-02T09:10:00Z»
Когда я отменяю бронирование
Тогда API возвращает статус 200
И состояние бронирования равно «CANCELLED»
Когда другой пользователь резервирует тот же интервал
Тогда API возвращает статус 201
Примеры:
| состояние |
| HELD |
| CONFIRMED |
@confirm @cancel @BR-07
Структура сценария: Повтор уже выполненной команды
Допустим моё бронирование имеет состояние «<состояние>»
Когда я выполняю команду «<команда>»
Тогда API возвращает статус 200
И состояние бронирования остаётся «<состояние>»
Примеры:
| состояние | команда |
| CONFIRMED | confirm |
| CANCELLED | cancel |
@confirm @BR-07
Сценарий: Отменённую бронь нельзя подтвердить
Допустим моё бронирование имеет состояние «CANCELLED»
Когда я подтверждаю резерв
Тогда API возвращает статус 409
И код ошибки равен «INVALID_STATE»
@cancel @BR-05
Сценарий: Истёкший резерв нельзя отменить
Допустим мой резерв находится в состоянии HELD
И его срок истекает в «2026-11-02T09:10:00Z»
И серверное время равно «2026-11-02T09:10:00Z»
Когда я отменяю бронирование
Тогда API возвращает статус 409
И код ошибки равен «RESERVATION_EXPIRED»
@confirm @cancel @BR-06
Структура сценария: Чужое и неизвестное бронирование не раскрываются
Допустим указан идентификатор «<вид>» бронирования
Когда я выполняю команду «<команда>»
Тогда API возвращает статус 404
И код ошибки равен «RESERVATION_NOT_FOUND»
И состояние чужих бронирований не изменяется
Примеры:
| вид | команда |
| чужого | confirm |
| чужого | cancel |
| неизвестного | confirm |
| неизвестного | cancel |
@create @BR-06
Сценарий: Запрос без действительной аутентификации
Допустим мой токен недействителен
Когда я создаю резерв «eq-042» с «2026-11-02T10:00:00Z» до «2026-11-02T11:00:00Z»
Тогда API возвращает статус 401
И код ошибки равен «UNAUTHENTICATED»
@create @BR-02 @BR-06
Сценарий: Нельзя передать владельца в теле запроса
Допустим остальные параметры создания резерва корректны
Когда я создаю резерв с дополнительным полем userId равным «user-b»
Тогда API возвращает статус 400
И код ошибки равен «INVALID_REQUEST»