from datetime import datetime, timezone from uuid import UUID from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.company import Company from app.models.kiosk_device import KioskDevice, KioskDeviceStatus from app.schemas.kiosk import HeartbeatRequest, KioskDeviceCreate, KioskDeviceUpdate class KioskService: # ── Lesende Operationen ─────────────────────────────────────────────────── async def list_devices( self, company_id: UUID, db: AsyncSession, status_filter: KioskDeviceStatus | None = None, ) -> list[KioskDevice]: query = ( select(KioskDevice) .where(KioskDevice.company_id == company_id) .order_by(KioskDevice.created_at.desc()) ) if status_filter is not None: query = query.where(KioskDevice.status == status_filter) result = await db.scalars(query) return list(result.all()) async def get_device( self, device_id: UUID, company_id: UUID, db: AsyncSession ) -> KioskDevice: device = await db.scalar( select(KioskDevice).where( KioskDevice.id == device_id, KioskDevice.company_id == company_id, ) ) if device is None: raise HTTPException(status_code=404, detail="Gerät nicht gefunden.") return device # ── Schreibende Operationen ─────────────────────────────────────────────── async def create_device( self, company_id: UUID, data: KioskDeviceCreate, db: AsyncSession, ) -> KioskDevice: """ Gerät anlegen. Status = pending wenn kiosk_require_approval, sonst approved. """ company = await db.get(Company, company_id) require_approval = company.kiosk_require_approval if company else True device = KioskDevice( company_id=company_id, name=data.name, location=data.location, public_key=data.public_key, ip_whitelist=data.ip_whitelist, key_algorithm="ed25519", status=KioskDeviceStatus.PENDING if require_approval else KioskDeviceStatus.APPROVED, ) db.add(device) await db.flush() return device async def update_device( self, device_id: UUID, company_id: UUID, data: KioskDeviceUpdate, db: AsyncSession, ) -> KioskDevice: device = await self.get_device(device_id, company_id, db) changes = data.model_dump(exclude_none=True) for field, value in changes.items(): setattr(device, field, value) return device async def delete_device( self, device_id: UUID, company_id: UUID, db: AsyncSession ) -> None: device = await self.get_device(device_id, company_id, db) await db.delete(device) # ── Status-Verwaltung ───────────────────────────────────────────────────── async def approve_device( self, device_id: UUID, company_id: UUID, db: AsyncSession ) -> KioskDevice: """Gerät freigeben: Status → approved.""" device = await self.get_device(device_id, company_id, db) device.status = KioskDeviceStatus.APPROVED return device async def revoke_device( self, device_id: UUID, company_id: UUID, db: AsyncSession ) -> KioskDevice: """Gerät sperren: Status → revoked.""" device = await self.get_device(device_id, company_id, db) device.status = KioskDeviceStatus.REVOKED return device # ── Heartbeat ───────────────────────────────────────────────────────────── async def process_heartbeat( self, device: KioskDevice, data: HeartbeatRequest, company: Company, db: AsyncSession, ) -> None: """ Heartbeat-Daten vom Kiosk-Gerät verarbeiten. last_heartbeat_at wird bereits in verify_kiosk_request gesetzt – hier werden nur die zusätzlichen Felder aktualisiert. """ if data.client_version is not None: device.client_version = data.client_version device.offline_queue_size = data.queued_offline_entries if data.current_user_id is not None and company.kiosk_track_current_user: device.current_user_id = data.current_user_id elif not company.kiosk_track_current_user: # DSGVO-Opt-Out: aktuellen User nicht speichern device.current_user_id = None await db.flush() kiosk_service = KioskService()