from __future__ import annotations import logging from datetime import datetime, timedelta import pytz from scripts.edoxid_calendar_config import ( get_db_config, get_google_config, get_state_sqlite_path, get_sync_min_date, ) from scripts.edoxid_calendar_database import Database from scripts.edoxid_calendar_gmail import GmailService from scripts.edoxid_calendar_google import ( GoogleCalendarService, build_attendee_emails, build_description, format_submission_summary, get_schedule_datetime, get_type_label, ) from scripts.edoxid_calendar_state_store import StateStore logger = logging.getLogger(__name__) JAKARTA_TZ = pytz.timezone("Asia/Jakarta") # Toleransi jendela waktu untuk cek reminder — harus >= interval polling DAG (5 menit) # supaya tidak ada window yang "kelewat" di antara 2 run. REMINDER_WINDOW = timedelta(minutes=10) def _check_and_send_reminders(submissions: list[dict], state: StateStore, gmail: GmailService) -> int: """Kirim email reminder H-1 (07:00) & 1 jam sebelum ke semua attendee — terpisah dari reminders.overrides Calendar (yang cuma berlaku untuk calendar TU sendiri, tidak ikut ke kalender pribadi tiap attendee).""" now = datetime.now(JAKARTA_TZ) sent = 0 for submission in submissions: schedule = get_schedule_datetime(submission) h1_time = schedule.replace(hour=7, minute=0, second=0, microsecond=0) - timedelta(days=1) one_hour_time = schedule - timedelta(hours=1) emails = build_attendee_emails(submission) if not emails: continue if h1_time <= now < h1_time + REMINDER_WINDOW and not state.is_notified(submission["id"], "h1"): subject = f"[PENGINGAT SEBELUM H-1] {get_type_label(submission)} — {submission['student_name']}" gmail.send(emails, subject, build_description(submission)) state.mark_notified(submission["id"], "h1") sent += 1 logger.info( "REMINDER H-1 terkirim | submission #%s | %s | penerima: %s", submission["id"], format_submission_summary(submission), ", ".join(emails), ) if one_hour_time <= now < one_hour_time + REMINDER_WINDOW and not state.is_notified( submission["id"], "1h" ): subject = f"[PENGINGAT 1 JAM SEBELUM] {get_type_label(submission)} — {submission['student_name']}" gmail.send(emails, subject, build_description(submission)) state.mark_notified(submission["id"], "1h") sent += 1 logger.info( "REMINDER 1-JAM terkirim | submission #%s | %s | penerima: %s", submission["id"], format_submission_summary(submission), ", ".join(emails), ) return sent def run_sync() -> dict: db = Database(get_db_config()) state = StateStore(get_state_sqlite_path()) google_config = get_google_config() calendar = GoogleCalendarService(google_config) gmail = GmailService(google_config) created = updated = deleted = skipped = 0 try: submissions = db.get_scheduled_submissions(get_sync_min_date()) for submission in submissions: content_hash = state.compute_hash(submission) existing = state.find(submission["id"]) if existing is None: event_id = calendar.create_event(submission) state.save(submission["id"], event_id, content_hash) created += 1 logger.info( "CREATE event %s | submission #%s | %s", event_id, submission["id"], format_submission_summary(submission), ) elif existing["content_hash"] != content_hash: calendar.update_event(existing["event_id"], submission) state.update_hash(submission["id"], content_hash) updated += 1 logger.info( "UPDATE event %s | submission #%s | %s", existing["event_id"], submission["id"], format_submission_summary(submission), ) else: skipped += 1 active_ids = db.get_ids_with_active_schedule() for submission_id, event_id in state.find_orphaned(active_ids).items(): calendar.delete_event(event_id) state.remove(submission_id) deleted += 1 logger.info("DELETE submission #%s -> event %s", submission_id, event_id) reminders_sent = _check_and_send_reminders(submissions, state, gmail) finally: db.close() state.close() summary = { "created": created, "updated": updated, "deleted": deleted, "skipped": skipped, "reminders_sent": reminders_sent, } logger.info("Sync selesai: %s", summary) return summary