from __future__ import annotations import logging from decimal import Decimal from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.modules.core.iam.models import User from app.modules.email_integration.attachment_service import invoice_attachment, receipt_attachment from app.modules.email_integration.services import _firm_name, get_email_setting, is_template_allowed_by_preferences, send_template_email logger = logging.getLogger("audit_firm.email_events") _TASK_ALERT_TYPES = {"task_assigned", "task_due", "task_overdue", "task_review"} _BILLING_TEMPLATE_CODES = {"INVOICE_GENERATED", "PAYMENT_REMINDER", "PAYMENT_RECEIVED_RECEIPT", "ONLINE_PAYMENT_SUCCESS", "ONLINE_PAYMENT_FAILED"} def _money(value: Any) -> str: try: amount = Decimal(str(value or "0")) return f"₹ {amount:,.2f}" except Exception: return str(value or "") def _user_display_name(user: User | None) -> str: if not user: return "User" return (getattr(user, "full_name", None) or getattr(user, "email", None) or "User").strip() def _load_user(db: Session, user_id: int | None) -> User | None: if not user_id: return None try: return db.execute(select(User).where(User.id == int(user_id))).scalar_one_or_none() except Exception: return None def _can_send_alert_email(db: Session, tenant_id: int | None, branch_id: int | None, alert_type: str | None) -> bool: setting = get_email_setting(db, tenant_id, branch_id) if not setting or not setting.is_active: return False if not getattr(setting, "send_alert_emails", False): return False template_code = _alert_template_code(alert_type) if not template_code: return False allowed, _reason = is_template_allowed_by_preferences(setting, template_code) return bool(allowed) def _can_send_billing_email(db: Session, tenant_id: int | None, branch_id: int | None, template_code: str | None = None) -> bool: setting = get_email_setting(db, tenant_id, branch_id) if not setting or not setting.is_active or not getattr(setting, "send_billing_emails", False): return False if template_code: allowed, _reason = is_template_allowed_by_preferences(setting, template_code) return bool(allowed) return True def _client_email(client: Any) -> str | None: if not client: return None for field in ("email", "alternate_email"): value = (getattr(client, field, None) or "").strip() if value: return value return None def _client_name(client: Any) -> str: return (getattr(client, "client_name", None) or getattr(client, "trade_name", None) or "Client").strip() def _invoice_pay_link(invoice: Any) -> str: invoice_id = getattr(invoice, "id", None) return f"/client/billing/{invoice_id}/pay-now" if invoice_id else "/client/billing" def _alert_template_code(alert_type: str | None) -> str | None: value = (alert_type or "general").strip().lower() if value == "task_assigned": return "TASK_ASSIGNED" if value == "task_due": return "TASK_DUE_TODAY" if value == "task_overdue": return "TASK_OVERDUE" if value == "task_review": return "PARTNER_REVIEW_REQUIRED" if value == "document_uploaded": return "CLIENT_DOCUMENT_RECEIVED" if value == "clarification": return "CLIENT_CLARIFICATION_REQUEST" if value == "attendance": return "ATTENDANCE_PUNCH_MISSING" if value == "leave": return "LEAVE_REQUEST_SUBMITTED" if value == "consultant": return "CONSULTANT_ASSIGNMENT" return None def send_alert_created_email(db: Session, alert: Any) -> None: """Best-effort email notification for any newly created in-app alert. This is intentionally non-blocking from business-flow perspective. SMTP failure is captured in email_logs by send_template_email and should not prevent alert creation, task updates, billing, attendance, etc. """ tenant_id = getattr(alert, "tenant_id", None) branch_id = getattr(alert, "branch_id", None) alert_type = getattr(alert, "alert_type", None) if not _can_send_alert_email(db, tenant_id, branch_id, alert_type): return template_code = _alert_template_code(alert_type) if not template_code: return user = _load_user(db, getattr(alert, "user_id", None)) recipient = (getattr(user, "email", None) or "").strip() if user else "" if not recipient: return title = getattr(alert, "title", None) or "Alert" message = getattr(alert, "message", None) or "" target_url = getattr(alert, "target_url", None) or "/alerts" context = { "recipient_name": _user_display_name(user), "user_name": _user_display_name(user), "partner_name": _user_display_name(user), "consultant_name": _user_display_name(user), "task_title": title, "work_title": title, "assignment_title": title, "service_name": "", "client_name": "", "engagement_code": "", "due_date": "", "clarification_text": message, "review_note": message, "action_url": target_url, "login_url": "/login", } try: send_template_email( db, tenant_id=tenant_id, branch_id=branch_id, recipient_email=recipient, template_code=template_code, context=context, related_module="alert", related_id=getattr(alert, "id", None), ) except Exception: logger.exception("Email alert notification failed for alert_id=%s", getattr(alert, "id", None)) def send_invoice_issued_email(db: Session, invoice: Any) -> None: tenant_id = getattr(invoice, "tenant_id", None) branch_id = getattr(invoice, "branch_id", None) if not _can_send_billing_email(db, tenant_id, branch_id, "INVOICE_GENERATED"): return client = getattr(invoice, "client", None) recipient = _client_email(client) if not recipient: return try: send_template_email( db, tenant_id=tenant_id, branch_id=branch_id, recipient_email=recipient, template_code="INVOICE_GENERATED", context={ "client_name": _client_name(client), "invoice_number": getattr(invoice, "invoice_no", None) or getattr(invoice, "invoice_number", None) or str(getattr(invoice, "id", "")), "invoice_amount": _money(getattr(invoice, "total_amount", None)), "outstanding_amount": _money(getattr(invoice, "balance_amount", None)), "due_date": getattr(getattr(invoice, "due_date", None), "isoformat", lambda: str(getattr(invoice, "due_date", "")))(), "payment_link": _invoice_pay_link(invoice), "action_url": _invoice_pay_link(invoice), }, related_module="billing_invoice", related_id=getattr(invoice, "id", None), attachments=[invoice_attachment(invoice, firm_name=_firm_name(db, tenant_id))], ) except Exception: logger.exception("Invoice email failed for invoice_id=%s", getattr(invoice, "id", None)) def send_payment_receipt_email(db: Session, payment: Any) -> None: invoice = getattr(payment, "invoice", None) tenant_id = getattr(payment, "tenant_id", None) or getattr(invoice, "tenant_id", None) branch_id = getattr(payment, "branch_id", None) or getattr(invoice, "branch_id", None) if not _can_send_billing_email(db, tenant_id, branch_id, "PAYMENT_RECEIVED_RECEIPT"): return client = getattr(payment, "client", None) or getattr(invoice, "client", None) recipient = _client_email(client) if not recipient: return try: send_template_email( db, tenant_id=tenant_id, branch_id=branch_id, recipient_email=recipient, template_code="PAYMENT_RECEIVED_RECEIPT", context={ "client_name": _client_name(client), "invoice_number": getattr(invoice, "invoice_no", None) or str(getattr(invoice, "id", "")), "receipt_number": getattr(payment, "receipt_no", None) or str(getattr(payment, "id", "")), "payment_amount": _money(getattr(payment, "amount_received", None)), "payment_date": getattr(getattr(payment, "payment_date", None), "isoformat", lambda: str(getattr(payment, "payment_date", "")))(), "payment_mode": getattr(payment, "mode", None) or "", "payment_link": _invoice_pay_link(invoice), "action_url": _invoice_pay_link(invoice), }, related_module="billing_payment", related_id=getattr(payment, "id", None), attachments=[receipt_attachment(payment, firm_name=_firm_name(db, tenant_id))], ) except Exception: logger.exception("Payment receipt email failed for payment_id=%s", getattr(payment, "id", None))