July 7 - Implement notification per contract

This commit is contained in:
2026-07-07 10:03:55 -04:00
parent 0d4a10d01d
commit e578fb6ca1
7 changed files with 634 additions and 6 deletions
+136 -3
View File
@@ -598,17 +598,150 @@ def notify_by_matrix(
)
notified.add(user.id)
# ── Custom email recipients ───────────────────────────────────────────
# ── Custom email recipients (global, per-event) ───────────────────────
custom_emails = get_custom_emails_for(event_type)
sent_emails = set() # dedupe email sends across global + per-contract
for email in custom_emails:
norm = (email or '').strip().lower()
if not norm or norm in sent_emails:
continue
_send_custom_email(email, title, body, link)
sent_emails.add(norm)
# ── Per-contract additional recipients ────────────────────────────────
# Extra users/emails attached to the contract that owns this event's
# facility. Fires in ADDITION to the global matrix routing above.
contract_count = _notify_contract_recipients(
event_type = event_type,
title = title,
body = body,
link = link,
issue_id = issue_id,
inspection_id = inspection_id,
facility_id = facility_id,
exclude = exclude,
notified = notified,
sent_emails = sent_emails,
)
logger.info(
'MATRIX NOTIFY | event=%s | notified=%s | custom_emails=%s',
event_type, len(notified), len(custom_emails),
'MATRIX NOTIFY | event=%s | notified=%s | custom_emails=%s | contract_extra=%s',
event_type, len(notified), len(sent_emails), contract_count,
)
def _resolve_project_id(facility_id=None, issue_id=None, inspection_id=None):
"""Best-effort resolution of the owning contract (project) id for an event.
Tries facility_id first, then the issue's facility, then the inspection's
facility. Returns None if no contract can be determined.
"""
from app.models.facility import Facility
fid = facility_id
try:
if fid is None and issue_id:
from app.models.issue import Issue
iss = db.session.get(Issue, issue_id)
if iss is not None:
fid = iss.facility_id
if fid is None and iss.area is not None:
fid = iss.area.facility_id
if fid is None and inspection_id:
from app.models.inspection import Inspection
insp = db.session.get(Inspection, inspection_id)
if insp is not None:
fid = insp.facility_id
if fid is None:
return None
facility = db.session.get(Facility, fid)
return facility.project_id if facility is not None else None
except Exception as exc:
logger.error('CONTRACT NOTIFY | project resolution failed | error=%s', exc)
return None
def _notify_contract_recipients(
event_type, title, body, link=None,
issue_id=None, inspection_id=None, facility_id=None,
exclude=None, notified=None, sent_emails=None,
):
"""Notify a contract's additional recipients for this event.
Deduplicates against the users already notified by the matrix (`notified`),
the excluded actor set (`exclude`), and emails already sent (`sent_emails`).
Best-effort: never raises into the caller.
Returns the number of extra recipients notified (users + emails).
"""
exclude = exclude if exclude is not None else set()
notified = notified if notified is not None else set()
sent_emails = sent_emails if sent_emails is not None else set()
try:
from app.models.notification_recipient import ContractNotificationRecipient
from app.models.user import User
project_id = _resolve_project_id(facility_id, issue_id, inspection_id)
if project_id is None:
return 0
rows = ContractNotificationRecipient.query.filter_by(project_id=project_id).all()
count = 0
for r in rows:
# Event subscription check
if event_type not in r.get_event_types():
continue
# ── Existing-user recipient → in-app + email ──
if r.user_id:
if r.user_id in exclude or r.user_id in notified:
continue
user = db.session.get(User, r.user_id)
if not user or not user.active:
continue
notify(
recipient = user,
title = title,
body = body,
link = link,
issue_id = issue_id,
inspection_id = inspection_id,
event_type = event_type,
send_email = True,
respect_preferences = False, # contract config is authoritative
)
notified.add(user.id)
count += 1
logger.info(
'CONTRACT NOTIFY | project=%s | event=%s | user=%s',
project_id, event_type, user.username,
)
# ── Free-form email recipient → email only ──
elif r.email:
norm = r.email.strip().lower()
if not norm or norm in sent_emails:
continue
_send_custom_email(r.email, title, body, link)
sent_emails.add(norm)
count += 1
logger.info(
'CONTRACT NOTIFY | project=%s | event=%s | email=%s',
project_id, event_type, r.email,
)
return count
except Exception as exc:
logger.error(
'CONTRACT NOTIFY | unexpected error | event=%s | error=%s',
event_type, exc,
)
return 0
def _send_custom_email(to_email: str, title: str, body: str, link: str = None):
"""Send a plain email to a custom (non-user) address. Best-effort."""
try: