06/12 Add features: 1. Vendor/Contractor assignment 2. Period-over-period comparison 3. Trend Alerts (cron)

This commit is contained in:
2026-06-12 16:27:09 -04:00
parent cf22fe87fe
commit 0fe5954794
12 changed files with 447 additions and 2 deletions
+128
View File
@@ -214,4 +214,132 @@ def send_sla_alerts():
if total_sent:
db.session.commit()
return total_sent
# ── Score trend alert dispatcher ──────────────────────────────────────────────
# Default drop threshold in percentage points that triggers an alert.
SCORE_DROP_THRESHOLD = 5.0
def send_score_alerts(threshold=SCORE_DROP_THRESHOLD):
"""
Compare each active facility's avg inspection score for the last 30 days
against the prior 30-day period. When the score has dropped by more than
*threshold* points, dispatch an in-app + email alert via notify_by_matrix
and record the alert in facility_score_alerts for deduplication.
A facility is skipped if it already received an alert within the last 24
hours (prevents repeat storms on persistent low scores).
Returns the number of alert notifications dispatched.
"""
from datetime import timedelta
from flask import current_app, url_for
from sqlalchemy import func
from app import db
from app.models.facility import Facility
from app.models.inspection import Inspection
from app.models.score_alert import FacilityScoreAlert
from app.utils.notifications import notify_by_matrix
import logging
logger = logging.getLogger(__name__)
now = now_eastern()
cur_start = now - timedelta(days=30)
pri_start = now - timedelta(days=60)
pri_end = cur_start
# Current-period avg score per facility
cur_rows = db.session.query(
Facility.id,
Facility.name,
func.avg(Inspection.overall_score).label('avg'),
).join(Inspection, Facility.id == Inspection.facility_id)\
.filter(
Facility.active == True,
Inspection.inspection_date >= cur_start,
Inspection.inspection_date <= now,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None),
).group_by(Facility.id, Facility.name).all()
# Prior-period avg score per facility
pri_rows = db.session.query(
Facility.id,
func.avg(Inspection.overall_score).label('avg'),
).join(Inspection, Facility.id == Inspection.facility_id)\
.filter(
Facility.active == True,
Inspection.inspection_date >= pri_start,
Inspection.inspection_date <= pri_end,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None),
).group_by(Facility.id).all()
prior_map = {r.id: float(r.avg) for r in pri_rows}
# Facilities that already received an alert in the last 24 hours
cutoff = now - timedelta(hours=24)
recent_alerts = db.session.query(FacilityScoreAlert.facility_id)\
.filter(FacilityScoreAlert.sent_at >= cutoff).all()
already_alerted = {r.facility_id for r in recent_alerts}
total_sent = 0
for row in cur_rows:
fid = row.id
cur_avg = float(row.avg)
pri_avg = prior_map.get(fid)
if pri_avg is None:
continue # no prior period data — nothing to compare
delta = cur_avg - pri_avg # negative = score dropped
if delta >= -threshold:
continue # drop is within acceptable range
if fid in already_alerted:
logger.debug('SCORE ALERT SKIPPED (already alerted) | facility_id=%s', fid)
continue
title = f'📉 Score Drop Alert — {row.name}'
body = (
f'{row.name} avg score has dropped {abs(delta):.1f} points '
f'(from {pri_avg:.1f}% to {cur_avg:.1f}%) over the last 30 days '
f'vs. the prior 30-day period.'
)
try:
link = url_for('reports.facility_scorecard', facility_id=fid)
except RuntimeError:
link = f'/reports/facility/{fid}/scorecard'
notify_by_matrix(
event_type = 'score_alert',
title = title,
body = body,
link = link,
)
total_sent += 1
db.session.add(FacilityScoreAlert(
facility_id = fid,
sent_at = now,
current_avg = round(cur_avg, 2),
prior_avg = round(pri_avg, 2),
delta = round(delta, 2),
))
logger.info(
'SCORE ALERT SENT | facility_id=%s | facility=%s | cur=%.1f | prior=%.1f | delta=%.1f',
fid, row.name, cur_avg, pri_avg, delta,
)
if total_sent:
db.session.commit()
return total_sent