Модель и права
Реализуйте 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 для одного сотрудника, конфликт переноса не освобождает прежний слот, а чужая студия не раскрывает календарь или бронь.