Comitando flow_respose_without_one_store
This commit is contained in:
parent
53d57e4fa3
commit
897b3b11ac
Binary file not shown.
1
app/active_sessions.json
Normal file
1
app/active_sessions.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@ -9,7 +9,10 @@ VERIFY_TOKEN = os.getenv("WEBHOOK_VERIFICATION_TOKEN")
|
|||||||
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN")
|
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN")
|
||||||
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
|
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
|
||||||
FLOW_PRIVATE_KEY_PASSWORD = os.getenv("FLOW_PRIVATE_KEY_PASSWORD")
|
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
|
# Verificação para garantir que as variáveis críticas estão presentes
|
||||||
if not VERIFY_TOKEN:
|
if not VERIFY_TOKEN:
|
||||||
raise ValueError("WEBHOOK_VERIFICATION_TOKEN não está definido no .env")
|
raise ValueError("WEBHOOK_VERIFICATION_TOKEN não está definido no .env")
|
||||||
|
|||||||
@ -14,27 +14,72 @@ DIALOG_GRAPH: Dict[str, Dict[str, Any]] = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"MENU_PRINCIPAL": {
|
"MENU_PRINCIPAL": {
|
||||||
"message": "*Olá você inicou o Consultme!:*",
|
"message": "*Olá, você iniciou o Consultme!:*",
|
||||||
"action_to_perform": "send_main_menu", # Ação para enviar um menu interativo (botões)
|
"action_to_perform": "send_main_menu", # Ação para enviar o menu principal (lista)
|
||||||
"expected_input_type": "button_click",
|
"expected_input_type": "list_reply", # Espera um clique em um item da lista
|
||||||
"transitions": {
|
"transitions": {
|
||||||
"OPTION_AGENDAR": "MENU_PRINCIPAL_STORE",
|
# Transições para os estados intermediários de tempo/período
|
||||||
"OPTION_CADASTRO_FLOW": "MENU_PRINCIPAL_STORE",
|
"OPTION_ANO": "MENU_PRINCIPAL_ANO",
|
||||||
"OPTION_STATUS": "MENU_PRINCIPAL_STORE",
|
"OPTION_MES": "MENU_PRINCIPAL_MES",
|
||||||
"OPTION_FALAR_ATENDENTE": "MENU_PRINCIPAL_STORE",
|
"OPTION_ONTEM": "MENU_PRINCIPAL_ONTEM",
|
||||||
"default": "MENU_PRINCIPAL_STORE" # Volta para o menu se a opção não for reconhecida
|
"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": {
|
"ENCERRAR_CONVERSA": {
|
||||||
"action_to_perform": "send_main_store", # Ação para enviar um menu interativo (botões)
|
"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",
|
"expected_input_type": "button_click",
|
||||||
"transitions": {
|
"transitions": {
|
||||||
"OPTION_AGENDAR": "AGENDAMENTO_INICIO",
|
"OPTION_TOTAL_CP": "RESPOSTA_ANO_TOTAL_CP", # Combinação final
|
||||||
"OPTION_CADASTRO_FLOW": "INICIAR_FLOW_CADASTRO",
|
"OPTION_TOTAL_LOJAS": "RESPOSTA_ANO_TOTAL_LOJAS",
|
||||||
"OPTION_STATUS": "PEDIR_NUMERO_PEDIDO",
|
"OPTION_TOTAL_UMA_LOJA": "RESPOSTA_ANO_UMA_LOJA",
|
||||||
"OPTION_FALAR_ATENDENTE": "ENCAMINHAR_ATENDENTE",
|
"OPTION_SAIR": "ENCERRAR_CONVERSA",
|
||||||
"default": "MENU_PRINCIPAL" # Volta para o menu se a opção não for reconhecida
|
"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": {
|
"transitions": {
|
||||||
"menu": "MENU_PRINCIPAL",
|
"menu": "MENU_PRINCIPAL",
|
||||||
"ajuda": "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": {
|
# --- ESTADOS FINAIS COMBINATÓRIOS (12 ESTADOS) ---
|
||||||
"action_to_perform": "send_flow_cadastro", # Ação para enviar o Flow
|
# Período: ANO
|
||||||
"flow_id": "COLOQUE_AQUI_O_FLOW_ID_DO_SEU_CADASTRO_PUBLICADO", # ID do seu Flow publicado
|
"RESPOSTA_ANO_TOTAL_CP": {
|
||||||
"flow_cta": "Abrir Cadastro",
|
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
|
||||||
"expected_input_type": "flow_nfm_reply", # Espera a resposta do Flow
|
"action_to_perform": "get_combined_indicator_data_ano_cp", # Ação para buscar e formatar dados
|
||||||
"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
|
|
||||||
"expected_input_type": "any",
|
"expected_input_type": "any",
|
||||||
"transitions": {
|
"transitions": {"default": "MENU_PRINCIPAL"}
|
||||||
"default": "MENU_PRINCIPAL"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
# --- FLUXO DE STATUS DO PEDIDO ---
|
"RESPOSTA_ANO_TOTAL_LOJAS": {
|
||||||
"PEDIR_NUMERO_PEDIDO": {
|
"message": " Consulta realizada com sucesso! \n\n💰 O realizado da Receita em GMV é: *${receita}* e o Boleto Médio é: *${boleto}*",
|
||||||
"message": "Por favor, digite o número do seu pedido para consultar o status:",
|
"action_to_perform": "get_combined_indicator_data_ano_lojas",
|
||||||
"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}*.",
|
|
||||||
"expected_input_type": "any",
|
"expected_input_type": "any",
|
||||||
"transitions": {
|
"transitions": {"default": "MENU_PRINCIPAL"}
|
||||||
"default": "MENU_PRINCIPAL"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"STATUS_NAO_ENCONTRADO": {
|
"RESPOSTA_ANO_UMA_LOJA": {
|
||||||
"message": "Não consegui encontrar o pedido *${numero_pedido}*. Verifique e digite novamente, ou 'menu'.",
|
"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",
|
"expected_input_type": "text",
|
||||||
"transitions": {
|
"transitions": {
|
||||||
"menu": "MENU_PRINCIPAL",
|
"default": "PROCESSAR_ID_LOJA_ANO" # Leva ao processamento do ID da loja
|
||||||
"default": "PEDIR_NUMERO_PEDIDO"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# --- FLUXO DE ATENDENTE ---
|
|
||||||
"ENCAMINHAR_ATENDENTE": {
|
# Período: MÊS
|
||||||
"message": "Encaminhando você para um de nossos atendentes. Por favor, aguarde.",
|
"RESPOSTA_MES_TOTAL_CP": {
|
||||||
"terminal": True # Indica que a conversa termina aqui (até o atendente assumir)
|
"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 ---
|
# --- ESTADO INICIAL ---
|
||||||
|
|||||||
Binary file not shown.
@ -3,6 +3,9 @@ from typing import List, Dict, Any, Optional
|
|||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
import pyodbc
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -20,11 +23,22 @@ scheduler = AsyncIOScheduler()
|
|||||||
|
|
||||||
# --- Variáveis Globais para Gerenciamento de Sessão (APENAS PARA TESTE) ---
|
# --- Variáveis Globais para Gerenciamento de Sessão (APENAS PARA TESTE) ---
|
||||||
# Em produção, isso seria um banco de dados
|
# 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
|
ACTIVE_SESSIONS = {} # Dicionário para armazenar sessões ativas
|
||||||
SESSION_TIMEOUT_SECONDS = 120 # 5 minutos (5 * 60 segundos)
|
SESSION_TIMEOUT_SECONDS = 120 # 5 minutos (5 * 60 segundos)
|
||||||
# --- FIM DAS VARIÁVEIS GLOBAIS DE SESSÃO ---
|
# --- 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
|
# Importações para criptografia
|
||||||
from cryptography.hazmat.primitives.asymmetric import padding # <-- Esta linha está correta!
|
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 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!
|
# Importe Message para type hinting. Certifique-se que webhook_model.py está correto primeiro!
|
||||||
# Certifique-se de importar o grafo e o estado inicial
|
# Certifique-se de importar o grafo e o estado inicial
|
||||||
from dialog_flow.graph_definition import DIALOG_GRAPH, INITIAL_STATE_ID
|
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
|
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]]:
|
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)
|
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.
|
Inicia uma nova sessão para o usuário ou atualiza o timestamp, estado e último ID de mensagem.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
ACTIVE_SESSIONS[sender_id] = {
|
session_info = ACTIVE_SESSIONS.get(sender_id, {})
|
||||||
"last_activity_time": current_time,
|
|
||||||
"current_state": "INICIO" # Ou qualquer estado inicial padrão
|
# Define o estado da sessão:
|
||||||
}
|
if new_state:
|
||||||
print(f"DEBUG_SESSION: Sessão para {sender_id} iniciada/atualizada em {current_time.strftime('%Y-%m-%d %H:%M:%S')}.")
|
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}"
|
job_id = f"session_clean_{sender_id}"
|
||||||
if scheduler.get_job(job_id):
|
if scheduler.get_job(job_id):
|
||||||
scheduler.remove_job(job_id)
|
scheduler.remove_job(job_id)
|
||||||
print(f"DEBUG_SESSION: Tarefa de limpeza anterior para {sender_id} cancelada.")
|
print(f"DEBUG_SESSION: Tarefa de limpeza anterior para {sender_id} cancelada.")
|
||||||
|
|
||||||
# Agendar nova tarefa de limpeza para esta sessão
|
|
||||||
scheduler.add_job(
|
scheduler.add_job(
|
||||||
clean_session_and_notify,
|
clean_session_and_notify,
|
||||||
'date', # Agendamento único para uma data/hora específica
|
'date',
|
||||||
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
|
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
|
||||||
args=[sender_id],
|
args=[sender_id],
|
||||||
id=job_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)}.")
|
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:
|
#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
|
# O message_content já é o payload do botão
|
||||||
next_state_id = current_state.get("transitions", {}).get(message_content, INITIAL_STATE_ID)
|
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":
|
elif message_type == "flow_nfm_reply":
|
||||||
# Quando uma resposta de Flow chega, o next_state depende do sucesso/falha do Flow
|
# 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
|
# Assumimos que handle_flow_response já processou o Flow e determinou sucesso/falha
|
||||||
@ -319,6 +721,33 @@ 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}'")
|
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)
|
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]):
|
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)
|
await send_time_menu(sender_id)
|
||||||
elif action == "send_main_store":
|
elif action == "send_main_store":
|
||||||
await send_store_menu(sender_id)
|
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":
|
elif action == "send_flow_cadastro":
|
||||||
flow_id = state_definition.get("flow_id")
|
flow_id = state_definition.get("flow_id")
|
||||||
flow_cta = state_definition.get("flow_cta")
|
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)
|
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) ---
|
# --- Enviar a mensagem do novo estado (se houver) ---
|
||||||
if "message" in state_definition:
|
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"):
|
if "${email}" in final_message and session_data.get("flow_data"):
|
||||||
flow_data = json.loads(session_data["flow_data"])
|
flow_data = json.loads(session_data["flow_data"])
|
||||||
final_message = final_message.replace("${email}", flow_data.get("email", "não informado"))
|
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}
|
# Para outros placeholders como ${servico_agendado}, ${data_horario_agendado}, ${numero_pedido}, ${status_retornado}
|
||||||
# você faria substituições semelhantes baseadas em session_data
|
# 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) ---
|
# --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) ---
|
||||||
# Estas funções contêm a lógica de como seu bot irá interagir.
|
# 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 ---
|
#--- FUNÇÃO handle_message_type: A VERSÃO CORRETA PARA O TIMEOUT E GRAFO ---
|
||||||
async def handle_message_type(message: Message):
|
async def handle_message_type(message: Message):
|
||||||
sender_id = message.from_
|
sender_id = message.from_
|
||||||
|
message_id = message.id # O ID da mensagem recebida
|
||||||
|
|
||||||
# 1. Limpar sessões inativas
|
# 1. ANTI-DUPLICAÇÃO DE MENSAGENS:
|
||||||
# Esta função limpa as sessões que expiraram e envia a mensagem de timeout via scheduler.
|
# Obtém a sessão ATUAL (se existir) para verificar o último ID processado.
|
||||||
start_or_update_session(sender_id) # Esta é a que precisa ser assíncrona
|
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.
|
# 2. VERIFICAR WHITELIST DE NÚMEROS (SEGUNDO)
|
||||||
# ISSO É ESSENCIAL PARA O TIMEOUT. Se o usuário mandar mensagem, a sessão dele é atualizada
|
if sender_id not in AUTHORIZED_NUMBERS:
|
||||||
# e o agendamento de timeout é resetado.
|
print(f"INFO_BOT_CONTROL: Número {sender_id} NÃO autorizado. Enviando mensagem de não cadastrado.")
|
||||||
start_or_update_session(sender_id)
|
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)
|
|
||||||
|
# 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)
|
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
|
|
||||||
|
|
||||||
|
# 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_content = None
|
||||||
message_type = None
|
message_type = None
|
||||||
|
|
||||||
# --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM ---
|
# --- IDENTIFICAÇÃO DO TIPO DE MENSAGEM (permanece a mesma) ---
|
||||||
# Esta parte permanece como estava, identificando o tipo e conteúdo.
|
|
||||||
if message.type == 'text' and message.text:
|
if message.type == 'text' and message.text:
|
||||||
message_type = "text"
|
message_type = "text"
|
||||||
message_content = message.text.body
|
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_type = "button_click"
|
||||||
message_content = message.button.payload
|
message_content = message.button.payload
|
||||||
elif message.type == 'interactive' and message.interactive:
|
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_type = "list_reply"
|
||||||
message_content = message.interactive.list_reply.id
|
message_content = message.interactive.list_reply.id
|
||||||
elif message.interactive.type == 'button_reply' and message.interactive.button_reply: # Resposta de botão interativo
|
elif message.interactive.type == 'button_reply' and message.interactive.button_reply:
|
||||||
message_type = "button_click" # Tratar como clique de botão
|
message_type = "button_click"
|
||||||
message_content = message.interactive.button_reply.id
|
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_type = "flow_nfm_reply"
|
||||||
message_content = message.interactive.nfm_reply.response_json
|
message_content = message.interactive.nfm_reply.response_json
|
||||||
else:
|
else:
|
||||||
print(f" Tipo interativo desconhecido recebido: {message.interactive.type}")
|
print(f" Tipo interativo desconhecido recebido: {message.interactive.type}")
|
||||||
await send_text_message(sender_id, "Desculpe, não entendi essa interação interativa.")
|
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
|
return
|
||||||
elif message.type == 'image': # Mensagens que não são processadas pelo grafo
|
elif message.type == 'image':
|
||||||
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.")
|
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
|
return
|
||||||
else: # Tipo de mensagem não suportado ou desconhecido
|
else:
|
||||||
message_type = "unsupported"
|
message_type = "unsupported"
|
||||||
message_content = "tipo_desconhecido"
|
message_content = "tipo_desconhecido"
|
||||||
await send_text_message(sender_id, "Desculpe, não entendi o tipo de mensagem que você enviou.")
|
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.
|
# 3. O motor do grafo processa a entrada e gerencia o estado da sessão.
|
||||||
# A `session_data` já foi atualizada no passo 2.
|
# A `session_data` já está atualizada com o último ID da mensagem.
|
||||||
await process_user_input_with_graph(sender_id, message_type, message_content)
|
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.
|
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"
|
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
|
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.
|
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"
|
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
|
button_title = "Clique aqui" # Texto do botão que o usuário clica para ABRIR a lista
|
||||||
|
|
||||||
sections = [
|
sections = [
|
||||||
{
|
{
|
||||||
"title": "Acumulados", # Título da primeira seção
|
"title": "Acumulado", # Título da primeira seção
|
||||||
"rows": [
|
"rows": [
|
||||||
{"id": 'OPTION_ANO', "title": 'Total do CP', "description": "Visualize o resultado Total do CP"},
|
{"id": 'OPTION_TOTAL_CP', "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"},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Por Loja", # Título da segunda seção
|
"title": "Por Loja", # Título da segunda seção
|
||||||
"rows": [
|
"rows": [
|
||||||
{"id": 'OPTION_ONTEM', "title": 'Total das suas Lojas', "description": "Visualize o total das suas Lojas"},
|
{"id": 'OPTION_TOTAL_LOJAS', "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_UMA_LOJA', "title": 'Total de uma Loja', "description": "Visualize o total de uma loja."}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user