488 lines
20 KiB
Python
488 lines
20 KiB
Python
"""
|
|
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"""
|
|
<h2>Final Reminder: Complete Your LOAF Registration</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>This is your final reminder to verify your email address and complete your LOAF membership registration.</p>
|
|
<p>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.</p>
|
|
<p>Click the link below to verify your email:</p>
|
|
<p><a href="{email_service.get_verification_link(user)}">Verify Email Address</a></p>
|
|
<p>Need help? Reply to this email or contact us at info@loaftx.org</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
else:
|
|
message = f"""
|
|
<h2>Reminder: Verify Your Email Address</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>You registered for LOAF membership {days_elapsed} days ago but haven't verified your email yet.</p>
|
|
<p>Click the link below to verify your email and continue your membership journey:</p>
|
|
<p><a href="{email_service.get_verification_link(user)}">Verify Email Address</a></p>
|
|
<p>Once verified, you'll receive our monthly newsletter with event announcements!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
|
|
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"""
|
|
<h2>Final Reminder: Only {days_remaining} Days to Attend an Event!</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p><strong>Important:</strong> You have only {days_remaining} days left to attend a LOAF event
|
|
and complete your membership application.</p>
|
|
<p>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.</p>
|
|
<p>Check out our upcoming events in the monthly newsletter or visit our events page!</p>
|
|
<p>Need help finding an event? Reply to this email or contact us at info@loaftx.org</p>
|
|
<p>We'd love to meet you soon!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
elif days_elapsed >= 80:
|
|
# 10 days left
|
|
message = f"""
|
|
<h2>Reminder: {days_remaining} Days to Attend a LOAF Event</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Just a friendly reminder that you have {days_remaining} days left to attend a LOAF event
|
|
and complete your membership application.</p>
|
|
<p>Per LOAF policy, new applicants must attend an event within 90 days of email verification
|
|
to continue the membership process.</p>
|
|
<p>Check your newsletter for upcoming events, and we look forward to meeting you soon!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
elif days_elapsed >= 60:
|
|
# 30 days left
|
|
message = f"""
|
|
<h2>Reminder: {days_remaining} Days to Attend a LOAF Event</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>You have {days_remaining} days remaining to attend a LOAF event as part of your membership application.</p>
|
|
<p>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!</p>
|
|
<p>We look forward to seeing you soon!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
else:
|
|
# 60 days left
|
|
message = f"""
|
|
<h2>Reminder: Attend a LOAF Event (60 Days Remaining)</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Welcome to LOAF! As part of your membership application, you have 90 days to attend one of our events.</p>
|
|
<p>You have {days_remaining} days remaining to attend an event and continue your membership journey.</p>
|
|
<p>Check out the events listed in your monthly newsletter. We can't wait to meet you!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
|
|
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"""
|
|
<h2>Final Payment Reminder</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Congratulations again on being validated for LOAF membership!</p>
|
|
<p>This is a final reminder to complete your membership payment. It's been {days_elapsed} days
|
|
since your application was validated.</p>
|
|
<p>Your payment link is still active. Click below to complete your payment and activate your membership:</p>
|
|
<p><a href="{email_service.get_payment_link(user)}">Complete Payment</a></p>
|
|
<p>Once payment is complete, you'll gain full access to all member benefits!</p>
|
|
<p>Questions? Contact us at info@loaftx.org</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
elif days_elapsed >= 45:
|
|
message = f"""
|
|
<h2>Payment Reminder - Complete Your Membership</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Your LOAF membership application was validated and is ready for payment!</p>
|
|
<p>Complete your payment to activate your membership and gain access to all member benefits:</p>
|
|
<p><a href="{email_service.get_payment_link(user)}">Complete Payment</a></p>
|
|
<p>We're excited to welcome you as a full member!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
else:
|
|
message = f"""
|
|
<h2>Payment Reminder</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>This is a friendly reminder to complete your LOAF membership payment.</p>
|
|
<p>Your application was validated {days_elapsed} days ago. Click below to complete payment:</p>
|
|
<p><a href="{email_service.get_payment_link(user)}">Complete Payment</a></p>
|
|
<p>Questions about payment options? Contact us at info@loaftx.org</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
|
|
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"""
|
|
<h2>Final Reminder: Renew Your LOAF Membership</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p><strong>Your LOAF membership expires in {days_until_expiration} days!</strong></p>
|
|
<p>Don't lose access to member benefits. Renew now to continue enjoying:</p>
|
|
<ul>
|
|
<li>Exclusive member events</li>
|
|
<li>Member directory access</li>
|
|
<li>Monthly newsletter</li>
|
|
<li>Community connection</li>
|
|
</ul>
|
|
<p><a href="{email_service.get_renewal_link(user)}">Renew Your Membership Now</a></p>
|
|
<p>Questions? Contact us at info@loaftx.org</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
else:
|
|
message = f"""
|
|
<h2>Reminder: Renew Your LOAF Membership</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Your LOAF membership will expire in {days_until_expiration} days.</p>
|
|
<p>Renew now to continue enjoying all member benefits without interruption:</p>
|
|
<p><a href="{email_service.get_renewal_link(user)}">Renew Your Membership</a></p>
|
|
<p>Thank you for being part of the LOAF community!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
|
|
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"""
|
|
<h2>We Miss You at LOAF!</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Your LOAF membership expired {days_since_expiration} days ago, and we'd love to have you back!</p>
|
|
<p>Rejoin the community and reconnect with friends:</p>
|
|
<p><a href="{email_service.get_renewal_link(user)}">Renew Your Membership</a></p>
|
|
<p>Questions? We're here to help: info@loaftx.org</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
elif days_since_expiration >= 30:
|
|
message = f"""
|
|
<h2>Renew Your LOAF Membership</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Your LOAF membership expired {days_since_expiration} days ago.</p>
|
|
<p>We'd love to have you back! Renew today to regain access to:</p>
|
|
<ul>
|
|
<li>Member events and gatherings</li>
|
|
<li>Member directory</li>
|
|
<li>Community connection</li>
|
|
</ul>
|
|
<p><a href="{email_service.get_renewal_link(user)}">Renew Your Membership</a></p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
else:
|
|
# 7 days after expiration
|
|
message = f"""
|
|
<h2>Your LOAF Membership Has Expired</h2>
|
|
<p>Hi {user.first_name},</p>
|
|
<p>Your LOAF membership expired recently. We hope it was just an oversight!</p>
|
|
<p>Renew now to restore your access to all member benefits:</p>
|
|
<p><a href="{email_service.get_renewal_link(user)}">Renew Your Membership</a></p>
|
|
<p>We look forward to seeing you at upcoming events!</p>
|
|
<p>Best regards,<br>LOAF Team</p>
|
|
"""
|
|
|
|
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
|