1340 lines
67 KiB
Python
1340 lines
67 KiB
Python
import httpx
|
|
from typing import List, Dict, Any, Optional
|
|
import json
|
|
import base64
|
|
import os
|
|
import pyodbc
|
|
import requests
|
|
|
|
|
|
|
|
|
|
# ... (imports existentes no topo) ...
|
|
from datetime import datetime, timedelta
|
|
import asyncio # <-- NOVO: Para agendamento assíncrono
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler # <-- NOVO: Agendador
|
|
|
|
|
|
# --- NOVO: Scheduler para agendar a limpeza de sessões ---
|
|
scheduler = AsyncIOScheduler()
|
|
# Iniciar o scheduler quando o aplicativo iniciar (isso será feito no main.py ou api/webhook.py)
|
|
|
|
# ... (carregamento de chaves e outras funções) ...
|
|
|
|
# --- 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!
|
|
from cryptography.hazmat.primitives import hashes # <-- Esta importação está correta!
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
|
|
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
|
|
# Certifique-se de que FlowStatusChangeEvent também está importado se ainda não estiver
|
|
|
|
from models.webhook_model import Message, TextMessage, ButtonMessage, InteractiveMessage, ListReply # Adicione os modelos necessários
|
|
|
|
FLOW_PRIVATE_KEY_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../private.pem')
|
|
|
|
FLOW_PRIVATE_KEY = None # Garante que a variável começa como None antes do try/except
|
|
|
|
# --- NOVOS PRINTS PARA DEPURACAO DA CHAVE PRIVADA ---
|
|
print("\n--- INICIANDO DEPURACAO DE CARREGAMENTO DE CHAVE PRIVADA DO FLOW ---")
|
|
print(f"DEBUG: Caminho configurado para a chave privada: {FLOW_PRIVATE_KEY_PATH}")
|
|
|
|
# Verifica se o arquivo existe antes de tentar abrir
|
|
if not os.path.exists(FLOW_PRIVATE_KEY_PATH):
|
|
print(f"DEBUG: ATENÇÃO: Arquivo da chave privada NÃO ENCONTRADO em {FLOW_PRIVATE_KEY_PATH} antes de tentar carregar.")
|
|
else:
|
|
print(f"DEBUG: Arquivo da chave privada ENCONTRADO em {FLOW_PRIVATE_KEY_PATH}. Tentando ler...")
|
|
try:
|
|
# Tenta ler o conteúdo do arquivo para ter certeza que é acessível e não vazio
|
|
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file_check:
|
|
key_content_check = key_file_check.read(100) # Lê apenas os primeiros 100 bytes
|
|
print(f"DEBUG: Primeiros 100 bytes do arquivo da chave (preview): {key_content_check!r}...")
|
|
if not key_content_check:
|
|
print("DEBUG: ATENÇÃO: Arquivo da chave privada está VAZIO.")
|
|
except Exception as e_check:
|
|
print(f"DEBUG: ERRO ao tentar PRÉ-LER o arquivo da chave: {type(e_check).__name__}: {e_check}")
|
|
|
|
print(f"DEBUG: Valor de FLOW_PRIVATE_KEY_PASSWORD (carregado do .env/config): '{FLOW_PRIVATE_KEY_PASSWORD}' (Tipo: {type(FLOW_PRIVATE_KEY_PASSWORD).__name__})")
|
|
# --- FIM DOS PRINTS DE DEPURACAO DA CHAVE PRIVADA ---
|
|
|
|
|
|
try:
|
|
with open(FLOW_PRIVATE_KEY_PATH, "rb") as key_file:
|
|
key_content = key_file.read()
|
|
|
|
# CONDIÇÃO CRÍTICA: Use a senha se ela existir, ou None se a chave não tiver senha
|
|
password_to_use = FLOW_PRIVATE_KEY_PASSWORD.encode('utf-8') if FLOW_PRIVATE_KEY_PASSWORD else None
|
|
|
|
print(f"DEBUG: Senha que será usada para carregar a chave: {'(presente)' if password_to_use else '(ausente/None)'}")
|
|
|
|
FLOW_PRIVATE_KEY = load_pem_private_key(
|
|
key_content,
|
|
password=password_to_use, # <<< ALTERADO PARA USAR A SENHA
|
|
backend=default_backend()
|
|
)
|
|
print(f"✅ Chave privada do Flow carregada de: {FLOW_PRIVATE_KEY_PATH}")
|
|
except FileNotFoundError:
|
|
print(f"❌ ERRO: Chave privada do Flow não encontrada em {FLOW_PRIVATE_KEY_PATH}. Verifique o caminho e as permissões.")
|
|
FLOW_PRIVATE_KEY = None
|
|
except Exception as e:
|
|
# A exceção agora provavelmente será relacionada à senha se o arquivo for encontrado
|
|
# OU se o formato da chave está errado para o password=None/password='suasenha'
|
|
print(f"❌ ERRO ao carregar chave privada do Flow: {type(e).__name__}: {e}. Verifique se a senha está correta ou se a chave não tem senha. Conteúdo da chave (primeiros 50 bytes): {key_content[:50]!r}...")
|
|
FLOW_PRIVATE_KEY = None
|
|
|
|
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES = """-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsFw96WUer6yyZLQFkUNS
|
|
Vkv++EEKVVVXca3TcCJqR9aaOepiyKuUT0Il8ctdaZHaTFeEQnM82xbaijEATtGv
|
|
qmX+zZRnMnRbI63GZm9QM3pnAbs/iXji5PfLbD5AGt8vDUyldlwGu+2e3PHWHPM2
|
|
MKR/+yHEEmgXWWkCYzgdhwIbsaGQNFdOXykfVpdQLt8237E1VEDEebEP3GlUagvo
|
|
7k+NQQ+tDHNfAeQjcIHiTCPE39hSFdH7W413HDiu/0sX02ATv+QCppovDBboQdL4
|
|
OP4nbnvbRn7HHIFIPskpkm5dPfRuDrejb/Q72c1FcuALBAPFpJYCsjUM3JSfa854
|
|
IwIDAQAB-----END PUBLIC KEY-----""" # <<< COLOQUE A SUA CHAVE PÚBLICA (DO public_flow_key.pem) AQUI! <<<
|
|
|
|
try:
|
|
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = load_pem_public_key(
|
|
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES.encode('utf-8'),
|
|
backend=default_backend()
|
|
)
|
|
print("✅ Chave pública da Meta (assumida) para criptografia de retorno (Flows) carregada.")
|
|
except Exception as e:
|
|
print(f"❌ ERRO ao carregar chave pública da Meta para Flow (para criptografia de retorno): {e}")
|
|
WHATSAPP_FLOW_PUBLIC_KEY_META_FOR_ENCRYPTION_OF_AES_OBJECT = None
|
|
|
|
def decrypt_flow_request_data(
|
|
encrypted_flow_data_b64: str,
|
|
encrypted_aes_key_b64: str,
|
|
initial_vector_b64: str
|
|
) -> Dict[str, Any]: # << AQUI ESTÁ A MUDANÇA: AGORA RETORNA UM DICIONÁRIO COMPLETO
|
|
"""
|
|
Descriptografa os dados de requisição recebidos de um Flow do WhatsApp.
|
|
Retorna os dados descriptografados, a chave AES e o IV.
|
|
"""
|
|
if not FLOW_PRIVATE_KEY:
|
|
raise ValueError("Chave privada do Flow não carregada. Não é possível descriptografar a requisição.")
|
|
|
|
flow_data = base64.b64decode(encrypted_flow_data_b64)
|
|
iv = base64.b64decode(initial_vector_b64)
|
|
|
|
encrypted_flow_data_body = flow_data[:-16]
|
|
encrypted_flow_data_tag = flow_data[-16:]
|
|
|
|
encrypted_aes_key = base64.b64decode(encrypted_aes_key_b64)
|
|
aes_key = FLOW_PRIVATE_KEY.decrypt(
|
|
encrypted_aes_key,
|
|
padding.OAEP(
|
|
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
algorithm=hashes.SHA256(),
|
|
label=None
|
|
)
|
|
)
|
|
|
|
decryptor = Cipher(
|
|
algorithms.AES(aes_key),
|
|
modes.GCM(iv, encrypted_flow_data_tag),
|
|
backend=default_backend()
|
|
).decryptor()
|
|
|
|
decrypted_data_bytes = decryptor.update(encrypted_flow_data_body) + decryptor.finalize()
|
|
decrypted_data = json.loads(decrypted_data_bytes.decode("utf-8"))
|
|
|
|
# Retorna os dados, a chave AES e o IV para uso na resposta
|
|
return {
|
|
"decrypted_payload": decrypted_data,
|
|
"aes_key": aes_key,
|
|
"initial_vector": iv
|
|
}
|
|
|
|
|
|
def encrypt_flow_response_data(response_data: Dict[str, Any], aes_key: bytes, initial_vector: bytes) -> str:
|
|
"""
|
|
Criptografa os dados de resposta do Flow para enviar ao WhatsApp.
|
|
Usa a chave AES e o IV (invertido) recebidos na solicitação original.
|
|
Retorna a string completa codificada em Base64.
|
|
"""
|
|
# Nenhuma chave pública RSA é usada AQUI para criptografar a AES_KEY.
|
|
# A AES_KEY e o IV são passados da requisição original.
|
|
|
|
# 1. Preparar initialization vector para resposta (invertendo bits do IV original)
|
|
flipped_iv = bytearray()
|
|
for byte in initial_vector:
|
|
flipped_iv.append(byte ^ 0xFF) # XOR com 0xFF para inverter bits
|
|
|
|
# 2. Criptografar o payload de resposta (JSON) usando AES-GCM
|
|
plaintext_bytes = json.dumps(response_data).encode('utf-8')
|
|
|
|
encryptor = Cipher(algorithms.AES(aes_key), modes.GCM(flipped_iv), backend=default_backend()).encryptor()
|
|
encrypted_data_bytes = encryptor.update(plaintext_bytes) + encryptor.finalize()
|
|
tag = encryptor.tag # Tag de autenticação do GCM
|
|
|
|
# 3. CONSTRUIR A STRING FINAL Base64:
|
|
# A Meta espera o IV e o TAG concatenados ao final dos dados criptografados,
|
|
# e então TUDO isso é Base64 codificado. O exemplo Django mostrou isso implicitamente.
|
|
|
|
# Payload final em bytes: encrypted_data_bytes + flipped_iv + tag
|
|
# ATENÇÃO: O exemplo Django concatena o IV no payload final, mas a documentação
|
|
# da Meta para AES-GCM geralmente não o faz, ela espera que o IV seja separado no cabeçalho.
|
|
# No entanto, a documentação que você me passou disse "append authentication tag
|
|
# generated during encryption to the end of the encryption result; encode the whole output as base64 string".
|
|
#
|
|
# Vamos seguir o exemplo Django para a resposta. O exemplo Django para `encrypt_response`
|
|
# retorna base64(encryptor.update(...) + encryptor.finalize() + encryptor.tag).
|
|
# Ele NÃO inclui a chave AES criptografada aqui, porque a Meta já tem a chave AES.
|
|
|
|
# O retorno deve ser SOMENTE o dado criptografado + tag, Base64 codificado.
|
|
# O IV e a chave AES são gerenciados pela Meta.
|
|
|
|
# A ÚLTIMA TENTATIVA DA METAMORA: Retornar a STRING Base64 pura dos dados criptografados + tag
|
|
# (sem o JSON aninhado que estávamos construindo)
|
|
|
|
# A resposta final é apenas a CONCATENAÇÃO dos dados criptografados e o tag, codificado em Base64.
|
|
final_payload_bytes = encrypted_data_bytes + tag # Dados criptografados + Tag
|
|
base64_encoded_final_response = base64.b64encode(final_payload_bytes).decode('utf-8')
|
|
|
|
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}")
|
|
|
|
|
|
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)
|
|
|
|
def start_or_update_session(sender_id: str, new_state: Optional[str] = None, last_message_id: Optional[str] = None):
|
|
"""
|
|
Inicia uma nova sessão para o usuário ou atualiza o timestamp, estado e último ID de mensagem.
|
|
"""
|
|
current_time = datetime.now()
|
|
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')}.")
|
|
|
|
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.")
|
|
|
|
scheduler.add_job(
|
|
clean_session_and_notify,
|
|
'date',
|
|
run_date=current_time + timedelta(seconds=SESSION_TIMEOUT_SECONDS),
|
|
args=[sender_id],
|
|
id=job_id,
|
|
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:
|
|
|
|
# --- NOVA FUNÇÃO: O MOTOR DO CHATBOT BASEADO EM GRAFO ---
|
|
async def process_user_input_with_graph(sender_id: str, message_type: str, message_content: Any):
|
|
"""
|
|
Processa a entrada do usuário e avança o estado da conversa no grafo.
|
|
"""
|
|
session_data = get_session_state(sender_id)
|
|
if not session_data:
|
|
# Se a sessão não existe (timeout ou novo usuário), inicie do estado INICIO
|
|
current_state_id = INITIAL_STATE_ID
|
|
start_or_update_session(sender_id) # Garante que a sessão existe
|
|
session_data = get_session_state(sender_id) # Recarrega para ter certeza
|
|
else:
|
|
current_state_id = session_data.get("current_state", INITIAL_STATE_ID) # Pega o estado atual
|
|
|
|
current_state = DIALOG_GRAPH.get(current_state_id)
|
|
if not current_state:
|
|
print(f"❌ ERRO GRAVE: Estado '{current_state_id}' não encontrado no DIALOG_GRAPH. Retornando ao INICIO.")
|
|
current_state_id = INITIAL_STATE_ID
|
|
current_state = DIALOG_GRAPH[INITIAL_STATE_ID]
|
|
session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado na sessão
|
|
|
|
print(f"DEBUG_GRAPH: Usuário {sender_id} no estado '{current_state_id}'. Tipo de entrada: {message_type}. Conteúdo: {message_content}")
|
|
|
|
next_state_id = None
|
|
user_input_processed = str(message_content).lower().strip() # Padroniza a entrada para transições
|
|
|
|
# --- Lógica para determinar o próximo estado (transição) ---
|
|
if message_type == "text":
|
|
# Verifica se alguma palavra-chave da transição corresponde ao texto
|
|
for keyword, target_state in current_state.get("transitions", {}).items():
|
|
if keyword != "default" and keyword in user_input_processed:
|
|
next_state_id = target_state
|
|
break
|
|
if not next_state_id: # Se nenhuma palavra-chave específica, usa o default
|
|
next_state_id = current_state.get("transitions", {}).get("default", INITIAL_STATE_ID)
|
|
|
|
session_data["last_text_input"] = message_content # Salva a última entrada de texto para uso futuro
|
|
|
|
elif message_type == "button_click":
|
|
# 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
|
|
# A lógica para a transição do Flow precisa ser mais sofisticada.
|
|
# Por enquanto, vamos para o estado de conclusão do Flow.
|
|
next_state_id = current_state.get("transitions", {}).get("success", INITIAL_STATE_ID) # Por padrão, vai para o sucesso
|
|
session_data["flow_data"] = message_content # Salva os dados do Flow para processamento posterior
|
|
|
|
# ... (adicionar outros tipos de entrada, como "api_response" para transições internas, se necessário) ...
|
|
|
|
# Atualiza o estado da sessão
|
|
session_data["current_state"] = next_state_id
|
|
start_or_update_session(sender_id) # Atualiza o timestamp da sessão
|
|
|
|
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]):
|
|
"""
|
|
Executa a ação de um estado e envia a resposta correspondente.
|
|
"""
|
|
state_definition = DIALOG_GRAPH.get(state_id)
|
|
if not state_definition:
|
|
print(f"❌ ERRO GRAVE: Definição para o estado '{state_id}' não encontrada. Não é possível responder.")
|
|
await send_text_message(sender_id, "Ops! Ocorreu um erro interno. Por favor, tente novamente.")
|
|
session_data["current_state"] = INITIAL_STATE_ID # Reseta o estado
|
|
start_or_update_session(sender_id)
|
|
return
|
|
|
|
# --- Ações a serem realizadas pelo bot no novo estado ---
|
|
if "action_to_perform" in state_definition:
|
|
action = state_definition["action_to_perform"]
|
|
print(f"DEBUG_GRAPH: Executando ação para estado '{state_id}': {action}")
|
|
|
|
if action == "send_main_menu":
|
|
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")
|
|
if flow_id:
|
|
# O Flow ID real virá da DIALOG_GRAPH, não do SendFlowRequest
|
|
await send_whatsapp_flow(sender_id, flow_id, flow_cta)
|
|
else:
|
|
await send_text_message(sender_id, "Ops! O Flow de cadastro não está configurado. Tente novamente mais tarde.")
|
|
session_data["current_state"] = INITIAL_STATE_ID
|
|
start_or_update_session(sender_id)
|
|
|
|
elif action == "process_cadastro_data":
|
|
# Aqui você processaria os dados do Flow que estão em session_data["flow_data"]
|
|
flow_data = json.loads(session_data.get("flow_data", "{}")) # Carregar os dados salvos
|
|
nome_completo = flow_data.get("nome_completo")
|
|
email = flow_data.get("email")
|
|
|
|
if nome_completo and email:
|
|
print(f"DEBUG_GRAPH: Processando cadastro para {nome_completo}, {email}")
|
|
# AQUI você salvaria no BD/CRM
|
|
# salvar_usuario_no_banco_de_dados(sender_id, nome_completo, email)
|
|
# enviar_para_crm(nome_completo, email)
|
|
response_message = state_definition["message"].replace("${nome_completo}", nome_completo).replace("${email}", email)
|
|
await send_text_message(sender_id, response_message)
|
|
else:
|
|
await send_text_message(sender_id, "Falha ao processar cadastro. Dados incompletos. Por favor, tente novamente.")
|
|
session_data["current_state"] = DIALOG_GRAPH.get("CADASTRO_FALHA", {}).get("transitions",{}).get("default", INITIAL_STATE_ID) # Ir para estado de falha
|
|
start_or_update_session(sender_id)
|
|
|
|
|
|
|
|
|
|
# --- Enviar a mensagem do novo estado (se houver) ---
|
|
if "message" in state_definition:
|
|
# Substitui placeholders na mensagem
|
|
final_message = state_definition["message"]
|
|
if "${nome_completo}" in final_message and session_data.get("flow_data"):
|
|
flow_data = json.loads(session_data["flow_data"])
|
|
final_message = final_message.replace("${nome_completo}", flow_data.get("nome_completo", "usuário"))
|
|
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
|
|
|
|
await send_text_message(sender_id, final_message)
|
|
|
|
# Lógica para limpar a sessão se o estado for terminal
|
|
if state_definition.get("terminal"):
|
|
await clean_session_and_notify(sender_id, send_timeout_message=False) # Não envia msg de timeout
|
|
print(f"DEBUG_GRAPH: Sessão para {sender_id} encerrada no estado terminal '{state_id}'.")
|
|
|
|
|
|
|
|
|
|
|
|
# --- Lógicas de Tratamento de Mensagens Recebidas (Funções Auxiliares) ---
|
|
# Estas funções contêm a lógica de como seu bot irá interagir.
|
|
|
|
# --- handle_message_type: Agora é o ponto de entrada principal que chama o motor do grafo ---
|
|
#--- 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. 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. 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. 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 (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:
|
|
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:
|
|
message_type = "list_reply"
|
|
message_content = message.interactive.list_reply.id
|
|
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:
|
|
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
|
|
elif message.type == 'image':
|
|
message_type = "image"
|
|
message_content = "imagem_recebida"
|
|
await send_text_message(sender_id, "Recebi sua imagem. No momento, só consigo processar texto e interações.")
|
|
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
|
|
|
|
# 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)
|
|
|
|
|
|
# --- Funções de Envio de Mensagens para o WhatsApp ---
|
|
# Estas funções fazem as chamadas à API da Meta para enviar mensagens.
|
|
|
|
async def send_text_message(to: str, text: str):
|
|
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
|
headers = {
|
|
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
payload = {
|
|
"messaging_product": "whatsapp",
|
|
"to": to,
|
|
"type": "text",
|
|
"text": {
|
|
"preview_url": False,
|
|
"body": text
|
|
}
|
|
}
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
response.raise_for_status()
|
|
print("✅ Mensagem de texto enviada com sucesso:", response.json())
|
|
except httpx.HTTPStatusError as e:
|
|
print(f"❌ Erro HTTP ao enviar mensagem de texto: {e.response.status_code} - {e.response.text}")
|
|
except httpx.RequestError as e:
|
|
print(f"❌ Erro de rede ao enviar mensagem de texto: {e}")
|
|
|
|
async def send_interactive_buttons(to: str, header_text: str, body_text: str, buttons_array: List[Dict[str, str]]):
|
|
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
|
headers = {
|
|
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
payload = {
|
|
"messaging_product": "whatsapp",
|
|
"to": to,
|
|
"type": "interactive",
|
|
"interactive": {
|
|
"type": "button",
|
|
"header": { "type": "text", "text": header_text },
|
|
"body": { "text": body_text },
|
|
"action": {
|
|
"buttons": [{"type": "reply", "reply": {"id": btn["id"], "title": btn["title"]}} for btn in buttons_array]
|
|
}
|
|
}
|
|
}
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
response.raise_for_status()
|
|
print("✅ Mensagem com botões enviada com sucesso:", response.json())
|
|
except httpx.HTTPStatusError as e:
|
|
print(f"❌ Erro HTTP ao enviar mensagem com botões: {e.response.status_code} - {e.response.text}")
|
|
except httpx.RequestError as e:
|
|
print(f"❌ Erro de rede ao enviar mensagem com botões: {e}")
|
|
|
|
async def send_interactive_list(to: str, header_text: str, body_text: str, button_title: str, sections_array: List[Dict[str, Any]]):
|
|
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
|
headers = {
|
|
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
payload = {
|
|
"messaging_product": "whatsapp",
|
|
"to": to,
|
|
"type": "interactive",
|
|
"interactive": {
|
|
"type": "list",
|
|
"header": { "type": "text", "text": header_text },
|
|
"body": { "text": body_text },
|
|
"action": {
|
|
"button": button_title,
|
|
"sections": [
|
|
{
|
|
"title": section["title"],
|
|
"rows": [{"id": row["id"], "title": row["title"], "description": row.get("description", "")} for row in section["rows"]]
|
|
} for section in sections_array
|
|
]
|
|
}
|
|
}
|
|
}
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
response.raise_for_status()
|
|
print("✅ Mensagem de lista enviada com sucesso:", response.json())
|
|
except httpx.HTTPStatusError as e:
|
|
print(f"❌ Erro HTTP ao enviar mensagem de lista: {e.response.status_code} - {e.response.text}")
|
|
except httpx.RequestError as e:
|
|
print(f"❌ Erro de rede ao enviar mensagem de lista: {e}")
|
|
|
|
async def mark_message_as_read(message_id: str):
|
|
url = f"https://graph.facebook.com/v23.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
|
headers = {
|
|
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
payload = {
|
|
"messaging_product": "whatsapp",
|
|
"status": "read",
|
|
"message_id": message_id
|
|
}
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
response.raise_for_status()
|
|
except httpx.HTTPStatusError as e:
|
|
print(f"❌ Erro HTTP ao marcar mensagem {message_id} como lida: {e.response.status_code} - {e.response.text}")
|
|
except httpx.RequestError as e:
|
|
print(f"❌ Erro de rede ao marcar mensagem {message_id} como lida: {e}")
|
|
|
|
|
|
|
|
# --- Função de Exemplo para Enviar um Menu Interativo ---
|
|
# --- NOVA FUNÇÃO: send_stores_list_menu (Para menu de LISTA de lojas) ---
|
|
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ê 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
|
|
"rows": [
|
|
{"id": 'OPTION_ANO', "title": 'Acumulado do Ano', "description": "Do do ano até ontem."},
|
|
{"id": 'OPTION_MES', "title": 'Acumulado do Mês', "description": "Do primeira do mês até ontem."},
|
|
]
|
|
},
|
|
{
|
|
"title": "Resultados", # Título da segunda seção
|
|
"rows": [
|
|
{"id": 'OPTION_ONTEM', "title": 'Resultado de Ontem', "description": "Ver o realizado de ontem."},
|
|
{"id": 'OPTION_HOJE', "title": 'Resultado de Hoje', "description": "Resultado Parcial de Hoje"}
|
|
]
|
|
},
|
|
{
|
|
"title": "Sair", # Título da segunda seção
|
|
"rows": [
|
|
{"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"},
|
|
]
|
|
}
|
|
]
|
|
# Relembrando: até 10 seções e 10 itens por seção (total 100 itens).
|
|
|
|
# CHAMA send_interactive_list DIRETAMENTE AQUI
|
|
await send_interactive_list(to, header_text, body_text, button_title, sections)
|
|
|
|
|
|
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 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": "Acumulado", # Título da primeira seção
|
|
"rows": [
|
|
{"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_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."}
|
|
]
|
|
},
|
|
{
|
|
"title": "Sair", # Título da segunda seção
|
|
"rows": [
|
|
{"id": 'OPTION_SAIR', "title": 'Sair', "description": "Encerre a conversa"},
|
|
]
|
|
}
|
|
]
|
|
# Relembrando: até 10 seções e 10 itens por seção (total 100 itens).
|
|
|
|
# CHAMA send_interactive_list DIRETAMENTE AQUI
|
|
await send_interactive_list(to, header_text, body_text, button_title, sections)
|
|
|
|
# Exemplo de como usar o MENU, só aceita 3 opções mas já aparece direto pro usuário as opções.
|
|
|
|
'''
|
|
async def send_stores_menu(to: str):
|
|
buttons = [
|
|
{"id": 'OPTION_AGENDAR', "title": 'Total do CP'},
|
|
{"id": 'OPTION_STATUS', "title": 'Total das suas Lojas'},
|
|
{"id": 'OPTION_FALAR_ATENDENTE', "title": 'De uma Loja'}
|
|
]
|
|
await send_interactive_buttons(to, "Menu Principal", "Escolha a Dimensão das Lojas que você quer visulizar o indicador:", buttons)
|
|
|
|
async def send_time_menu(to: str):
|
|
buttons = [
|
|
{"id": 'OPTION_AGENDAR', "title": 'Acumulado do Ano'},
|
|
{"id": 'OPTION_STATUS', "title": 'Acumulado do Mês'},
|
|
{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado de Ontem'}
|
|
#{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Resultado Parcial de Hoje'},
|
|
#{"id": 'OPTION_FALAR_ATENDENTE', "title": 'Sair'}
|
|
]
|
|
await send_interactive_buttons(to, "Escolha o Período que você quer visualizar o indicador", "Veja as opções abaixo:", buttons)
|
|
|
|
'''
|
|
async def send_whatsapp_flow(to: str, flow_id: str, flow_cta: str, screen: str = "welcome_screen") -> Dict[str, Any]:
|
|
url = f"https://graph.facebook.com/v18.0/{WHATSAPP_PHONE_NUMBER_ID}/messages"
|
|
headers = {
|
|
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
payload = {
|
|
"recipient_type": "individual", # <-- ADICIONADO: Conforme a documentação
|
|
"messaging_product": "whatsapp",
|
|
"to": to,
|
|
"type": "interactive",
|
|
"interactive": {
|
|
"type": "flow",
|
|
"header": {
|
|
"type": "text",
|
|
"text": "Preencha seu Cadastro"
|
|
},
|
|
"body": {
|
|
"text": "Clique no botão abaixo para preencher o formulário de cadastro."
|
|
},
|
|
"footer": {
|
|
"text": "Consultme - Seu parceiro digital"
|
|
},
|
|
"action": {
|
|
"name": "flow",
|
|
"parameters": {
|
|
"flow_id": flow_id, # Usando flow_id (geralmente melhor que flow_name)
|
|
"flow_cta": flow_cta,
|
|
# --- ATENÇÃO: MUDAR PARA flow_message_version: "3" (STRING LITERAL "3") ---
|
|
# Já que o exemplo da documentação é v18.0 com "3", vamos seguir esse exato
|
|
"flow_message_version": "3" # <--- ALTERE ESTA LINHA PARA "3"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# ... (restante da função, incluindo DEBUG PRINTS e try/except) ...
|
|
print(f"DEBUG_FLOW: Payload a ser enviado para o Flow API: {json.dumps(payload, indent=2)}") # Verifique o payload final no log
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
response.raise_for_status()
|
|
print("✅ Flow enviado com sucesso:", response.json())
|
|
return {"status": "success", "data": response.json()}
|
|
except httpx.HTTPStatusError as e:
|
|
print(f"❌ ERRO HTTP ao enviar Flow (Status: {e.response.status_code}, Detalhe: {e.response.text}): {e}")
|
|
# print(f"DEBUG_FLOW_ERROR_RESPONSE: {e.response.json()}") # Descomente para ver o erro JSON completo da Meta
|
|
return {"status": "error", "message": e.response.text, "code": e.response.status_code}
|
|
except httpx.RequestError as e:
|
|
print(f"❌ ERRO de rede ao enviar Flow: {e}")
|
|
return {"status": "error", "message": str(e), "code": 500}
|
|
except Exception as e:
|
|
print(f"❌ ERRO INESPERADO ao enviar Flow: {type(e).__name__}: {e}")
|
|
return {"status": "error", "message": f"Erro inesperado: {str(e)}", "code": 500}
|
|
# ... (send_interactive_menu, handle_button_response, etc.) . |