Files
2026-06-20 15:01:44 +05:30

232 lines
8.9 KiB
Python

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))