90 lines
3.4 KiB
Python
90 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 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] = datetime.utcnow().isoformat()
|
|
listing.attributes = attrs
|
|
|
|
|
|
def warn_expiring(days: int = 3) -> int:
|
|
"""Send warning emails for listings expiring within `days` days. Returns count."""
|
|
now = datetime.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
|