BEGIN; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'group_join_policy') THEN CREATE TYPE group_join_policy AS ENUM ( 'NOT_ACCEPTING', 'AUTO_ACCEPT', 'APPROVAL_REQUIRED' ); END IF; END $$; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'group_join_request_status') THEN CREATE TYPE group_join_request_status AS ENUM ( 'PENDING', 'APPROVED', 'DENIED', 'CANCELED' ); END IF; END $$; CREATE TABLE IF NOT EXISTS group_settings ( group_id INTEGER PRIMARY KEY REFERENCES households(id) ON DELETE CASCADE, join_policy group_join_policy NOT NULL DEFAULT 'NOT_ACCEPTING', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); INSERT INTO group_settings (group_id, join_policy) SELECT h.id, 'NOT_ACCEPTING'::group_join_policy FROM households h WHERE NOT EXISTS ( SELECT 1 FROM group_settings gs WHERE gs.group_id = h.id ); CREATE TABLE IF NOT EXISTS group_join_requests ( id SERIAL PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status group_join_request_status NOT NULL DEFAULT 'PENDING', decided_by INTEGER REFERENCES users(id) ON DELETE SET NULL, decided_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS uq_group_join_requests_pending ON group_join_requests(group_id, user_id) WHERE status = 'PENDING'; CREATE INDEX IF NOT EXISTS idx_group_join_requests_group ON group_join_requests(group_id); CREATE TABLE IF NOT EXISTS group_invite_links ( id SERIAL PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE, created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(64) NOT NULL UNIQUE, policy group_join_policy NOT NULL DEFAULT 'NOT_ACCEPTING', single_use BOOLEAN NOT NULL DEFAULT FALSE, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_group_invite_links_group_id ON group_invite_links(group_id); CREATE TABLE IF NOT EXISTS group_audit_log ( id SERIAL PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES households(id) ON DELETE CASCADE, actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, actor_role VARCHAR(20), event_type VARCHAR(100) NOT NULL, request_id VARCHAR(128) NOT NULL, ip INET, user_agent TEXT, success BOOLEAN NOT NULL DEFAULT TRUE, error_code VARCHAR(100), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_group_audit_group_created ON group_audit_log(group_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_group_audit_request_id ON group_audit_log(request_id); DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'households' AND indexdef ILIKE 'CREATE UNIQUE INDEX%' AND indexdef ILIKE '%(invite_code)%' ) THEN CREATE UNIQUE INDEX idx_households_invite_code_unique ON households(invite_code); END IF; END $$; ALTER TABLE household_members DROP CONSTRAINT IF EXISTS household_members_role_check; UPDATE household_members SET role = 'member' WHERE role = 'user'; WITH ranked_admins AS ( SELECT hm.id, ROW_NUMBER() OVER ( PARTITION BY hm.household_id ORDER BY hm.joined_at ASC, hm.id ASC ) AS admin_rank FROM household_members hm WHERE hm.role = 'admin' ) UPDATE household_members hm SET role = CASE WHEN ra.admin_rank = 1 THEN 'owner' ELSE 'admin' END FROM ranked_admins ra WHERE hm.id = ra.id; WITH ownerless_households AS ( SELECT h.id AS household_id FROM households h WHERE NOT EXISTS ( SELECT 1 FROM household_members hm WHERE hm.household_id = h.id AND hm.role = 'owner' ) ), first_member AS ( SELECT DISTINCT ON (hm.household_id) hm.id, hm.household_id FROM household_members hm JOIN ownerless_households oh ON oh.household_id = hm.household_id ORDER BY hm.household_id, hm.joined_at ASC, hm.id ASC ) UPDATE household_members hm SET role = 'owner' FROM first_member fm WHERE hm.id = fm.id; ALTER TABLE household_members ADD CONSTRAINT household_members_role_check CHECK (role IN ('owner', 'admin', 'member')); COMMIT;