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']); } }