Files
ladill-pos/app/Services/Pos/DeviceService.php
T
isaacclad ede4aaa10c
Deploy Ladill POS / deploy (push) Successful in 46s
Add public token-gated kitchen display for POS devices.
Kitchen display devices open a full-screen KDS without staff login,
with feed/stream/bump over the device token. Shared kitchen board
logic is extracted so the signed-in Kitchen page stays in sync.
2026-07-15 22:52:28 +00:00

149 lines
4.4 KiB
PHP

<?php
namespace App\Services\Pos;
use App\Models\PosCustomerDisplay;
use App\Models\PosDevice;
use App\Models\PosLocation;
use Illuminate\Support\Str;
class DeviceService
{
public function __construct(private CustomerDisplayService $displays) {}
public function generateToken(): string
{
return Str::random(48);
}
public function findByToken(string $token): ?PosDevice
{
return PosDevice::query()->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<string, mixed> */
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-display.show', ['token' => $device->device_token]),
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();
}
}