"""
Reminder Email System
This module handles all reminder emails sent before status transitions.
Ensures users receive multiple reminders before any auto-abandonment occurs.
"""
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional
import logging
logger = logging.getLogger(__name__)
# Reminder schedules (in days since status started)
REMINDER_SCHEDULES = {
'email_verification': [3, 7, 14, 30], # Before potential abandonment
'event_attendance': [30, 60, 80, 85], # Before 90-day deadline
'payment_pending': [7, 14, 21, 30, 45, 60], # Before potential abandonment
'renewal': [60, 30, 14, 7], # Before expiration
'post_expiration': [7, 30, 90] # After expiration
}
def get_days_since_status_change(user, current_status: str) -> int:
"""
Calculate number of days since user entered current status.
Args:
user: User object
current_status: Current status to check
Returns:
Number of days since status change
"""
if not user.updated_at:
return 0
delta = datetime.now(timezone.utc) - user.updated_at
return delta.days
def should_send_reminder(days_elapsed: int, schedule: List[int], last_reminder_day: Optional[int] = None) -> Optional[int]:
"""
Determine if a reminder should be sent based on elapsed days.
Args:
days_elapsed: Days since status change
schedule: List of reminder days
last_reminder_day: Day of last reminder sent (optional)
Returns:
Reminder day if should send, None otherwise
"""
for reminder_day in schedule:
if days_elapsed >= reminder_day:
# Check if we haven't sent this reminder yet
if last_reminder_day is None or last_reminder_day < reminder_day:
return reminder_day
return None
def send_email_verification_reminder(user, days_elapsed: int, email_service, db_session=None):
"""
Send email verification reminder.
Args:
user: User object
days_elapsed: Days since registration
email_service: Email service instance
db_session: Database session (optional, for tracking)
Returns:
True if email sent successfully
"""
reminder_number = REMINDER_SCHEDULES['email_verification'].index(days_elapsed) + 1 if days_elapsed in REMINDER_SCHEDULES['email_verification'] else 0
subject = f"Reminder: Verify your email to complete registration"
if reminder_number == 4:
# Final reminder
message = f"""
Final Reminder: Complete Your LOAF Registration
Hi {user.first_name},
This is your final reminder to verify your email address and complete your LOAF membership registration.
It's been {days_elapsed} days since you registered. If you don't verify your email soon,
your application will be marked as abandoned and you'll need to contact us to restart the process.
Click the link below to verify your email:
Verify Email Address
Need help? Reply to this email or contact us at info@loaftx.org
Best regards,
LOAF Team
"""
else:
message = f"""
Reminder: Verify Your Email Address
Hi {user.first_name},
You registered for LOAF membership {days_elapsed} days ago but haven't verified your email yet.
Click the link below to verify your email and continue your membership journey:
Verify Email Address
Once verified, you'll receive our monthly newsletter with event announcements!
Best regards,
LOAF Team
"""
try:
email_service.send_email(user.email, subject, message)
logger.info(f"Sent email verification reminder #{reminder_number} to user {user.id} (day {days_elapsed})")
# Track reminder in database for admin visibility
if db_session:
user.email_verification_reminders_sent = (user.email_verification_reminders_sent or 0) + 1
user.last_email_verification_reminder_at = datetime.now(timezone.utc)
db_session.commit()
logger.info(f"Updated reminder tracking: user {user.id} has received {user.email_verification_reminders_sent} verification reminders")
return True
except Exception as e:
logger.error(f"Failed to send email verification reminder to user {user.id}: {str(e)}")
return False
def send_event_attendance_reminder(user, days_elapsed: int, email_service, db_session=None):
"""
Send event attendance reminder.
Args:
user: User object
days_elapsed: Days since email verification
email_service: Email service instance
db_session: Database session (optional, for tracking)
Returns:
True if email sent successfully
"""
days_remaining = 90 - days_elapsed
subject = f"Reminder: Attend a LOAF event ({days_remaining} days remaining)"
if days_elapsed >= 85:
# Final reminder (5 days left)
message = f"""
Final Reminder: Only {days_remaining} Days to Attend an Event!
Hi {user.first_name},
Important: You have only {days_remaining} days left to attend a LOAF event
and complete your membership application.
If you don't attend an event within the 90-day period, your application will be marked as
abandoned per LOAF policy, and you'll need to contact us to restart.
Check out our upcoming events in the monthly newsletter or visit our events page!
Need help finding an event? Reply to this email or contact us at info@loaftx.org
We'd love to meet you soon!
Best regards,
LOAF Team
"""
elif days_elapsed >= 80:
# 10 days left
message = f"""
Reminder: {days_remaining} Days to Attend a LOAF Event
Hi {user.first_name},
Just a friendly reminder that you have {days_remaining} days left to attend a LOAF event
and complete your membership application.
Per LOAF policy, new applicants must attend an event within 90 days of email verification
to continue the membership process.
Check your newsletter for upcoming events, and we look forward to meeting you soon!
Best regards,
LOAF Team
"""
elif days_elapsed >= 60:
# 30 days left
message = f"""
Reminder: {days_remaining} Days to Attend a LOAF Event
Hi {user.first_name},
You have {days_remaining} days remaining to attend a LOAF event as part of your membership application.
Attending an event is a great way to meet other members and learn more about LOAF.
Check out the upcoming events in your monthly newsletter!
We look forward to seeing you soon!
Best regards,
LOAF Team
"""
else:
# 60 days left
message = f"""
Reminder: Attend a LOAF Event (60 Days Remaining)
Hi {user.first_name},
Welcome to LOAF! As part of your membership application, you have 90 days to attend one of our events.
You have {days_remaining} days remaining to attend an event and continue your membership journey.
Check out the events listed in your monthly newsletter. We can't wait to meet you!
Best regards,
LOAF Team
"""
try:
email_service.send_email(user.email, subject, message)
logger.info(f"Sent event attendance reminder to user {user.id} (day {days_elapsed}, {days_remaining} days left)")
# Track reminder in database for admin visibility
if db_session:
user.event_attendance_reminders_sent = (user.event_attendance_reminders_sent or 0) + 1
user.last_event_attendance_reminder_at = datetime.now(timezone.utc)
db_session.commit()
logger.info(f"Updated reminder tracking: user {user.id} has received {user.event_attendance_reminders_sent} event attendance reminders")
return True
except Exception as e:
logger.error(f"Failed to send event attendance reminder to user {user.id}: {str(e)}")
return False
def send_payment_reminder(user, days_elapsed: int, email_service, db_session=None):
"""
Send payment reminder.
Args:
user: User object
days_elapsed: Days since admin validation
email_service: Email service instance
db_session: Database session (optional, for tracking)
Returns:
True if email sent successfully
"""
reminder_count = sum(1 for day in REMINDER_SCHEDULES['payment_pending'] if day <= days_elapsed)
subject = f"Reminder: Complete your LOAF membership payment"
if days_elapsed >= 60:
# Final reminder
message = f"""
Final Payment Reminder
Hi {user.first_name},
Congratulations again on being validated for LOAF membership!
This is a final reminder to complete your membership payment. It's been {days_elapsed} days
since your application was validated.
Your payment link is still active. Click below to complete your payment and activate your membership:
Complete Payment
Once payment is complete, you'll gain full access to all member benefits!
Questions? Contact us at info@loaftx.org
Best regards,
LOAF Team
"""
elif days_elapsed >= 45:
message = f"""
Payment Reminder - Complete Your Membership
Hi {user.first_name},
Your LOAF membership application was validated and is ready for payment!
Complete your payment to activate your membership and gain access to all member benefits:
Complete Payment
We're excited to welcome you as a full member!
Best regards,
LOAF Team
"""
else:
message = f"""
Payment Reminder
Hi {user.first_name},
This is a friendly reminder to complete your LOAF membership payment.
Your application was validated {days_elapsed} days ago. Click below to complete payment:
Complete Payment
Questions about payment options? Contact us at info@loaftx.org
Best regards,
LOAF Team
"""
try:
email_service.send_email(user.email, subject, message)
logger.info(f"Sent payment reminder #{reminder_count} to user {user.id} (day {days_elapsed})")
# Track reminder in database for admin visibility
if db_session:
user.payment_reminders_sent = (user.payment_reminders_sent or 0) + 1
user.last_payment_reminder_at = datetime.now(timezone.utc)
db_session.commit()
logger.info(f"Updated reminder tracking: user {user.id} has received {user.payment_reminders_sent} payment reminders")
return True
except Exception as e:
logger.error(f"Failed to send payment reminder to user {user.id}: {str(e)}")
return False
def send_renewal_reminder(user, subscription, days_until_expiration: int, email_service, db_session=None):
"""
Send membership renewal reminder.
Args:
user: User object
subscription: Subscription object
days_until_expiration: Days until subscription expires
email_service: Email service instance
db_session: Database session (optional, for tracking)
Returns:
True if email sent successfully
"""
subject = f"Reminder: Your LOAF membership expires in {days_until_expiration} days"
if days_until_expiration <= 7:
# Final reminder
message = f"""
Final Reminder: Renew Your LOAF Membership
Hi {user.first_name},
Your LOAF membership expires in {days_until_expiration} days!
Don't lose access to member benefits. Renew now to continue enjoying:
- Exclusive member events
- Member directory access
- Monthly newsletter
- Community connection
Renew Your Membership Now
Questions? Contact us at info@loaftx.org
Best regards,
LOAF Team
"""
else:
message = f"""
Reminder: Renew Your LOAF Membership
Hi {user.first_name},
Your LOAF membership will expire in {days_until_expiration} days.
Renew now to continue enjoying all member benefits without interruption:
Renew Your Membership
Thank you for being part of the LOAF community!
Best regards,
LOAF Team
"""
try:
email_service.send_email(user.email, subject, message)
logger.info(f"Sent renewal reminder to user {user.id} ({days_until_expiration} days until expiration)")
# Track reminder in database for admin visibility
if db_session:
user.renewal_reminders_sent = (user.renewal_reminders_sent or 0) + 1
user.last_renewal_reminder_at = datetime.now(timezone.utc)
db_session.commit()
logger.info(f"Updated reminder tracking: user {user.id} has received {user.renewal_reminders_sent} renewal reminders")
return True
except Exception as e:
logger.error(f"Failed to send renewal reminder to user {user.id}: {str(e)}")
return False
def send_post_expiration_reminder(user, days_since_expiration: int, email_service):
"""
Send reminder to renew after membership has expired.
Args:
user: User object
days_since_expiration: Days since expiration
email_service: Email service instance
Returns:
True if email sent successfully
"""
subject = "We'd love to have you back at LOAF!"
if days_since_expiration >= 90:
# Final reminder
message = f"""
We Miss You at LOAF!
Hi {user.first_name},
Your LOAF membership expired {days_since_expiration} days ago, and we'd love to have you back!
Rejoin the community and reconnect with friends:
Renew Your Membership
Questions? We're here to help: info@loaftx.org
Best regards,
LOAF Team
"""
elif days_since_expiration >= 30:
message = f"""
Renew Your LOAF Membership
Hi {user.first_name},
Your LOAF membership expired {days_since_expiration} days ago.
We'd love to have you back! Renew today to regain access to:
- Member events and gatherings
- Member directory
- Community connection
Renew Your Membership
Best regards,
LOAF Team
"""
else:
# 7 days after expiration
message = f"""
Your LOAF Membership Has Expired
Hi {user.first_name},
Your LOAF membership expired recently. We hope it was just an oversight!
Renew now to restore your access to all member benefits:
Renew Your Membership
We look forward to seeing you at upcoming events!
Best regards,
LOAF Team
"""
try:
email_service.send_email(user.email, subject, message)
logger.info(f"Sent post-expiration reminder to user {user.id} ({days_since_expiration} days since expiration)")
return True
except Exception as e:
logger.error(f"Failed to send post-expiration reminder to user {user.id}: {str(e)}")
return False
# Background job for sending reminder emails
def process_reminder_emails(db_session, email_service):
"""
Process and send all due reminder emails.
This should be run as an hourly background job.
Args:
db_session: Database session
email_service: Email service instance
Returns:
Dictionary with counts of emails sent
"""
from models import User, UserStatus, Subscription
from datetime import date
results = {
'email_verification': 0,
'event_attendance': 0,
'payment': 0,
'renewal': 0,
'post_expiration': 0
}
# 1. Email Verification Reminders
for reminder_day in REMINDER_SCHEDULES['email_verification']:
users = db_session.query(User).filter(
User.status == UserStatus.pending_email,
User.email_verified == False
).all()
for user in users:
days_elapsed = get_days_since_status_change(user, 'pending_email')
if days_elapsed == reminder_day:
if send_email_verification_reminder(user, days_elapsed, email_service, db_session):
results['email_verification'] += 1
# 2. Event Attendance Reminders
for reminder_day in REMINDER_SCHEDULES['event_attendance']:
users = db_session.query(User).filter(
User.status == UserStatus.pending_validation
).all()
for user in users:
days_elapsed = get_days_since_status_change(user, 'pending_validation')
if days_elapsed == reminder_day:
if send_event_attendance_reminder(user, days_elapsed, email_service, db_session):
results['event_attendance'] += 1
# 3. Payment Reminders
for reminder_day in REMINDER_SCHEDULES['payment_pending']:
users = db_session.query(User).filter(
User.status == UserStatus.payment_pending
).all()
for user in users:
days_elapsed = get_days_since_status_change(user, 'payment_pending')
if days_elapsed == reminder_day:
if send_payment_reminder(user, days_elapsed, email_service, db_session):
results['payment'] += 1
# 4. Renewal Reminders (before expiration)
for days_before in REMINDER_SCHEDULES['renewal']:
# Find active subscriptions expiring in X days
target_date = date.today() + timedelta(days=days_before)
subscriptions = db_session.query(User, Subscription).join(
Subscription, User.id == Subscription.user_id
).filter(
User.status == UserStatus.active,
Subscription.end_date == target_date
).all()
for user, subscription in subscriptions:
if send_renewal_reminder(user, subscription, days_before, email_service, db_session):
results['renewal'] += 1
# 5. Post-Expiration Reminders
for days_after in REMINDER_SCHEDULES['post_expiration']:
target_date = date.today() - timedelta(days=days_after)
subscriptions = db_session.query(User, Subscription).join(
Subscription, User.id == Subscription.user_id
).filter(
User.status == UserStatus.expired,
Subscription.end_date == target_date
).all()
for user, subscription in subscriptions:
if send_post_expiration_reminder(user, days_after, email_service):
results['post_expiration'] += 1
logger.info(f"Reminder email batch complete: {results}")
return results