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

153 lines
8.9 KiB
Markdown

## 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;
$$;
2) 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;
$$;
3) 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.
4) 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.
5) 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`.
6) 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.
7) 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.