where('device_token', $token)->first(); } public function recordHeartbeat(PosDevice $device): PosDevice { $device->update([ 'status' => PosDevice::STATUS_ONLINE, 'last_online_at' => now(), ]); return $device->fresh(); } /** Mark token-bearing devices online when their public endpoints are hit. */ public function touchByToken(?string $token): void { if (! $token) { return; } $device = $this->findByToken($token); if ($device) { $this->recordHeartbeat($device); } } public function markStaleDevicesOffline(int $minutes = 10): int { return PosDevice::query() ->where('status', PosDevice::STATUS_ONLINE) ->where(function ($q) use ($minutes) { $q->whereNull('last_online_at') ->orWhere('last_online_at', '<', now()->subMinutes($minutes)); }) ->update(['status' => PosDevice::STATUS_OFFLINE]); } /** @return array */ public function defaultConfigForType(string $type): array { return match ($type) { 'customer_display' => ['role' => 'customer_facing'], 'kitchen_display' => ['role' => 'kitchen'], 'receipt_printer' => ['paper_mm' => 80, 'driver' => 'browser'], 'register' => ['role' => 'till'], default => [], }; } /** * Create a device; customer_display types share the branch display token. * * @param array{name: string, type: string, location_id?: ?int, owner_ref: string} $data */ public function register(array $data): PosDevice { $type = $data['type']; $locationId = isset($data['location_id']) ? (int) $data['location_id'] : null; $token = null; if (in_array($type, config('pos.device_token_types', []), true)) { $token = $this->tokenForNewDevice($type, $locationId, $data['owner_ref']); } return PosDevice::create([ 'owner_ref' => $data['owner_ref'], 'location_id' => $locationId ?: null, 'name' => $data['name'], 'type' => $type, 'status' => PosDevice::STATUS_OFFLINE, 'device_token' => $token, 'config' => $this->defaultConfigForType($type), ]); } public function regenerateToken(PosDevice $device): PosDevice { if (! $device->usesToken()) { return $device; } $token = $this->generateToken(); if ($device->type === 'customer_display' && $device->location_id) { $location = PosLocation::query()->find($device->location_id); if ($location) { $display = $this->displays->ensureForLocation($location); $display->update(['token' => $token]); } } $device->update(['device_token' => $token]); return $device->fresh(); } public function openUrl(PosDevice $device): ?string { if (! $device->device_token) { return null; } return match ($device->type) { 'customer_display' => route('pos.customer-display.show', ['token' => $device->device_token]), 'kitchen_display' => route('pos.kitchen'), default => null, }; } private function tokenForNewDevice(string $type, ?int $locationId, string $ownerRef): string { if ($type === 'customer_display' && $locationId) { $location = PosLocation::query() ->where('owner_ref', $ownerRef) ->whereKey($locationId) ->first(); if ($location) { $display = $this->displays->ensureForLocation($location); // Keep a single active display token per branch; reuse it for the device. return $display->token; } } return $this->generateToken(); } }