Files
ladill-events/app/Http/Controllers/Api/QrCodeController.php
T
isaaccladandCursor d8dbc83e2d
Deploy Ladill QR Plus / deploy (push) Successful in 28s
Extract Ladill Events as a standalone events suite at events.ladill.com.
Full control center for ticketed events, contributions, attendees, badges,
and programmes — not a QR utility clone. Includes SSO shell, import command,
and platform cutover runbook.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 09:39:53 +00:00

154 lines
5.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Services\Qr\QrAnalyticsService;
use App\Services\Qr\QrCodeManagerService;
use App\Support\Qr\QrTypeCatalog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
class QrCodeController extends Controller
{
public function __construct(
private QrCodeManagerService $manager,
private QrAnalyticsService $analytics,
) {}
public function index(Request $request): JsonResponse
{
$codes = $this->accountQuery()
->latest()
->get()
->map(fn (QrCode $code) => $this->present($code));
return response()->json(['data' => $codes]);
}
public function show(QrCode $qrCode): JsonResponse
{
$this->ensurePlusCode($qrCode);
$this->authorize('view', $qrCode);
return response()->json(['data' => $this->present($qrCode, detailed: true)]);
}
public function analytics(QrCode $qrCode): JsonResponse
{
$this->ensurePlusCode($qrCode);
$this->authorize('view', $qrCode);
return response()->json([
'data' => [
'summary' => $this->analytics->summaryFor($qrCode),
'daily_scans' => $this->analytics->dailyScans($qrCode, 30)->values(),
'devices' => $this->analytics->breakdown($qrCode, 'device_type'),
'browsers' => $this->analytics->breakdown($qrCode, 'browser'),
],
]);
}
public function store(Request $request): JsonResponse
{
abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.');
$validated = $request->validate([
'label' => ['required', 'string', 'max:120'],
'type' => ['required', 'in:'.implode(',', QrTypeCatalog::keys())],
'destination_url' => ['nullable', 'url', 'max:2048'],
'custom_short_code' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/', 'unique:qr_codes,short_code'],
'is_active' => ['sometimes', 'boolean'],
]);
if ($validated['type'] === QrCode::TYPE_DOCUMENT) {
return response()->json([
'message' => 'PDF QR codes must be created in the QR Plus app (file upload required).',
], 422);
}
try {
$qrCode = $this->manager->create(ladill_account(), array_merge(
$request->all(),
$validated,
));
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
return response()->json(['data' => $this->present($qrCode->fresh(), detailed: true)], 201);
}
public function update(Request $request, QrCode $qrCode): JsonResponse
{
abort_unless($request->user()->tokenCan('qr:write'), 403, 'Token requires qr:write ability.');
$this->ensurePlusCode($qrCode);
$this->authorize('update', $qrCode);
$request->validate([
'label' => ['sometimes', 'string', 'max:120'],
'destination_url' => ['nullable', 'url', 'max:2048'],
'is_active' => ['sometimes', 'boolean'],
]);
if ($qrCode->isDocumentType() && $request->hasFile('document')) {
return response()->json([
'message' => 'Replace PDF files in the QR Plus app.',
], 422);
}
try {
$updated = $this->manager->update($qrCode, $request->all());
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
return response()->json(['data' => $this->present($updated->fresh(), detailed: true)]);
}
/** @return \Illuminate\Database\Eloquent\Builder<QrCode> */
private function accountQuery()
{
return QrCode::query()
->where('user_id', ladill_account()->id)
->whereIn('type', QrTypeCatalog::eventTypes());
}
private function ensurePlusCode(QrCode $qrCode): void
{
abort_unless(
$qrCode->user_id === ladill_account()->id && QrTypeCatalog::isValid($qrCode->type),
404,
);
}
/** @return array<string, mixed> */
private function present(QrCode $qrCode, bool $detailed = false): array
{
$data = [
'id' => $qrCode->id,
'label' => $qrCode->label,
'type' => $qrCode->type,
'type_label' => $qrCode->typeLabel(),
'short_code' => $qrCode->short_code,
'public_url' => $qrCode->publicUrl(),
'is_active' => $qrCode->is_active,
'scans_total' => $qrCode->scans_total,
'unique_scans_total' => $qrCode->unique_scans_total,
'last_scanned_at' => $qrCode->last_scanned_at?->toIso8601String(),
'created_at' => $qrCode->created_at?->toIso8601String(),
'updated_at' => $qrCode->updated_at?->toIso8601String(),
];
if ($detailed) {
$data['destination_url'] = $qrCode->destination_url;
$data['content'] = $qrCode->content();
}
return $data;
}
}