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>
47 lines
985 B
PHP
47 lines
985 B
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use Database\Factories\SlotFactory;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* @property int $id
|
|
* @property Carbon $starts_at
|
|
* @property Carbon $ends_at
|
|
* @property int $capacity
|
|
* @property int $remaining
|
|
*/
|
|
class Slot extends Model
|
|
{
|
|
/** @use HasFactory<SlotFactory> */
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'starts_at',
|
|
'ends_at',
|
|
'capacity',
|
|
'remaining',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'starts_at' => 'datetime',
|
|
'ends_at' => 'datetime',
|
|
'capacity' => 'integer',
|
|
'remaining' => 'integer',
|
|
];
|
|
}
|
|
|
|
/** @return HasMany<Hold, $this> */
|
|
public function holds(): HasMany
|
|
{
|
|
return $this->hasMany(Hold::class);
|
|
}
|
|
}
|