# Slot Booking API API бронирования слотов: горячий кеш доступности, временные холды с идемпотентностью, защита от оверсела. **Стек:** Laravel 12 (PHP 8.2+), MySQL 8+. Внешних сервисов не требуется — кеш и блокировки хранятся в той же базе. Описание устройства сервиса — [docs/README.md](docs/README.md). Коллекция Postman — [docs/postman](docs/postman/slot-booking-api.postman_collection.json). ## Быстрый старт ```bash docker run -d --name stl-mysql \ -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=slots \ -p 3307:3306 mysql:8 ``` ```bash cp .env.example .env && composer install && php artisan key:generate ``` ```bash php artisan migrate --seed ``` ```bash php artisan serve --port=8001 ``` Сидер создаёт три слота с вместимостью 10, 5 и 1. Слот с одним местом позволяет воспроизвести конфликт оверсела двумя запросами. ## Тесты Тесты выполняются против MySQL (см. `phpunit.xml`), поэтому нужна отдельная база: ```bash docker exec stl-mysql mysql -uroot -psecret -e "CREATE DATABASE IF NOT EXISTS slots_test" ``` ```bash php artisan test ``` 27 тестов, 90 проверок: кеш и его инвалидация, поведение при потере гонки за пересчёт, идемпотентность, истечение холдов, защита от оверсела при конкурентном подтверждении. ## Эндпоинты | Метод | Путь | Ответ | |---|---|---| | `GET` | `/slots/availability` | `200` — массив слотов | | `POST` | `/slots/{id}/hold` | `201` создан / `200` повтор по тому же ключу / `409` мест нет / `422` некорректный `Idempotency-Key` / `404` | | `POST` | `/holds/{id}/confirm` | `200` / `409` мест нет либо холд неактивен / `404` | | `DELETE` | `/holds/{id}` | `200` / `404` | Ошибки приходят в виде `{"message": "...", "reason": "<код>"}`. Коды: `capacity_exhausted`, `hold_cancelled`, `hold_expired`, `idempotency_key_reuse`, `not_found`. ## Примеры запросов Прогон против запущенного сервера. Слот `3` имеет `capacity = 1`. ### Доступность ```bash curl -s http://127.0.0.1:8001/slots/availability ``` ```json [{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":1}] ``` ### Создание холда ```bash curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0' ``` ``` HTTP/1.1 201 Created {"hold_id":1,"slot_id":3,"status":"held","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":null,"cancelled_at":null} ``` ### Повтор с тем же ключом — тот же холд, `200` вместо `201` ```bash curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0' ``` ``` HTTP/1.1 200 OK {"hold_id":1,"slot_id":3,"status":"held","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":null,"cancelled_at":null} ``` Второй строки в `holds` не появляется. ### Конфликт при оверселе ```bash curl -si -X POST http://127.0.0.1:8001/slots/3/hold -H 'Idempotency-Key: bab7c4be-3184-40d6-ab90-b4118c40bcb3' ``` ``` HTTP/1.1 409 Conflict {"message":"Slot 3 has no remaining capacity.","reason":"capacity_exhausted"} ``` ### Переиспользование ключа для другого слота ```bash curl -si -X POST http://127.0.0.1:8001/slots/1/hold -H 'Idempotency-Key: 6e517e8e-1470-4a78-bdbd-e2b50776a6a0' ``` ``` HTTP/1.1 422 Unprocessable Content {"message":"Idempotency-Key 6e517e8e-... was already used for a different slot than 1.","reason":"idempotency_key_reuse"} ``` ### Подтверждение ```bash curl -si -X POST http://127.0.0.1:8001/holds/1/confirm ``` ``` HTTP/1.1 200 OK {"hold_id":1,"slot_id":3,"status":"confirmed","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":"2026-07-29T18:14:57+00:00","cancelled_at":null} ``` Кеш инвалидирован, остаток слота `3` обновился сразу: ```bash curl -s http://127.0.0.1:8001/slots/availability ``` ```json [{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":0}] ``` Повторный `confirm` того же холда возвращает `200` и тот же результат — место списывается один раз. ### Отмена ```bash curl -si -X DELETE http://127.0.0.1:8001/holds/1 ``` ``` HTTP/1.1 200 OK {"hold_id":1,"slot_id":3,"status":"cancelled","expires_at":"2026-07-29T18:19:56+00:00","confirmed_at":"2026-07-29T18:14:57+00:00","cancelled_at":"2026-07-29T18:14:57+00:00"} ``` ```bash curl -s http://127.0.0.1:8001/slots/availability ``` ```json [{"slot_id":1,"capacity":10,"remaining":10},{"slot_id":2,"capacity":5,"remaining":5},{"slot_id":3,"capacity":1,"remaining":1}] ``` Подтвердить отменённый холд уже нельзя: ```bash curl -si -X POST http://127.0.0.1:8001/holds/1/confirm ``` ``` HTTP/1.1 409 Conflict {"message":"Hold 1 is no longer active and cannot be confirmed.","reason":"hold_cancelled"} ``` ## Структура ``` app/Enums/HoldStatus.php held | confirmed | cancelled app/Exceptions/SlotBookingException.php доменные отказы и их HTTP-статус app/Http/Controllers/AvailabilityController.php app/Http/Controllers/HoldController.php app/Http/Requests/StoreHoldRequest.php валидация заголовка Idempotency-Key app/Http/Resources/HoldResource.php app/Models/Slot.php app/Models/Hold.php scopeActive() — определение активного холда app/Services/SlotService.php транзакции, кеш, идемпотентность config/slots.php TTL кеша и TTL холда routes/api.php docs/README.md устройство сервиса docs/postman/ коллекция Postman ```