← Все проекты уровня 13
Уровень 13 · Сервис для нескольких организацийВариант C

Запись в студии

Бронирование услуг по календарям сотрудников с разделением данных студий и защитой от пересечения записей.

Техническое задание

Модель и права

Реализуйте REST-сервис записи для нескольких студий. Seed создаёт customer, staff и studio_admin для двух studio tenant; фиксированные токены передаются через Authorization header, не тело. Studio tenant содержит сотрудников, услуги, расписания, исключения, клиентов и брони. customer читает доступные слоты и собственные записи, создаёт и отменяет свои; staff видит календарь своей студии и не может менять чужие брони; studio_admin управляет сотрудниками, услугами и расписанием. Все ID-чтения, поиски, календари, отмены и экспорты tenant-scoped. Бронь содержит studio_id, staff_id, service_id, customer_id, начало/конец UTC, timezone запроса, статус booked/cancelled, цену и снимок длительности, revision и audit time.

REST и время

GET /api/v1/studios/{studio_id}/slots?service_id=...&staff_id=...&date=YYYY-MM-DD&timezone=Area/City возвращает начало слотов в UTC и с offset запрошенной зоны. POST /api/v1/studios/{studio_id}/bookings создаёт бронь, GET /bookings/{id} читает, PATCH /bookings/{id}/move переносит, POST /bookings/{id}/cancel отменяет. POST /services и PATCH /services/{id}, POST /staff и PUT /calendar управляются admin. Услуга длится от 15 до 240 минут с шагом 15; цена неотрицательна в RUB. Слоты начинаются на границе 15 минут. Интервалы полуоткрытые [start,end), поэтому бронь, заканчивающаяся ровно при начале следующей, допустима. График указывается локальными интервалами по дням; перерыв и отпуск исключают время. Запрос в прошлом отвергается. Локальное время, которое неоднозначно из-за DST, требует offset; несуществующее время возвращает 400 invalid_local_time. Все записи хранятся UTC.

Идемпотентный ключ создающей операции уникален в пределах studio и customer на 24 часа. Повтор с тем же нормализованным payload возвращает ту же бронь; другой payload под тем же ключом получает 409. Перенос использует revision и при stale revision возвращает 409. Отмена разрешена не позднее чем за 2 часа до начала по UTC, кроме admin, который может отменить с обязательной причиной 10–500 символов.

Структура программы

Предметные правила вычисляют доступность слотов, отмену и пересечение брони; HTTP отвечает за REST-доступ, SQL сохраняет расписания и запись атомарно. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.

Конкурентность, база и приёмка

PostgreSQL и миграции с ручным SQL обязательны. Активные брони защищены exclusion constraint по staff_id и диапазону [start,end); конфликт базы преобразуется в 409 slot_unavailable. Проверка слотов для интерфейса не считается защитой от гонки. Создание двух параллельных броней одного сотрудника и времени должно дать ровно один успех. Перенос атомарно освобождает старый слот только вместе с резервированием нового; при конфликте исходная бронь остаётся без изменения. Сохраняйте снимки длительности и цены. Audit row и изменение статуса фиксируются одной транзакцией. Все SQL вызовы имеют deadline 3 секунды.

Офлайн-тесты с PostgreSQL и двумя студиями проверяют пересекающиеся и смежные диапазоны, гонку, перенос и конфликт, tenant границу, DST, перерыв, отпуск, отмену за 2 часа, повтор ключа и изменение услуги после бронирования. Параллельный тест синхронизируется барьером. Критерии готовности: в таблице никогда нет двух пересекающихся active booking для одного сотрудника, конфликт переноса не освобождает прежний слот, а чужая студия не раскрывает календарь или бронь.

Критерии готовности

Ожидаемый результат

Проверяемый результат

  • README показывает границы и направление зависимостей; main.go только связывает конфигурацию и запуск; модульные тесты ядра обходятся без HTTP и БД, интеграционные тесты отделены.
  • Клиент получает доступные слоты с локальным offset и UTC, затем создаёт и отменяет собственные брони.
  • PostgreSQL exclusion constraint запрещает пересечение активных броней одного сотрудника; синхронная гонка даёт один успех и один 409.
  • Перенос атомарен: при конфликте прежний интервал остаётся занят, новый не создаётся.
  • DST, смежные интервалы, перерывы, отпуска и правило отмены за два часа проверяются фиксированными тестами.
  • Повтор idempotency key возвращает прежнюю бронь, а несовпадающий payload конфликтует.
  • Две студии в тестах подтверждают tenant isolation для слотов, чтения, изменения и отмены.
  • Документация включает расписание API, настройки сервиса, миграции и команды локального запуска.
  • Snapshot цены и длительности остаётся неизменным после обновления услуги; audit сохраняет перенос и отмену.