mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
chore(staging): integrate verified changes
Combine the Android API 36 release, dares loop, RevenueCat documentation correction, and daily engagement features. Resolve migration ordering and preserve both dares expiry and streak reminder operations.
This commit is contained in:
commit
03443fe898
49 files changed
+3094
-569
No files matched your search
@@ -0,0 +1,223 @@
|
||||
-- Dares loop: catalogue review parity, durable dare instances, and outbox notifications.
|
||||
--
|
||||
-- Three concerns land together because the send path depends on all of them:
|
||||
-- 1. `content_status` parity with the legacy catalogue so unreviewed dares stay hidden.
|
||||
-- 2. Text snapshots on `sent_dares` so catalogue edits never rewrite a couple's history.
|
||||
-- 3. Outbox rows for every status transition, which is the whole retention argument.
|
||||
|
||||
-- `to_regtype` resolves through the active search_path, so this creates the type in
|
||||
-- the current schema rather than skipping because an unrelated schema already has it.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regtype('content_review_status') IS NULL THEN
|
||||
CREATE TYPE content_review_status AS ENUM ('unreviewed', 'allowed', 'archived');
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE dare_packs
|
||||
ADD COLUMN IF NOT EXISTS content_status content_review_status NOT NULL DEFAULT 'unreviewed',
|
||||
ADD COLUMN IF NOT EXISTS content_review_reason text,
|
||||
ADD COLUMN IF NOT EXISTS content_reviewed_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS content_reviewed_by uuid REFERENCES profiles(id) ON DELETE SET NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE dares
|
||||
ADD COLUMN IF NOT EXISTS content_status content_review_status NOT NULL DEFAULT 'unreviewed',
|
||||
ADD COLUMN IF NOT EXISTS content_review_reason text,
|
||||
ADD COLUMN IF NOT EXISTS content_reviewed_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS content_reviewed_by uuid REFERENCES profiles(id) ON DELETE SET NULL;
|
||||
--> statement-breakpoint
|
||||
COMMENT ON COLUMN dare_packs.content_status IS
|
||||
'Store-safety review state. Only `allowed` packs are exposed to the mobile catalogue.';
|
||||
--> statement-breakpoint
|
||||
COMMENT ON COLUMN dares.content_status IS
|
||||
'Store-safety review state. Only `allowed` dares are sendable.';
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS dare_packs_content_status_idx
|
||||
ON dare_packs (content_status, is_public, sort_order);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS dares_content_status_idx
|
||||
ON dares (pack_id, content_status, intensity);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- `sent_dares.dare_id` is ON DELETE SET NULL, but `sent_dares_dare_or_custom_check`
|
||||
-- requires one of the two source columns to survive. Deleting a dare that had ever
|
||||
-- been sent therefore aborted with a check violation. Snapshotting the text at send
|
||||
-- time removes the dependency entirely and keeps history stable across content edits.
|
||||
ALTER TABLE sent_dares
|
||||
ADD COLUMN IF NOT EXISTS dare_text_snapshot text,
|
||||
ADD COLUMN IF NOT EXISTS dare_intensity_snapshot integer,
|
||||
ADD COLUMN IF NOT EXISTS submitted_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
||||
--> statement-breakpoint
|
||||
UPDATE sent_dares sd
|
||||
SET dare_text_snapshot = COALESCE(sd.custom_dare_text, d.text),
|
||||
dare_intensity_snapshot = COALESCE(sd.custom_dare_intensity, d.intensity)
|
||||
FROM dares d
|
||||
WHERE d.id = sd.dare_id AND sd.dare_text_snapshot IS NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE sent_dares
|
||||
SET dare_text_snapshot = custom_dare_text,
|
||||
dare_intensity_snapshot = COALESCE(custom_dare_intensity, 1)
|
||||
WHERE dare_text_snapshot IS NULL AND custom_dare_text IS NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares
|
||||
ALTER COLUMN dare_text_snapshot SET NOT NULL,
|
||||
ALTER COLUMN dare_intensity_snapshot SET NOT NULL;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares DROP CONSTRAINT IF EXISTS sent_dares_dare_or_custom_check;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares DROP CONSTRAINT IF EXISTS sent_dares_intensity_snapshot_check;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares
|
||||
ADD CONSTRAINT sent_dares_intensity_snapshot_check
|
||||
CHECK (dare_intensity_snapshot BETWEEN 1 AND 5);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares DROP CONSTRAINT IF EXISTS sent_dares_participants_distinct_check;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares
|
||||
ADD CONSTRAINT sent_dares_participants_distinct_check
|
||||
CHECK (sender_id <> recipient_id);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- `submitted` closes the loop the original design left open: the recipient performs
|
||||
-- the dare, so the recipient reports it done and the sender confirms.
|
||||
ALTER TABLE sent_dares DROP CONSTRAINT IF EXISTS sent_dares_status_check;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE sent_dares
|
||||
ADD CONSTRAINT sent_dares_status_check CHECK (
|
||||
status IN ('pending', 'active', 'submitted', 'completed', 'expired', 'declined', 'cancelled')
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS sent_dares_recipient_status_idx ON sent_dares(recipient_id, status);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS sent_dares_couple_status_idx ON sent_dares(couple_id, status, sent_at DESC);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS sent_dares_sender_sent_at_idx ON sent_dares(sender_id, sent_at DESC);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS sent_dares_expiry_idx
|
||||
ON sent_dares(expires_at) WHERE status IN ('pending', 'active', 'submitted');
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE notification_preferences
|
||||
ADD COLUMN IF NOT EXISTS dares_enabled boolean NOT NULL DEFAULT true;
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE OR REPLACE FUNCTION touch_sent_dare() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = now();
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DROP TRIGGER IF EXISTS sent_dares_touch ON sent_dares;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER sent_dares_touch BEFORE UPDATE ON sent_dares
|
||||
FOR EACH ROW EXECUTE FUNCTION touch_sent_dare();
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Push copy is deliberately generic. Store compliance forbids dare, question,
|
||||
-- response, or message text in a notification preview.
|
||||
CREATE OR REPLACE FUNCTION queue_dare_operations() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_recipients uuid[];
|
||||
v_recipient uuid;
|
||||
v_title text;
|
||||
v_body text;
|
||||
v_event text;
|
||||
BEGIN
|
||||
IF current_setting('sauci.suppress_operations', true) = 'on' THEN RETURN NEW; END IF;
|
||||
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
v_event := 'sent';
|
||||
v_recipients := ARRAY[NEW.recipient_id];
|
||||
v_title := 'New dare';
|
||||
v_body := 'Your partner sent you a dare';
|
||||
ELSIF NEW.status IS DISTINCT FROM OLD.status THEN
|
||||
v_event := NEW.status;
|
||||
CASE NEW.status
|
||||
WHEN 'active' THEN
|
||||
v_recipients := ARRAY[NEW.sender_id];
|
||||
v_title := 'Dare accepted';
|
||||
v_body := 'Your partner accepted your dare';
|
||||
WHEN 'declined' THEN
|
||||
v_recipients := ARRAY[NEW.sender_id];
|
||||
v_title := 'Dare declined';
|
||||
v_body := 'Your partner passed on your dare';
|
||||
WHEN 'submitted' THEN
|
||||
v_recipients := ARRAY[NEW.sender_id];
|
||||
v_title := 'Dare done';
|
||||
v_body := 'Your partner says they completed your dare';
|
||||
WHEN 'completed' THEN
|
||||
v_recipients := ARRAY[NEW.recipient_id];
|
||||
v_title := 'Dare complete';
|
||||
v_body := 'Your partner confirmed your dare';
|
||||
WHEN 'cancelled' THEN
|
||||
v_recipients := ARRAY[NEW.recipient_id];
|
||||
v_title := 'Dare cancelled';
|
||||
v_body := 'Your partner cancelled the dare';
|
||||
WHEN 'expired' THEN
|
||||
-- Both sides lose the dare, so both sides hear about it.
|
||||
v_recipients := ARRAY[NEW.sender_id, NEW.recipient_id];
|
||||
v_title := 'Dare expired';
|
||||
v_body := 'A dare ran out of time';
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END CASE;
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
FOREACH v_recipient IN ARRAY v_recipients LOOP
|
||||
CONTINUE WHEN NOT EXISTS (
|
||||
SELECT 1 FROM profiles p
|
||||
LEFT JOIN notification_preferences np ON np.user_id = p.id
|
||||
WHERE p.id = v_recipient AND COALESCE(np.dares_enabled, true) = true
|
||||
);
|
||||
|
||||
INSERT INTO operations_outbox(kind, dedupe_key, recipient_id, payload)
|
||||
VALUES (
|
||||
'expo',
|
||||
'dare:' || NEW.id || ':' || v_event || ':' || v_recipient,
|
||||
v_recipient,
|
||||
jsonb_build_object(
|
||||
'title', v_title,
|
||||
'body', v_body,
|
||||
'data', jsonb_build_object('type', 'dare', 'event', v_event, 'sent_dare_id', NEW.id)
|
||||
)
|
||||
)
|
||||
ON CONFLICT (dedupe_key) DO NOTHING;
|
||||
END LOOP;
|
||||
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DROP TRIGGER IF EXISTS operations_dare_changed ON sent_dares;
|
||||
--> statement-breakpoint
|
||||
CREATE TRIGGER operations_dare_changed AFTER INSERT OR UPDATE ON sent_dares
|
||||
FOR EACH ROW EXECUTE FUNCTION queue_dare_operations();
|
||||
|
||||
--> statement-breakpoint
|
||||
-- Mirror the review decision already enforced on the legacy catalogue
|
||||
-- (20260828114256_enforce_allowed_catalogue): "Romantic Gestures" passed store-safety
|
||||
-- review, the other seeded packs did not. Only rows still `unreviewed` are touched, so
|
||||
-- this never overrides a later decision made in the admin portal.
|
||||
UPDATE dare_packs
|
||||
SET content_status = 'allowed',
|
||||
content_review_reason = 'Reviewed as non-sexual romantic gestures'
|
||||
WHERE id = 'a1b2c3d4-1111-1111-1111-111111111111'::uuid AND content_status = 'unreviewed';
|
||||
--> statement-breakpoint
|
||||
UPDATE dare_packs
|
||||
SET content_status = 'archived',
|
||||
content_review_reason = 'Pack contains suggestive, sexual, dominance, or fantasy dares'
|
||||
WHERE id IN (
|
||||
'a1b2c3d4-2222-2222-2222-222222222222'::uuid,
|
||||
'a1b2c3d4-3333-3333-3333-333333333333'::uuid,
|
||||
'a1b2c3d4-4444-4444-4444-444444444444'::uuid
|
||||
) AND content_status = 'unreviewed';
|
||||
--> statement-breakpoint
|
||||
UPDATE dares d
|
||||
SET content_status = dp.content_status,
|
||||
content_review_reason = CASE
|
||||
WHEN dp.content_status = 'allowed' THEN 'Reviewed with Romantic Gestures pack'
|
||||
ELSE 'Archived with non-store-safe dare pack'
|
||||
END
|
||||
FROM dare_packs dp
|
||||
WHERE d.pack_id = dp.id AND d.content_status = 'unreviewed' AND dp.content_status <> 'unreviewed';
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -96,29 +96,36 @@
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "7",
|
||||
"when": 1787994900000,
|
||||
"tag": "0013_daily_limit_local_reset",
|
||||
"when": 1787909400000,
|
||||
"tag": "0013_dares_loop",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1787994960000,
|
||||
"tag": "0014_daily_limit_response_index",
|
||||
"when": 1787994900000,
|
||||
"tag": "0014_daily_limit_local_reset",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1788081360000,
|
||||
"tag": "0015_couple_streak_locality",
|
||||
"when": 1787994960000,
|
||||
"tag": "0015_daily_limit_response_index",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1788081360000,
|
||||
"tag": "0016_couple_streak_locality",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1788081420000,
|
||||
"tag": "0016_streak_reminder_preference",
|
||||
"tag": "0017_streak_reminder_preference",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user