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

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:

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