06/12 Add features: 1. Vendor/Contractor assignment 2. Period-over-period comparison 3. Trend Alerts (cron)
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user