feat(api): Add separate sync failure notifications.

- Create dedicated sync failure notification templates for Telegram and Discord
- Add send_sync_failure_notification method to NotificationService
- Update scheduler to use proper notification method instead of expiry notifications
- Telegram: Shows error details with retry count and failure status
- Discord: Color-coded embeds (orange for retries, red for final failures)
- Fixes KeyError: 'bank' when sync failures occur

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-24 15:04:01 +01:00
parent 36d698f7ce
commit e4e3f885ea
4 changed files with 144 additions and 2 deletions

View File

@@ -112,7 +112,7 @@ class BackgroundScheduler:
# Send notification about the failure
try:
await self.notification_service.send_expiry_notification(
await self.notification_service.send_sync_failure_notification(
{
"type": "sync_failure",
"error": str(e),
@@ -145,7 +145,7 @@ class BackgroundScheduler:
logger.error("Maximum retries exceeded for sync job")
# Send final failure notification
try:
await self.notification_service.send_expiry_notification(
await self.notification_service.send_sync_failure_notification(
{
"type": "sync_final_failure",
"error": str(e),

View File

@@ -55,3 +55,44 @@ def send_transactions_message(ctx: click.Context, transactions: list):
response.raise_for_status()
except Exception as e:
raise Exception(f"Discord notification failed: {e}\n{response.text}") from e
def send_sync_failure_notification(ctx: click.Context, notification: dict):
info("Sending sync failure notification to Discord")
webhook = DiscordWebhook(url=ctx.obj["notifications"]["discord"]["webhook"])
# Determine color and title based on failure type
if notification.get("type") == "sync_final_failure":
color = "ff0000" # Red for final failure
title = "🚨 Sync Final Failure"
description = (
f"Sync failed permanently after {notification['retry_count']} attempts"
)
else:
color = "ffaa00" # Orange for retry
title = "⚠️ Sync Failure"
description = f"Sync failed (attempt {notification['retry_count']}/{notification['max_retries']}). Will retry automatically..."
embed = DiscordEmbed(
title=title,
description=description,
color=color,
)
embed.set_author(
name="Leggen",
url="https://github.com/elisiariocouto/leggen",
)
embed.add_embed_field(
name="Error",
value=notification["error"][:1024], # Discord has field value limits
inline=False,
)
embed.set_footer(text="Sync failure notification")
embed.set_timestamp()
webhook.add_embed(embed)
response = webhook.execute()
try:
response.raise_for_status()
except Exception as e:
raise Exception(f"Discord notification failed: {e}\n{response.text}") from e

View File

@@ -79,3 +79,38 @@ def send_transaction_message(ctx: click.Context, transactions: list):
res.raise_for_status()
except Exception as e:
raise Exception(f"Telegram notification failed: {e}\n{res.text}") from e
def send_sync_failure_notification(ctx: click.Context, notification: dict):
token = ctx.obj["notifications"]["telegram"]["token"]
chat_id = ctx.obj["notifications"]["telegram"]["chat_id"]
bot_url = f"https://api.telegram.org/bot{token}/sendMessage"
info("Sending sync failure notification to Telegram")
message = "*🚨 [Leggen](https://github.com/elisiariocouto/leggen)*\n"
message += "*Sync Failed*\n\n"
message += escape_markdown(f"Error: {notification['error']}\n")
if notification.get("type") == "sync_final_failure":
message += escape_markdown(
f"❌ Final failure after {notification['retry_count']} attempts\n"
)
else:
message += escape_markdown(
f"🔄 Attempt {notification['retry_count']}/{notification['max_retries']}\n"
)
message += escape_markdown("Will retry automatically...\n")
res = requests.post(
bot_url,
json={
"chat_id": chat_id,
"text": message,
"parse_mode": "MarkdownV2",
},
)
try:
res.raise_for_status()
except Exception as e:
raise Exception(f"Telegram notification failed: {e}\n{res.text}") from e

View File

@@ -289,3 +289,69 @@ class NotificationService:
except Exception as e:
logger.error(f"Failed to send Telegram expiry notification: {e}")
raise
async def send_sync_failure_notification(
self, notification_data: Dict[str, Any]
) -> None:
"""Send notification about sync failure"""
if self._is_discord_enabled():
await self._send_discord_sync_failure(notification_data)
if self._is_telegram_enabled():
await self._send_telegram_sync_failure(notification_data)
async def _send_discord_sync_failure(
self, notification_data: Dict[str, Any]
) -> None:
"""Send Discord sync failure notification"""
try:
import click
from leggen.notifications.discord import send_sync_failure_notification
# Create a mock context with the webhook
ctx = click.Context(click.Command("sync_failure"))
ctx.obj = {
"notifications": {
"discord": {
"webhook": self.notifications_config.get("discord", {}).get(
"webhook"
)
}
}
}
# Send sync failure notification using the actual implementation
send_sync_failure_notification(ctx, notification_data)
logger.info(f"Sent Discord sync failure notification: {notification_data}")
except Exception as e:
logger.error(f"Failed to send Discord sync failure notification: {e}")
raise
async def _send_telegram_sync_failure(
self, notification_data: Dict[str, Any]
) -> None:
"""Send Telegram sync failure notification"""
try:
import click
from leggen.notifications.telegram import send_sync_failure_notification
# Create a mock context with the telegram config
ctx = click.Context(click.Command("sync_failure"))
telegram_config = self.notifications_config.get("telegram", {})
ctx.obj = {
"notifications": {
"telegram": {
"token": telegram_config.get("token"),
"chat_id": telegram_config.get("chat_id"),
}
}
}
# Send sync failure notification using the actual implementation
send_sync_failure_notification(ctx, notification_data)
logger.info(f"Sent Telegram sync failure notification: {notification_data}")
except Exception as e:
logger.error(f"Failed to send Telegram sync failure notification: {e}")
raise