"""Listing expiry warning and expired notification emails. Two sweeps (called from nightly systemd timer): - warn_expiring(days=3): send one warning email per listing expiring within N days that hasn't already received one. - notify_expired(): send "your listing expired" email for newly-expired listings that haven't been notified yet. We track state via a JSON field on the listing. Rather than adding columns, we use the existing `attributes` JSON and a private `_notified` sub-key so no migration is required. The key is prefixed with `_` to avoid colliding with user attributes. """ from datetime import datetime, timedelta from app.utils.time import utcnow from flask import current_app from app.extensions import db from app.models.listing import Listing from app.models.enums import ListingStatus from app.services.email import send_email def _already_notified(listing, key: str) -> bool: attrs = listing.attributes or {} return bool(attrs.get(key)) def _mark_notified(listing, key: str): attrs = dict(listing.attributes or {}) attrs[key] = utcnow().isoformat() listing.attributes = attrs def warn_expiring(days: int = 3) -> int: """Send warning emails for listings expiring within `days` days. Returns count.""" now = utcnow() window_end = now + timedelta(days=days) soon = (Listing.query .filter(Listing.status == ListingStatus.active, Listing.expires_at > now, Listing.expires_at <= window_end) .all()) sent = 0 for listing in soon: if _already_notified(listing, "_warn_sent"): continue user = listing.user if not user or not user.email: continue days_left = (listing.expires_at - now).days subject = f"Your listing '{listing.title[:50]}' expires in {days_left} day(s)" body = ( f"Hi {user.display_name},\n\n" f"Your listing \"{listing.title}\" will expire in {days_left} day(s) " f"({listing.expires_at.strftime('%Y-%m-%d')}).\n\n" f"To keep it active, edit and re-save it, or purchase a boost.\n\n" f"View your listing: {current_app.config.get('SERVER_NAME', '')}/listings/{listing.id}\n\n" f"— Classifieds" ) if send_email(user.email, subject, body): _mark_notified(listing, "_warn_sent") db.session.commit() sent += 1 return sent def notify_expired() -> int: """Send 'your listing expired' emails for newly-expired listings. Returns count.""" expired = (Listing.query .filter(Listing.status == ListingStatus.expired) .all()) sent = 0 for listing in expired: if _already_notified(listing, "_expired_sent"): continue user = listing.user if not user or not user.email: continue subject = f"Your listing '{listing.title[:50]}' has expired" body = ( f"Hi {user.display_name},\n\n" f"Your listing \"{listing.title}\" expired on " f"{listing.expires_at.strftime('%Y-%m-%d')}.\n\n" f"To re-list it, create a new listing or upgrade your plan for longer listing life.\n\n" f"— Classifieds" ) if send_email(user.email, subject, body): _mark_notified(listing, "_expired_sent") db.session.commit() sent += 1 return sent