Разработка большой фичи с нуля часто начинается обманчиво легко. Заказчик объяснил идею, команда обсудила экраны, разработчики примерно представляют архитектуру. Можно приступать. Но стоит перейти от общего замысла к конкретному поведению, как появляются вопросы, на которые у каждого участника свой ответ.
Возьмём веб-систему бронирования общего оборудования. Сотрудник выбирает проектор и время, подтверждает бронь, при необходимости отменяет её. Звучит как несколько форм и CRUD. Иллюзия простоты исчезает, когда два человека одновременно бронируют один проектор, временный резерв зависает навсегда, а отменённая заявка продолжает занимать время в календаре.
Разберём, как пройти путь от такой идеи до реализации через LLM: сначала сделать правила видимыми, затем согласовать модель и контракты, после этого передавать агенту небольшие задачи.
Между бесконечными встречами и преждевременным кодом
Первый способ справиться с неопределённостью, это больше разговаривать. На встрече всё выглядит понятным: «резервируем на некоторое время», «не допускаем двойной брони», «после отмены освобождаем». На следующем обсуждении выясняется, что один участник понимал под резервом десять минут, второй — весь выбранный интервал, а третий считал, что занятость проверяется только при подтверждении.
Команда снова собирается, уточняет термины, договаривается. Так можно потратить часы и дни: разговоры создают ощущение прогресса. Проблема начинается, когда результат не превращается в описание, которое можно проверить целиком.
Другая крайность — быстрее написать код, чтобы было что обсуждать. Здесь тоже есть понятная логика: работающий экран конкретнее разговора. Однако предположения уже становятся таблицами, обработчиками, клиентскими моделями и тестами. Когда выясняется, что временный резерв должен блокировать оборудование сразу, менять приходится несколько связанных частей. Чем больше реализации построено на неверном предположении, тем дороже исправление и шире область возможных ошибок.
*Рисунок 1.Точность описания растёт вместе с трудоёмкостью его подготовки и изменения. *
Код наиболее однозначно задаёт исполняемые инструкции. Но можно реализовать неправильное поведение.
Между разговором и реализацией есть удобная область: модель данных, диаграммы поведения и контракты интерфейсов. Они уже вынуждают ответить на неудобные вопросы, но изменить переход состояния или связи сущностей обычно проще, чем переделать затронутую ими функциональность. В этой статье именно такой подход будем называть Spec driven development: реализация опирается на согласованное описание решения.
Маршрут от идеи до реализации
Рисунок 2. Основное проектное внимание приходится на середину маршрута. LLM помогает на всех этапах; человек проверяет смысл и принимает решения.
Для примера договоримся, что резерв живёт десять минут, после чего его нельзя подтвердить. Подтверждённая бронь и действующий резерв занимают выбранный интервал. Отмена освобождает его. Оплату, выдачу и возврат оборудования оставим за границами фичи.
Схема задаёт порядок уточнения, а не запрет возвращаться назад. Обнаруженный в Sequence конфликт может потребовать изменить требование. Тогда обновляем связанные артефакты до реализации зависимого шага. Также не нужно описывать весь будущий продукт: сначала проходим маршрут для одного законченного сценария.
1. Встречи и обсуждения: выясняем, что вообще строим
Начинаем с людей, которым нужна фича. Какая проблема возникает сейчас? Кто бронирует оборудование? Что пользователь должен получить после подтверждения? Какие ситуации считаются недопустимыми? Здесь полезны конкретные истории: «два сотрудника пришли за одним проектором», «человек открыл форму и ушёл, оборудование осталось занятым».
В нашем примере нужно выяснить момент блокировки, срок резерва, смысл отмены и поведение при конкуренции запросов. Отдельно уточняем границы: бронируем конкретный проектор с инвентарным номером, а не произвольную единицу из категории.
LLM может заранее составить вопросы, сгруппировать противоречивые пожелания, предложить пограничные ситуации. Но ответ «пользователь, вероятно, ожидает десять минут» остаётся предположением модели. Выбор срока принадлежит людям, отвечающим за процесс.
Полезный результат встречи — согласованные решения и короткий список вопросов с ответственными. Если вопрос влияет на данные или публичный интерфейс, его нужно разрешить до проектирования соответствующей части. Другую, независимую часть работы можно продолжать.
2. Конспекты и заметки: сохраняем принятое решение
После встречи нужен компактный конспект. Полная расшифровка сохраняет разговор, но заставляет следующего читателя заново извлекать из него решения. В заметке должны быть видны принятый вариант, его причина и оставшаяся неопределённость.
Например:
Решение: резерв блокирует оборудование на десять минут с момента создания. Причина: пользователь должен успеть проверить параметры перед подтверждением. Истечение определяется сервером. Вопрос: можно ли продлевать резерв? Решение для первой версии: продление не поддерживается.
Такой текст уже отличает согласованный срок от случайно прозвучавшей идеи.
LLM удобно поручить черновик конспекта и список противоречий между встречами. Участники проверяют, что модель не превратила вопрос в решение и не потеряла исключение. На этом этапе особенно легко получить аккуратный, убедительный текст, который приписывает команде несуществующую договорённость.
3. Формальные требования: заменяем «понятно» на проверяемые правила
Теперь формулируем правила так, чтобы два исполнителя получили одинаковый результат. Вместо «не допускаем двойную бронь» записываем, какие заявки занимают ресурс и что считать пересечением.
| Правило | Согласованное поведение | | — | — | | BR-01 | Интервал имеет вид [startsAt, endsAt): начало включено, конец исключён; начало раньше конца. | | BR-02 | При создании устанавливается HELD, срок резерва — десять минут по серверному времени. | | BR-03 | Оборудование блокируют CONFIRMED и HELD, у которого now < holdExpiresAt. | | BR-04 | Проверка пересечения и создание резерва составляют одну атомарную операцию. | | BR-05 | При now >= holdExpiresAt резерв истёк; подтвердить его нельзя. | | BR-06 | Подтверждение и отмена доступны только владельцу бронирования. |
Идентификаторы помогают связывать решения с диаграммами и сценариями. Агенту можно поручить найти правило без примера или неоднозначное слово. Финальный смысл утверждает человек. Подробные переходы и обработку повторных действий зафиксируем дальше, когда рассмотрим жизненный цикл бронирования.
Хорошее требование выдерживает вопрос «как мы поймём, что оно выполнено?». «Система корректно обрабатывает одновременные запросы» такого ответа не даёт. «Из двух пересекающихся заявок на свободное оборудование успешно создаётся ровно одна» уже задаёт наблюдаемую проверку.
4. Модель данных и ERD: определяем, что существует в системе
В модели достаточно трёх сущностей: пользователь, единица оборудования и бронирование. Каждое бронирование принадлежит одному пользователю и относится к одной единице. У пользователя и оборудования может быть много бронирований.
Рисунок 3. startsAt и endsAt описывают использование оборудования, holdExpiresAt — срок принятия решения пользователем.
ERD показывает структуру и кратность связей. Правило запрета пересечений нужно записать рядом: обычного внешнего ключа для него недостаточно.
LLM может подготовить текстовую схему и проверить имена полей относительно требований.
Сразу полезно отделить хранимые данные от публичного ответа. Серверу нужен владелец записи, но возвращать клиенту внешнюю идентичность пользователя для подтверждения своей брони незачем. И наоборот, интерфейсу необходимо знать срок резерва, чтобы объяснить оставшееся время. Такой разбор предотвращает привычное копирование таблицы целиком в API и помогает сформулировать будущую схему ответа без привязки к используемому ORM.
5. Use Case: описываем цели пользователя
Use Case помогает проверить, что проект решает пользовательскую задачу. Для сотрудника выделим четыре цели: посмотреть доступность, создать временный резерв, подтвердить и отменить бронирование.
Рисунок 4. Линии связывают участника с целями; они не задают последовательность HTTP-вызовов.
Не стоит превращать этот рисунок в перечень всех методов сервиса. Сохранить запись в базе — действие реализации, а сотруднику нужен результат бронирования. Аутентификацию принимаем как предусловие рассматриваемых действий.
LLM легко строит такую схему по списку целей. Польза ревью состоит в проверке границ: не появилась ли у пользователя возможность подтвердить чужую заявку, не пропала ли отмена, не добавилось ли управление оборудованием, которое пока не входит в задачу. Общую нотацию этих и следующих поведенческих диаграмм определяет UML.
6. C4 Context: обозначаем место системы
Следующий вопрос — где заканчивается наша ответственность. На Context-диаграмме есть сотрудник, система бронирования и внешний провайдер идентификации. Мы используем существующие учётные записи, а правила резервирования принадлежат создаваемой системе.
Рисунок 5. Контекст системы: пользователь, целевая система и внешняя зависимость.
Context удобно обсуждать с людьми, которым не нужны подробности серверного кода. Они могут заметить отсутствующую внешнюю зависимость или неверно очерченную ответственность. Например, если оборудование на самом деле выдаёт отдельная служба, её участие станет новым требованием.
7. C4 Container: распределяем ответственность
Раскроем систему до трёх частей: веб-интерфейс, API бронирования и реляционная база. Интерфейс показывает данные и отправляет команды. API проверяет пользователя и правила. База хранит состояние и участвует в обеспечении атомарности.
Рисунок 6. Контейнер C4 — приложение или хранилище данных
На схеме указываем протоколы и сохраняем названия для будущей Sequence. LLM помогает собрать и отрисовать архитектуру. Для выбранного поведения здесь достаточно одного серверного приложения. Назначение этого уровня подробнее описано в документации C4 Container.
8. State Machine: разбираемся с жизненным циклом
У бронирования четыре состояния: временный резерв HELD, подтверждённая бронь CONFIRMED, отмена CANCELLED и истечение EXPIRED. Из временного резерва можно перейти в любое из трёх остальных состояний. Подтверждённую бронь можно отменить. Отмена и истечение завершают жизненный цикл в пределах нашего примера.
Рисунок 7. Условия переходов важнее самого списка статусов. Ровно на границе срока подтверждение уже недопустимо.
Для подготовки диаграммы достаточно короткого задания:
По правилам BR-02, BR-05 и BR-07 версии equipment-reservation-v1построй State Machine в PlantUML. Покажи условия переходов.Повторные действия опиши отдельно. Не добавляй новых состояний.Если правило неоднозначно, перечисли вопросы перед диаграммой.
Текстовую диаграмму легко править через LLM и хранить рядом с документами. Синтаксис и рендер автоматизируются; проверка смысла остаётся задачей ревью. Описание State Diagram в PlantUML.
9. Sequence: проверяем взаимодействие участников
Теперь проследим создание резерва: веб-интерфейс отправляет параметры, API проверяет их, обращается к базе, создаёт запись и возвращает результат. Названия участников берём из Container, полей — из ERD, условий — из требований.
Рисунок 8. Проверка и запись защищены одной транзакцией и сериализацией операций по оборудованию.
LLM полезно попросить найти сообщения без обработчика, разрывы транзакции и альтернативные ветки. На ревью проверяем также момент ответа: 201 отправляется после успешного завершения транзакции.
10. Activity: раскрываем алгоритм и развилки
Sequence отвечает на вопрос «кто с кем взаимодействует», Activity — «какие решения принимаются». В алгоритме создания резерва проверяем входные данные, затем в защищённой операции определяем блокирующие брони и ищем пересечение.
Рисунок 9. Истёкший HELD исключается из блокирующих броней до проверки пересечения.
Условия удобно записать без привязки к языку программирования:
overlap = existing.startsAt < requested.endsAtand requested.startsAt < existing.endsAtblocks = status == CONFIRMEDor (status == HELD and now < holdExpiresAt)
LLM может развернуть правила в Activity и предложить примеры для каждой ветки. При ревью важно сверить порядок действий с Sequence. Красивые картинки, описывающие разные алгоритмы, только увеличат неопределённость. Одна и та же атомарная область должна сохраняться в обоих представлениях.
11. OpenAPI: закрепляем внешний интерфейс
Когда поведение прояснилось, фиксируем HTTP-контракт. В учебном документе есть создание резерва, подтверждение и отмена. Просмотр оборудования и весь процесс входа в него не включены. Используем OpenAPI 3.0.3, как в исходной презентации; это выбор формата примера, а не рекомендация считать его последней версией.
Для создания нужны equipmentId, startsAt и endsAt. Владелец и срок резерва назначаются сервером. Ни userId, ни желаемый статус клиент не передаёт. Короткий фрагмент операции выглядит так:
/api/v1/reservations:post:operationId: createReservationsecurity:— bearerAuth: []requestBody:required: truecontent: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.
12. Markdown-спецификация: собираем согласованный проект
К этому моменту появились несколько связанных представлений. Теперь собираем документ, через который разработчик или агент может пройти от цели до конкретного решения: границы, термины, правила, модель, диаграммы, контракт, принятые ограничения и версия.
Первоначальные требования объясняли, чего мы хотим. Итоговая Markdown-спецификация фиксирует согласованное устройство выбранного сценария. Не нужно вручную переписывать в неё весь OpenAPI: достаточно ссылок и объяснения решений, которые нельзя вывести из структуры запроса.
В приложениях есть спецификация примера версии equipment-reservation-v1. Это обозначение редакции учебного комплекта, не ссылка на существующий Git-тег. В ней, например, явно разделены сохранённый статус и фактическое истечение по серверному времени.
LLM помогает проверить согласованность: все ли состояния существуют в модели, соответствует ли ответ ветке алгоритма, нет ли правила без сценария.
13. BDD-сценарии: договариваемся о наблюдаемом результате
Перед кодом задаём конкретные исходные данные, действие и ожидаемый результат. Например, в терминах Gherkin:
# language: ruФункциональность: Резервирование оборудованияСценарий: Подтверждение ровно на границе срокаДопустим мой резерв находится в состоянии HELDИ его срок истекает в «2026-11-02T09:10:00Z»И серверное время равно «2026-11-02T09:10:00Z»Когда я подтверждаю резервТогда API возвращает статус 409И код ошибки равен «RESERVATION_EXPIRED»И резерв не блокирует оборудование
Нужны также успешное создание, пересечение, соседние интервалы, отмена, повторные команды и доступ к чужой брони. Для конкуренции фиксируем результат: из двух одновременных пересекающихся запросов к свободному оборудованию один получает 201, другой — 409; блокирующая запись остаётся одна. Победитель заранее не определён.
Сценарии хранятся в отдельном файле. Пока у них нет шаговых определений и приложения, это согласованные примеры, а не выполненные автоматические тесты. Их автоматизацию поручаем LLM при реализации соответствующего шага.
14. Код и тесты: одна задача, одно ревью, следующий шаг
Теперь можно передавать реализацию агенту. Большую фичу разбиваем так, чтобы результат каждого задания можно было проверить отдельно: модель и переходы; атомарное резервирование; подтверждение и отмена; 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 — openapi.yaml
openapi: 3.0.3info:title: Бронирование оборудования — учебный HTTP-контрактversion: 1.0.0description: ’Редакция комплекта: equipment-reservation-v1. Охват: создание, подтверждениеи отмена. Просмотр доступности, вход, эксплуатационные ошибки и выдача оборудованияне описаны. Бизнес-правила и модель находятся в reservation-spec.md. Это проектконтракта, сервис не реализован.’paths:/api/v1/reservations:post:operationId: createReservationsummary: Создать временный резервdescription: ’BR-01—BR-04. Атомарно проверить пересечение и создать HELD на10 минут. Владелец берётся из проверенной идентичности. Повторный POST неимеет специального ключа идемпотентности: при действующем первом резерве пересекающийсяповтор получит 409 EQUIPMENT_BUSY.’security:— bearerAuth: []requestBody:required: truecontent:application/json:schema:$ref: ’#/components/schemas/CreateReservation’example:equipmentId: eq-042startsAt: ’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-001equipmentId: eq-042startsAt: ’2026-11-02T10:00:00Z’endsAt: ’2026-11-02T11:00:00Z’status: HELDholdExpiresAt: ’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: confirmReservationsummary: Подтвердить собственный резервdescription: BR-05—BR-07. HELD подтверждается только при now < holdExpiresAt.CONFIRMED возвращается без изменения с 200. EXPIRED, включая HELD с истёкшимсроком, возвращает 409 RESERVATION_EXPIRED. CANCELLED возвращает 409 INVALID_STATE.security:— bearerAuth: []parameters:— name: reservationIdin: pathrequired: trueschema: &id005type: stringminLength: 1maxLength: 80pattern: ^[A-Za-z0-9_-]+$responses:’200′:description: Операция выполненаcontent:application/json:schema:$ref: ’#/components/schemas/Reservation’example:id: res-001equipmentId: eq-042startsAt: ’2026-11-02T10:00:00Z’endsAt: ’2026-11-02T11:00:00Z’status: CONFIRMEDholdExpiresAt: ’2026-11-02T09:10:00Z’’400′: *id001’401′: *id002’404′: *id003’409′: *id004/api/v1/reservations/{reservationId}/cancel:post:operationId: cancelReservationsummary: Отменить собственное бронированиеdescription: BR-05—BR-07. Действующий HELD и CONFIRMED переходят в CANCELLED.Повторная отмена CANCELLED возвращает 200 без изменения. EXPIRED, включаяHELD с истёкшим сроком, возвращает 409 RESERVATION_EXPIRED.security:— bearerAuth: []parameters:— name: reservationIdin: pathrequired: trueschema: *id005responses:’200′:description: Операция выполненаcontent:application/json:schema:$ref: ’#/components/schemas/Reservation’example:id: res-001equipmentId: eq-042startsAt: ’2026-11-02T10:00:00Z’endsAt: ’2026-11-02T11:00:00Z’status: CANCELLEDholdExpiresAt: ’2026-11-02T09:10:00Z’’400′: *id001’401′: *id002’404′: *id003’409′: *id004components:securitySchemes:bearerAuth:type: httpscheme: bearerbearerFormat: JWTdescription: API проверяет подпись, издателя, аудиторию и срок токена. Локальныйпользователь определяется по паре issuer/subject; userId из запроса не принимается.schemas:CreateReservation:type: objectadditionalProperties: falserequired:— equipmentId— startsAt— endsAtdescription: ’BR-01: startsAt < endsAt. Сравнение дат и проверка пересечениявыполняются сервером.’properties:equipmentId: *id005startsAt: &id006type: stringformat: date-timepattern: Z$description: Время UTC; строка RFC 3339 с суффиксом Z.endsAt: *id006Reservation:type: objectadditionalProperties: falserequired:— id— equipmentId— startsAt— endsAt— status— holdExpiresAtproperties:id: *id005equipmentId: *id005startsAt: *id006endsAt: *id006status:type: stringenum:— HELD— CONFIRMED— CANCELLED— EXPIREDdescription: ’Эффективное состояние: HELD с now >= holdExpiresAt считаетсяEXPIRED.’holdExpiresAt:type: stringformat: date-timepattern: Z$description: Исходный срок временного резерва; после CONFIRMED или CANCELLEDсохраняется для истории и больше не ограничивает эти состояния.Error:type: objectadditionalProperties: falserequired:— code— messageproperties:code:type: stringenum:— INVALID_REQUEST— UNAUTHENTICATED— EQUIPMENT_NOT_FOUND— RESERVATION_NOT_FOUND— EQUIPMENT_BUSY— RESERVATION_EXPIRED— INVALID_STATEmessage:type: stringdescription: Пояснение для человека; клиент ветвится по code.responses:BadRequest:description: Некорректные параметры, лишние поля или startsAt >= endsAt.content:application/json:schema:$ref: ’#/components/schemas/Error’example:code: INVALID_REQUESTmessage: Некорректные параметры, лишние поля или startsAt >= endsAt.Unauthorized:description: Отсутствует действительная аутентификация.content:application/json:schema:$ref: ’#/components/schemas/Error’example:code: UNAUTHENTICATEDmessage: Отсутствует действительная аутентификация.headers:WWW-Authenticate:schema:type: stringexample: Bearerdescription: Схема аутентификации.NotFound:description: Оборудование не найдено; бронирование отсутствует или принадлежитдругому пользователю.content:application/json:schema:$ref: ’#/components/schemas/Error’example:code: RESERVATION_NOT_FOUNDmessage: Оборудование не найдено; бронирование отсутствует или принадлежитдругому пользователю.Conflict:description: Конфликт интервала либо недопустимое действие над состоянием.content:application/json:schema:$ref: ’#/components/schemas/Error’examples:EQUIPMENT_BUSY:value:code: EQUIPMENT_BUSYmessage: Интервал занят.RESERVATION_EXPIRED:value:code: RESERVATION_EXPIREDmessage: Срок резерва истёк.INVALID_STATE:value:code: INVALID_STATEmessage: Действие недопустимо в текущем состоянии.
Спецификация и исходники диаграмм — reservation-spec.md
Бронирование оборудования: спецификация учебного примера
Редакция: 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 после неизвестного результата не считается безопасным решением по умолчанию. Пример описывает нормальное завершение транзакций и бизнес-конфликты.
Публичный HTTP-контракт
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-файла не означает, что тесты выполнены: сервис и шаговые определения в этот комплект не входят.
Текстовые исходники диаграмм
03-erd.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65hide circlehide methodsentity «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
04-use-case.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65actor «Сотрудник» as Urectangle «Система бронирования» {usecase «Посмотреть\nдоступность» as Viewusecase «Создать\nвременный резерв» as Holdusecase «Подтвердить\nсвоё бронирование» as Confirmusecase «Отменить\nсвоё бронирование» as CancelView -[hidden]down- HoldHold -[hidden]down- ConfirmConfirm -[hidden]down- Cancel}U -right- ViewU -right- HoldU -right- ConfirmU -right- Cancel@enduml
05-c4-context.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65actor «Сотрудник\n[Person]» as Urectangle «Система бронирования\n[Software System]\n\nДоступность, резервы,\nподтверждение и отмена» as Srectangle «Провайдер идентификации\n[External Software System]\n\nУчётные записи\nи выпуск токенов» as I #E8E9EDU -down-> S : Бронирует оборудованиеS -down-> I : Использует внешнюю идентичностьU -right-> I : Входит в учётную запись@enduml
06-c4-container.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65actor «Сотрудник\n[Person]» as Urectangle «Система\nбронирования» {rectangle «Веб-интерфейс\n[Container: браузерное приложение]\n\nВыбор времени и команды» as Wrectangle «API бронирования\n[Container: серверное приложение]\n\nПроверка пользователя и правил» as Adatabase «База бронирований\n[Container: реляционная БД]\n\nДанные, транзакции, блокировки\n» as DW -down-> A : Команды / ответы\n[HTTPS / JSON]\nBearer-токенA -down-> D : Чтение и изменение\n[SQL]}rectangle «Провайдер\nидентификации\n[External System]» as I #E8E9EDU -down-> W : ИспользуетW -right-> I : Вход\n[OIDC / HTTPS]A -right-> I : Ключи\nпроверки\n[HTTPS]@enduml
07-state-machine.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65state «Временный резерв\nHELD» as Hstate «Подтверждённая бронь\nCONFIRMED» as Cstate «Отмена\nCANCELLED» as Xstate «Истечение\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
08-sequence.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65hide footboxskinparam maxMessageSize 350participant «Веб-интерфейс» as Wparticipant «API бронирования» as Adatabase «База бронирований» as Dnote over W,DПредусловие: аутентификация и параметры проверены;оборудование существует. Показана операция создания.end noteW -> A : POST /api/v1/reservationsactivate Agroup Одна транзакция / сериализация по оборудованиюA -> D : BEGIN; заблокировать строку EquipmentD —> A : Блокировка полученаA -> A : now = серверное времяA -> D : Прочитать блокирующие брони\nи проверить пересечениеD —> A : Результат проверкиalt Есть пересечениеA -> D : ROLLBACK; освободить блокировкуD —> A : Транзакция завершенаA —> W : 409 EQUIPMENT_BUSYelse Интервал свободенA -> D : INSERT Reservation: HELD\nholdExpiresAt = now + 10 минA -> D : COMMIT; освободить блокировкуD —> A : Изменение сохраненоA —> W : 201 Reservationendenddeactivate A@enduml
09-activity.puml
@startuml!pragma layout smetanaskinparam backgroundColor #F7F6F2skinparam defaultFontName DejaVu Sansskinparam defaultFontSize 23skinparam shadowing falseskinparam roundcorner 12skinparam ArrowColor #315DF5skinparam ArrowThickness 2skinparam LineThickness 2skinparam defaultFontColor #182020skinparam classBackgroundColor #E9EEFFskinparam classBorderColor #315DF5skinparam classAttributeFontSize 22skinparam classFontSize 25skinparam rectangleBackgroundColor #E9EEFFskinparam rectangleBorderColor #315DF5skinparam rectangleFontSize 23skinparam packageBackgroundColor #F7F6F2skinparam packageBorderColor #9BAACDskinparam stateBackgroundColor #E9EEFFskinparam stateBorderColor #315DF5skinparam stateFontSize 24skinparam usecaseBackgroundColor #E9EEFFskinparam usecaseBorderColor #315DF5skinparam activityBackgroundColor #E9EEFFskinparam activityBorderColor #315DF5skinparam activityDiamondBackgroundColor #FFFFFFskinparam activityDiamondBorderColor #315DF5skinparam participantBackgroundColor #E9EEFFskinparam participantBorderColor #315DF5skinparam sequenceLifeLineBorderColor #9BAACDskinparam sequenceGroupBorderColor #315DF5skinparam sequenceGroupBackgroundColor #F7F6F2skinparam noteBackgroundColor #FFFFFFskinparam noteBorderColor #9BAACDskinparam nodesep 55skinparam ranksep 65start:Аутентифицированный запрос;if (Параметры корректны?) then (нет):400 INVALID_REQUEST;stopelse (да)endifif (Оборудование существует?) then (нет):404 EQUIPMENT_NOT_FOUND;stopelse (да)endifpartition «Атомарная операция» {:Начать транзакцию;\nзаблокировать строку Equipment;:Получить серверное now;:CONFIRMED блокирует без проверки срока;\nHELD — только при now < holdExpiresAt;if (Есть пересечение\nинтервалов?) then (да):Откатить транзакцию;\nосвободить блокировку;:409 EQUIPMENT_BUSY;stopelse (нет):Создать HELD;\nholdExpiresAt = now + 10 мин;:Зафиксировать транзакцию;\nосвободить блокировку;endif}:201 Reservation;stop@enduml
BDD-сценарии — reservations.feature
# 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»
ссылка на оригинал статьи https://habr.com/ru/articles/1101186/