Files
ladill-events/app/Services/Qr/QrScanRecorder.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

90 lines
3.0 KiB
PHP

<?php
namespace App\Services\Qr;
use App\Models\QrCode;
use App\Models\QrScanEvent;
use App\Models\QrWallet;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class QrScanRecorder
{
public function record(QrCode $qrCode, Request $request): QrScanEvent
{
$parsed = $this->parseUserAgent((string) $request->userAgent());
$ipHash = $this->hashIp((string) $request->ip());
$windowHours = (int) config('qr.scan_unique_window_hours', 24);
$isUnique = ! QrScanEvent::query()
->where('qr_code_id', $qrCode->id)
->where('ip_hash', $ipHash)
->where('scanned_at', '>=', now()->subHours($windowHours))
->exists();
$event = QrScanEvent::create([
'qr_code_id' => $qrCode->id,
'scanned_at' => now(),
'ip_hash' => $ipHash,
'user_agent' => Str::limit((string) $request->userAgent(), 500, ''),
'device_type' => $parsed['device_type'],
'browser' => $parsed['browser'],
'os' => $parsed['os'],
'referrer' => Str::limit((string) $request->headers->get('referer'), 255, ''),
'is_unique' => $isUnique,
]);
$qrCode->increment('scans_total');
if ($isUnique) {
$qrCode->increment('unique_scans_total');
}
$qrCode->update(['last_scanned_at' => now()]);
QrWallet::query()
->where('user_id', $qrCode->user_id)
->increment('scans_total');
return $event;
}
private function hashIp(string $ip): string
{
return hash('sha256', $ip . '|' . (string) config('app.key'));
}
/**
* @return array{device_type: string, browser: string, os: string}
*/
private function parseUserAgent(string $ua): array
{
$uaLower = strtolower($ua);
$device = str_contains($uaLower, 'mobile') || str_contains($uaLower, 'android') || str_contains($uaLower, 'iphone')
? 'mobile'
: (str_contains($uaLower, 'tablet') || str_contains($uaLower, 'ipad') ? 'tablet' : 'desktop');
$browser = match (true) {
str_contains($uaLower, 'edg/') => 'Edge',
str_contains($uaLower, 'chrome/') && ! str_contains($uaLower, 'edg/') => 'Chrome',
str_contains($uaLower, 'safari/') && ! str_contains($uaLower, 'chrome/') => 'Safari',
str_contains($uaLower, 'firefox/') => 'Firefox',
default => 'Other',
};
$os = match (true) {
str_contains($uaLower, 'iphone') || str_contains($uaLower, 'ipad') => 'iOS',
str_contains($uaLower, 'android') => 'Android',
str_contains($uaLower, 'windows') => 'Windows',
str_contains($uaLower, 'mac os') || str_contains($uaLower, 'macintosh') => 'macOS',
str_contains($uaLower, 'linux') => 'Linux',
default => 'Other',
};
return [
'device_type' => $device,
'browser' => $browser,
'os' => $os,
];
}
}