""" app/models/scheduled_inspection.py ----------------------------------- Planned/recurring inspection assignments (phase36). A ScheduledInspection is a PLAN, not an inspection: it names a facility, a template, the responsible inspector, and a due date. The inspector opens it ("Start"), which creates a normal in_progress Inspection linked back via Inspection.scheduled_inspection_id; when that inspection is completed the schedule is marked fulfilled — deactivated (one-time) or rolled forward to the next occurrence (recurring). Reminders are dispatched by the cron endpoint POST /scheduled-inspections/run?token=DIGEST_SECRET: - advance reminder to the inspector 1 day before the due date - due reminder to the inspector on the due date - overdue alert to admin/director once the due date passes uncompleted The *_notified flags make each of those fire at most once per occurrence and reset when a recurring schedule rolls forward. """ from datetime import timedelta from app import db from app.utils.time_utils import now_eastern FREQUENCY_CHOICES = ('once', 'daily', 'weekly', 'monthly') class ScheduledInspection(db.Model): __tablename__ = 'scheduled_inspections' id = db.Column(db.Integer, primary_key=True) facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id', ondelete='CASCADE'), nullable=False, index=True) template_id = db.Column(db.Integer, db.ForeignKey('inspection_templates.id', ondelete='CASCADE'), nullable=False) inspector_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True) frequency = db.Column(db.Enum(*FREQUENCY_CHOICES), nullable=False, default='once') next_due_date = db.Column(db.Date, nullable=False, index=True) active = db.Column(db.Boolean, nullable=False, default=True) notes = db.Column(db.Text, nullable=True) created_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_at = db.Column(db.DateTime, nullable=False, default=now_eastern) last_completed_at = db.Column(db.DateTime, nullable=True) # Per-occurrence reminder de-dup flags (reset when a recurring one rolls forward) advance_notified = db.Column(db.Boolean, nullable=False, default=False) due_notified = db.Column(db.Boolean, nullable=False, default=False) overdue_notified = db.Column(db.Boolean, nullable=False, default=False) # Relationships facility = db.relationship('Facility', foreign_keys=[facility_id]) template = db.relationship('InspectionTemplate', foreign_keys=[template_id]) inspector = db.relationship('User', foreign_keys=[inspector_id]) creator = db.relationship('User', foreign_keys=[created_by]) FREQUENCY_LABELS = { 'once': 'One-time', 'daily': 'Daily', 'weekly': 'Weekly', 'monthly': 'Monthly', } @property def frequency_label(self): return self.FREQUENCY_LABELS.get(self.frequency, self.frequency) @staticmethod def _add_interval(d, frequency): """Return d advanced by one interval of the given frequency.""" if frequency == 'daily': return d + timedelta(days=1) if frequency == 'weekly': return d + timedelta(weeks=1) if frequency == 'monthly': # Add ~1 month by stepping 28–31 days to the same day-of-month where possible. month = d.month + 1 year = d.year + (1 if month > 12 else 0) month = 1 if month > 12 else month day = min(d.day, 28) # clamp to avoid invalid dates (e.g. Feb 30) return d.replace(year=year, month=month, day=day) return d # 'once' has no next interval def is_overdue(self, today=None): today = today or now_eastern().date() return self.active and self.next_due_date < today def fulfill(self): """Mark this occurrence complete. One-time schedules deactivate; recurring ones roll their due date forward past today and reset the reminder flags. Caller commits.""" self.last_completed_at = now_eastern() if self.frequency == 'once': self.active = False return # Recurring: advance until the next due date is in the future. today = now_eastern().date() nxt = self._add_interval(self.next_due_date, self.frequency) guard = 0 while nxt <= today and guard < 400: nxt = self._add_interval(nxt, self.frequency) guard += 1 self.next_due_date = nxt self.advance_notified = False self.due_notified = False self.overdue_notified = False def __repr__(self): return (f'')