-- ============================================================================ -- Teams Schema: Team Members Table -- Manages team/organization membership -- ============================================================================ -- Create teams schema if not exists CREATE SCHEMA IF NOT EXISTS teams; -- Grant usage GRANT USAGE ON SCHEMA teams TO trading_user; -- ============================================================================ -- TEAM_MEMBERS TABLE -- Tracks membership status and details for users in a tenant -- ============================================================================ CREATE TABLE IF NOT EXISTS teams.team_members ( -- Primary key id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Tenant relationship tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, -- User relationship user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Membership status status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('pending', 'active', 'suspended', 'removed')), -- Member type member_type VARCHAR(20) NOT NULL DEFAULT 'member' CHECK (member_type IN ('owner', 'admin', 'member', 'guest')), -- Join information joined_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, invited_by UUID REFERENCES users.users(id), invitation_id UUID, -- Reference to invitation (if joined via invite) -- Department/Team within organization department VARCHAR(100), job_title VARCHAR(100), -- Member settings settings JSONB NOT NULL DEFAULT '{}'::jsonb, -- Notification preferences for team notifications_enabled BOOLEAN NOT NULL DEFAULT true, -- Audit fields created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, removed_at TIMESTAMPTZ, removed_by UUID REFERENCES users.users(id), removal_reason TEXT, -- Constraints CONSTRAINT uq_team_member UNIQUE (tenant_id, user_id) ); -- ============================================================================ -- INDEXES -- ============================================================================ CREATE INDEX IF NOT EXISTS idx_team_members_tenant_id ON teams.team_members(tenant_id); CREATE INDEX IF NOT EXISTS idx_team_members_user_id ON teams.team_members(user_id); CREATE INDEX IF NOT EXISTS idx_team_members_status ON teams.team_members(status); CREATE INDEX IF NOT EXISTS idx_team_members_member_type ON teams.team_members(member_type); CREATE INDEX IF NOT EXISTS idx_team_members_department ON teams.team_members(tenant_id, department); CREATE INDEX IF NOT EXISTS idx_team_members_joined_at ON teams.team_members(joined_at); -- ============================================================================ -- ROW LEVEL SECURITY -- ============================================================================ ALTER TABLE teams.team_members ENABLE ROW LEVEL SECURITY; -- Policy: Users can only see members in their tenant CREATE POLICY team_members_tenant_isolation ON teams.team_members FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- ============================================================================ -- TRIGGERS -- ============================================================================ -- Auto-update updated_at timestamp CREATE OR REPLACE FUNCTION teams.update_team_members_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_team_members_updated_at BEFORE UPDATE ON teams.team_members FOR EACH ROW EXECUTE FUNCTION teams.update_team_members_timestamp(); -- ============================================================================ -- VIEW: Team members with user details -- ============================================================================ CREATE OR REPLACE VIEW teams.v_team_members AS SELECT tm.id, tm.tenant_id, tm.user_id, u.email, u.first_name, u.last_name, u.display_name, u.avatar_url, u.status AS user_status, tm.status AS membership_status, tm.member_type, tm.department, tm.job_title, tm.joined_at, tm.notifications_enabled, inviter.email AS invited_by_email, inviter.display_name AS invited_by_name, -- Get primary role ( SELECT r.name FROM rbac.user_roles ur JOIN rbac.roles r ON ur.role_id = r.id WHERE ur.user_id = tm.user_id AND ur.tenant_id = tm.tenant_id AND ur.is_primary = true AND ur.is_active = true LIMIT 1 ) AS primary_role FROM teams.team_members tm JOIN users.users u ON tm.user_id = u.id LEFT JOIN users.users inviter ON tm.invited_by = inviter.id WHERE tm.status != 'removed'; -- ============================================================================ -- FUNCTION: Add user to team -- ============================================================================ CREATE OR REPLACE FUNCTION teams.add_team_member( p_tenant_id UUID, p_user_id UUID, p_member_type VARCHAR(20) DEFAULT 'member', p_invited_by UUID DEFAULT NULL, p_invitation_id UUID DEFAULT NULL, p_department VARCHAR(100) DEFAULT NULL, p_job_title VARCHAR(100) DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_member_id UUID; BEGIN INSERT INTO teams.team_members ( tenant_id, user_id, member_type, invited_by, invitation_id, department, job_title, status ) VALUES ( p_tenant_id, p_user_id, p_member_type, p_invited_by, p_invitation_id, p_department, p_job_title, 'active' ) ON CONFLICT (tenant_id, user_id) DO UPDATE SET status = 'active', member_type = EXCLUDED.member_type, department = COALESCE(EXCLUDED.department, teams.team_members.department), job_title = COALESCE(EXCLUDED.job_title, teams.team_members.job_title), removed_at = NULL, removed_by = NULL, removal_reason = NULL, updated_at = CURRENT_TIMESTAMP RETURNING id INTO v_member_id; RETURN v_member_id; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- FUNCTION: Remove user from team -- ============================================================================ CREATE OR REPLACE FUNCTION teams.remove_team_member( p_tenant_id UUID, p_user_id UUID, p_removed_by UUID, p_reason TEXT DEFAULT NULL ) RETURNS BOOLEAN AS $$ BEGIN UPDATE teams.team_members SET status = 'removed', removed_at = CURRENT_TIMESTAMP, removed_by = p_removed_by, removal_reason = p_reason, updated_at = CURRENT_TIMESTAMP WHERE tenant_id = p_tenant_id AND user_id = p_user_id AND status = 'active'; RETURN FOUND; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- GRANTS -- ============================================================================ GRANT SELECT, INSERT, UPDATE, DELETE ON teams.team_members TO trading_user; GRANT SELECT ON teams.v_team_members TO trading_user; GRANT EXECUTE ON FUNCTION teams.add_team_member TO trading_user; GRANT EXECUTE ON FUNCTION teams.remove_team_member TO trading_user; -- ============================================================================ -- COMMENTS -- ============================================================================ COMMENT ON TABLE teams.team_members IS 'Tracks team/organization membership for users'; COMMENT ON COLUMN teams.team_members.member_type IS 'Type of membership: owner, admin, member, guest'; COMMENT ON COLUMN teams.team_members.settings IS 'JSON settings for member-specific preferences'; COMMENT ON VIEW teams.v_team_members IS 'Team members with user details and primary role';