Jul 7 - Implement additional recipient per contract

This commit is contained in:
2026-07-07 20:28:17 -04:00
parent 8f971a403b
commit a3c6fd73bd
9 changed files with 613 additions and 8 deletions
+128
View File
@@ -603,12 +603,140 @@ def notify_by_matrix(
for email in custom_emails:
_send_custom_email(email, title, body, link)
# ── Per-contract additional recipients (phase37) ──────────────────────
_notify_project_recipients(
event_type = event_type,
title = title,
body = body,
link = link,
issue_id = issue_id,
inspection_id = inspection_id,
facility_id = facility_id,
exclude_user_ids = exclude,
already_notified = notified,
already_emailed = {e.strip().lower() for e in custom_emails},
)
logger.info(
'MATRIX NOTIFY | event=%s | notified=%s | custom_emails=%s',
event_type, len(notified), len(custom_emails),
)
def _notify_project_recipients(
event_type: str,
title: str,
body: str,
link: str = None,
issue_id: int = None,
inspection_id: int = None,
facility_id: int = None,
exclude_user_ids: set = None,
already_notified: set = None,
already_emailed: set = None,
):
"""
Dispatch to per-contract additional recipients (ProjectNotificationRecipient).
Called from notify_by_matrix() AFTER the global matrix roles and custom
emails. Resolves the contract via the event's facility:
facility_id arg → else issue.resolved_facility → else inspection.facility_id
then notifies every recipient of that facility's contract whose subscribed
event list contains event_type.
Staff recipients (user_id set) get an in-app Notification + email via
notify() with respect_preferences=False (matrix-authority mode, same as
role broadcasts). External recipients (email set) get a plain email only.
Deduplication: `already_notified` (user IDs notified by the matrix roles)
and `already_emailed` (lowercased global custom emails) are honoured and
mutated in place so a recipient is never contacted twice per event.
Best-effort: any failure is logged and never propagates to the caller.
"""
exclude = set(exclude_user_ids or [])
notified = already_notified if already_notified is not None else set()
emailed = already_emailed if already_emailed is not None else set()
try:
from app.models.project_recipient import ProjectNotificationRecipient
from app.models.facility import Facility
# Resolve the facility this event occurred at
fid = facility_id
if fid is None and issue_id:
from app.models.issue import Issue
issue = db.session.get(Issue, issue_id)
if issue:
fac = issue.resolved_facility
fid = fac.id if fac else None
if fid is None and inspection_id:
from app.models.inspection import Inspection
insp = db.session.get(Inspection, inspection_id)
fid = insp.facility_id if insp else None
if fid is None:
return # no facility context — contract cannot be determined
facility = db.session.get(Facility, fid)
if not facility or not facility.project_id:
return # facility unknown or not linked to a contract
recipients = ProjectNotificationRecipient.query.filter_by(
project_id=facility.project_id
).all()
sent = 0
for r in recipients:
if event_type not in r.get_events():
continue
if r.user_id:
if r.user_id in exclude or r.user_id in notified:
continue
user = r.user
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 the authority
)
notified.add(user.id)
sent += 1
logger.info(
'CONTRACT NOTIFY | project_id=%s | event=%s | user=%s',
facility.project_id, event_type, user.username,
)
elif r.email:
key = r.email.strip().lower()
if not key or key in emailed:
continue
_send_custom_email(r.email.strip(), title, body, link)
emailed.add(key)
sent += 1
logger.info(
'CONTRACT NOTIFY | project_id=%s | event=%s | email=%s',
facility.project_id, event_type, key,
)
if sent:
logger.info(
'CONTRACT NOTIFY DONE | project_id=%s | event=%s | sent=%s',
facility.project_id, event_type, sent,
)
except Exception as exc:
logger.error(
'CONTRACT NOTIFY FAILED | event=%s | facility_id=%s | error=%s',
event_type, facility_id, exc,
)
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: