Deploy Ladill Mini / deploy (push) Successful in 23s
Staff-facing counter register at pos.ladill.com with catalog cart, cash and MoMo/card checkout via Ladill Pay, CRM timeline/import, invoice prefill, and Merchant catalog import. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.0 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|