Files
membership-be/reminder_emails.py
2025-12-16 20:03:50 +07:00

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