Comitando flow_respose_without_one_store

This commit is contained in:
Joao Monezi 2025-06-23 07:03:40 +00:00
parent 53d57e4fa3
commit 897b3b11ac
6 changed files with 873 additions and 206 deletions

1
app/active_sessions.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -9,7 +9,10 @@ VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFICATION_TOKEN")
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN")
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
FLOW_PRIVATE_KEY_PASSWORD = os.getenv("FLOW_PRIVATE_KEY_PASSWORD")
DATABASE_ODBC_CONN_STR = os.getenv("DATABASE_ODBC_CONN_STR")
if not DATABASE_ODBC_CONN_STR:
raise ValueError("DATABASE_ODBC_CONN_STR não está definida no arquivo .env")
# Verificação para garantir que as variáveis críticas estão presentes
if not VERIFY_TOKEN:
raise ValueError("WEBHOOK_VERIFICATION_TOKEN não está definido no .env")

View File

@ -13,28 +13,73 @@ DIALOG_GRAPH: Dict[str, Dict[str, Any]] = {
},
"MENU_PRINCIPAL": {
"message": "*Olá você inicou o Consultme!:*",
"action_to_perform": "send_main_menu", # Ação para enviar um menu interativo (botões)
"expected_input_type": "button_click",
"MENU_PRINCIPAL": {
"message": "*Olá, você iniciou o Consultme!:*",
"action_to_perform": "send_main_menu", # Ação para enviar o menu principal (lista)
"expected_input_type": "list_reply", # Espera um clique em um item da lista
"transitions": {
"OPTION_AGENDAR": "MENU_PRINCIPAL_STORE",
"OPTION_CADASTRO_FLOW": "MENU_PRINCIPAL_STORE",
"OPTION_STATUS": "MENU_PRINCIPAL_STORE",
"OPTION_FALAR_ATENDENTE": "MENU_PRINCIPAL_STORE",
"default": "MENU_PRINCIPAL_STORE" # Volta para o menu se a opção não for reconhecida
# Transições para os estados intermediários de tempo/período
"OPTION_ANO": "MENU_PRINCIPAL_ANO",
"OPTION_MES": "MENU_PRINCIPAL_MES",
"OPTION_ONTEM": "MENU_PRINCIPAL_ONTEM",
"OPTION_HOJE": "MENU_PRINCIPAL_HOJE",
"OPTION_SAIR": "ENCERRAR_CONVERSA",
"default": "RESPOSTA_NAO_ENTENDIDA" # Volta para o menu se a opção não for reconhecida
}
},
"MENU_PRINCIPAL_STORE": {
"action_to_perform": "send_main_store", # Ação para enviar um menu interativo (botões)
"ENCERRAR_CONVERSA": {
"message": "Você encerrou o Chat, digite algo caso precise consultar novamente.",
"action_to_perform": "saindo_da_sessao" # Ação para enviar o menu principal (lista)
},
# --- ESTADOS INTERMEDIÁRIOS APÓS ESCOLHA DO PERÍODO (MANDAM O SEGUNDO MENU) ---
"MENU_PRINCIPAL_ANO": {
"message": "Você escolheu o *Ano*. Agora, qual indicador deseja visualizar?",
"action_to_perform": "send_main_store",
"expected_input_type": "button_click",
"transitions": {
"OPTION_AGENDAR": "AGENDAMENTO_INICIO",
"OPTION_CADASTRO_FLOW": "INICIAR_FLOW_CADASTRO",
"OPTION_STATUS": "PEDIR_NUMERO_PEDIDO",
"OPTION_FALAR_ATENDENTE": "ENCAMINHAR_ATENDENTE",
"default": "MENU_PRINCIPAL" # Volta para o menu se a opção não for reconhecida
"OPTION_TOTAL_CP": "RESPOSTA_ANO_TOTAL_CP", # Combinação final
"OPTION_TOTAL_LOJAS": "RESPOSTA_ANO_TOTAL_LOJAS",
"OPTION_TOTAL_UMA_LOJA": "RESPOSTA_ANO_UMA_LOJA",
"OPTION_SAIR": "ENCERRAR_CONVERSA",
"default": "RESPOSTA_NAO_ENTENDIDA" # Se não reconhecer, não entendi
}
},
"MENU_PRINCIPAL_MES": {
"message": "Você escolheu o *Mês*. Agora, qual indicador deseja visualizar?",
"action_to_perform": "send_main_store",
"expected_input_type": "button_click",
"transitions": {
"OPTION_TOTAL_CP": "RESPOSTA_MES_TOTAL_CP",
"OPTION_TOTAL_LOJAS": "RESPOSTA_MES_TOTAL_LOJAS",
"OPTION_TOTAL_UMA_LOJA": "RESPOSTA_MES_UMA_LOJA",
"OPTION_SAIR": "ENCERRAR_CONVERSA",
"default": "RESPOSTA_NAO_ENTENDIDA"
}
},
"MENU_PRINCIPAL_ONTEM": {
"message": "Você escolheu *Ontem*. Agora, qual indicador deseja visualizar?",
"action_to_perform": "send_main_store",
"expected_input_type": "button_click",
"transitions": {
"OPTION_TOTAL_CP": "RESPOSTA_ONTEM_TOTAL_CP",
"OPTION_TOTAL_LOJAS": "RESPOSTA_ONTEM_TOTAL_LOJAS",
"OPTION_TOTAL_UMA_LOJA": "RESPOSTA_ONTEM_UMA_LOJA",
"OPTION_SAIR": "ENCERRAR_CONVERSA",
"default": "RESPOSTA_NAO_ENTENDIDA"
}
},
"MENU_PRINCIPAL_HOJE": {
"message": "Você escolheu *Hoje*. Agora, qual indicador deseja visualizar?",
"action_to_perform": "send_main_store",
"expected_input_type": "button_click",
"transitions": {
"OPTION_TOTAL_CP": "RESPOSTA_HOJE_TOTAL_CP",
"OPTION_TOTAL_LOJAS": "RESPOSTA_HOJE_TOTAL_LOJAS",
"OPTION_TOTAL_UMA_LOJA": "RESPOSTA_HOJE_UMA_LOJA",
"OPTION_SAIR": "ENCERRAR_CONVERSA",
"default": "RESPOSTA_NAO_ENTENDIDA"
}
},
@ -44,97 +89,149 @@ DIALOG_GRAPH: Dict[str, Dict[str, Any]] = {
"transitions": {
"menu": "MENU_PRINCIPAL",
"ajuda": "MENU_PRINCIPAL",
"default": "RESPOSTA_NAO_ENTENDIDA" # Continua não entendendo
"default": "RESPOSTA_NAO_ENTENDIDA"
}
},
# --- FLUXO DE CADASTRO (com Flow) ---
"INICIAR_FLOW_CADASTRO": {
"action_to_perform": "send_flow_cadastro", # Ação para enviar o Flow
"flow_id": "COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO", # ID do seu Flow publicado
"flow_cta": "Abrir Cadastro",
"expected_input_type": "flow_nfm_reply", # Espera a resposta do Flow
"transitions": {
"success": "CADASTRO_CONCLUIDO", # Se o Flow for concluído com sucesso
"failure": "CADASTRO_FALHA" # Se houver um problema no Flow (ou o usuário não preencher)
}
},
"CADASTRO_CONCLUIDO": {
"message": "Obrigado por se cadastrar, ${nome_completo}! Seu e-mail é: ${email}. Já pode explorar nossos serviços!",
"action_to_perform": "process_cadastro_data", # Ação para salvar no BD/CRM
"expected_input_type": "any", # Qualquer coisa leva ao menu
"transitions": {
"default": "MENU_PRINCIPAL"
}
},
"CADASTRO_FALHA": {
"message": "Não foi possível completar seu cadastro. Por favor, tente novamente ou digite 'ajuda'.",
"expected_input_type": "text",
"transitions": {
"default": "MENU_PRINCIPAL"
}
},
# --- FLUXO DE AGENDAMENTO (Exemplo Básico) ---
"AGENDAMENTO_INICIO": {
"message": "Certo! Para agendar, qual serviço você precisa?",
"expected_input_type": "text",
"transitions": {
"default": "AGENDAMENTO_CONFIRMAR_SERVICO"
}
},
"AGENDAMENTO_CONFIRMAR_SERVICO": {
"message": "Você precisa de *${servico_agendado}*. Qual data e horário você prefere?",
"expected_input_type": "text",
"transitions": {
"default": "AGENDAMENTO_FINALIZAR"
},
"action_to_perform": "save_temp_service" # Ação para guardar o serviço temporariamente
},
"AGENDAMENTO_FINALIZAR": {
"message": "Seu agendamento para *${servico_agendado}* em *${data_horario_agendado}* foi confirmado! Te vejo lá!",
"action_to_perform": "confirm_appointment", # Ação para agendar no sistema real
# --- ESTADOS FINAIS COMBINATÓRIOS (12 ESTADOS) ---
# Período: ANO
"RESPOSTA_ANO_TOTAL_CP": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_ano_cp", # Ação para buscar e formatar dados
"expected_input_type": "any",
"transitions": {
"default": "MENU_PRINCIPAL"
}
"transitions": {"default": "MENU_PRINCIPAL"}
},
# --- FLUXO DE STATUS DO PEDIDO ---
"PEDIR_NUMERO_PEDIDO": {
"message": "Por favor, digite o número do seu pedido para consultar o status:",
"expected_input_type": "text",
"transitions": {
"default": "CONSULTAR_STATUS_API"
}
},
"CONSULTAR_STATUS_API": {
"message": "Consultando o status do pedido *${numero_pedido}*...",
"action_to_perform": "call_external_status_api", # Ação para chamar um sistema externo (API futura)
"expected_input_type": "api_response", # A resposta vem de uma API, não do usuário
"transitions": {
"success": "STATUS_EXIBIR",
"failure": "STATUS_NAO_ENCONTRADO"
}
},
"STATUS_EXIBIR": {
"message": "O status do pedido *${numero_pedido}* é: *${status_retornado}*.",
"RESPOSTA_ANO_TOTAL_LOJAS": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_ano_lojas",
"expected_input_type": "any",
"transitions": {
"default": "MENU_PRINCIPAL"
}
"transitions": {"default": "MENU_PRINCIPAL"}
},
"STATUS_NAO_ENCONTRADO": {
"message": "Não consegui encontrar o pedido *${numero_pedido}*. Verifique e digite novamente, ou 'menu'.",
"RESPOSTA_ANO_UMA_LOJA": {
"message": "Você escolheu o Total de uma Loja do *Ano*. Por favor, digite o ID da loja.",
"action_to_perform": "set_context_for_store_id_input_ano_loja", # Ação para guardar o contexto de "Ano" e esperar o ID da loja
"expected_input_type": "text",
"transitions": {
"menu": "MENU_PRINCIPAL",
"default": "PEDIR_NUMERO_PEDIDO"
"default": "PROCESSAR_ID_LOJA_ANO" # Leva ao processamento do ID da loja
}
},
# --- FLUXO DE ATENDENTE ---
"ENCAMINHAR_ATENDENTE": {
"message": "Encaminhando você para um de nossos atendentes. Por favor, aguarde.",
"terminal": True # Indica que a conversa termina aqui (até o atendente assumir)
# Período: MÊS
"RESPOSTA_MES_TOTAL_CP": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_mes_cp",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
# ... Adicione mais estados conforme a complexidade da sua conversa ...
"RESPOSTA_MES_TOTAL_LOJAS": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_mes_lojas",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"RESPOSTA_MES_UMA_LOJA": {
"message": "Você escolheu o Total de uma Loja do *Mês*. Por favor, digite o ID da loja.",
"action_to_perform": "set_context_for_store_id_input_mes_loja",
"expected_input_type": "text",
"transitions": {
"default": "PROCESSAR_ID_LOJA_MES"
}
},
# Período: ONTEM
"RESPOSTA_ONTEM_TOTAL_CP": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_ontem_cp",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"RESPOSTA_ONTEM_TOTAL_LOJAS": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_ontem_lojas",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"RESPOSTA_ONTEM_UMA_LOJA": {
"message": "Você escolheu o Total de uma Loja de *Ontem*. Por favor, digite o ID da loja.",
"action_to_perform": "set_context_for_store_id_input_ontem_loja",
"expected_input_type": "text",
"transitions": {
"default": "PROCESSAR_ID_LOJA_ONTEM"
}
},
# Período: HOJE
"RESPOSTA_HOJE_TOTAL_CP": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_hoje_cp",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"RESPOSTA_HOJE_TOTAL_LOJAS": {
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
"action_to_perform": "get_combined_indicator_data_hoje_lojas",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"RESPOSTA_HOJE_UMA_LOJA": {
"message": "Você escolheu o Total de uma Loja de *Hoje*. Por favor, digite o ID da loja.",
"action_to_perform": "set_context_for_store_id_input_hoje_loja",
"expected_input_type": "text",
"transitions": {
"default": "PROCESSAR_ID_LOJA_HOJE"
}
},
# --- ESTADOS PARA PROCESSAR O ID DA LOJA (PARA 'UMA LOJA') ---
"PROCESSAR_ID_LOJA_ANO": {
"message": "Consultando indicador da loja ${id_loja} para o *Ano*...",
"action_to_perform": "get_store_indicator_ano",
"expected_input_type": "api_response", # Ação para pegar o ID digitado e consultar a API
"transitions": {
"success": "EXIBIR_INDICADOR_LOJA_ANO",
"failure": "LOJA_NAO_ENCONTRADA_ANO"
}
},
"PROCESSAR_ID_LOJA_MES": {
"message": "Consultando indicador da loja ${id_loja} para o *Mês*...",
"action_to_perform": "get_store_indicator_mes",
"expected_input_type": "api_response",
"transitions": {
"success": "EXIBIR_INDICADOR_LOJA_MES",
"failure": "LOJA_NAO_ENCONTRADA_MES"
}
},
"PROCESSAR_ID_LOJA_ONTEM": {
"message": "Consultando indicador da loja ${id_loja} para *Ontem*...",
"action_to_perform": "get_store_indicator_ontem",
"expected_input_type": "api_response",
"transitions": {
"success": "EXIBIR_INDICADOR_LOJA_ONTEM",
"failure": "LOJA_NAO_ENCONTRADA_ONTEM"
}
},
"PROCESSAR_ID_LOJA_HOJE": {
"message": "Consultando indicador da loja ${id_loja} para *Hoje*...",
"action_to_perform": "get_store_indicator_hoje",
"expected_input_type": "api_response",
"transitions": {
"success": "EXIBIR_INDICADOR_LOJA_HOJE",
"failure": "LOJA_NAO_ENCONTRADA_HOJE"
}
},
# --- ESTADOS PARA EXIBIR INDICADOR DE LOJA ESPECÍFICA (COMBINAÇÕES) ---
"EXIBIR_INDICADOR_LOJA_ANO": {
"message": "O indicador da loja *${id_loja}* do *Ano* é: *${indicador_loja}*.",
"expected_input_type": "any",
"transitions": {"default": "MENU_PRINCIPAL"}
},
"LOJA_NAO_ENCONTRADA_ANO": {
"message": "Loja *${id_loja}* não encontrada para o *Ano*. Digite novamente ou 'menu'.",
"expected_input_type": "text",
"transitions": {"default": "RESPOSTA_ANO_UMA_LOJA", "menu": "MENU_PRINCIPAL"} # Volta para pedir a loja para o Ano
},
}
# --- ESTADO INICIAL ---

View File

@ -3,6 +3,9 @@ from typing import List, Dict, Any, Optional
import json
import base64
import os
import pyodbc
import requests
@ -20,11 +23,22 @@ scheduler = AsyncIOScheduler()
# --- Variáveis Globais para Gerenciamento de Sessão (APENAS PARA TESTE) ---
# Em produção, isso seria um banco de dados
ACTIVE_SESSIONS_FILE = "active_sessions.json" # Nome do arquivo de persistência
ACTIVE_SESSIONS = {} # Dicionário para armazenar sessões ativas
SESSION_TIMEOUT_SECONDS = 120 # 5 minutos (5 * 60 segundos)
# --- FIM DAS VARIÁVEIS GLOBAIS DE SESSÃO ---
# Em produção, esta lista viria de um banco de dados ou de um sistema de gerenciamento de usuários.
AUTHORIZED_NUMBERS = {
"558291655353", # João <-- SEU NÚMERO DE TELEFONE (com 55 DDD)
"558282309484", # Fernanda
"558298104313", # Efigenia
"557981017347", # Taciana
"558291202979", # Gabrielle
"557196046142" # Laiane Exemplo de outro número autorizado
# Adicione os números de telefone (apenas dígitos, no formato 55DDDxxxxxxxx) que você quer autorizar
}
UNAUTHORIZED_MESSAGE = "Desculpe, seu número não está cadastrado em nosso sistema. Por favor, entre em contato com nosso suporte para mais informações. Obrigado!"
# Importações para criptografia
from cryptography.hazmat.primitives.asymmetric import padding # <-- Esta linha está correta!
@ -34,7 +48,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key, l
from cryptography.hazmat.backends import default_backend
from config import WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, VERIFY_TOKEN, FLOW_PRIVATE_KEY_PASSWORD # <-- ADICIONADO
from config import WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, VERIFY_TOKEN, FLOW_PRIVATE_KEY_PASSWORD, DATABASE_ODBC_CONN_STR # <-- ADICIONADO
# Importe Message para type hinting. Certifique-se que webhook_model.py está correto primeiro!
# Certifique-se de importar o grafo e o estado inicial
from dialog_flow.graph_definition import DIALOG_GRAPH, INITIAL_STATE_ID
@ -204,59 +218,443 @@ def encrypt_flow_response_data(response_data: Dict[str, Any], aes_key: bytes, in
return base64_encoded_final_response
# Persistência de Sessão:
# --- Funções de Persistência de Sessão ---
def load_sessions():
"""Carrega sessões ativas de um arquivo JSON. Chamada na inicialização do app."""
global ACTIVE_SESSIONS
if os.path.exists(ACTIVE_SESSIONS_FILE):
try:
with open(ACTIVE_SESSIONS_FILE, 'r') as f:
data = json.load(f)
loaded_count = 0
for sender_id, session_info in data.items():
# Converte last_activity_time de string ISO para datetime
session_info['last_activity_time'] = datetime.fromisoformat(session_info['last_activity_time'])
ACTIVE_SESSIONS[sender_id] = session_info
loaded_count += 1
print(f"DEBUG_SESSION_PERSIST: {loaded_count} sessões carregadas do arquivo {ACTIVE_SESSIONS_FILE}.")
except json.JSONDecodeError as e:
print(f"❌ ERRO_SESSION_PERSIST: Falha ao carregar sessões (JSON inválido): {e}")
except Exception as e:
print(f"❌ ERRO_SESSION_PERSIST: Erro inesperado ao carregar sessões: {e}")
else:
print(f"DEBUG_SESSION_PERSIST: Nenhum arquivo de sessão '{ACTIVE_SESSIONS_FILE}' encontrado para carregar.")
def save_sessions():
"""Salva sessões ativas em um arquivo JSON. Chamada após modificações e no desligamento do app."""
data_to_save = {}
for sender_id, session_info in ACTIVE_SESSIONS.items():
# Converte datetime para string ISO para serialização JSON
session_info_copy = session_info.copy()
session_info_copy['last_activity_time'] = session_info_copy['last_activity_time'].isoformat()
data_to_save[sender_id] = session_info_copy
try:
with open(ACTIVE_SESSIONS_FILE, 'w') as f:
json.dump(data_to_save, f, indent=4)
print(f"DEBUG_SESSION_PERSIST: {len(ACTIVE_SESSIONS)} sessões salvas no arquivo {ACTIVE_SESSIONS_FILE}.")
except Exception as e:
print(f"❌ ERRO_SESSION_PERSIST: Falha ao salvar sessões: {e}")
# --- Funções de Gerenciamento de Sessão ---
def get_session_state(sender_id: str) -> Optional[Dict[str, Any]]:
"""
Retorna o estado atual da sessão para um usuário.
"""
return ACTIVE_SESSIONS.get(sender_id)
async def clean_session_and_notify(sender_id: str): # <-- FUNÇÃO ASSÍNCRONA PARA LIMPAR E NOTIFICAR
def start_or_update_session(sender_id: str, new_state: Optional[str] = None, last_message_id: Optional[str] = None):
"""
Função assíncrona que limpa a sessão e envia a mensagem de timeout.
Chamada pelo scheduler.
"""
if sender_id in ACTIVE_SESSIONS: # Verifica se a sessão ainda está ativa (não foi atualizada antes do timeout)
timeout_message = "Sua sessão foi encerrada por inatividade. Por favor, envie uma nova mensagem para iniciar um novo atendimento. 😊"
print(f"DEBUG_SESSION: Enviando mensagem de timeout para {sender_id}.")
await send_text_message(sender_id, timeout_message) # <-- CHAMA A FUNÇÃO DE ENVIO
del ACTIVE_SESSIONS[sender_id]
print(f"DEBUG_SESSION: Sessão para {sender_id} encerrada/limpa pelo agendador.")
else:
print(f"DEBUG_SESSION: Sessão para {sender_id} já limpa ou atualizada antes do agendamento.")
def start_or_update_session(sender_id: str):
"""
Inicia uma nova sessão para o usuário ou atualiza o timestamp da última atividade.
Agenda ou reagenda a tarefa de limpeza.
Inicia uma nova sessão para o usuário ou atualiza o timestamp, estado e último ID de mensagem.
"""
current_time = datetime.now()
ACTIVE_SESSIONS[sender_id] = {
"last_activity_time": current_time,
"current_state": "INICIO" # Ou qualquer estado inicial padrão
}
print(f"DEBUG_SESSION: Sessão para {sender_id} iniciada/atualizada em {current_time.strftime('%Y-%m-%d %H:%M:%S')}.")
session_info = ACTIVE_SESSIONS.get(sender_id, {})
# Define o estado da sessão:
if new_state:
session_info["current_state"] = new_state
elif "current_state" not in session_info: # Apenas se for uma sessão nova (não tem estado ainda)
session_info["current_state"] = INITIAL_STATE_ID # Usar INITIAL_STATE_ID do graph_definition
session_info["last_activity_time"] = current_time
if last_message_id:
session_info["last_processed_message_id"] = last_message_id
ACTIVE_SESSIONS[sender_id] = session_info # Salva as informações atualizadas na memória
save_sessions() # Persiste na memória para o arquivo
print(f"DEBUG_SESSION: Sessão para {sender_id} iniciada/atualizada em {current_time.strftime('%Y-%m-%d %H:%M:%S')}. Estado: {session_info['current_state']}.")
if last_message_id:
print(f"DEBUG_SESSION: Último ID de mensagem para {sender_id}: {session_info.get('last_processed_message_id')}.")
# Remover tarefa agendada anterior para esta sessão, se houver
job_id = f"session_clean_{sender_id}"
if scheduler.get_job(job_id):
scheduler.remove_job(job_id)
print(f"DEBUG_SESSION: Tarefa de limpeza anterior para {sender_id} cancelada.")
# Agendar nova tarefa de limpeza para esta sessão
scheduler.add_job(
clean_session_and_notify,
'date', # Agendamento único para uma data/hora específica
'date',
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
args=[sender_id],
id=job_id,
replace_existing=True # Para garantir que não haja duplicatas, embora remove_job já ajude
replace_existing=True
)
print(f"DEBUG_SESSION: Tarefa de limpeza para {sender_id} agendada para {current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS)}.")
async def clean_session_and_notify(sender_id: str):
"""
Função assíncrona que limpa a sessão e envia a mensagem de timeout.
Chamada pelo scheduler.
"""
# A função send_text_message deve estar disponível no webhook_service.py
# (ou importada, mas ela já deveria estar definida no seu código)
if sender_id in ACTIVE_SESSIONS:
timeout_message = "Sua sessão foi encerrada por inatividade. Caso precise, inicie uma nova conversa."
print(f"DEBUG_SESSION: Enviando mensagem de timeout para {sender_id}.")
# Você deve ter a função send_text_message definida e funcional neste arquivo
# ou importada de outro módulo de serviço.
await send_text_message(sender_id, timeout_message) # <-- Assume que send_text_message está disponível
del ACTIVE_SESSIONS[sender_id]
print(f"DEBUG_SESSION: Sessão para {sender_id} encerrada/limpa pelo agendador.")
save_sessions() # Persiste no arquivo após limpeza
else:
print(f"DEBUG_SESSION: Sessão para {sender_id} já limpa ou atualizada antes do agendamento.")
async def saindo_da_sessao(sender_id: str):
"""
Função assíncrona que limpa a sessão e envia a mensagem de timeout.
Chamada pelo scheduler.
"""
# A função send_text_message deve estar disponível no webhook_service.py
# (ou importada, mas ela já deveria estar definida no seu código)
if sender_id in ACTIVE_SESSIONS:
timeout_message = "Sua sessão foi encerrada por inatividade. Caso precise, inicie uma nova conversa."
print(f"DEBUG_SESSION: Enviando mensagem de timeout para {sender_id}.")
# Você deve ter a função send_text_message definida e funcional neste arquivo
# ou importada de outro módulo de serviço.
del ACTIVE_SESSIONS[sender_id]
print(f"DEBUG_SESSION: Sessão para {sender_id} encerrada/limpa pelo agendador.")
save_sessions() # Persiste no arquivo após limpeza
else:
print(f"DEBUG_SESSION: Sessão para {sender_id} já limpa ou atualizada antes do agendamento.")
# Funções que fazem a consulta no Banco de Dados.
async def get_combined_indicator_data_ano_cp() -> Dict[str, Any]:
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = "SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_ano WHERE PDV != 'TOTAL';"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_ano_lojas(sender_id: str) -> Dict[str, Any]:
supervisores_pdvs = {
"558298104313": "AND PDV in ('4560', '12522', '12823', '12826', '12828', '12829', '14617', '19103', '20969', '20991', '21647', '910173', '910291')",
"558282309484": "AND PDV in ('20005', '20006', '20009', '20056', '21068', '21375', '21381')",
"557981017347": "AND PDV in ('20441', '20968')",
"557196046142": "AND PDV in ('20006', '20056', '21068')",
"558291655353" : "AND PDV in ('20006', '20056', '21068')",
"558291202979" :"AND PDV in ('20441', '20968')"
}
print(supervisores_pdvs[sender_id])
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = f"SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_ano WHERE PDV != 'TOTAL' {supervisores_pdvs[sender_id]};"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_mes_cp() -> Dict[str, Any]:
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = "SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_mês WHERE PDV != 'TOTAL';"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_mes_lojas(sender_id: str) -> Dict[str, Any]:
supervisores_pdvs = {
"558298104313": "AND PDV in ('4560', '12522', '12823', '12826', '12828', '12829', '14617', '19103', '20969', '20991', '21647', '910173', '910291')",
"558282309484": "AND PDV in ('20005', '20006', '20009', '20056', '21068', '21375', '21381')",
"557981017347": "AND PDV in ('20441', '20968')",
"557196046142": "AND PDV in ('20006', '20056', '21068')",
"558291655353" : "AND PDV in ('20006', '20056', '21068')",
"558291202979" :"AND PDV in ('20441', '20968')"
}
print(supervisores_pdvs[sender_id])
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = f"SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_mês WHERE PDV != 'TOTAL' {supervisores_pdvs[sender_id]};"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_ontem_cp() -> Dict[str, Any]:
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = "SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_ontem WHERE PDV != 'TOTAL';"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_ontem_lojas(sender_id: str) -> Dict[str, Any]:
supervisores_pdvs = {
"558298104313": "AND PDV in ('4560', '12522', '12823', '12826', '12828', '12829', '14617', '19103', '20969', '20991', '21647', '910173', '910291')",
"558282309484": "AND PDV in ('20005', '20006', '20009', '20056', '21068', '21375', '21381')",
"557981017347": "AND PDV in ('20441', '20968')",
"557196046142": "AND PDV in ('20006', '20056', '21068')",
"558291655353" : "AND PDV in ('20006', '20056', '21068')",
"558291202979" :"AND PDV in ('20441', '20968')"
}
print(supervisores_pdvs[sender_id])
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = f"SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_ontem WHERE PDV != 'TOTAL' {supervisores_pdvs[sender_id]};"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_hoje_cp() -> Dict[str, Any]:
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = "SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_hoje WHERE PDV != 'TOTAL';"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
async def get_combined_indicator_data_hoje_lojas(sender_id: str) -> Dict[str, Any]:
supervisores_pdvs = {
"558298104313": "AND PDV in ('4560', '12522', '12823', '12826', '12828', '12829', '14617', '19103', '20969', '20991', '21647', '910173', '910291')",
"558282309484": "AND PDV in ('20005', '20006', '20009', '20056', '21068', '21375', '21381')",
"557981017347": "AND PDV in ('20441', '20968')",
"557196046142": "AND PDV in ('20006', '20056', '21068')",
"558291655353" : "AND PDV in ('20006', '20056', '21068')",
"558291202979" :"AND PDV in ('20441', '20968')"
}
print(supervisores_pdvs[sender_id])
try:
conn = pyodbc.connect(DATABASE_ODBC_CONN_STR)
cursor = conn.cursor()
# Comando SQL para selecionar o token
sql = f"SELECT SUM([RECEITA (R$)]) AS receita_total, SUM([RECEITA (R$)]) / SUM([NUMERO DE BOLETOS]) AS boleto_medio FROM HUBSUPPLY.dbo.gmv_hoje WHERE PDV != 'TOTAL' {supervisores_pdvs[sender_id]};"
cursor.execute(sql)
# Fetch o resultado
row = cursor.fetchone()
if row:
receita = str(row[0])
boleto = str(row[1])
except pyodbc.Error as ex:
sqlstate = ex.args[0] if ex.args else 'N/A'
# NOVO: Imprima o erro completo, incluindo a mensagem detalhada do pyodbc
print(f"❌ ERRO_DB: Erro de conexão ou execução no SQL Server.")
print(f"❌ ERRO_DB: SQLSTATE={sqlstate}")
print(f"❌ ERRO_DB: Mensagem detalhada: {ex.args[1] if len(ex.args) > 1 else 'N/A'}")
print(f"❌ ERRO_DB: Objeto de exceção completo: {ex}") # Isso mostrará o erro completo
return {}
print(f"DEBUG_ACTION: Valores obtidos: CP={receita}, Boleto Médio={boleto}")
return {
"receita": receita,
"boleto": boleto
}
#Fluxo de mensagens:
@ -302,6 +700,10 @@ async def process_user_input_with_graph(sender_id: str, message_type: str, messa
# O message_content já é o payload do botão
next_state_id = current_state.get("transitions", {}).get(message_content, INITIAL_STATE_ID)
elif message_type == "list_reply":
# O message_content já é o payload do botão
next_state_id = current_state.get("transitions", {}).get(message_content, INITIAL_STATE_ID)
elif message_type == "flow_nfm_reply":
# Quando uma resposta de Flow chega, o next_state depende do sucesso/falha do Flow
# Assumimos que handle_flow_response já processou o Flow e determinou sucesso/falha
@ -318,7 +720,34 @@ async def process_user_input_with_graph(sender_id: str, message_type: str, messa
print(f"DEBUG_GRAPH: Usuário {sender_id} transicionou para o estado: '{next_state_id}'")
await execute_state_action_and_respond(sender_id, next_state_id, session_data)
def format_currency_brl(value: Any) -> str: # <-- Mude o tipo para Any para ser mais flexível na entrada
"""
Formata um valor para o formato de moeda BRL (R$ 0.000,00),
garantindo a conversão para float primeiro.
"""
if value is None:
return "N/A"
# Tenta converter o valor para float, capturando possíveis erros
try:
float_value = float(value)
except (ValueError, TypeError):
print(f"❌ ERRO_FORMAT: Valor '{value}' não pode ser convertido para float para formatação de moeda. Retornando como string.")
return str(value) # Se não puder converter, retorna o valor original como string
# Formatação original (robusta para floats)
formatted_value = f"{float_value:,.2f}"
if '.' in formatted_value and ',' in formatted_value:
parts = formatted_value.split('.')
integer_part_with_commas = parts[0]
decimal_part = parts[1]
formatted_value = integer_part_with_commas.replace(',', '.') + ',' + decimal_part
elif '.' in formatted_value:
formatted_value = formatted_value.replace('.', ',')
return f"R$ {formatted_value}"
async def execute_state_action_and_respond(sender_id: str, state_id: str, session_data: Dict[str, Any]):
"""
@ -341,6 +770,159 @@ async def execute_state_action_and_respond(sender_id: str, state_id: str, sessio
await send_time_menu(sender_id)
elif action == "send_main_store":
await send_store_menu(sender_id)
elif action == "get_combined_indicator_data_ano_cp": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_ano_cp()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_ano_lojas": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_ano_lojas(sender_id)
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_mes_cp": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_mes_cp()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_mes_lojas": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_mes_lojas(sender_id)
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_ontem_cp": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_ontem_cp()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_ontem_lojas": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_ontem_lojas()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_hoje_cp": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_hoje_cp()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "get_combined_indicator_data_hoje_lojas": # <-- NOVA AÇÃO AGORA
db_results = await get_combined_indicator_data_hoje_lojas()
if db_results:
# Armazene cada valor retornado em chaves separadas da sessão
session_data["receita"] = format_currency_brl(db_results.get('receita', 0.0))
session_data["boleto"] = format_currency_brl(db_results.get('boleto', 0.0)) # Aplica a formatação também para boleto
print(f"DEBUG_GRAPH: Valores CP='{session_data["receita"]}' e Boleto_Medio='{session_data["boleto"]}' salvos na sessão.")
start_or_update_session(sender_id, new_state=state_id, last_message_id=session_data.get("last_processed_message_id")) # Atualiza sessão com os dados
else:
session_data["receita"] = "indisponível"
session_data["boleto"] = "indisponível"
print("DEBUG_GRAPH: Falha ao obter Total do CP e Lojas Ativas.")
await send_text_message(sender_id, "Não foi possível obter os indicadores no momento. Por favor, tente mais tarde.")
# Opcional: Transicionar para um estado de erro ou para o menu principal
session_data["current_state"] = DIALOG_GRAPH.get("MENU_PRINCIPAL", {}).get("transitions", {}).get("default", INITIAL_STATE_ID)
start_or_update_session(sender_id, new_state=session_data["current_state"], last_message_id=session_data.get("last_processed_message_id"))
return # Interrompe a execução para não enviar a mensagem do estado normal
elif action == "send_flow_cadastro":
flow_id = state_definition.get("flow_id")
flow_cta = state_definition.get("flow_cta")
@ -371,34 +953,7 @@ async def execute_state_action_and_respond(sender_id: str, state_id: str, sessio
start_or_update_session(sender_id)
elif action == "save_temp_service":
# Exemplo: guardar o serviço agendado
# service = session_data.get("last_text_input") # Pega o último texto que o usuário enviou
# session_data["servico_agendado"] = service
# start_or_update_session(sender_id) # Atualiza a sessão
pass # Ação interna, não envia mensagem aqui
elif action == "confirm_appointment":
# Exemplo: confirmar agendamento no sistema externo
# (Chamada de API para sistema de agendamento)
pass # Ação interna, a mensagem já vem do state_definition
elif action == "call_external_status_api":
# AQUI é onde você faria a chamada para o seu sistema externo
# Por enquanto, apenas um placeholder
# numero_pedido = session_data.get("last_text_input")
# print(f"DEBUG_GRAPH: Chamando API externa para pedido {numero_pedido}")
# try:
# status_api = await external_api_call(numero_pedido)
# session_data["status_retornado"] = status_api # Salva na sessão
# # Transicionar internamente para STATUS_EXIBIR ou STATUS_NAO_ENCONTRADO
# await execute_state_action_and_respond(sender_id, "STATUS_EXIBIR", session_data) # Transição interna
# return
# except Exception:
# await execute_state_action_and_respond(sender_id, "STATUS_NAO_ENCONTRADO", session_data) # Transição interna
# return
pass # Implementação futura, a mensagem vem do estado.
# --- Enviar a mensagem do novo estado (se houver) ---
if "message" in state_definition:
@ -410,6 +965,11 @@ async def execute_state_action_and_respond(sender_id: str, state_id: str, sessio
if "${email}" in final_message and session_data.get("flow_data"):
flow_data = json.loads(session_data["flow_data"])
final_message = final_message.replace("${email}", flow_data.get("email", "não informado"))
if "${receita}" in final_message:
final_message = final_message.replace("${receita}", str(session_data.get("receita", "N/A")))
if "${boleto}" in final_message:
final_message = final_message.replace("${boleto}", str(session_data.get("boleto", "N/A")))
# Para outros placeholders como ${servico_agendado}, ${data_horario_agendado}, ${numero_pedido}, ${status_retornado}
# você faria substituições semelhantes baseadas em session_data
@ -425,18 +985,6 @@ async def execute_state_action_and_respond(sender_id: str, state_id: str, sessio
# --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) ---
# Estas funções contêm a lógica de como seu bot irá interagir.
@ -444,66 +992,85 @@ async def execute_state_action_and_respond(sender_id: str, state_id: str, sessio
#--- FUNÇÃO handle_message_type: A VERSÃO CORRETA PARA O TIMEOUT E GRAFO ---
async def handle_message_type(message: Message):
sender_id = message.from_
message_id = message.id # O ID da mensagem recebida
# 1. Limpar sessões inativas
# Esta função limpa as sessões que expiraram e envia a mensagem de timeout via scheduler.
start_or_update_session(sender_id) # Esta é a que precisa ser assíncrona
# 1. ANTI-DUPLICAÇÃO DE MENSAGENS:
# Obtém a sessão ATUAL (se existir) para verificar o último ID processado.
session_data_for_check = get_session_state(sender_id)
if session_data_for_check and session_data_for_check.get("last_processed_message_id") == message_id:
print(f"⚠️ Mensagem duplicada recebida para {sender_id} (ID: {message_id}). Ignorando.")
# Retorna imediatamente para evitar reprocessamento.
# Nenhuma sessão é atualizada, nem agendamento refeito.
return
# 2. INICIAR OU ATUALIZAR A SESSÃO PARA O REMETENTE ATUAL.
# ISSO É ESSENCIAL PARA O TIMEOUT. Se o usuário mandar mensagem, a sessão dele é atualizada
# e o agendamento de timeout é resetado.
start_or_update_session(sender_id)
# 2. VERIFICAR WHITELIST DE NÚMEROS (SEGUNDO)
if sender_id not in AUTHORIZED_NUMBERS:
print(f"INFO_BOT_CONTROL: Número {sender_id} NÃO autorizado. Enviando mensagem de não cadastrado.")
await send_text_message(sender_id, UNAUTHORIZED_MESSAGE)
return # Não processa mais nada se o número não estiver na whitelist
# NENHUMA CHAMADA explícita para clean_inactive_sessions() AQUI.
# A limpeza de sessões inativas já é tratada pelo scheduler em background.
# 3. Recuperar o estado da sessão (já atualizado)
session_data = get_session_state(sender_id)
if session_data:
print(f"DEBUG_SESSION: Estado atual da sessão para {sender_id}: {session_data['current_state']}")
else:
# Isso não deveria acontecer se start_or_update_session funcionou
print(f"DEBUG_SESSION: ERRO: Sessão para {sender_id} não encontrada após atualização. Isso é inesperado.")
# Se por algum motivo não tiver sessão, podemos resetar para o estado inicial
start_or_update_session(sender_id)
session_data = get_session_state(sender_id) # Tenta novamente
# 3. INICIAR OU ATUALIZAR A SESSÃO, INCLUINDO O ID DA MENSAGEM ATUAL:
# Isso atualizará o last_activity_time, agendará o timeout, e salvará o novo message_id.
start_or_update_session(sender_id, last_message_id=message_id)
# Recarrega session_data para garantir que 'last_processed_message_id' esteja lá e para
# obter o 'current_state' mais recente que start_or_update_session pode ter inicializado.
session_data = get_session_state(sender_id)
# Esta verificação agora é mais um fallback, pois start_or_update_session deve garantir a existência.
if not session_data:
print(f"DEBUG_SESSION: ERRO GRAVE: Sessão para {sender_id} ainda não encontrada após start_or_update_session. Isso é crítico.")
# Tenta uma recuperação de emergência, mas indica um problema
start_or_update_session(sender_id, new_state=INITIAL_STATE_ID, last_message_id=message_id)
session_data = get_session_state(sender_id)
if not session_data: # Se ainda assim falhar, algo está muito errado
print("❌ ERRO FATAL: Falha crítica na gestão de sessão. Não é possível processar a mensagem.")
return # Não é seguro continuar
print(f"DEBUG_SESSION: Estado atual da sessão para {sender_id}: {session_data['current_state']}")
print(f"DEBUG_SESSION: ID da mensagem atual processada: {message_id}")
message_content = None
message_type = None
# --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM ---
# Esta parte permanece como estava, identificando o tipo e conteúdo.
# --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM (permanece a mesma) ---
if message.type == 'text' and message.text:
message_type = "text"
message_content = message.text.body
elif message.type == 'button' and message.button: # Resposta de botão de resposta rápida
elif message.type == 'button' and message.button:
message_type = "button_click"
message_content = message.button.payload
elif message.type == 'interactive' and message.interactive:
if message.interactive.type == 'list_reply' and message.interactive.list_reply: # Resposta de lista
if message.interactive.type == 'list_reply' and message.interactive.list_reply:
message_type = "list_reply"
message_content = message.interactive.list_reply.id
elif message.interactive.type == 'button_reply' and message.interactive.button_reply: # Resposta de botão interativo
message_type = "button_click" # Tratar como clique de botão
elif message.interactive.type == 'button_reply' and message.interactive.button_reply:
message_type = "button_click"
message_content = message.interactive.button_reply.id
elif message.interactive.type == 'nfm_reply' and message.interactive.nfm_reply: # Resposta de Flow
elif message.interactive.type == 'nfm_reply' and message.interactive.nfm_reply:
message_type = "flow_nfm_reply"
message_content = message.interactive.nfm_reply.response_json
else:
print(f" Tipo interativo desconhecido recebido: {message.interactive.type}")
await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.")
return # Sai, não há transição no grafo para isso
elif message.type == 'image': # Mensagens que não são processadas pelo grafo
return
elif message.type == 'image':
message_type = "image"
message_content = "imagem_recebida" # Placeholder
message_content = "imagem_recebida"
await send_text_message(sender_id, "Recebi sua imagem. No momento, só consigo processar texto e interações.")
return # Sai, não há transição no grafo para isso
else: # Tipo de mensagem não suportado ou desconhecido
return
else:
message_type = "unsupported"
message_content = "tipo_desconhecido"
await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.")
return # Sai
return
# 4. O motor do grafo processa a entrada e gerencia o estado da sessão.
# A `session_data` já foi atualizada no passo 2.
# 3. O motor do grafo processa a entrada e gerencia o estado da sessão.
# A `session_data` já está atualizada com o último ID da mensagem.
await process_user_input_with_graph(sender_id, message_type, message_content)
@ -627,7 +1194,7 @@ async def send_time_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU
"""
Envia uma mensagem interativa de LISTA para o usuário com opções de lojas.
"""
header_text = "Escolha o Período que você quer visualizar o indicador"
header_text = "Escolha o Período que você deseja visualizar:"
body_text = "Veja as opções a baixo"
button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista
@ -663,23 +1230,22 @@ async def send_store_menu(to: str): # <-- FUNÇÃO QUE VOCÊ PEDIU
"""
Envia uma mensagem interativa de LISTA para o usuário com opções de lojas.
"""
header_text = "Escolha o Período que você quer visualizar o indicador"
header_text = "Escolha a Dimensão das Lojas que você deseja visualizar:"
body_text = "Veja as opções a baixo"
button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista
sections = [
{
"title": "Acumulados", # Título da primeira seção
"title": "Acumulado", # Título da primeira seção
"rows": [
{"id": 'OPTION_ANO', "title": 'Total do CP', "description": "Visualize o resultado Total do CP"},
{"id": 'OPTION_MES', "title": 'Total do Estado', "description": "Visualize o resultado Total das suas Lojas"},
{"id": 'OPTION_TOTAL_CP', "title": 'Total do CP', "description": "Visualize o resultado Total do CP"}
]
},
{
"title": "Por Loja", # Título da segunda seção
"rows": [
{"id": 'OPTION_ONTEM', "title": 'Total das suas Lojas', "description": "Visualize o total das suas Lojas"},
{"id": 'OPTION_HOJE', "title": 'Total de uma Loja', "description": "Visualize o total de uma loja."}
{"id": 'OPTION_TOTAL_LOJAS', "title": 'Total das suas Lojas', "description": "Visualize o total das suas Lojas"},
{"id": 'OPTION_TOTAL_UMA_LOJA', "title": 'Total de uma Loja', "description": "Visualize o total de uma loja."}
]
},
{