""" app/scheduler_jobs.py APScheduler job definitions for the tenant portal. Registered in create_tenant_app() after scheduler.init_app(). Jobs: - send_appointment_reminders every 5 minutes - send_review_requests every 10 minutes """ import logging from datetime import datetime, timezone, timedelta logger = logging.getLogger(__name__) def send_appointment_reminders(app): """ Scan appointment_reminders for pending reminders due to be sent. Sends email via Flask-Mail and marks status = 'sent' or 'failed'. Runs every 5 minutes. """ with app.app_context(): from app.extensions import db, mail from app.models.salon import AppointmentReminder, Appointment, Customer now = datetime.now(timezone.utc) due = AppointmentReminder.query.filter_by(status="pending").filter( AppointmentReminder.scheduled_for <= now ).limit(50).all() if not due: return sent = failed = 0 for reminder in due: appt = Appointment.query.get(reminder.appointment_id) if not appt or appt.status in ("cancelled", "no_show"): reminder.status = "cancelled" continue customer = Customer.query.get(appt.customer_id) if appt.customer_id else None if not customer or not customer.email: # No email — mark sent so we don't retry endlessly reminder.status = "sent" reminder.sent_at = now sent += 1 continue try: from flask_mail import Message from app.models.platform import Tenant tenant = Tenant.query.get(appt.tenant_id) tenant_name = tenant.name if tenant else "Your salon" msg = Message( subject=f"Appointment Reminder — {tenant_name}", recipients=[customer.email], body=( f"Hi {customer.name},\n\n" f"This is a reminder of your upcoming appointment:\n" f"Date: {appt.start_time.strftime('%B %d, %Y at %I:%M %p')}\n" f"Service: {appt.service.name if appt.service else 'N/A'}\n\n" f"See you soon!\n{tenant_name}" ), ) mail.send(msg) reminder.status = "sent" reminder.sent_at = now sent += 1 except Exception as exc: logger.error("Reminder send failed id=%s: %s", reminder.id, exc) reminder.status = "failed" failed += 1 db.session.commit() if sent or failed: logger.info("Appointment reminders: sent=%d failed=%d", sent, failed) def send_review_requests(app): """ Find completed transactions where review_request_sent_at IS NULL and the transaction is old enough (per tenant setting review_request_delay_minutes). Sends review email with smart routing: - rating ≥ 4 → shows Google/Facebook/Yelp links - rating < 4 → silent internal feedback only One send per transaction enforced by review_request_sent_at. Runs every 10 minutes. """ with app.app_context(): from app.extensions import db, mail from app.models.salon import Transaction, Customer, TenantSetting, CheckoutReview from app.models.platform import Tenant now = datetime.now(timezone.utc) # Find eligible transactions: completed, not voided, no review sent yet candidates = Transaction.query.filter( Transaction.voided_at.is_(None), Transaction.review_request_sent_at.is_(None), Transaction.customer_id.isnot(None), ).limit(100).all() sent = 0 for txn in candidates: # Get tenant-specific delay (default 60 minutes) delay_setting = TenantSetting.query.filter_by( tenant_id=txn.tenant_id, setting_key="review_request_delay_minutes", ).first() delay_minutes = int(delay_setting.setting_value or 60) if delay_setting else 60 eligible_after = txn.created_at.replace(tzinfo=timezone.utc) + timedelta(minutes=delay_minutes) if now < eligible_after: continue customer = Customer.query.get(txn.customer_id) if not customer or not customer.email: # Mark sent to avoid retrying txn.review_request_sent_at = now continue tenant = Tenant.query.get(txn.tenant_id) tenant_name = tenant.name if tenant else "Your salon" # Fetch review settings def _get_setting(key): s = TenantSetting.query.filter_by( tenant_id=txn.tenant_id, setting_key=key).first() return s.setting_value if s else None google_url = _get_setting("google_review_url") yelp_url = _get_setting("yelp_review_url") facebook_url = _get_setting("facebook_review_url") # Check if a review already submitted existing_review = CheckoutReview.query.filter_by( transaction_id=txn.id ).first() try: from flask_mail import Message if existing_review and existing_review.rating >= 4: # High rating — encourage public review links = [] if google_url: links.append(f"Google: {google_url}") if yelp_url: links.append(f"Yelp: {yelp_url}") if facebook_url: links.append(f"Facebook: {facebook_url}") body = ( f"Hi {customer.name},\n\n" f"Thank you for visiting {tenant_name}! We're so glad you enjoyed your visit.\n" f"Would you mind sharing your experience online?\n\n" + ("\n".join(links) if links else "(No review links configured)") + f"\n\nThank you!\n{tenant_name}" ) else: # No review yet or low rating — generic thank you only body = ( f"Hi {customer.name},\n\n" f"Thank you for visiting {tenant_name}! We hope to see you again soon.\n\n" f"{tenant_name}" ) msg = Message( subject=f"Thank you for visiting {tenant_name}!", recipients=[customer.email], body=body, ) mail.send(msg) txn.review_request_sent_at = now sent += 1 except Exception as exc: logger.error("Review request failed txn=%s: %s", txn.id, exc) # Don't mark sent_at — will retry next cycle db.session.commit() if sent: logger.info("Review requests sent: %d", sent)