You are working in my existing renewal assistant project. Please create a new Python script at: campaigns/send_appt_reminders.py that does the following, reusing the existing patterns and utils in this repo: 1. Imports: - Use the existing Supabase helper from utils.tools: from utils.tools import get_client, log_message - Also import: from datetime import datetime, timezone import argparse import pytz (if already used elsewhere; if not, you can skip timezone logic and just use UTC now) 2. Purpose: - This script sends **appointment reminder** messages for upcoming renewal appointments. - It should read from the existing view **v_appt_reminders** in Supabase. - v_appt_reminders already contains only appointments that: - are in an appropriate status (e.g. 'new' / 'confirmed') - are due a reminder - have reminder_sent_at IS NULL - For each row returned, send one WhatsApp reminder using the **reply_template** with slug='appt_reminder'. 3. CLI interface: - Use argparse with: --channel (default: "whatsapp") --limit (default: 50) --dry-run (flag; if present, do NOT write to DB, just print what would happen) - Example usage: python campaigns/send_appt_reminders.py python campaigns/send_appt_reminders.py --limit 20 python campaigns/send_appt_reminders.py --dry-run 4. Loading the template: - Query the table reply_template to get the row where: slug = 'appt_reminder' AND channel = AND active = true - If no template is found, print a clear error and exit with non-zero code. 5. Querying reminders: - From v_appt_reminders select up to rows, ordered by preferred_date, then created_at. - Columns you can expect from v_appt_reminders (based on our schema): appt_id agreement_id (if present) agreement_number first_name last_name make model registration channel preferred_date preferred_time status reminder_sent_at reminder_channel tco_token tco_completed_at 6. Rendering the message: - Take the template body from reply_template.body. - Do simple string replacements for: {{first_name}} -> row["first_name"] or "" {{centre_name}} -> "Lincoln Audi" (hard-code for now) {{model}} -> row["model"] or "Audi" - Leave any other {{...}} placeholders unchanged if they exist. 7. Logging the outbound message: - For each reminder to send, call log_message with: agreement_ref = row["agreement_number"] channel = channel argument (e.g. "whatsapp") direction = "outbound" message_text = final rendered message intent_label = "appt_reminder" template_slug = "appt_reminder" source = "ai" model = None - Respect the --dry-run flag: - If --dry-run is set, DO NOT call log_message. - Instead, just print a preview line like: [DRY RUN] Would send reminder for AGREEMENT_NUMBER: "MESSAGE..." 8. Updating appointment_request: - For real sends (no --dry-run): - Update appointment_request where id = appt_id: reminder_sent_at = now() (UTC) reminder_channel = channel argument (e.g. "whatsapp") - Use the Supabase Python client from get_client() to perform this update. - If an update fails for one row, log an error but continue with others. 9. Output / summary: - At the start, print a header: "APPOINTMENT REMINDER SENDER" - For each processed row, print: - Agreement number - First name - Preferred_date - Whether it was sent or dry-run - At the end, print a summary: - Total reminders considered - Total actually sent - Total skipped (if any) - If dry-run, clearly mark that nothing was written. 10. Code style: - Follow the same style as the existing campaigns/send_campaign_batch.py file in this repo: - main() function - if __name__ == "__main__": main() - Clear prints, graceful error handling. When you’ve created campaigns/send_appt_reminders.py, please run: python campaigns/send_appt_reminders.py --dry-run --limit 10 and show me the console output so I can check it.