tasq/.github/prompts/plan-attendanceShiftReminders.prompt.md

8.9 KiB

Plan: Attendance Shift Reminders (pg_cron / Supabase-scheduled)

TL;DR — Use pg_cron in your Supabase Postgres to schedule a SQL job every minute that enqueues due notifications into a Postgres table (scheduled_notifications). A lightweight processor (preferred: Cloud Run worker) will pick up queued notifications, deduplicate via notification_pushes/try_mark_notification_pushed, and call the existing send_fcm function to deliver pushes. This makes scheduling fully server-side and reliable even when user devices are offline.

High-level approach

  • Use Postgres-side scheduling (pg_cron) to run a stored procedure every minute. The stored procedure finds:
    • shifts starting in ~15 minutes (15-minute reminder), and
    • shifts ending now (exact end reminder).
  • The procedure enqueues one row per user per reminder into scheduled_notifications (idempotent insert).
  • A processor service (Cloud Run worker or Edge Function) polls or streams scheduled_notifications rows, marks them using try_mark_notification_pushed(notification_id) and calls send_fcm for delivery.

Why this design?

  • pg_cron keeps scheduling close to data and avoids external cron reliance.
  • Enqueue + processor splits concerns: SQL is simple and reliable; sending FCM is a network task better handled in application code.
  • Deduplication and skip-if-checked logic are enforced before enqueue and again at send-time for safety.

Detailed Steps

  1. Add DB schema and helper functions
  • notification_pushes (dedupe table) — if not present, create: CREATE TABLE IF NOT EXISTS notification_pushes ( notification_id text PRIMARY KEY, created_at timestamptz DEFAULT now(), payload jsonb );

  • scheduled_notifications (queue table): CREATE TABLE IF NOT EXISTS scheduled_notifications ( id bigserial PRIMARY KEY, schedule_id bigint NOT NULL, user_id uuid NOT NULL, notify_type text NOT NULL, -- 'start_15' | 'end' scheduled_for timestamptz NOT NULL, created_at timestamptz DEFAULT now(), processed boolean DEFAULT false, processed_at timestamptz, retry_count int DEFAULT 0, last_error text ); CREATE INDEX ON scheduled_notifications (processed, scheduled_for); ALTER TABLE scheduled_notifications ADD CONSTRAINT IF NOT EXISTS uniq_sched_user_type UNIQUE (schedule_id, user_id, notify_type);

  • try_mark_notification_pushed SQL helper (if not present): CREATE OR REPLACE FUNCTION try_mark_notification_pushed(nid text, payload jsonb DEFAULT '{}'::jsonb) RETURNS boolean LANGUAGE plpgsql AS $$ BEGIN INSERT INTO notification_pushes(notification_id, payload) VALUES (nid, payload); RETURN TRUE; EXCEPTION WHEN unique_violation THEN RETURN FALSE; END;

    ;
    
    
  1. Stored procedure to enqueue due reminders
  • Function: enqueue_due_shift_notifications()

  • Behavior:

    • Compute current UTC now().
    • For 15-minute reminders: find duty_schedules where start_at BETWEEN now() + interval '15 minutes' - interval '30 seconds' AND now() + interval '15 minutes' + interval '30 seconds' (1-minute window). Adjust tolerance as desired.
    • For end reminders: find duty_schedules where end_at BETWEEN now() - interval '30 seconds' AND now() + interval '30 seconds'.
    • For each schedule, resolve the assigned user_id (or multiple participants).
    • Skip enqueue if attendance_logs already shows a check-in for that schedule and remind type is start_15 (you selected this behavior).
    • Insert into scheduled_notifications with ON CONFLICT DO NOTHING to avoid duplicates.
  • Example function outline (PL/pgSQL): CREATE OR REPLACE FUNCTION enqueue_due_shift_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; BEGIN FOR rec IN SELECT id AS schedule_id, assigned_user_id AS user_id, start_at FROM duty_schedules WHERE start_at BETWEEN now() + interval '15 minutes' - interval '30 seconds' AND now() + interval '15 minutes' + interval '30 seconds' AND status = 'active' LOOP IF EXISTS (SELECT 1 FROM attendance_logs al WHERE al.schedule_id = rec.schedule_id AND al.type = 'check_in') THEN CONTINUE; END IF; INSERT INTO scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'start_15', rec.start_at) ON CONFLICT (schedule_id, user_id, notify_type) DO NOTHING; END LOOP;

    FOR rec IN SELECT id AS schedule_id, assigned_user_id AS user_id, end_at FROM duty_schedules WHERE end_at BETWEEN now() - interval '30 seconds' AND now() + interval '30 seconds' AND status = 'active' LOOP INSERT INTO scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'end', rec.end_at) ON CONFLICT (schedule_id, user_id, notify_type) DO NOTHING; END LOOP; END;

    ;
    
    
  1. Schedule pg_cron job
  • Enable pg_cron (Supabase project admin). Schedule: SELECT cron.schedule('shift_reminders_every_min', '*/1 * * * *', $$SELECT enqueue_due_shift_notifications();$$);
  • Monitor cron.job and cron.job_run_details tables for health.
  1. Processor options (choose one)
  • Option A — Cloud Run / long-running worker (recommended):

    • Worker responsibilities:
      • Poll scheduled_notifications WHERE processed = false AND scheduled_for <= now() in batches, or LISTEN for notifications.
      • For each row: build notification_id = 'shift-' || schedule_id || '-' || user_id || '-' || notify_type.
      • Call SELECT try_mark_notification_pushed(notification_id, to_jsonb(row)); if it returns false, skip (already sent).
      • Otherwise, fetch FCM tokens for user_id from fcm_tokens table, and call existing send_fcm edge function (HTTP) with payload {user_id, title, body, data, notification_id} batching tokens if needed.
      • Mark scheduled row processed (processed = true, processed_at = now()). On transient failure increment retry_count and update last_error.
    • Deploy worker to Cloud Run with SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SEND_FCM_URL as secrets.
  • Option B — Supabase Edge Function polling (if you prefer serverless):

    • Deploy an Edge Function process_scheduled_notifications that runs quickly and processes a batch then exits. Use pg_cron to call it via HTTP if pg_http is enabled, or use GitHub Actions scheduled trigger to call it frequently. Less real-time but serverless-friendly.
  • Option C — Postgres direct HTTP (only if pg_http / pg_net available):

    • Extend enqueue function to directly POST to send_fcm endpoint. Not recommended unless your Supabase instance supports and you accept putting external calls in DB.
  1. Security & roles
  • pg_cron runs as the role that created the cron job; ensure it has SELECT on duty_schedules and INSERT on scheduled_notifications.
  • Processor uses SUPABASE_SERVICE_ROLE_KEY for API calls and to read fcm_tokens.
  1. Observability, retries, and failure handling
  • Add a retry_count and last_error to scheduled_notifications (already included) for DB-visible retries.
  • Structured logs in the processor: notification_id, schedule_id, user_id, token_count, status.
  • Monitor scheduled_notifications rows with retry_count > threshold.
  1. Tests & verification
  • Unit tests for enqueue_due_shift_notifications().
  • Manual test: insert a test duty_schedules record 15 minutes ahead; run the function manually; assert a row in scheduled_notifications.
  • Run processor in staging and confirm notification_pushes has the notification_id and device receives the push.

Files / artifacts to add

  • supabase/migrations/xxxx_add_scheduled_notifications.sql — create tables and helper functions above.
  • supabase/migrations/xxxx_add_notification_pushes.sql — if missing (or reuse existing migration).
  • tools/notification_processor/ — worker service (Node or Dart) with Dockerfile for Cloud Run.
  • DEPLOYMENT.md — enable pg_cron, run migrations, deploy processor, and set secrets.

Operational notes

  • Ensure start_at / end_at columns are timestamptz and queries are UTC-aware.
  • Skip-if-checked enforced at enqueue; processor double-checks dedupe before sending.
  • Tune time window and polling batch sizes for expected scale.

Verification checklist

  1. Enable pg_cron and run migrations to create scheduled_notifications and notification_pushes.
  2. Manually call enqueue_due_shift_notifications() and confirm expected rows are created.
  3. Start processor locally against staging DB; verify processing updates notification_pushes and calls send_fcm (mock/staging endpoint).
  4. Validate on-device push delivery and skip-if-checked behavior.

Decision / assumptions

  • Preferred flow: pg_cron enqueue + Cloud Run processor.
  • If DB http extensions are available and approved, a simpler single-step approach is possible but less portable.
  • You asked to skip reminders if user already checked in — implemented at enqueue time.