Files
classifieds/app/services/expiry_notifications.py
T
2026-07-13 16:52:00 -04:00

91 lines
3.4 KiB
Python

"""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