Files
Sauci/apps/supabase/migrations/20260117000003_add_couple_streaks.sql
T
jjbrunton 494ed17fe2 Add streaks, scheduled releases, and notification settings
- Couple streaks with milestone notifications
- Scheduled pack releases with admin UI
- Notification preferences screen
- Refactored profile settings screens
- Partner activity notifications
2026-01-17 16:38:47 +00:00

198 lines
6.2 KiB
PL/PgSQL

-- Add gentle streaks system
-- Tracks consecutive days both partners answered questions
-- Celebrates milestones without guilt-tripping on breaks
-- Create couple_streaks table
CREATE TABLE couple_streaks (
couple_id UUID PRIMARY KEY REFERENCES couples(id) ON DELETE CASCADE,
current_streak INTEGER DEFAULT 0,
longest_streak INTEGER DEFAULT 0,
last_active_date DATE,
user1_answered_today BOOLEAN DEFAULT false,
user2_answered_today BOOLEAN DEFAULT false,
streak_celebrated_at INTEGER DEFAULT 0, -- Last milestone that was celebrated
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Enable RLS
ALTER TABLE couple_streaks ENABLE ROW LEVEL SECURITY;
-- RLS policies
CREATE POLICY "Users can view their couple's streaks"
ON couple_streaks FOR SELECT
USING (couple_id = get_auth_user_couple_id());
CREATE POLICY "Super admins can view all streaks"
ON couple_streaks FOR SELECT
USING (is_super_admin());
-- Updated_at trigger
CREATE TRIGGER update_couple_streaks_updated_at
BEFORE UPDATE ON couple_streaks
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Function to update streak when a response is submitted
CREATE OR REPLACE FUNCTION update_couple_streak()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
DECLARE
v_user_id UUID;
v_couple_id UUID;
v_partner_id UUID;
v_today DATE;
v_streak RECORD;
v_user_is_user1 BOOLEAN;
BEGIN
-- Get the user making the response
v_user_id := auth.uid();
-- Skip if no auth context
IF v_user_id IS NULL THEN
RETURN NEW;
END IF;
-- Get user's couple_id
SELECT couple_id INTO v_couple_id
FROM profiles
WHERE id = v_user_id;
-- Skip if user is not in a couple
IF v_couple_id IS NULL THEN
RETURN NEW;
END IF;
v_today := CURRENT_DATE;
-- Get or create streak record
INSERT INTO couple_streaks (couple_id, last_active_date)
VALUES (v_couple_id, v_today)
ON CONFLICT (couple_id) DO NOTHING;
SELECT * INTO v_streak
FROM couple_streaks
WHERE couple_id = v_couple_id
FOR UPDATE;
-- Determine if this user is "user1" or "user2"
-- We use a deterministic method: compare UUIDs
SELECT id INTO v_partner_id
FROM profiles
WHERE couple_id = v_couple_id AND id != v_user_id;
-- User with smaller UUID is user1
v_user_is_user1 := v_user_id < COALESCE(v_partner_id, v_user_id);
-- Check if streak needs to be reset (missed a day)
IF v_streak.last_active_date IS NOT NULL AND v_streak.last_active_date < v_today - 1 THEN
-- Streak is broken - silently reset (no guilt-tripping!)
UPDATE couple_streaks
SET
current_streak = 0,
user1_answered_today = CASE WHEN v_user_is_user1 THEN true ELSE false END,
user2_answered_today = CASE WHEN v_user_is_user1 THEN false ELSE true END,
last_active_date = v_today
WHERE couple_id = v_couple_id;
RETURN NEW;
END IF;
-- Check if this is a new day
IF v_streak.last_active_date IS NULL OR v_streak.last_active_date < v_today THEN
-- New day: reset daily flags, keep streak
UPDATE couple_streaks
SET
user1_answered_today = CASE WHEN v_user_is_user1 THEN true ELSE false END,
user2_answered_today = CASE WHEN v_user_is_user1 THEN false ELSE true END,
last_active_date = v_today
WHERE couple_id = v_couple_id;
ELSE
-- Same day: mark this user as having answered
IF v_user_is_user1 THEN
UPDATE couple_streaks
SET user1_answered_today = true
WHERE couple_id = v_couple_id;
ELSE
UPDATE couple_streaks
SET user2_answered_today = true
WHERE couple_id = v_couple_id;
END IF;
END IF;
-- Check if BOTH partners have now answered today
SELECT * INTO v_streak
FROM couple_streaks
WHERE couple_id = v_couple_id;
IF v_streak.user1_answered_today AND v_streak.user2_answered_today THEN
-- Both answered! Increment streak if not already counted for today
IF v_streak.last_active_date = v_today AND v_streak.current_streak = 0 THEN
-- First time both answered on a new streak
UPDATE couple_streaks
SET
current_streak = 1,
longest_streak = GREATEST(longest_streak, 1)
WHERE couple_id = v_couple_id;
ELSIF v_streak.last_active_date = v_today THEN
-- Already counted for today, nothing to do
NULL;
ELSE
-- Continuing streak from previous day
UPDATE couple_streaks
SET
current_streak = current_streak + 1,
longest_streak = GREATEST(longest_streak, current_streak + 1)
WHERE couple_id = v_couple_id;
END IF;
END IF;
RETURN NEW;
END;
$$;
-- Create trigger on responses table
CREATE TRIGGER on_response_update_streak
AFTER INSERT OR UPDATE OF answer ON responses
FOR EACH ROW
EXECUTE FUNCTION update_couple_streak();
-- Function to reset daily flags (called by cron at UTC midnight)
CREATE OR REPLACE FUNCTION reset_daily_streak_flags()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = 'public'
AS $$
BEGIN
UPDATE couple_streaks
SET
user1_answered_today = false,
user2_answered_today = false
WHERE last_active_date < CURRENT_DATE;
END;
$$;
-- Schedule cron job for daily flag reset (runs at 00:00 UTC)
SELECT cron.schedule(
'reset-daily-streak-flags',
'0 0 * * *',
$$
SELECT reset_daily_streak_flags();
$$
);
-- Schedule cron job for checking streak milestones (runs at 00:05 UTC)
SELECT cron.schedule(
'check-streak-milestones',
'5 0 * * *',
$$
SELECT net.http_post(
url := 'https://ckjcrkjpmhqhiucifukx.supabase.co/functions/v1/check-streak-milestones',
headers := jsonb_build_object('Content-Type', 'application/json'),
body := '{}'::jsonb
)
$$
);