import os import logging import requests from repositories.monitoring_repository import monitoring_repository from repositories.alert_repository import alert_repository from repositories.tenant_repository import tenant_repository logger = logging.getLogger(__name__) GOTIFY_URL = os.getenv('GOTIFY_URL') GOTIFY_APP_TOKEN = os.getenv('GOTIFY_APP_TOKEN') def send_gotify(title, message, priority=5): """ Sends a push notification via Gotify to the owner's phone. Logs the alert attempt to monitoring_events regardless of delivery success. Never shown to clients — owner monitoring only, per CLAUDE.md §3/§27. Data flow: title + message + priority → HTTP POST {GOTIFY_URL}/message → monitoring_events record created (alert_sent reflects HTTP outcome) """ delivered = False try: response = requests.post( f'{GOTIFY_URL}/message', headers={'X-Gotify-Key': GOTIFY_APP_TOKEN}, json={'title': title, 'message': message, 'priority': priority}, timeout=5 ) delivered = response.status_code < 300 except Exception as e: # Side-effect failure — must never propagate into the caller's # primary operation (a job completing, a threshold check, etc.). logger.error(f'Gotify alert failed: {str(e)}') monitoring_repository.log_event( event_type='api_health' if priority >= 10 else 'failure_rate', severity='critical' if priority >= 10 else ('warning' if priority >= 7 else 'advisory'), message=f'{title}: {message}', alert_sent=delivered, ) def create_alert(tenant_id, competitor_id, alert_type, message, product_id=None, change_amount=None, change_percent=None): """ Creates a tenant-facing alert record — always visible on the dashboard activity feed immediately (delivered_dashboard is always true). Email/Slack/Teams/WhatsApp delivery flags are set later by the digest workflows that actually deliver through those channels. Data flow: alert fields → alerts table record created with delivered_dashboard=true, all other delivered_* flags false → created alert record returned """ return alert_repository.create({ 'tenant_id': tenant_id, 'competitor_id': competitor_id, 'product_id': product_id, 'alert_type': alert_type, 'message': message, 'change_amount': change_amount, 'change_percent': change_percent, 'delivered_dashboard': True, 'delivered_email': False, 'delivered_slack': False, 'delivered_teams': False, 'delivered_whatsapp': False, 'read': False, }) def send_daily_digest(tenant_id): """ Sends the 6pm daily digest for one tenant — every alert not yet included in a digest, in a single email, per CLAUDE.md §15. Called once per active tenant by the n8n daily-digest workflow (system- level iteration, not a PM-facing route — see api.py's /internal/daily-digest/). Data flow: tenant_id → tenant record (email/name) → alert_repository.list_undelivered_email() → no alerts: skip entirely, no empty email sent → alerts found: email_service.send_daily_digest_email() → success: every included alert's delivered_email flipped true """ tenant = tenant_repository.get_by_id(tenant_id) if not tenant or not tenant.get('email'): logger.error(f'No tenant/email on file for {tenant_id} — skipping daily digest') return False alerts = alert_repository.list_undelivered_email(tenant_id) if not alerts: return False # Imported lazily — same reasoning as brief_service/email_service's # own lazy `import resend`: keeps this module importable without # every dependency configured in test environments. from services import email_service sent = email_service.send_daily_digest_email(tenant['email'], tenant.get('name'), alerts) if sent: for alert in alerts: alert_repository.mark_email_delivered(alert['id']) return sent