Files
accounted/supabase/migrations/20260904215005_sandbox_seed_attempts.sql
T
Mattsson 7c36d471b5 fix(sandbox): use posting engine and recover failed seeds (#2297)
* fix(sandbox): seed through posting engine and recover failed attempts

* test(sandbox): align CI auth schema for anonymous users
2026-09-05 10:42:14 +02:00

120 lines
6.1 KiB
PL/PgSQL

-- A settings row is created before demo data and is not a completion marker.
-- User-scoped coordination also covers the first request, before a company exists.
CREATE TABLE public.sandbox_seed_attempts (
user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
company_id uuid REFERENCES public.companies(id) ON DELETE SET NULL,
attempt_id uuid NOT NULL DEFAULT gen_random_uuid(),
status text NOT NULL CHECK (status IN ('running', 'failed', 'complete')),
started_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX sandbox_seed_attempts_company_id ON public.sandbox_seed_attempts(company_id);
ALTER TABLE public.sandbox_seed_attempts ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.sandbox_seed_attempts FROM anon, authenticated;
GRANT SELECT ON public.sandbox_seed_attempts TO authenticated;
CREATE POLICY sandbox_seed_attempts_read_own ON public.sandbox_seed_attempts
FOR SELECT TO authenticated USING (user_id = (SELECT auth.uid()));
CREATE TRIGGER sandbox_seed_attempts_updated_at BEFORE UPDATE ON public.sandbox_seed_attempts
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE FUNCTION public.claim_sandbox_seed()
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
AS $$
DECLARE
v_user uuid := auth.uid();
v_attempt public.sandbox_seed_attempts%ROWTYPE;
v_company uuid;
v_complete boolean := false;
BEGIN
-- Read the server-owned auth record, not editable user_metadata.
IF v_user IS NULL OR NOT EXISTS (
SELECT 1 FROM auth.users WHERE id = v_user AND is_anonymous IS TRUE
) THEN
RAISE EXCEPTION 'Sandbox requires an anonymous user' USING ERRCODE = '42501';
END IF;
PERFORM pg_advisory_xact_lock(hashtextextended('sandbox-seed:' || v_user::text, 0));
SELECT * INTO v_attempt FROM public.sandbox_seed_attempts WHERE user_id = v_user FOR UPDATE;
IF FOUND THEN
IF v_attempt.status = 'running' AND v_attempt.started_at > now() - interval '15 minutes' THEN
RETURN jsonb_build_object('status', 'busy');
END IF;
v_company := v_attempt.company_id;
v_complete := v_attempt.status = 'complete';
ELSE
SELECT active_company_id INTO v_company FROM public.user_preferences WHERE user_id = v_user;
-- Legacy successful seeds ended by linking all three payroll vouchers.
-- Older or partial demos without that evidence are conservatively replaced.
SELECT EXISTS (
SELECT 1 FROM public.salary_runs sr
JOIN public.journal_entries s ON s.id = sr.salary_entry_id AND s.company_id = sr.company_id AND s.status = 'posted'
JOIN public.journal_entries a ON a.id = sr.avgifter_entry_id AND a.company_id = sr.company_id AND a.status = 'posted'
JOIN public.journal_entries v ON v.id = sr.vacation_entry_id AND v.company_id = sr.company_id AND v.status = 'posted'
WHERE sr.company_id = v_company AND sr.status = 'booked'
) INTO v_complete;
END IF;
IF v_company IS NOT NULL THEN
-- Never archive, adopt or seed a real/shared company, even for an anonymous caller.
IF NOT EXISTS (
SELECT 1 FROM public.companies c JOIN public.company_members cm ON cm.company_id = c.id
WHERE c.id = v_company AND c.created_by = v_user
AND cm.user_id = v_user AND cm.role = 'owner'
) OR EXISTS (
SELECT 1 FROM public.company_members WHERE company_id = v_company AND user_id <> v_user
) OR EXISTS (
SELECT 1 FROM public.company_settings WHERE company_id = v_company AND is_sandbox IS NOT TRUE
) THEN
RAISE EXCEPTION 'Not an exclusively owned sandbox company' USING ERRCODE = '42501';
END IF;
IF v_complete AND EXISTS (
SELECT 1 FROM public.companies c JOIN public.company_settings cs ON cs.company_id = c.id
WHERE c.id = v_company AND c.archived_at IS NULL AND cs.is_sandbox
) THEN
INSERT INTO public.sandbox_seed_attempts(user_id, company_id, status, completed_at)
VALUES (v_user, v_company, 'complete', now())
ON CONFLICT (user_id) DO NOTHING;
INSERT INTO public.user_preferences(user_id, active_company_id) VALUES (v_user, v_company)
ON CONFLICT (user_id) DO UPDATE SET active_company_id = EXCLUDED.active_company_id;
RETURN jsonb_build_object('status', 'complete', 'company_id', v_company);
END IF;
-- Preserve partial entries and documents; ordinary sandbox expiry owns deletion.
UPDATE public.companies SET archived_at = now(), archived_by = v_user
WHERE id = v_company AND archived_at IS NULL;
END IF;
v_company := public.create_company_with_owner('Sandlådan Konsult', 'enskild_firma', true);
-- Make even a crash before the first seed write discoverable by sandbox cleanup.
INSERT INTO public.company_settings(user_id, company_id, is_sandbox)
VALUES (v_user, v_company, true);
INSERT INTO public.sandbox_seed_attempts(user_id, company_id, status)
VALUES (v_user, v_company, 'running')
ON CONFLICT (user_id) DO UPDATE SET company_id = EXCLUDED.company_id,
attempt_id = gen_random_uuid(), status = 'running', started_at = now(), completed_at = NULL
RETURNING * INTO v_attempt;
RETURN jsonb_build_object('status', 'running', 'company_id', v_company, 'attempt_id', v_attempt.attempt_id);
END;
$$;
CREATE FUNCTION public.finish_sandbox_seed(p_attempt_id uuid, p_success boolean)
RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
AS $$
BEGIN
IF auth.uid() IS NULL OR NOT EXISTS (
SELECT 1 FROM auth.users WHERE id = auth.uid() AND is_anonymous IS TRUE
) THEN
RAISE EXCEPTION 'Sandbox requires an anonymous user' USING ERRCODE = '42501';
END IF;
UPDATE public.sandbox_seed_attempts
SET status = CASE WHEN p_success THEN 'complete' ELSE 'failed' END,
completed_at = CASE WHEN p_success THEN now() ELSE NULL END
WHERE user_id = auth.uid() AND attempt_id = p_attempt_id AND status = 'running';
RETURN FOUND;
END;
$$;
REVOKE ALL ON FUNCTION public.claim_sandbox_seed() FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.finish_sandbox_seed(uuid, boolean) FROM PUBLIC, anon;
GRANT EXECUTE ON FUNCTION public.claim_sandbox_seed() TO authenticated;
GRANT EXECUTE ON FUNCTION public.finish_sandbox_seed(uuid, boolean) TO authenticated;
NOTIFY pgrst, 'reload schema';