232 lines
8.9 KiB
Python
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))
|