""" Notification Service Handles email notifications for presence reminders and parking assignments TODO: This service is NOT YET ACTIVE. To enable notifications: 1. Add APScheduler or similar to run run_scheduled_notifications() periodically 2. Configure SMTP environment variables (SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM) 3. Notifications will be sent for: - Presence reminders (Thursday at 12:00) - Weekly parking summary (Friday at 12:00) - Daily parking reminders (at user's preferred time) - Immediate parking change notifications (via queue) """ import smtplib import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime, timedelta from sqlalchemy.orm import Session import uuid from database.models import ( User, UserPresence, DailyParkingAssignment, NotificationLog, NotificationQueue ) from services.parking import get_spot_display_name # Email configuration (from environment variables) SMTP_HOST = os.getenv("SMTP_HOST", "localhost") SMTP_PORT = int(os.getenv("SMTP_PORT", "587")) SMTP_USER = os.getenv("SMTP_USER", "") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "") SMTP_FROM = os.getenv("SMTP_FROM", "noreply@parkingmanager.local") SMTP_USE_TLS = os.getenv("SMTP_USE_TLS", "true").lower() == "true" def send_email(to_email: str, subject: str, body_html: str, body_text: str = None): """Send an email""" if not SMTP_USER or not SMTP_PASSWORD: print(f"[NOTIFICATION] Email not configured. Would send to {to_email}: {subject}") return False try: msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = SMTP_FROM msg["To"] = to_email if body_text: msg.attach(MIMEText(body_text, "plain")) msg.attach(MIMEText(body_html, "html")) with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: if SMTP_USE_TLS: server.starttls() server.login(SMTP_USER, SMTP_PASSWORD) server.sendmail(SMTP_FROM, to_email, msg.as_string()) print(f"[NOTIFICATION] Email sent to {to_email}: {subject}") return True except Exception as e: print(f"[NOTIFICATION] Failed to send email to {to_email}: {e}") return False def get_week_dates(reference_date: datetime): """Get Monday-Sunday dates for the week containing reference_date""" # Find Monday of this week monday = reference_date - timedelta(days=reference_date.weekday()) return [monday + timedelta(days=i) for i in range(7)] def get_next_week_dates(reference_date: datetime): """Get Monday-Sunday dates for the week after reference_date""" # Find Monday of next week days_until_next_monday = 7 - reference_date.weekday() next_monday = reference_date + timedelta(days=days_until_next_monday) return [next_monday + timedelta(days=i) for i in range(7)] def check_week_presence_compiled(user_id: str, week_dates: list, db: Session) -> bool: """Check if user has filled presence for all working days in a week""" date_strs = [d.strftime("%Y-%m-%d") for d in week_dates] presences = db.query(UserPresence).filter( UserPresence.user_id == user_id, UserPresence.date.in_(date_strs) ).all() # Consider week compiled if at least 5 days have presence marked # (allowing for weekends or holidays) return len(presences) >= 5 def get_week_reference(date: datetime) -> str: """Get ISO week reference string (e.g., 2024-W48)""" return date.strftime("%Y-W%W") def send_presence_reminder(user: User, next_week_dates: list, db: Session) -> bool: """Send presence compilation reminder for next week""" week_ref = get_week_reference(next_week_dates[0]) # Check if already sent today for this week today = datetime.now().strftime("%Y-%m-%d") existing = db.query(NotificationLog).filter( NotificationLog.user_id == user.id, NotificationLog.notification_type == "presence_reminder", NotificationLog.reference_date == week_ref, NotificationLog.sent_at >= today ).first() if existing: return False # Already sent today # Check if week is compiled if check_week_presence_compiled(user.id, next_week_dates, db): return False # Already compiled # Send reminder start_date = next_week_dates[0].strftime("%B %d") end_date = next_week_dates[-1].strftime("%B %d, %Y") subject = f"Reminder: Please fill your presence for {start_date} - {end_date}" body_html = f"""

Presence Reminder

Hi {user.name},

This is a friendly reminder to fill your presence for the upcoming week ({start_date} - {end_date}).

Please log in to the Parking Manager to mark your presence.

Best regards,
Parking Manager

""" if send_email(user.email, subject, body_html): # Log the notification log = NotificationLog( id=str(uuid.uuid4()), user_id=user.id, notification_type="presence_reminder", reference_date=week_ref, sent_at=datetime.now().isoformat() ) db.add(log) db.commit() return True return False def send_weekly_parking_summary(user: User, next_week_dates: list, db: Session) -> bool: """Send weekly parking assignment summary for next week (Friday at 12)""" if not user.notify_weekly_parking: return False week_ref = get_week_reference(next_week_dates[0]) # Check if already sent for this week existing = db.query(NotificationLog).filter( NotificationLog.user_id == user.id, NotificationLog.notification_type == "weekly_parking", NotificationLog.reference_date == week_ref ).first() if existing: return False # Get parking assignments for next week date_strs = [d.strftime("%Y-%m-%d") for d in next_week_dates] assignments = db.query(DailyParkingAssignment).filter( DailyParkingAssignment.user_id == user.id, DailyParkingAssignment.date.in_(date_strs) ).all() if not assignments: return False # No assignments, no need to notify # Build assignment list assignment_lines = [] for a in sorted(assignments, key=lambda x: x.date): date_obj = datetime.strptime(a.date, "%Y-%m-%d") day_name = date_obj.strftime("%A") spot_name = get_spot_display_name(a.spot_id, a.manager_id, db) assignment_lines.append(f"
  • {day_name}, {date_obj.strftime('%B %d')}: Spot {spot_name}
  • ") start_date = next_week_dates[0].strftime("%B %d") end_date = next_week_dates[-1].strftime("%B %d, %Y") subject = f"Your parking spots for {start_date} - {end_date}" body_html = f"""

    Weekly Parking Summary

    Hi {user.name},

    Here are your parking spot assignments for the upcoming week:

    Parking assignments are now frozen. You can still release or reassign your spots if needed.

    Best regards,
    Parking Manager

    """ if send_email(user.email, subject, body_html): log = NotificationLog( id=str(uuid.uuid4()), user_id=user.id, notification_type="weekly_parking", reference_date=week_ref, sent_at=datetime.now().isoformat() ) db.add(log) db.commit() return True return False def send_daily_parking_reminder(user: User, date: datetime, db: Session) -> bool: """Send daily parking reminder for a specific date""" if not user.notify_daily_parking: return False date_str = date.strftime("%Y-%m-%d") # Check if already sent for this date existing = db.query(NotificationLog).filter( NotificationLog.user_id == user.id, NotificationLog.notification_type == "daily_parking", NotificationLog.reference_date == date_str ).first() if existing: return False # Get parking assignment for this date assignment = db.query(DailyParkingAssignment).filter( DailyParkingAssignment.user_id == user.id, DailyParkingAssignment.date == date_str ).first() if not assignment: return False # No assignment today spot_name = get_spot_display_name(assignment.spot_id, assignment.manager_id, db) day_name = date.strftime("%A, %B %d") subject = f"Parking reminder for {day_name}" body_html = f"""

    Daily Parking Reminder

    Hi {user.name},

    You have a parking spot assigned for today ({day_name}):

    Spot {spot_name}

    Best regards,
    Parking Manager

    """ if send_email(user.email, subject, body_html): log = NotificationLog( id=str(uuid.uuid4()), user_id=user.id, notification_type="daily_parking", reference_date=date_str, sent_at=datetime.now().isoformat() ) db.add(log) db.commit() return True return False def queue_parking_change_notification( user: User, date: str, change_type: str, # "assigned", "released", "reassigned" spot_name: str, new_user_name: str = None, db: Session = None ): """Queue an immediate notification for a parking assignment change""" if not user.notify_parking_changes: return date_obj = datetime.strptime(date, "%Y-%m-%d") day_name = date_obj.strftime("%A, %B %d") if change_type == "assigned": subject = f"Parking spot assigned for {day_name}" body = f"""

    Parking Spot Assigned

    Hi {user.name},

    You have been assigned a parking spot for {day_name}:

    Spot {spot_name}

    Best regards,
    Parking Manager

    """ elif change_type == "released": subject = f"Parking spot released for {day_name}" body = f"""

    Parking Spot Released

    Hi {user.name},

    Your parking spot (Spot {spot_name}) for {day_name} has been released.

    Best regards,
    Parking Manager

    """ elif change_type == "reassigned": subject = f"Parking spot reassigned for {day_name}" body = f"""

    Parking Spot Reassigned

    Hi {user.name},

    Your parking spot (Spot {spot_name}) for {day_name} has been reassigned to {new_user_name}.

    Best regards,
    Parking Manager

    """ else: return # Add to queue notification = NotificationQueue( id=str(uuid.uuid4()), user_id=user.id, notification_type="parking_change", subject=subject, body=body, created_at=datetime.now().isoformat() ) db.add(notification) db.commit() def process_notification_queue(db: Session): """Process and send all pending notifications in the queue""" pending = db.query(NotificationQueue).filter( NotificationQueue.sent_at.is_(None) ).all() for notification in pending: user = db.query(User).filter(User.id == notification.user_id).first() if user and send_email(user.email, notification.subject, notification.body): notification.sent_at = datetime.now().isoformat() db.commit() def run_scheduled_notifications(db: Session): """Run all scheduled notifications - called by a scheduler/cron job""" now = datetime.now() today = now.date() current_hour = now.hour current_minute = now.minute current_weekday = now.weekday() # 0=Monday, 6=Sunday users = db.query(User).all() for user in users: # Thursday at 12: Presence reminder (unmanageable) if current_weekday == 3 and current_hour == 12 and current_minute < 5: next_week = get_next_week_dates(now) send_presence_reminder(user, next_week, db) # Friday at 12: Weekly parking summary if current_weekday == 4 and current_hour == 12 and current_minute < 5: next_week = get_next_week_dates(now) send_weekly_parking_summary(user, next_week, db) # Daily parking reminder at user's preferred time (working days only) if current_weekday < 5: # Monday to Friday user_hour = user.notify_daily_parking_hour or 8 user_minute = user.notify_daily_parking_minute or 0 if current_hour == user_hour and abs(current_minute - user_minute) < 5: send_daily_parking_reminder(user, now, db) # Process queued notifications process_notification_queue(db)