Release v4928.1.4.2 stable
This commit is contained in:
324
app/db.py
Normal file
324
app/db.py
Normal file
@@ -0,0 +1,324 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
engine = create_engine(settings.database_url, pool_pre_ping=True)
|
||||
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
)
|
||||
|
||||
|
||||
_CORE_SCHEMA_READY = False
|
||||
|
||||
|
||||
def ensure_core_schema() -> None:
|
||||
"""Cria o schema base do ClientFlow numa base PostgreSQL vazia.
|
||||
|
||||
As versões anteriores assumiam que `tasks`, `messages`, `raw_events` e
|
||||
`action_runs` já existiam. Numa instalação limpa isso fazia o arranque
|
||||
falhar quando a camada de oportunidades tentava executar:
|
||||
|
||||
ALTER TABLE tasks ADD COLUMN ...
|
||||
|
||||
Esta função é aditiva e segura para bases existentes: cria tabelas e
|
||||
índices apenas se não existirem e adiciona colunas opcionais em falta.
|
||||
"""
|
||||
global _CORE_SCHEMA_READY
|
||||
if _CORE_SCHEMA_READY:
|
||||
return
|
||||
|
||||
with engine.begin() as conn:
|
||||
# Necessário para DEFAULT gen_random_uuid(). Em PostgreSQL moderno,
|
||||
# pgcrypto é a forma mais simples de gerar UUIDs no próprio servidor.
|
||||
conn.execute(text("CREATE EXTENSION IF NOT EXISTS pgcrypto"))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS raw_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
source_system TEXT NOT NULL DEFAULT 'clientflow',
|
||||
event_type TEXT,
|
||||
source_event_id TEXT,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
processed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ignored BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
processing_error TEXT,
|
||||
message_id UUID,
|
||||
action_run_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
processed_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_raw_events_source_event
|
||||
ON raw_events(source_system, source_event_id)
|
||||
WHERE source_event_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
raw_event_id UUID,
|
||||
source_system TEXT NOT NULL DEFAULT 'clientflow',
|
||||
source_event_id TEXT,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
direction TEXT NOT NULL DEFAULT 'inbound',
|
||||
raw_body TEXT,
|
||||
clean_body TEXT,
|
||||
previous_context TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS action_runs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id UUID,
|
||||
raw_event_id UUID,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
source_system TEXT NOT NULL DEFAULT 'clientflow',
|
||||
model TEXT,
|
||||
provider TEXT,
|
||||
openrouter_generation_id TEXT,
|
||||
decision_source TEXT,
|
||||
action_decision JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
action_result JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
total_tokens INTEGER,
|
||||
cost NUMERIC(12,6),
|
||||
usage JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
needs_review BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
action_run_id UUID,
|
||||
message_id UUID,
|
||||
raw_event_id UUID,
|
||||
opportunity_id UUID,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
customer_id TEXT,
|
||||
action_code TEXT NOT NULL DEFAULT 'REVIEW_MANUALLY',
|
||||
route TEXT NOT NULL DEFAULT 'rever',
|
||||
action TEXT NOT NULL DEFAULT 'Rever manualmente',
|
||||
note TEXT,
|
||||
action_required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
safe_to_post BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
source_system TEXT NOT NULL DEFAULT 'clientflow',
|
||||
source_event_id TEXT,
|
||||
idempotency_key TEXT,
|
||||
due_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
done_at TIMESTAMPTZ,
|
||||
done_by TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||
)
|
||||
"""))
|
||||
|
||||
# Colunas adicionadas em upgrades anteriores. Mantidas aqui para
|
||||
# compatibilidade quando a tabela já existe numa base antiga.
|
||||
for statement in [
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS opportunity_id UUID",
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS due_at TIMESTAMPTZ",
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS done_at TIMESTAMPTZ",
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS done_by TEXT",
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}'::jsonb",
|
||||
"ALTER TABLE tasks ADD COLUMN IF NOT EXISTS idempotency_key TEXT",
|
||||
]:
|
||||
conn.execute(text(statement))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_tasks_idempotency_key
|
||||
ON tasks(idempotency_key)
|
||||
WHERE idempotency_key IS NOT NULL
|
||||
"""))
|
||||
conn.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_idempotency_key ON tasks(idempotency_key)"))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS task_preparations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_id UUID NOT NULL,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
prep_type TEXT NOT NULL DEFAULT 'generic',
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
extracted_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
missing_fields JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
suggested_reply TEXT,
|
||||
confidence NUMERIC(4,3),
|
||||
model TEXT,
|
||||
provider TEXT,
|
||||
total_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cost NUMERIC(12,6) NOT NULL DEFAULT 0,
|
||||
raw_response JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_task_preparations_task ON task_preparations(task_id, created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_task_preparations_conversation ON task_preparations(conversation_id)"))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS task_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_id UUID NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_by TEXT NOT NULL DEFAULT 'system',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS business_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_type TEXT NOT NULL,
|
||||
task_id UUID,
|
||||
action_run_id UUID,
|
||||
message_id UUID,
|
||||
raw_event_id UUID,
|
||||
customer_id TEXT,
|
||||
conversation_id TEXT,
|
||||
contact_id TEXT,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
idempotency_key TEXT,
|
||||
created_by TEXT NOT NULL DEFAULT 'system',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_business_events_idempotency_key
|
||||
ON business_events(idempotency_key)
|
||||
WHERE idempotency_key IS NOT NULL
|
||||
"""))
|
||||
conn.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS idx_business_events_idempotency_key ON business_events(idempotency_key)"))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS integration_outbox (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
business_event_id UUID,
|
||||
target_system TEXT NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
idempotency_key TEXT,
|
||||
last_error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
sent_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
|
||||
for statement in [
|
||||
"ALTER TABLE integration_outbox ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ",
|
||||
"ALTER TABLE integration_outbox ADD COLUMN IF NOT EXISTS lock_owner TEXT",
|
||||
"ALTER TABLE integration_outbox ADD COLUMN IF NOT EXISTS ignored_at TIMESTAMPTZ",
|
||||
]:
|
||||
conn.execute(text(statement))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_integration_outbox_idempotency_key
|
||||
ON integration_outbox(idempotency_key)
|
||||
WHERE idempotency_key IS NOT NULL
|
||||
"""))
|
||||
conn.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS idx_integration_outbox_idempotency_key ON integration_outbox(idempotency_key)"))
|
||||
conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_integration_outbox_pending_dispatch
|
||||
ON integration_outbox(target_system, created_at)
|
||||
WHERE status = 'pending'
|
||||
"""))
|
||||
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS external_mappings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
local_system TEXT NOT NULL DEFAULT 'clientflow',
|
||||
local_entity_type TEXT NOT NULL,
|
||||
local_entity_id TEXT NOT NULL,
|
||||
external_system TEXT NOT NULL,
|
||||
external_entity_type TEXT NOT NULL,
|
||||
external_entity_id TEXT,
|
||||
external_url TEXT,
|
||||
match_key TEXT,
|
||||
match_value TEXT,
|
||||
confidence NUMERIC(4,3),
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
# Índices de consulta mais usados no admin e webhooks.
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tasks_route ON tasks(route)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tasks_conversation ON tasks(conversation_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tasks_contact ON tasks(contact_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_raw_events_created_at ON raw_events(created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_raw_events_conversation ON raw_events(conversation_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_action_runs_created_at ON action_runs(created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_task_events_task ON task_events(task_id, created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_business_events_created_at ON business_events(created_at DESC)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_outbox_status ON integration_outbox(status)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_outbox_target ON integration_outbox(target_system)"))
|
||||
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_external_mappings_local ON external_mappings(local_system, local_entity_type, local_entity_id)"))
|
||||
|
||||
_CORE_SCHEMA_READY = True
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""Inicialização do schema ClientFlow.
|
||||
|
||||
Ordem importante para bases novas:
|
||||
1. core: raw_events/messages/action_runs/tasks/outbox;
|
||||
2. oportunidades: depende de tasks;
|
||||
3. produtos: depende de opportunities.
|
||||
"""
|
||||
try:
|
||||
ensure_core_schema()
|
||||
|
||||
from app.opportunity_service import ensure_opportunity_schema
|
||||
from app.product_service import ensure_product_schema
|
||||
from app.operation_service import ensure_operation_schema
|
||||
from app.commercial_service import ensure_commercial_schema
|
||||
from app.communication_service import ensure_communication_schema
|
||||
from app.reconciliation_service import ensure_reconciliation_schema
|
||||
|
||||
ensure_opportunity_schema()
|
||||
ensure_product_schema()
|
||||
ensure_operation_schema()
|
||||
ensure_commercial_schema()
|
||||
ensure_communication_schema()
|
||||
ensure_reconciliation_schema()
|
||||
except Exception as exc:
|
||||
print(f"ClientFlow schema init failed: {exc}", flush=True)
|
||||
raise
|
||||
return None
|
||||
Reference in New Issue
Block a user