'error', 'message' => 'Method not allowed' ], JSON_UNESCAPED_UNICODE); exit; } $rawBody = file_get_contents('php://input'); $data = json_decode($rawBody, true); if (!is_array($data)) { http_response_code(400); echo json_encode([ 'status' => 'error', 'message' => 'Invalid JSON payload' ], JSON_UNESCAPED_UNICODE); exit; } $payload = isset($data['payload']) && is_array($data['payload']) ? $data['payload'] : []; if ( (isset($data['skipAnalytics']) && $data['skipAnalytics'] === true) || (isset($payload['staffPreview']) && $payload['staffPreview'] === true) ) { echo json_encode(['status' => 'success', 'skipped' => true], JSON_UNESCAPED_UNICODE); exit; } $eventName = isset($data['eventName']) ? trim((string)$data['eventName']) : ''; $sessionId = isset($data['sessionId']) ? trim((string)$data['sessionId']) : ''; $tableId = isset($data['tableId']) ? trim((string)$data['tableId']) : null; $zone = isset($data['zone']) ? trim((string)$data['zone']) : null; $qrHash = isset($data['qrHash']) ? trim((string)$data['qrHash']) : null; $deviceType = isset($data['deviceType']) ? trim((string)$data['deviceType']) : null; $browser = isset($data['browser']) ? trim((string)$data['browser']) : null; $allowedEvents = [ 'qr_scan', 'session_start', 'geo_check_started', 'geo_check_passed', 'geo_check_failed', 'geo_bypass_host', 'waiter_call_requested', 'view_status', 'view_menu', 'menu_search', 'bill_dialog_opened', 'bill_request_sent', 'menu_only_entered', 'geo_gate_prompted', 'geo_retry_from_menu', ]; if ($eventName === '' || !in_array($eventName, $allowedEvents, true)) { http_response_code(422); echo json_encode([ 'status' => 'error', 'message' => 'Invalid eventName' ], JSON_UNESCAPED_UNICODE); exit; } if ($sessionId === '' || strlen($sessionId) > 64) { http_response_code(422); echo json_encode([ 'status' => 'error', 'message' => 'Invalid sessionId' ], JSON_UNESCAPED_UNICODE); exit; } if ($qrHash && !$tableId && isset($conn)) { $resolved = getTableNameByHash($conn, $qrHash); if ($resolved !== '') { $tableId = $resolved; } } if ($tableId !== null && strlen($tableId) > 32) { $tableId = substr($tableId, 0, 32); } if ($zone !== null && strlen($zone) > 16) { $zone = substr($zone, 0, 16); } if ($qrHash !== null && strlen($qrHash) > 128) { $qrHash = substr($qrHash, 0, 128); } if ($deviceType !== null && strlen($deviceType) > 16) { $deviceType = substr($deviceType, 0, 16); } if ($browser !== null && strlen($browser) > 32) { $browser = substr($browser, 0, 32); } $ip = ''; if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $parts = explode(',', (string)$_SERVER['HTTP_X_FORWARDED_FOR']); $ip = trim($parts[0]); } elseif (!empty($_SERVER['REMOTE_ADDR'])) { $ip = trim((string)$_SERVER['REMOTE_ADDR']); } $ipHash = $ip !== '' ? hash('sha256', $ip . '|karczma_analytics_v1') : null; $payload = is_array($payload) ? $payload : []; if ($ip !== '' && !isset($payload['ipAddress'])) { $payload['ipAddress'] = $ip; } $payloadJson = $payload ? json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; try { $pdo = getAnalyticsPdo(); $stmt = $pdo->prepare(" INSERT INTO analytics_events ( created_at, event_name, session_id, table_id, zone, qr_hash, device_type, browser, ip_hash, payload_json ) VALUES ( NOW(3), :event_name, :session_id, :table_id, :zone, :qr_hash, :device_type, :browser, :ip_hash, :payload_json ) "); $stmt->execute([ ':event_name' => $eventName, ':session_id' => $sessionId, ':table_id' => $tableId, ':zone' => $zone, ':qr_hash' => $qrHash, ':device_type' => $deviceType, ':browser' => $browser, ':ip_hash' => $ipHash, ':payload_json' => $payloadJson, ]); echo json_encode([ 'status' => 'success' ], JSON_UNESCAPED_UNICODE); } catch (Throwable $e) { // Best-effort analytics: return success=false, but do not crash frontend flow. http_response_code(200); echo json_encode([ 'status' => 'error', 'message' => 'Analytics insert failed' ], JSON_UNESCAPED_UNICODE); }