GET /slots/availability, POST /slots/{id}/hold, POST /holds/{id}/confirm
and DELETE /holds/{id} on Laravel 12 and MySQL 8.
- availability cached for 10s behind a single-flight lock with a stale
copy, invalidated after commit on confirm and cancel
- oversell prevented by a conditional decrement, verified by the number
of affected rows rather than a preceding select
- idempotency enforced by a unique index on holds.idempotency_key
- active holds counted with an expires_at filter, so no background
cleanup is required for correctness
- 27 feature tests against MySQL, service documentation, Postman collection
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
39 lines
1.8 KiB
PHP
39 lines
1.8 KiB
PHP
<?php
|
||
|
||
use App\Exceptions\SlotBookingException;
|
||
use Illuminate\Foundation\Application;
|
||
use Illuminate\Foundation\Configuration\Exceptions;
|
||
use Illuminate\Foundation\Configuration\Middleware;
|
||
use Illuminate\Http\Request;
|
||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||
|
||
return Application::configure(basePath: dirname(__DIR__))
|
||
->withRouting(
|
||
web: __DIR__.'/../routes/web.php',
|
||
// Пути в задании заданы без префикса. Маршруты при этом остаются
|
||
// в routes/api.php со stateless-мидлварами группы api.
|
||
api: __DIR__.'/../routes/api.php',
|
||
apiPrefix: '',
|
||
commands: __DIR__.'/../routes/console.php',
|
||
health: '/up',
|
||
)
|
||
->withMiddleware(function (Middleware $middleware): void {
|
||
//
|
||
})
|
||
->withExceptions(function (Exceptions $exceptions): void {
|
||
// Доменные отказы несут свой статус и код причины.
|
||
$exceptions->render(fn (SlotBookingException $e) => response()->json([
|
||
'message' => $e->getMessage(),
|
||
'reason' => $e->reason,
|
||
], $e->status));
|
||
|
||
// 404 без имени класса модели в ответе. ModelNotFoundException ловить
|
||
// бесполезно: Laravel подменяет её на NotFoundHttpException до колбэков.
|
||
$exceptions->render(fn (NotFoundHttpException $e) => response()->json([
|
||
'message' => 'Resource not found.',
|
||
'reason' => 'not_found',
|
||
], 404));
|
||
|
||
// Только API: JSON даже клиенту без Accept: application/json.
|
||
$exceptions->shouldRenderJsonWhen(fn (Request $request, Throwable $e) => true);
|
||
})->create();
|