Files
ladill-mini/app/Services/Qr/QrScanRecorder.php
T
isaaccladandCursor db66d99895 Initial Ladill Mini app — trader payment QRs without styling.
Lean control center at mini.ladill.com: payment QR CRUD, Paystack checkout with 5% fee settlement via Billing API, payments feed, and payouts. QR codes use a fixed black-and-white preset only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 18:43:39 +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,
];
}
}