ramadanproject/app/Http/Controllers/PublicGridController.php
Ahmed Darrazi 45a147253c
Some checks failed
tests / ci (push) Failing after 6m13s
linter / quality (pull_request) Failing after 58s
linter / quality (push) Failing after 1m19s
tests / ci (pull_request) Failing after 5m28s
feat(public-grid): add QA, quickstart, decision docs; scheduler docs; ignore files; tasks updates; run pint
2026-01-03 04:56:12 +01:00

173 lines
5.1 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\ValidateSelectionRequest;
use App\Models\MasterImage;
use App\Models\Reservation;
use App\Services\SelectionMapper;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class PublicGridController extends Controller
{
public function show()
{
$meta = $this->meta();
return inertia('PublicGrid/Index', [
'master_image_url' => $meta['master_image_url'],
'master_version' => $meta['master_version'],
'cell_size' => $meta['cell_size'],
]);
}
public function meta(): array
{
$masterPath = config('pixel_grid.master_path');
$url = Storage::disk('public')->url($masterPath);
$master = MasterImage::latest()->first();
$version = $master ? $master->version : 1;
return [
'master_image_url' => $url.'?v='.$version,
'master_version' => $version,
'cell_size' => (int) config('pixel_grid.cell_size'),
];
}
public function price(): array
{
return [
'price_per_cell' => (int) config('pixel_grid.price_per_cell'),
];
}
public function availability(): array
{
// Stub for MVP — returns empty array
return [
'occupied' => [],
];
}
public function validateSelection(ValidateSelectionRequest $request, SelectionMapper $mapper)
{
$data = $request->validated();
$offsetX = $data['offsetX'] ?? 0;
$offsetY = $data['offsetY'] ?? 0;
$cellSize = (int) config('pixel_grid.cell_size', 20);
$mapped = $mapper->mapPixelsToCells(
(float) $data['x'],
(float) $data['y'],
(float) $data['w'],
(float) $data['h'],
(float) $offsetX,
(float) $offsetY,
$cellSize
);
return response()->json(['selection' => $mapped]);
}
public function reserveSelection(ValidateSelectionRequest $request, SelectionMapper $mapper)
{
$data = $request->validated();
$offsetX = $data['offsetX'] ?? 0;
$offsetY = $data['offsetY'] ?? 0;
$cellSize = (int) config('pixel_grid.cell_size', 20);
$mapped = $mapper->mapPixelsToCells(
(float) $data['x'],
(float) $data['y'],
(float) $data['w'],
(float) $data['h'],
(float) $offsetX,
(float) $offsetY,
$cellSize
);
// Hold duration in minutes
$holdMinutes = (int) config('pixel_grid.hold_minutes', 5);
$now = Carbon::now();
$expiresAt = $now->copy()->addMinutes($holdMinutes);
// Transaction + FOR UPDATE style check for overlapping active reservations
$result = DB::transaction(function () use ($mapped, $expiresAt, $now) {
$overlaps = Reservation::where(function ($q) use ($mapped) {
$q->whereRaw('NOT (x + w <= ? OR x >= ? OR y + h <= ? OR y >= ?)', [
$mapped['x'],
$mapped['x'] + $mapped['w'],
$mapped['y'],
$mapped['y'] + $mapped['h'],
]);
})->where(function ($q) use ($now) {
$q->where('status', 'held')->where('reserved_until', '>', $now)
->orWhere('status', 'confirmed');
})->lockForUpdate()->exists();
if ($overlaps) {
return null;
}
$reservation = Reservation::create([
'x' => $mapped['x'],
'y' => $mapped['y'],
'w' => $mapped['w'],
'h' => $mapped['h'],
'status' => 'held',
'reserved_until' => $expiresAt,
]);
return $reservation;
});
if (! $result) {
return response()->json(['message' => 'Selection conflicts with an existing reservation'], 409);
}
return response()->json(['reservation_id' => $result->id, 'expires_at' => $result->reserved_until]);
}
public function confirmReservation(Request $request)
{
$id = $request->input('reservation_id');
if (! $id) {
return response()->json(['message' => 'reservation_id required'], 422);
}
$now = Carbon::now();
$reservation = Reservation::where('id', $id)->lockForUpdate()->first();
if (! $reservation) {
return response()->json(['message' => 'not_found'], 404);
}
if ($reservation->status !== 'held') {
return response()->json(['message' => 'invalid_status'], 409);
}
if ($reservation->reserved_until && $reservation->reserved_until->lessThanOrEqualTo($now)) {
$reservation->status = 'expired';
$reservation->save();
return response()->json(['message' => 'expired'], 409);
}
$reservation->status = 'confirmed';
$reservation->save();
return response()->json(['reservation_id' => $reservation->id, 'status' => 'confirmed']);
}
}