NOW() LIMIT 1", [$companyId, $hash] ); return self::decryptState($state); } /** * Save or update conversation state securely */ public static function saveState(array $data): string { self::ensureTableExists(); if (!empty($data['contact_phone'])) { $data['contact_phone_hash'] = Security::blindIndex($data['contact_phone']); $data['contact_phone'] = Security::encrypt($data['contact_phone']); } $existing = Database::selectOne( "SELECT id FROM " . static::$table . " WHERE company_id = ? AND contact_phone_hash = ? LIMIT 1", [$data['company_id'], $data['contact_phone_hash']] ); if ($existing) { self::update($existing['id'], $data); return $existing['id']; } else { return self::create($data); } } /** * Delete conversation state by ID */ public static function deleteState(int $id): int { self::ensureTableExists(); return self::delete($id); } /** * Clean up all expired states */ public static function cleanExpired(): void { self::ensureTableExists(); try { Database::execute("DELETE FROM " . static::$table . " WHERE expires_at < NOW()"); } catch (\Exception $e) { error_log("Failed to clean expired conversation states: " . $e->getMessage()); } } /** * Helper to decrypt sensitive fields */ private static function decryptState(?array $state): ?array { if ($state) { $state['contact_phone'] = !empty($state['contact_phone']) ? Security::decrypt($state['contact_phone']) : null; } return $state; } /** * Ensure the conversation_states table exists in the database */ public static function ensureTableExists(): void { static $checked = false; if ($checked) return; try { Database::execute(" CREATE TABLE IF NOT EXISTS `conversation_states` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `company_id` INT NOT NULL, `contact_phone` VARCHAR(512) NOT NULL, `contact_phone_hash` VARCHAR(64) NOT NULL, `flow_name` VARCHAR(100) NOT NULL, `current_step` VARCHAR(100) NOT NULL, `context_data` JSON DEFAULT NULL, `expires_at` TIMESTAMP NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `unique_company_contact` (`company_id`, `contact_phone_hash`), FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE, INDEX `idx_conv_expires` (`expires_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); $checked = true; } catch (\Exception $e) { error_log("Failed to ensure conversation_states table: " . $e->getMessage()); } } }