Feature: Implement multi-stage Conversation Flow Engine with TestFlow
This commit is contained in:
119
backend/app/Models/ConversationState.php
Normal file
119
backend/app/Models/ConversationState.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Security;
|
||||
|
||||
/**
|
||||
* ConversationState Model
|
||||
* Manages conversation states for multi-step bot flows.
|
||||
*/
|
||||
class ConversationState extends BaseModel
|
||||
{
|
||||
protected static string $table = 'conversation_states';
|
||||
|
||||
/**
|
||||
* Find active conversation state for a company and contact phone
|
||||
*/
|
||||
public static function findActive(int $companyId, string $phone): ?array
|
||||
{
|
||||
self::ensureTableExists();
|
||||
$hash = Security::blindIndex($phone);
|
||||
$state = Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . " WHERE company_id = ? AND contact_phone_hash = ? AND expires_at > 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user