diff --git a/KnowledgeBaseCline.md b/KnowledgeBaseCline.md
index d3795f3..ebe5b10 100644
--- a/KnowledgeBaseCline.md
+++ b/KnowledgeBaseCline.md
@@ -8,6 +8,16 @@ Non fare commit sul repository GitHub, ci penso io
Non effettuare test da Browser, ci penso io
+Questi i container:
+
+pecflow-worker-1
+pecflow-frontend-1
+pecflow-backend-1
+pecflow-nginx-1
+pecflow-db-1
+pecflow-redis-1
+pecflow-minio-1
+
Queste le caselle PEC e i loro parametri IMAP/SMTP che puoi usare per test, non effettuare invii per adesso
Casella: matteo.giustini@arubapec.it
@@ -26,7 +36,7 @@ Tutto il frontend deve essere in italiano
Credenziali admin
Ruolo Email Password
-Super Admin superadmin@pechub.it SuperAdmin@PEChub2026!
-Admin (tenant demo) admin@demo.pechub.it Demo@PEChub2026!
-Operator (tenant demo) operator@demo.pechub.it Oper@PEChub2026!
+Super Admin superadmin@pecflow.it SuperAdmin@PecFlow2026!
+Admin (tenant demo) admin@demo.pecflow.it Demo@PecFlow2026!
+Operator (tenant demo) operator@demo.pecflow.it Oper@PecFlow2026!
Per accedere all'applicazione usa le credenziali Admin del tenant demo.
\ No newline at end of file
diff --git a/backend/app/notifications/__init__.py b/backend/app/notifications/__init__.py
new file mode 100644
index 0000000..089f831
--- /dev/null
+++ b/backend/app/notifications/__init__.py
@@ -0,0 +1 @@
+# Modulo notifiche – mittenti multi-canale
diff --git a/backend/app/notifications/telegram.py b/backend/app/notifications/telegram.py
new file mode 100644
index 0000000..0718368
--- /dev/null
+++ b/backend/app/notifications/telegram.py
@@ -0,0 +1,113 @@
+"""
+Telegram Bot API – invio messaggi via sendMessage.
+
+Usato da NotificationService.test_channel() e dalle notifiche real-time.
+
+Formato configurazione canale:
+ config: { "chat_id": "-100123456789" } # pubblico
+ config_secret: { "bot_token": "123:AAA..." } # cifrato in config_enc
+
+API Telegram:
+ POST https://api.telegram.org/bot{token}/sendMessage
+ Body: { "chat_id": "...", "text": "...", "parse_mode": "MarkdownV2" }
+"""
+
+import httpx
+
+TELEGRAM_API_BASE = "https://api.telegram.org"
+DEFAULT_TIMEOUT = 10.0 # secondi
+
+
+class TelegramError(Exception):
+ """Errore durante l'invio di un messaggio Telegram."""
+
+ def __init__(self, message: str, http_status: int | None = None, api_code: int | None = None):
+ super().__init__(message)
+ self.http_status = http_status
+ self.api_code = api_code
+
+
+async def send_message(
+ bot_token: str,
+ chat_id: str,
+ text: str,
+ parse_mode: str = "HTML",
+ disable_web_page_preview: bool = True,
+ timeout: float = DEFAULT_TIMEOUT,
+) -> dict:
+ """
+ Invia un messaggio a un canale/gruppo/utente Telegram.
+
+ Args:
+ bot_token: Token del bot Telegram (es. "123456789:AAF...")
+ chat_id: ID della chat/canale (es. "-100123456789" o "@mychannel")
+ text: Testo del messaggio (supporta HTML o MarkdownV2)
+ parse_mode: "HTML" (default) | "MarkdownV2" | "" (plain text)
+ disable_web_page_preview: Disabilita anteprima link
+ timeout: Timeout HTTP in secondi
+
+ Returns:
+ dict con il risultato della API Telegram (result.message_id, ecc.)
+
+ Raises:
+ TelegramError: in caso di errore HTTP o risposta API non-ok
+ """
+ url = f"{TELEGRAM_API_BASE}/bot{bot_token}/sendMessage"
+ payload: dict = {
+ "chat_id": chat_id,
+ "text": text,
+ }
+ if parse_mode:
+ payload["parse_mode"] = parse_mode
+ if disable_web_page_preview:
+ payload["link_preview_options"] = {"is_disabled": True}
+
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ try:
+ response = await client.post(url, json=payload)
+ except httpx.TimeoutException as exc:
+ raise TelegramError(
+ f"Timeout nella connessione a Telegram ({timeout}s)"
+ ) from exc
+ except httpx.RequestError as exc:
+ raise TelegramError(f"Errore di rete Telegram: {exc}") from exc
+
+ if response.status_code != 200:
+ raise TelegramError(
+ f"Telegram API ha risposto con HTTP {response.status_code}: {response.text[:200]}",
+ http_status=response.status_code,
+ )
+
+ data = response.json()
+ if not data.get("ok"):
+ api_code = data.get("error_code")
+ description = data.get("description", "Errore sconosciuto")
+ raise TelegramError(
+ f"Telegram API error {api_code}: {description}",
+ http_status=response.status_code,
+ api_code=api_code,
+ )
+
+ return data.get("result", {})
+
+
+async def send_test_message(
+ bot_token: str,
+ chat_id: str,
+ channel_name: str = "PecFlow",
+) -> dict:
+ """
+ Invia un messaggio di test formattato al canale configurato.
+
+ Returns:
+ dict result da Telegram (con message_id)
+ """
+ from datetime import datetime
+
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ text = (
+ f"✅ PecFlow – Test canale Telegram\n\n"
+ f"Il canale {channel_name} è configurato correttamente.\n\n"
+ f"🕐 {ts}"
+ )
+ return await send_message(bot_token=bot_token, chat_id=chat_id, text=text, parse_mode="HTML")
diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py
index 2f9c9ba..699f397 100644
--- a/backend/app/services/notification_service.py
+++ b/backend/app/services/notification_service.py
@@ -161,6 +161,35 @@ class NotificationService:
elif channel_type == "telegram":
if not config.get("chat_id"):
return ChannelTestResult(success=False, message="Chat ID Telegram non configurato")
+ # Invio reale via Bot API
+ secret = _decrypt(channel.config_enc) if channel.config_enc else {}
+ bot_token = secret.get("bot_token")
+ if not bot_token:
+ return ChannelTestResult(success=False, message="Bot token Telegram non configurato")
+ try:
+ from app.notifications.telegram import TelegramError, send_test_message
+ result = await send_test_message(
+ bot_token=bot_token,
+ chat_id=str(config["chat_id"]),
+ channel_name=channel.name,
+ )
+ msg_id = result.get("message_id")
+ return ChannelTestResult(
+ success=True,
+ message=f"Messaggio Telegram inviato con successo (message_id={msg_id}).",
+ http_status=200,
+ )
+ except TelegramError as exc:
+ return ChannelTestResult(
+ success=False,
+ message=f"Errore Telegram: {exc}",
+ http_status=exc.http_status,
+ )
+ except Exception as exc:
+ return ChannelTestResult(
+ success=False,
+ message=f"Errore imprevisto durante il test Telegram: {exc}",
+ )
elif channel_type == "whatsapp":
if not config.get("phone_number"):
return ChannelTestResult(success=False, message="Numero WhatsApp non configurato")
diff --git a/backend/tests/integration/test_telegram_real.py b/backend/tests/integration/test_telegram_real.py
new file mode 100644
index 0000000..86d4fc6
--- /dev/null
+++ b/backend/tests/integration/test_telegram_real.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python3
+"""
+Test integrazione Telegram REALE – Canale configurato
+======================================================
+
+Testa:
+1. Lettura canale Telegram dal database (bot_token + chat_id)
+2. Chiamata diretta alla funzione send_message (httpx → Bot API)
+3. Verifica risposta Telegram (message_id, chat, testo)
+4. Test via NotificationService.test_channel (flusso completo)
+
+Eseguire DENTRO il container backend:
+ docker exec pecflow-backend-1 python \
+ /app/tests/integration/test_telegram_real.py
+
+Oppure specificando credenziali manuali via env:
+ docker exec -e TELEGRAM_BOT_TOKEN=xxx -e TELEGRAM_CHAT_ID=-100yyy \
+ pecflow-backend-1 python /app/tests/integration/test_telegram_real.py
+"""
+
+import asyncio
+import base64
+import json
+import os
+import sys
+from datetime import datetime
+
+# ─── Variabili d'ambiente ─────────────────────────────────────────────────────
+os.environ.setdefault("ENCRYPTION_KEY", "6465762d656e6372797074696f6e2d6b65792d6e6f742d666f722d70726f6400")
+os.environ.setdefault("SECRET_KEY", "dev-secret-key-not-for-production-use-only-for-local-0000000000000")
+os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://pecflow:pecflow_dev_password@db:5432/pecflow")
+os.environ.setdefault("REDIS_URL", "redis://redis:6379/0")
+os.environ.setdefault("MINIO_ENDPOINT", "minio:9000")
+
+
+# ─── Utilities ────────────────────────────────────────────────────────────────
+
+def _sep(char: str = "─", width: int = 60) -> None:
+ print(char * width)
+
+
+def _banner(bot_token_masked: str, chat_id: str) -> None:
+ _sep("═")
+ print(" PecFlow – Test Telegram Reale")
+ _sep("═")
+ print(f" Timestamp : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ print(f" Bot Token : {bot_token_masked}")
+ print(f" Chat ID : {chat_id}")
+ _sep("═")
+ print()
+
+
+def _mask_token(token: str) -> str:
+ """Nasconde la parte centrale del token per sicurezza."""
+ parts = token.split(":")
+ if len(parts) == 2:
+ return f"{parts[0]}:{'*' * 10}...{parts[1][-6:]}"
+ return token[:8] + "..." + token[-6:]
+
+
+def _decrypt_b64(enc: str) -> dict:
+ """Decifra il config_enc (base64 semplice come in notification_service.py)."""
+ raw = base64.b64decode(enc.encode())
+ return json.loads(raw.decode())
+
+
+# ─── STEP 1: Lettura canale dal DB ───────────────────────────────────────────
+
+async def load_channel_from_db() -> tuple[str, str, str] | None:
+ """
+ Carica bot_token e chat_id dal primo canale Telegram nel DB.
+ Restituisce (channel_id, bot_token, chat_id) o None se non trovato.
+ """
+ _sep()
+ print("STEP 1 – LETTURA CANALE TELEGRAM DAL DATABASE")
+ _sep()
+
+ try:
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
+ from sqlalchemy import text
+
+ db_url = os.environ["DATABASE_URL"]
+ engine = create_async_engine(db_url)
+ async with AsyncSession(engine) as session:
+ result = await session.execute(
+ text(
+ "SELECT id, name, config, config_enc "
+ "FROM notification_channels "
+ "WHERE channel_type = 'telegram' AND is_active = true "
+ "ORDER BY created_at DESC LIMIT 1"
+ )
+ )
+ row = result.fetchone()
+
+ if not row:
+ print(" ⚠️ Nessun canale Telegram attivo trovato nel database.")
+ print(" Crea un canale Telegram tramite l'interfaccia o via API.")
+ return None
+
+ channel_id = str(row[0])
+ channel_name = row[1]
+ config = row[2] or {}
+ config_enc = row[3]
+
+ chat_id = str(config.get("chat_id", ""))
+ if not chat_id:
+ print(f" ❌ Il canale '{channel_name}' non ha chat_id configurato.")
+ return None
+
+ if not config_enc:
+ print(f" ❌ Il canale '{channel_name}' non ha bot_token configurato (config_enc mancante).")
+ return None
+
+ secret = _decrypt_b64(config_enc)
+ bot_token = secret.get("bot_token", "")
+ if not bot_token:
+ print(f" ❌ Il canale '{channel_name}' non ha bot_token nel config_enc.")
+ return None
+
+ print(f" ✅ Canale trovato: '{channel_name}'")
+ print(f" ID : {channel_id}")
+ print(f" Chat ID : {chat_id}")
+ print(f" Bot Token : {_mask_token(bot_token)}")
+ print()
+ return channel_id, bot_token, chat_id
+
+ except Exception as exc:
+ print(f" ❌ Errore lettura DB: {exc}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+
+# ─── STEP 2: Invio diretto via httpx ─────────────────────────────────────────
+
+async def test_direct_send(bot_token: str, chat_id: str) -> bool:
+ """
+ Testa send_message() direttamente (senza passare da DB/service).
+ """
+ _sep()
+ print("STEP 2 – INVIO DIRETTO VIA BOT API (httpx)")
+ _sep()
+
+ from app.notifications.telegram import TelegramError, send_message
+
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ text = (
+ f"🧪 PecFlow – Test integrazione\n\n"
+ f"Test invio diretto via send_message()\n\n"
+ f"🕐 {ts}"
+ )
+
+ print(f" Chiamata: send_message(chat_id={chat_id}, parse_mode=HTML)")
+ print()
+
+ try:
+ result = await send_message(
+ bot_token=bot_token,
+ chat_id=chat_id,
+ text=text,
+ parse_mode="HTML",
+ )
+ msg_id = result.get("message_id")
+ chat_info = result.get("chat", {})
+ chat_title = chat_info.get("title") or chat_info.get("username") or chat_info.get("id")
+ date = result.get("date")
+
+ print(f" ✅ INVIO OK")
+ print(f" message_id : {msg_id}")
+ print(f" chat : {chat_title}")
+ print(f" date : {date}")
+ print()
+ return True
+
+ except TelegramError as exc:
+ print(f" ❌ TelegramError: {exc}")
+ if exc.http_status:
+ print(f" HTTP status: {exc.http_status}")
+ if exc.api_code:
+ print(f" API code : {exc.api_code}")
+ print()
+ return False
+
+ except Exception as exc:
+ print(f" ❌ Errore imprevisto: {exc}")
+ import traceback
+ traceback.print_exc()
+ print()
+ return False
+
+
+# ─── STEP 3: Test via send_test_message ──────────────────────────────────────
+
+async def test_send_test_message(bot_token: str, chat_id: str) -> bool:
+ """
+ Testa send_test_message() che invia il messaggio formattato standard.
+ """
+ _sep()
+ print("STEP 3 – INVIO MESSAGGIO DI TEST FORMATTATO")
+ _sep()
+
+ from app.notifications.telegram import TelegramError, send_test_message
+
+ print(" Chiamata: send_test_message(channel_name='Test PecFlow')")
+ print()
+
+ try:
+ result = await send_test_message(
+ bot_token=bot_token,
+ chat_id=chat_id,
+ channel_name="Test PecFlow",
+ )
+ msg_id = result.get("message_id")
+ print(f" ✅ INVIO OK – message_id={msg_id}")
+ print()
+ return True
+
+ except TelegramError as exc:
+ print(f" ❌ TelegramError: {exc}")
+ print()
+ return False
+
+
+# ─── STEP 4: Test via NotificationService ────────────────────────────────────
+
+async def test_via_notification_service(channel_id: str) -> bool:
+ """
+ Testa il flusso completo: NotificationService.test_channel()
+ che carica dal DB, decifra il token e chiama la Bot API.
+ """
+ _sep()
+ print("STEP 4 – FLUSSO COMPLETO VIA NotificationService.test_channel()")
+ _sep()
+
+ import uuid
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
+ from sqlalchemy import text
+
+ # Recupera il tenant_id del canale
+ db_url = os.environ["DATABASE_URL"]
+ engine = create_async_engine(db_url)
+
+ try:
+ async with AsyncSession(engine) as session:
+ r = await session.execute(
+ text("SELECT tenant_id FROM notification_channels WHERE id = :id"),
+ {"id": channel_id},
+ )
+ row = r.fetchone()
+ if not row:
+ print(f" ❌ Canale {channel_id} non trovato nel DB.")
+ return False
+ tenant_id = uuid.UUID(str(row[0]))
+
+ print(f" Tenant ID : {tenant_id}")
+ print(f" Channel ID: {channel_id}")
+ print()
+
+ from app.services.notification_service import NotificationService
+
+ async with AsyncSession(engine) as session:
+ service = NotificationService(session)
+ result = await service.test_channel(
+ channel_id=uuid.UUID(channel_id),
+ tenant_id=tenant_id,
+ )
+
+ if result.success:
+ print(f" ✅ test_channel OK")
+ print(f" Messaggio: {result.message}")
+ print(f" HTTP status: {result.http_status}")
+ else:
+ print(f" ❌ test_channel FALLITO")
+ print(f" Motivo: {result.message}")
+ print(f" HTTP status: {result.http_status}")
+
+ print()
+ return result.success
+
+ except Exception as exc:
+ print(f" ❌ Errore: {exc}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+# ─── Main ─────────────────────────────────────────────────────────────────────
+
+async def main() -> None:
+ # Controlla se le credenziali sono passate via env (override manuale)
+ manual_bot_token = os.environ.get("TELEGRAM_BOT_TOKEN")
+ manual_chat_id = os.environ.get("TELEGRAM_CHAT_ID")
+
+ if manual_bot_token and manual_chat_id:
+ channel_id = None
+ bot_token = manual_bot_token
+ chat_id = manual_chat_id
+ print("⚙️ Usando credenziali da variabili d'ambiente TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID")
+ print()
+ else:
+ # Carica dal DB
+ db_result = await load_channel_from_db()
+ if not db_result:
+ print("❌ Impossibile procedere: nessun canale Telegram configurato.")
+ sys.exit(1)
+ channel_id, bot_token, chat_id = db_result
+
+ _banner(_mask_token(bot_token), chat_id)
+
+ results: list[tuple[str, bool]] = []
+
+ # STEP 2: invio diretto
+ ok = await test_direct_send(bot_token, chat_id)
+ results.append(("Invio diretto (send_message)", ok))
+
+ # STEP 3: messaggio formattato
+ ok = await test_send_test_message(bot_token, chat_id)
+ results.append(("Messaggio di test formattato (send_test_message)", ok))
+
+ # STEP 4: flusso completo via NotificationService (solo se abbiamo il channel_id dal DB)
+ if channel_id:
+ ok = await test_via_notification_service(channel_id)
+ results.append(("Flusso completo (NotificationService.test_channel)", ok))
+
+ # ── Riepilogo ────────────────────────────────────────────────────────────
+ _sep("═")
+ print(" RIEPILOGO TEST TELEGRAM")
+ _sep("═")
+ all_ok = True
+ for name, success in results:
+ icon = "✅" if success else "❌"
+ print(f" {icon} {name}")
+ if not success:
+ all_ok = False
+ _sep("═")
+ print()
+
+ if all_ok:
+ print("🎉 Tutti i test Telegram superati con successo!")
+ else:
+ print("⚠️ Alcuni test Telegram sono falliti.")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())