class MaintenanceScheduleStatus UPCOMING_PERCENT = 0.05 attr_reader :schedule, :latest_entry, :status, :due_odometer, :due_date def initialize(schedule, latest_entry, current_odometer:) @schedule = schedule @latest_entry = latest_entry @current_odometer = current_odometer @due_odometer = baseline_odometer + schedule.mileage_interval @due_date = baseline_date + schedule.time_interval.days @status = calculate_status end def overdue? status == "overdue" end def upcoming? status == "upcoming" end def no_history? status == "no_history" end def current? status == "current" end def status_label case status when "overdue" then "Overdue" when "upcoming" then "Upcoming" when "no_history" then "No history" else "Current" end end def reason case status when "overdue" overdue_reasons.join(" and ") when "upcoming" upcoming_reasons.join(" and ") when "no_history" "No matching maintenance entry has been recorded." else "Maintenance is current." end end private attr_reader :current_odometer def calculate_status return "overdue" if mileage_overdue? || time_overdue? return "upcoming" if mileage_upcoming? || time_upcoming? return "no_history" if latest_entry.blank? "current" end def baseline_odometer latest_entry&.odometer || schedule.baseline_odometer end def baseline_date latest_entry&.date || schedule.baseline_date end def mileage_overdue? current_odometer >= due_odometer end def time_overdue? Date.current >= due_date end def mileage_upcoming? current_odometer >= due_odometer - mileage_upcoming_window end def time_upcoming? Date.current >= due_date - time_upcoming_window.days end def mileage_upcoming_window (schedule.mileage_interval * UPCOMING_PERCENT).ceil end def time_upcoming_window (schedule.time_interval * UPCOMING_PERCENT).ceil end def overdue_reasons reasons = [] reasons << "mileage interval reached" if mileage_overdue? reasons << "time interval reached" if time_overdue? reasons end def upcoming_reasons reasons = [] reasons << "within #{mileage_upcoming_window} miles of due odometer" if mileage_upcoming? reasons << "within #{time_upcoming_window} days of due date" if time_upcoming? reasons end end