From f9c93ca3ed6dc3e168e0a7f3dbb7a6384b698c8f Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 31 Jul 2026 15:03:47 -0400 Subject: [PATCH] Jul 31 - Update to ensure customers don't get notification for internal comments --- .claude/settings.json | 3 ++- app/routes/issues.py | 54 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 10f8ca9..ef27add 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,8 @@ "Bash(python docs/convert_manual.py)", "Bash(grep -n -A3 dateOnlyFormatter __TRACKED_VAR__/Sync/SyncManager.swift)", "Bash(python -c \"import ast; ast.parse\\(open\\('app/utils/pdf_export.py'\\).read\\(\\)\\); print\\('OK'\\)\")", - "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/pdf_export.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")" + "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/pdf_export.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")", + "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/issues.py',encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")" ] } } diff --git a/app/routes/issues.py b/app/routes/issues.py index 3c3ed7a..e77eddc 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -30,17 +30,31 @@ logger = logging.getLogger(__name__) # ── Shared helper ───────────────────────────────────────────────────────────── -def _notify_followers(issue, title, body, exclude_user_ids=None): - """Dispatch a notification to every follower of the given issue.""" +def _notify_followers(issue, title, body, exclude_user_ids=None, + customer_body=None, skip_customers=False): + """Dispatch a notification to every follower of the given issue. + + Customer followers are handled separately so a staff-only (internal) comment + never leaks to them: pass ``skip_customers=True`` to omit customer followers + entirely, or ``customer_body`` to send them a customer-safe message in place + of ``body``. Non-customer followers always receive ``body``. + """ exclude = set(exclude_user_ids or []) issue_link = url_for('issues.view', issue_id=issue.id) for follower in issue.followers.all(): if follower.user_id in exclude: continue + is_customer = bool(follower.user) and follower.user.role == 'customer' + if is_customer: + if skip_customers: + continue + f_body = customer_body if customer_body is not None else body + else: + f_body = body notify( recipient = follower.user, title = title, - body = body, + body = f_body, link = issue_link, issue_id = issue.id, event_type = EVENT_ISSUE_FOLLOW, @@ -585,34 +599,58 @@ def view(issue_id): if old_assigned_to: exclude_ids.add(old_assigned_to) + # Whether the comment just added is visible to customers. Internal + # (staff-only) comments must NEVER reach customer accounts — they only + # ever hear about comments explicitly shared with them. Only staff reach + # this branch (customers POST via the earlier customer-only path), so the + # checkbox governs. `changes` drives staff-facing notifications; + # `customer_changes` drives every customer-facing dispatch. + comment_customer_visible = bool(comment_body) and ('is_customer_visible' in request.form) + changes = [] + customer_changes = [] if old_status != issue.status: - changes.append( + _c = ( f'status changed from "{old_status.replace("_"," ").title()}" ' f'to "{issue.status.replace("_"," ").title()}"' ) + changes.append(_c) + customer_changes.append(_c) if old_assigned_to != new_assigned_to: _new_assignee_obj = db.session.get(User, new_assigned_to) if new_assigned_to else None new_name = _new_assignee_obj.username if _new_assignee_obj else 'Unassigned' - changes.append(f'reassigned to {new_name}') + _c = f'reassigned to {new_name}' + changes.append(_c) + customer_changes.append(_c) if comment_body: changes.append(f'new comment added by {current_user.username}') + if comment_customer_visible: + customer_changes.append(f'new comment added by {current_user.username}') if changes: + _loc = issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—' + customer_relevant = bool(customer_changes) _notify_followers( issue = issue, title = f'Issue #{issue.id} Updated', body = ( - f'Issue #{issue.id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} was updated by ' + f'Issue #{issue.id} in {_loc} was updated by ' f'{current_user.username}: {"; ".join(changes)}.' ), + customer_body = ( + f'Issue #{issue.id} in {_loc} was updated by ' + f'{current_user.username}: {"; ".join(customer_changes)}.' + ) if customer_relevant else None, + skip_customers = not customer_relevant, exclude_user_ids = exclude_ids, ) # ── Notify via matrix (issue_updated_customer) ─────────────── + # Gated on `customer_changes`: an update whose ONLY change is an internal + # comment leaves this empty, so no customer notification fires. facility_id = issue.resolved_facility.id if issue.resolved_facility else None - if facility_id and changes: - changes_summary = '; '.join(changes) + if facility_id and customer_changes: + changes_summary = '; '.join(customer_changes) notify_by_matrix( event_type = 'issue_updated_customer', title = f'Issue #{issue.id} Updated at {issue.resolved_facility.name if issue.resolved_facility else '—'}',