From 18775e1d950a854519882217170467fdc1acf30e Mon Sep 17 00:00:00 2001
From: codinja1188 <3358152+vasubabu@users.noreply.github.com>
Date: Wed, 6 Mar 2024 20:30:44 +0530
Subject: [PATCH 1/3] refactor: migrate resource metal_organization_member to
 framework

---
 equinix/provider.go                           |   1 -
 equinix/resource_metal_organization_member.go | 285 -----------------
 internal/provider/provider.go                 |   2 +
 .../metal/organization_member/models.go       |  45 +++
 .../metal/organization_member/resource.go     | 297 ++++++++++++++++++
 .../organization_member/resource_schema.go    |  67 ++++
 .../organization_member/resource_test.go      |  53 ++--
 7 files changed, 441 insertions(+), 309 deletions(-)
 delete mode 100644 equinix/resource_metal_organization_member.go
 create mode 100644 internal/resources/metal/organization_member/models.go
 create mode 100644 internal/resources/metal/organization_member/resource.go
 create mode 100644 internal/resources/metal/organization_member/resource_schema.go
 rename equinix/resource_metal_organization_member_acc_test.go => internal/resources/metal/organization_member/resource_test.go (75%)

diff --git a/equinix/provider.go b/equinix/provider.go
index 45dc2f0b9..e1af82f75 100644
--- a/equinix/provider.go
+++ b/equinix/provider.go
@@ -133,7 +133,6 @@ func Provider() *schema.Provider {
 			"equinix_metal_project_api_key":      resourceMetalProjectAPIKey(),
 			"equinix_metal_device":               resourceMetalDevice(),
 			"equinix_metal_device_network_type":  resourceMetalDeviceNetworkType(),
-			"equinix_metal_organization_member":  resourceMetalOrganizationMember(),
 			"equinix_metal_port":                 resourceMetalPort(),
 			"equinix_metal_project":              metal_project.Resource(),
 			"equinix_metal_reserved_ip_block":    resourceMetalReservedIPBlock(),
diff --git a/equinix/resource_metal_organization_member.go b/equinix/resource_metal_organization_member.go
deleted file mode 100644
index 585a6ec44..000000000
--- a/equinix/resource_metal_organization_member.go
+++ /dev/null
@@ -1,285 +0,0 @@
-package equinix
-
-import (
-	"fmt"
-	"log"
-	"path"
-	"strings"
-
-	"github.com/equinix/terraform-provider-equinix/internal/converters"
-
-	equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
-	equinix_schema "github.com/equinix/terraform-provider-equinix/internal/schema"
-
-	"github.com/equinix/terraform-provider-equinix/internal/config"
-
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/packethost/packngo"
-)
-
-type member struct {
-	*packngo.Member
-	*packngo.Invitation
-}
-
-func (m *member) isMember() bool {
-	return m.Member != nil
-}
-
-func (m *member) isInvitation() bool {
-	return m.Invitation != nil
-}
-
-func resourceMetalOrganizationMember() *schema.Resource {
-	return &schema.Resource{
-		Create: resourceMetalOrganizationMemberCreate,
-		Read:   resourceMetalOrganizationMemberRead,
-		Delete: resourceMetalOrganizationMemberDelete,
-		Importer: &schema.ResourceImporter{
-			State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
-				parts := strings.Split(d.Id(), ":")
-				invitee := parts[0]
-				orgID := parts[1]
-				d.SetId(d.Id())
-				d.Set("invitee", invitee)
-				d.Set("organization_id", orgID)
-				if err := resourceMetalOrganizationMemberRead(d, meta); err != nil {
-					return nil, err
-				}
-				if d.Id() == "" {
-					return nil, fmt.Errorf("Member %s does not exist in organization %s.", invitee, orgID)
-				}
-				return []*schema.ResourceData{d}, nil
-			},
-		},
-
-		Schema: map[string]*schema.Schema{
-			"invitee": {
-				Type:         schema.TypeString,
-				Description:  "The email address of the user to invite",
-				Required:     true,
-				ForceNew:     true,
-				ValidateFunc: validation.StringIsNotEmpty,
-			},
-			"invited_by": {
-				Type:        schema.TypeString,
-				Description: "The user id of the user that sent the invitation (only known in the invitation stage)",
-				Computed:    true,
-			},
-			"organization_id": {
-				Type:         schema.TypeString,
-				Description:  "The organization to invite the user to",
-				Required:     true,
-				ForceNew:     true,
-				ValidateFunc: validation.StringIsNotEmpty,
-			},
-			"projects_ids": {
-				Type:        schema.TypeSet,
-				Description: "Project IDs the member has access to within the organization. If the member is an 'owner', the projects list should be empty.",
-				Required:    true,
-				Elem: &schema.Schema{
-					Type:         schema.TypeString,
-					ValidateFunc: validation.StringIsNotEmpty,
-				},
-				// TODO: Update should be supported. packngo.InvitationService does not offer an Update	method.
-				ForceNew: true,
-			},
-			"nonce": {
-				Type:        schema.TypeString,
-				Description: "The nonce for the invitation (only known in the invitation stage)",
-				Computed:    true,
-			},
-			"message": {
-				Type:        schema.TypeString,
-				Description: "A message to the invitee (only used during the invitation stage)",
-				Optional:    true,
-				ForceNew:    true,
-			},
-			"created": {
-				Type:        schema.TypeString,
-				Description: "When the invitation was created (only known in the invitation stage)",
-				Computed:    true,
-			},
-			"updated": {
-				Type:        schema.TypeString,
-				Description: "When the invitation was updated (only known in the invitation stage)",
-				Computed:    true,
-			},
-			"roles": {
-				Type:        schema.TypeSet,
-				Description: "Organization roles (owner, collaborator, limited_collaborator, billing)",
-				Required:    true,
-				Elem:        &schema.Schema{Type: schema.TypeString},
-				// TODO: Update should be supported. packngo.InvitationService does not offer an Update	method.
-				ForceNew: true,
-			},
-			"state": {
-				Type:        schema.TypeString,
-				Description: "The state of the membership ('invited' when an invitation is open, 'active' when the user is an organization member)",
-				Computed:    true,
-			},
-		},
-	}
-}
-
-func resourceMetalOrganizationMemberCreate(d *schema.ResourceData, meta interface{}) error {
-	client := meta.(*config.Config).Metal
-
-	email := d.Get("invitee").(string)
-	createRequest := &packngo.InvitationCreateRequest{
-		Invitee:     email,
-		Roles:       converters.IfArrToStringArr(d.Get("roles").(*schema.Set).List()),
-		ProjectsIDs: converters.IfArrToStringArr(d.Get("projects_ids").(*schema.Set).List()),
-		Message:     strings.TrimSpace(d.Get("message").(string)),
-	}
-
-	orgID := d.Get("organization_id").(string)
-	_, _, err := client.Invitations.Create(orgID, createRequest, nil)
-	if err != nil {
-		return equinix_errors.FriendlyError(err)
-	}
-
-	d.SetId(fmt.Sprintf("%s:%s", email, orgID))
-
-	return resourceMetalOrganizationMemberRead(d, meta)
-}
-
-func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) {
-	for _, mbr := range members {
-		if mbr.User.Email == invitee {
-			return &member{Member: &mbr}, nil
-		}
-	}
-
-	for _, inv := range invitations {
-		if inv.Invitee == invitee {
-			return &member{Invitation: &inv}, nil
-		}
-	}
-	return nil, fmt.Errorf("member not found")
-}
-
-func resourceMetalOrganizationMemberRead(d *schema.ResourceData, meta interface{}) error {
-	client := meta.(*config.Config).Metal
-	parts := strings.Split(d.Id(), ":")
-	invitee := parts[0]
-	orgID := parts[1]
-
-	listOpts := &packngo.ListOptions{Includes: []string{"user"}}
-	invitations, _, err := client.Invitations.List(orgID, listOpts)
-	if err != nil {
-		err = equinix_errors.FriendlyError(err)
-		// If the org was destroyed, mark as gone.
-		if equinix_errors.IsNotFound(err) {
-			d.SetId("")
-			return nil
-		}
-		return err
-	}
-
-	members, _, err := client.Members.List(orgID, &packngo.GetOptions{Includes: []string{"user"}})
-	if err != nil {
-		err = equinix_errors.FriendlyError(err)
-		// If the org was destroyed, mark as gone.
-		if equinix_errors.IsNotFound(err) {
-			d.SetId("")
-			return nil
-		}
-		return err
-	}
-	member, err := findMember(invitee, members, invitations)
-	if !d.IsNewResource() && err != nil {
-		log.Printf("[WARN] Could not find member %s in organization, removing from state", d.Id())
-		d.SetId("")
-		return nil
-	}
-
-	if member.isMember() {
-		projectIDs := []string{}
-		for _, project := range member.Member.Projects {
-			projectIDs = append(projectIDs, path.Base(project.URL))
-		}
-		return equinix_schema.SetMap(d, map[string]interface{}{
-			"state":           "active",
-			"roles":           converters.StringArrToIfArr(member.Member.Roles),
-			"projects_ids":    converters.StringArrToIfArr(projectIDs),
-			"organization_id": path.Base(member.Member.Organization.URL),
-		})
-	} else if member.isInvitation() {
-		projectIDs := []string{}
-		for _, project := range member.Invitation.Projects {
-			projectIDs = append(projectIDs, path.Base(project.Href))
-		}
-		return equinix_schema.SetMap(d, map[string]interface{}{
-			"state":           "invited",
-			"organization_id": path.Base(member.Invitation.Organization.Href),
-			"roles":           member.Invitation.Roles,
-			"projects_ids":    projectIDs,
-			"created":         member.Invitation.CreatedAt.String(),
-			"updated":         member.Invitation.UpdatedAt.String(),
-			"nonce":           member.Invitation.Nonce,
-			"invited_by":      path.Base(member.Invitation.InvitedBy.Href),
-		})
-	}
-	return fmt.Errorf("got an invalid member object")
-}
-
-func resourceMetalOrganizationMemberDelete(d *schema.ResourceData, meta interface{}) error {
-	client := meta.(*config.Config).Metal
-
-	listOpts := &packngo.ListOptions{Includes: []string{"user"}}
-	invitations, _, err := client.Invitations.List(d.Get("organization_id").(string), listOpts)
-	if err != nil {
-		err = equinix_errors.FriendlyError(err)
-		// If the org was destroyed, mark as gone.
-		if equinix_errors.IsNotFound(err) {
-			d.SetId("")
-			return nil
-		}
-		return err
-	}
-
-	orgID := d.Get("organization_id").(string)
-	org, _, err := client.Organizations.Get(orgID, &packngo.GetOptions{Includes: []string{"members", "members.user"}})
-	if err != nil {
-		err = equinix_errors.FriendlyError(err)
-		// If the org was destroyed, mark as gone.
-		if equinix_errors.IsNotFound(err) {
-			d.SetId("")
-			return nil
-		}
-		return err
-	}
-
-	member, err := findMember(d.Get("invitee").(string), org.Members, invitations)
-	if err != nil {
-		d.SetId("")
-		return nil
-	}
-
-	if member.isMember() {
-		_, err = client.Members.Delete(orgID, member.Member.ID)
-		if err != nil {
-			err = equinix_errors.FriendlyError(err)
-			// If the member was deleted, mark as gone.
-			if equinix_errors.IsNotFound(err) {
-				d.SetId("")
-				return nil
-			}
-			return err
-		}
-	} else if member.isInvitation() {
-		_, err = client.Invitations.Delete(member.Invitation.ID)
-		if err != nil {
-			err = equinix_errors.FriendlyError(err)
-			// If the invitation was deleted, mark as gone.
-			if equinix_errors.IsNotFound(err) {
-				d.SetId("")
-				return nil
-			}
-			return err
-		}
-	}
-	return nil
-}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index e682f63c3..f5677c607 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -8,6 +8,7 @@ import (
 	metalconnection "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection"
 	metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway"
 	metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization"
+	metalorganizationmember "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization_member"
 	metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key"
 	metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key"
 	equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation"
@@ -116,6 +117,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res
 		metalsshkey.NewResource,
 		metalconnection.NewResource,
 		metalorganization.NewResource,
+		metalorganizationmember.NewResource,
 	}
 }
 
diff --git a/internal/resources/metal/organization_member/models.go b/internal/resources/metal/organization_member/models.go
new file mode 100644
index 000000000..e220bbfcd
--- /dev/null
+++ b/internal/resources/metal/organization_member/models.go
@@ -0,0 +1,45 @@
+package organizationmember
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+
+	"github.com/packethost/packngo"
+)
+
+type ResourceModel struct {
+	ID             types.String `tfsdk:"id"`
+	Invitee        types.String `tfsdk:"invitee"`
+	InvitedBy      types.String `tfsdk:"invited_by"`
+	OrganizationID types.String `tfsdk:"organization_id"`
+	ProjectsIDs    types.Set    `tfsdk:"projects_ids"`
+	Nonce          types.String `tfsdk:"nonce"`
+	Message        types.String `tfsdk:"message"`
+	Created        types.String `tfsdk:"created"`
+	Updated        types.String `tfsdk:"updated"`
+	Roles          types.Set    `tfsdk:"roles"`
+	State          types.String `tfsdk:"state"`
+}
+
+func (m *ResourceModel) parse(ctx context.Context, org *packngo.Invitation) diag.Diagnostics {
+	var diags diag.Diagnostics
+	m.Invitee = types.StringValue(org.Invitee)
+	m.InvitedBy = types.StringValue(org.InvitedBy.Href)
+	m.OrganizationID = types.StringValue(org.ID)
+
+	ProjectList, _ := types.SetValueFrom(ctx, types.StringType, org.Projects)
+	m.ProjectsIDs = ProjectList
+
+	m.Nonce = types.StringValue(org.Nonce)
+	m.Created = types.StringValue(org.CreatedAt.String())
+	m.Updated = types.StringValue(org.UpdatedAt.String())
+
+	rolesList, _ := types.SetValueFrom(ctx, types.StringType, org.Roles)
+	m.Roles = rolesList
+	m.State = types.StringValue("active")
+
+	m.ID = types.StringValue(org.ID)
+	return diags
+}
diff --git a/internal/resources/metal/organization_member/resource.go b/internal/resources/metal/organization_member/resource.go
new file mode 100644
index 000000000..5e2857676
--- /dev/null
+++ b/internal/resources/metal/organization_member/resource.go
@@ -0,0 +1,297 @@
+package organizationmember
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"path"
+	"strings"
+
+	equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
+	"github.com/equinix/terraform-provider-equinix/internal/framework"
+	tfpath "github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+	"github.com/packethost/packngo"
+)
+
+type member struct {
+	*packngo.Member
+	*packngo.Invitation
+}
+
+func (m *member) isMember() bool {
+	return m.Member != nil
+}
+
+func (m *member) isInvitation() bool {
+	return m.Invitation != nil
+}
+
+func NewResource() resource.Resource {
+	return &Resource{
+		BaseResource: framework.NewBaseResource(
+			framework.BaseResourceConfig{
+				Name:   "equinix_metal_organization_member",
+				Schema: GetResourceSchema(),
+			},
+		),
+	}
+}
+
+type Resource struct {
+	framework.BaseResource
+}
+
+func (r *Resource) ImportState(
+	ctx context.Context,
+	req resource.ImportStateRequest,
+	resp *resource.ImportStateResponse,
+) {
+	tflog.Debug(ctx, "importer Organization")
+
+	parts := strings.Split(req.ID, ":")
+	if len(parts) != 2 {
+		return
+
+	}
+	invitee := parts[0]
+	orgID := parts[1]
+
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, tfpath.Root("invitee"), invitee)...)
+	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, tfpath.Root("organization_id"), orgID)...)
+}
+
+func (r *Resource) Create(
+	ctx context.Context,
+	req resource.CreateRequest,
+	resp *resource.CreateResponse,
+) {
+	r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
+	client := r.Meta.Metal
+
+	var plan ResourceModel
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	email := plan.Invitee.ValueString()
+
+	roles := make([]string, 0)
+	for _, elem := range plan.Roles.Elements() {
+		if strValue, ok := elem.(types.String); ok {
+
+			if !strValue.IsNull() {
+				roles = append(roles, strValue.ValueString())
+			}
+		}
+	}
+	projects := make([]string, 0)
+	for _, elem := range plan.ProjectsIDs.Elements() {
+		if strValue, ok := elem.(types.String); ok {
+			projects = append(projects, strValue.ValueString())
+		}
+	}
+	createRequest := &packngo.InvitationCreateRequest{
+		Invitee:     email,
+		Roles:       roles,
+		ProjectsIDs: projects,
+		Message:     strings.TrimSpace(plan.Message.ValueString()),
+	}
+
+	orgID := plan.OrganizationID.ValueString()
+	invitationRequest, _, err := client.Invitations.Create(orgID, createRequest, nil)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Failed to create Organizations",
+			equinix_errors.FriendlyError(err).Error(),
+		)
+		return
+	}
+
+	// Parse API response into the Terraform state
+	plan.parse(ctx, invitationRequest)
+
+	// Set state to fully populated data
+	resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+}
+
+func (r *Resource) Read(
+	ctx context.Context,
+	req resource.ReadRequest,
+	resp *resource.ReadResponse,
+) {
+	tflog.Debug(ctx, "Read Organization")
+	r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
+	client := r.Meta.Metal
+
+	// Retrieve values from plan
+	var data ResourceModel
+	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	invitee := data.Invitee.ValueString()
+	orgID := data.OrganizationID.ValueString()
+
+	listOpts := &packngo.ListOptions{Includes: []string{"user"}}
+	invitations, _, err := client.Invitations.List(orgID, listOpts)
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			data.OrganizationID = basetypes.NewStringNull()
+			return
+		}
+		return
+	}
+
+	members, _, err := client.Members.List(orgID, &packngo.GetOptions{Includes: []string{"user"}})
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			data.OrganizationID = basetypes.NewStringNull()
+			return
+		}
+		return
+	}
+	member, err := findMember(invitee, members, invitations)
+	if err != nil {
+		log.Printf("[WARN] Could not find member %s in organization, removing from state", data.OrganizationID)
+		data.OrganizationID = basetypes.NewStringNull()
+		return
+	}
+
+	if member.isMember() {
+		projectsList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Projects)
+		if diags.HasError() {
+			return
+		}
+		data.ProjectsIDs = projectsList
+		data.State = types.StringValue("active")
+
+		rolesList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Roles)
+		if diags.HasError() {
+			return
+		}
+		data.Roles = rolesList
+		data.OrganizationID = types.StringValue(member.Member.Organization.URL)
+
+		// data.Created = types.StringValue(member.CreatedAt.String())
+		// data.Updated = types.StringValue(member.UpdatedAt.String())
+	} else if member.isInvitation() {
+		projectsList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Projects)
+		if diags.HasError() {
+			return
+		}
+		data.ProjectsIDs = projectsList
+		data.State = types.StringValue("active")
+
+		rolesList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Roles)
+		if diags.HasError() {
+			return
+		}
+		data.Roles = rolesList
+		data.OrganizationID = types.StringValue(member.Member.Organization.URL)
+		data.Created = types.StringValue(member.Invitation.CreatedAt.String())
+		data.Updated = types.StringValue(member.Invitation.UpdatedAt.String())
+		data.Nonce = types.StringValue(member.Invitation.Nonce)
+		data.InvitedBy = types.StringValue(path.Base(member.Invitation.InvitedBy.Href))
+	}
+
+	// Set state to fully populated data
+	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) {
+	for _, mbr := range members {
+		if mbr.User.Email == invitee {
+			return &member{Member: &mbr}, nil
+		}
+	}
+
+	for _, inv := range invitations {
+		if inv.Invitee == invitee {
+			return &member{Invitation: &inv}, nil
+		}
+	}
+	return nil, fmt.Errorf("member not found")
+}
+
+func (r *Resource) Delete(
+	ctx context.Context,
+	req resource.DeleteRequest,
+	resp *resource.DeleteResponse,
+) {
+	tflog.Debug(ctx, "Delete Organization")
+	r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta)
+	client := r.Meta.Metal
+
+	var data ResourceModel
+
+	listOpts := &packngo.ListOptions{Includes: []string{"user"}}
+	invitations, _, err := client.Invitations.List(data.OrganizationID.ValueString(), listOpts)
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			data.OrganizationID = types.StringNull()
+			return
+		}
+		return
+	}
+
+	org, _, err := client.Organizations.Get(data.OrganizationID.ValueString(), &packngo.GetOptions{Includes: []string{"members", "members.user"}})
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			data.OrganizationID = types.StringNull()
+			return
+		}
+		return
+	}
+
+	member, err := findMember(data.Invitee.ValueString(), org.Members, invitations)
+	if err != nil {
+		data.OrganizationID = types.StringNull()
+		return
+	}
+
+	if member.isMember() {
+		_, err = client.Members.Delete(data.OrganizationID.ValueString(), member.Member.ID)
+		if err != nil {
+			err = equinix_errors.FriendlyError(err)
+			// If the member was deleted, mark as gone.
+			if equinix_errors.IsNotFound(err) {
+				data.OrganizationID = types.StringNull()
+				return
+			}
+			return
+		}
+	} else if member.isInvitation() {
+		_, err = client.Invitations.Delete(member.Invitation.ID)
+		if err != nil {
+			err = equinix_errors.FriendlyError(err)
+			// If the invitation was deleted, mark as gone.
+			if equinix_errors.IsNotFound(err) {
+				data.OrganizationID = types.StringNull()
+				return
+			}
+			return
+		}
+	}
+}
+
+func (r *Resource) Update(
+	ctx context.Context,
+	req resource.UpdateRequest,
+	resp *resource.UpdateResponse,
+) {
+	// This resource does not support updates
+}
diff --git a/internal/resources/metal/organization_member/resource_schema.go b/internal/resources/metal/organization_member/resource_schema.go
new file mode 100644
index 000000000..7808020bd
--- /dev/null
+++ b/internal/resources/metal/organization_member/resource_schema.go
@@ -0,0 +1,67 @@
+package organizationmember
+
+import (
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+func GetResourceSchema() *schema.Schema {
+	return &schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: "The unique identifier for the organization member.",
+				Computed:    true,
+			},
+			"invitee": schema.StringAttribute{
+				Description: "The email address of the user to invite",
+				Required:    true,
+				Validators: []validator.String{
+					stringvalidator.LengthAtLeast(1),
+				},
+			},
+			"invited_by": schema.StringAttribute{
+				Description: "The user id of the user that sent the invitation (only known in the invitation stage)",
+				Computed:    true,
+			},
+			"organization_id": schema.StringAttribute{
+				Description: "The organization to invite the user to",
+				Required:    true,
+				Validators: []validator.String{
+					stringvalidator.LengthAtLeast(1),
+				},
+			},
+			"projects_ids": schema.SetAttribute{
+				Description: "Project IDs the member has access to within the organization. If the member is an 'owner', the projects list should be empty.",
+				Required:    true,
+				ElementType: types.StringType,
+			},
+			"nonce": schema.StringAttribute{
+				Description: "The nonce for the invitation (only known in the invitation stage)",
+				Computed:    true,
+			},
+			"message": schema.StringAttribute{
+				Description: "A message to the invitee (only used during the invitation stage)",
+				Optional:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "When the invitation was created (only known in the invitation stage)",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "When the invitation was updated (only known in the invitation stage)",
+				Computed:    true,
+			},
+			"roles": schema.SetAttribute{
+				ElementType: types.StringType,
+				Description: "Organization roles (owner, collaborator, limited_collaborator, billing)",
+				Required:    true,
+			},
+			"state": schema.StringAttribute{
+				Description: "The state of the membership ('invited' when an invitation is open, 'active' when the user is an organization member)",
+				Computed:    true,
+			},
+		},
+	}
+}
diff --git a/equinix/resource_metal_organization_member_acc_test.go b/internal/resources/metal/organization_member/resource_test.go
similarity index 75%
rename from equinix/resource_metal_organization_member_acc_test.go
rename to internal/resources/metal/organization_member/resource_test.go
index 9e201437c..98a466bcd 100644
--- a/equinix/resource_metal_organization_member_acc_test.go
+++ b/internal/resources/metal/organization_member/resource_test.go
@@ -1,9 +1,10 @@
-package equinix
+package organizationmember_test
 
 import (
 	"fmt"
 	"testing"
 
+	"github.com/equinix/terraform-provider-equinix/internal/acceptance"
 	"github.com/equinix/terraform-provider-equinix/internal/config"
 	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -15,11 +16,9 @@ func TestAccResourceMetalOrganizationMember_owner(t *testing.T) {
 	rInt := acctest.RandInt()
 	org := &packngo.Organization{}
 	resource.ParallelTest(t, resource.TestCase{
-		PreCheck:                 func() { testAccPreCheck(t) },
-		ExternalProviders:        testExternalProviders,
-		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories,
-		// TODO: CheckDestroy: testAccMetalOrganizationMemberCheckDestroyed,
-		CheckDestroy: nil,
+		PreCheck:                 func() { acceptance.TestAccPreCheckMetal(t); acceptance.TestAccPreCheckProviderConfigured(t) },
+		ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories,
+		CheckDestroy:             testAccMetalOrganizationCheckDestroyed,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccResourceMetalOrganizationMember_basic(rInt),
@@ -35,13 +34,6 @@ func TestAccResourceMetalOrganizationMember_owner(t *testing.T) {
 				}),
 				ImportState: true,
 			},
-			/*
-				{
-					ResourceName: "equinix_metal_organization_member.owner",
-					Config:       testAccResourceMetalOrganizationMember_basic(rInt) + testAccResourceMetalOrganizationMember_owner(),
-					ExpectError:  regexp.MustCompile("User is already a member of the Organization"),
-				},
-			*/
 			{
 				Config: testAccResourceMetalOrganizationMember_basic(rInt),
 			},
@@ -53,11 +45,9 @@ func TestAccResourceMetalOrganizationMember_basic(t *testing.T) {
 	rInt := acctest.RandInt()
 	org := &packngo.Organization{}
 	resource.ParallelTest(t, resource.TestCase{
-		PreCheck:                 func() { testAccPreCheck(t) },
-		ExternalProviders:        testExternalProviders,
-		ProtoV5ProviderFactories: testAccProtoV5ProviderFactories,
-		// TODO: CheckDestroy: testAccMetalOrganizationMemberCheckDestroyed,
-		CheckDestroy: nil,
+		PreCheck:                 func() { acceptance.TestAccPreCheckMetal(t); acceptance.TestAccPreCheckProviderConfigured(t) },
+		ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories,
+		CheckDestroy:             nil,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccResourceMetalOrganizationMember_basic(rInt),
@@ -104,21 +94,21 @@ resource "equinix_metal_project" "test" {
 }
 
 func testAccResourceMetalOrganizationMember_owner() string {
-	return fmt.Sprintf(`
+	return `
 	resource "equinix_metal_organization_member" "owner" {
-		invitee = "/* TODO: Add org owner email or token owner email here */"
+		invitee = "tfacc.testing.member@equinixmetal.com"
 		roles = ["owner"]
 		projects_ids = []
 		organization_id = equinix_metal_organization.test.id
 	}
-	`)
+	`
 }
 
 func testAccResourceMetalOrganizationMember_member() string {
 	return `
 resource "equinix_metal_organization_member" "member" {
     invitee = "tfacc.testing.member@equinixmetal.com"
-    roles = ["limited_collaborator"]
+	roles = ["limited_collaborator"]
     projects_ids = [equinix_metal_project.test.id]
     organization_id = equinix_metal_organization.test.id
 	message = "This invitation was sent by the github.com/equinix/terraform-provider-equinix acceptance tests to test equinix_metal_organization_member resources."
@@ -126,6 +116,23 @@ resource "equinix_metal_organization_member" "member" {
 `
 }
 
+// invitee = "/* TODO: Add org owner email or token owner email here */"
+// invitee = "tfacc.testing.member@equinixmetal.com"
+func testAccMetalOrganizationCheckDestroyed(s *terraform.State) error {
+	client := acceptance.TestAccProvider.Meta().(*config.Config).Metal
+
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != "equinix_metal_organization" {
+			continue
+		}
+		if _, _, err := client.Organizations.Get(rs.Primary.ID, nil); err == nil {
+			return fmt.Errorf("Metal Organization still exists")
+		}
+	}
+
+	return nil
+}
+
 func testAccMetalOrganizationExists(n string, org *packngo.Organization) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		rs, ok := s.RootModule().Resources[n]
@@ -136,7 +143,7 @@ func testAccMetalOrganizationExists(n string, org *packngo.Organization) resourc
 			return fmt.Errorf("No Record ID is set")
 		}
 
-		client := testAccProvider.Meta().(*config.Config).Metal
+		client := acceptance.TestAccProvider.Meta().(*config.Config).Metal
 
 		foundOrg, _, err := client.Organizations.Get(rs.Primary.ID, &packngo.GetOptions{Includes: []string{"address", "primary_owner"}})
 		if err != nil {

From 8762d89195c3a5f93beba9354022635b09d0f575 Mon Sep 17 00:00:00 2001
From: codinja1188 <3358152+vasubabu@users.noreply.github.com>
Date: Thu, 14 Mar 2024 21:46:49 +0530
Subject: [PATCH 2/3] refactor: improved the code

---
 .../metal/organization_member/models.go       |  58 +++++--
 .../metal/organization_member/resource.go     | 158 +++++++++++-------
 .../organization_member/resource_test.go      |   6 +-
 3 files changed, 144 insertions(+), 78 deletions(-)

diff --git a/internal/resources/metal/organization_member/models.go b/internal/resources/metal/organization_member/models.go
index e220bbfcd..a09e6ec03 100644
--- a/internal/resources/metal/organization_member/models.go
+++ b/internal/resources/metal/organization_member/models.go
@@ -2,11 +2,10 @@ package organizationmember
 
 import (
 	"context"
+	"path"
 
+	"github.com/hashicorp/terraform-plugin-framework/diag"
 	"github.com/hashicorp/terraform-plugin-framework/types"
-	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
-
-	"github.com/packethost/packngo"
 )
 
 type ResourceModel struct {
@@ -23,23 +22,50 @@ type ResourceModel struct {
 	State          types.String `tfsdk:"state"`
 }
 
-func (m *ResourceModel) parse(ctx context.Context, org *packngo.Invitation) diag.Diagnostics {
+// func (m *ResourceModel) parse(ctx context.Context, invitee string, members []packngo.Member, invitations []packngo.Invitation) diag.Diagnostics {
+func (m *ResourceModel) parse(ctx context.Context, member *member) diag.Diagnostics {
 	var diags diag.Diagnostics
-	m.Invitee = types.StringValue(org.Invitee)
-	m.InvitedBy = types.StringValue(org.InvitedBy.Href)
-	m.OrganizationID = types.StringValue(org.ID)
 
-	ProjectList, _ := types.SetValueFrom(ctx, types.StringType, org.Projects)
-	m.ProjectsIDs = ProjectList
+	if member.isMember() {
+		projectsList, diag := types.SetValueFrom(ctx, types.StringType, member.Member.Projects)
+		if diag.HasError() {
+			return diag
+		}
+		m.ProjectsIDs = projectsList
+		m.State = types.StringValue("active")
+
+		rolesList, diag := types.SetValueFrom(ctx, types.StringType, member.Member.Roles)
+		if diag.HasError() {
+			return diag
+		}
+		m.Roles = rolesList
+		m.OrganizationID = types.StringValue(member.Member.Organization.URL)
+
+	} else if member.isInvitation() {
+
+		projectsList, diag := types.SetValueFrom(ctx, types.StringType, member.Invitation.Projects)
+		if diag.HasError() {
+			return diag
+		}
+		m.ProjectsIDs = projectsList
+
+		m.State = types.StringValue("invited")
 
-	m.Nonce = types.StringValue(org.Nonce)
-	m.Created = types.StringValue(org.CreatedAt.String())
-	m.Updated = types.StringValue(org.UpdatedAt.String())
+		rolesList, diag := types.SetValueFrom(ctx, types.StringType, member.Invitation.Roles)
+		if diag.HasError() {
+			return diag
+		}
+		m.Roles = rolesList
 
-	rolesList, _ := types.SetValueFrom(ctx, types.StringType, org.Roles)
-	m.Roles = rolesList
-	m.State = types.StringValue("active")
+		//m.OrganizationID = types.StringValue(member.Invitation.Organization.Href)
+		m.OrganizationID = types.StringValue(path.Base(member.Invitation.Organization.Href))
+		m.Created = types.StringValue(member.Invitation.CreatedAt.String())
+		m.Updated = types.StringValue(member.Invitation.UpdatedAt.String())
+		m.Nonce = types.StringValue(member.Invitation.Nonce)
 
-	m.ID = types.StringValue(org.ID)
+		//m.InvitedBy = types.StringValue(member.Invitation.InvitedBy.Href)
+		m.InvitedBy = types.StringValue(path.Base(member.Invitation.InvitedBy.Href))
+		m.ID = types.StringValue(member.Invitation.ID)
+	}
 	return diags
 }
diff --git a/internal/resources/metal/organization_member/resource.go b/internal/resources/metal/organization_member/resource.go
index 5e2857676..7b7c7ec88 100644
--- a/internal/resources/metal/organization_member/resource.go
+++ b/internal/resources/metal/organization_member/resource.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"log"
-	"path"
 	"strings"
 
 	equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors"
@@ -78,7 +77,7 @@ func (r *Resource) Create(
 		return
 	}
 
-	email := plan.Invitee.ValueString()
+	invite := plan.Invitee.ValueString()
 
 	roles := make([]string, 0)
 	for _, elem := range plan.Roles.Elements() {
@@ -89,31 +88,80 @@ func (r *Resource) Create(
 			}
 		}
 	}
+
 	projects := make([]string, 0)
 	for _, elem := range plan.ProjectsIDs.Elements() {
 		if strValue, ok := elem.(types.String); ok {
 			projects = append(projects, strValue.ValueString())
 		}
 	}
+
 	createRequest := &packngo.InvitationCreateRequest{
-		Invitee:     email,
+		Invitee:     invite,
 		Roles:       roles,
 		ProjectsIDs: projects,
 		Message:     strings.TrimSpace(plan.Message.ValueString()),
 	}
 
 	orgID := plan.OrganizationID.ValueString()
-	invitationRequest, _, err := client.Invitations.Create(orgID, createRequest, nil)
+	_, _, err := client.Invitations.Create(orgID, createRequest, nil)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Failed to create invitation",
+			err.Error(),
+		)
+		return
+	}
+
+	listOpts := &packngo.ListOptions{Includes: []string{"user"}}
+	invitations, _, err := client.Invitations.List(orgID, listOpts)
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			plan.OrganizationID = basetypes.NewStringNull()
+			return
+		}
+		resp.Diagnostics.AddError(
+			"Failed to list invitations",
+			err.Error(),
+		)
+		return
+	}
+
+	members, _, err := client.Members.List(orgID, listOpts)
+	if err != nil {
+		err = equinix_errors.FriendlyError(err)
+		// If the org was destroyed, mark as gone.
+		if equinix_errors.IsNotFound(err) {
+			return
+		}
+		resp.Diagnostics.AddError(
+			"Failed to List members",
+			err.Error(),
+		)
+		return
+	}
+
+	member, err := findMember(invite, members, invitations)
 	if err != nil {
+		log.Printf("[WARN] Could not find member %s in organization, removing from state", plan.OrganizationID)
+		plan.OrganizationID = basetypes.NewStringNull()
 		resp.Diagnostics.AddError(
-			"Failed to create Organizations",
-			equinix_errors.FriendlyError(err).Error(),
+			"Failed to find members",
+			err.Error(),
 		)
 		return
 	}
 
 	// Parse API response into the Terraform state
-	plan.parse(ctx, invitationRequest)
+	if member != nil {
+		diags := plan.parse(ctx, member)
+		if diags.HasError() {
+			resp.Diagnostics.Append(diags...)
+			return
+		}
+	}
 
 	// Set state to fully populated data
 	resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
@@ -147,6 +195,10 @@ func (r *Resource) Read(
 			data.OrganizationID = basetypes.NewStringNull()
 			return
 		}
+		resp.Diagnostics.AddError(
+			"Failed to list invitations",
+			err.Error(),
+		)
 		return
 	}
 
@@ -158,6 +210,10 @@ func (r *Resource) Read(
 			data.OrganizationID = basetypes.NewStringNull()
 			return
 		}
+		resp.Diagnostics.AddError(
+			"Failed to list members",
+			err.Error(),
+		)
 		return
 	}
 	member, err := findMember(invitee, members, invitations)
@@ -167,62 +223,15 @@ func (r *Resource) Read(
 		return
 	}
 
-	if member.isMember() {
-		projectsList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Projects)
-		if diags.HasError() {
-			return
-		}
-		data.ProjectsIDs = projectsList
-		data.State = types.StringValue("active")
-
-		rolesList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Roles)
-		if diags.HasError() {
-			return
-		}
-		data.Roles = rolesList
-		data.OrganizationID = types.StringValue(member.Member.Organization.URL)
-
-		// data.Created = types.StringValue(member.CreatedAt.String())
-		// data.Updated = types.StringValue(member.UpdatedAt.String())
-	} else if member.isInvitation() {
-		projectsList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Projects)
-		if diags.HasError() {
-			return
-		}
-		data.ProjectsIDs = projectsList
-		data.State = types.StringValue("active")
-
-		rolesList, diags := types.SetValueFrom(context.Background(), types.StringType, member.Member.Roles)
-		if diags.HasError() {
-			return
-		}
-		data.Roles = rolesList
-		data.OrganizationID = types.StringValue(member.Member.Organization.URL)
-		data.Created = types.StringValue(member.Invitation.CreatedAt.String())
-		data.Updated = types.StringValue(member.Invitation.UpdatedAt.String())
-		data.Nonce = types.StringValue(member.Invitation.Nonce)
-		data.InvitedBy = types.StringValue(path.Base(member.Invitation.InvitedBy.Href))
+	diags := data.parse(ctx, member)
+	if diags.HasError() {
+		resp.Diagnostics.Append(diags...)
+		return
 	}
-
 	// Set state to fully populated data
 	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
 }
 
-func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) {
-	for _, mbr := range members {
-		if mbr.User.Email == invitee {
-			return &member{Member: &mbr}, nil
-		}
-	}
-
-	for _, inv := range invitations {
-		if inv.Invitee == invitee {
-			return &member{Invitation: &inv}, nil
-		}
-	}
-	return nil, fmt.Errorf("member not found")
-}
-
 func (r *Resource) Delete(
 	ctx context.Context,
 	req resource.DeleteRequest,
@@ -243,6 +252,10 @@ func (r *Resource) Delete(
 			data.OrganizationID = types.StringNull()
 			return
 		}
+		resp.Diagnostics.AddError(
+			"Failed to list invitations",
+			err.Error(),
+		)
 		return
 	}
 
@@ -254,6 +267,11 @@ func (r *Resource) Delete(
 			data.OrganizationID = types.StringNull()
 			return
 		}
+
+		resp.Diagnostics.AddError(
+			"Failed to get Organizations",
+			err.Error(),
+		)
 		return
 	}
 
@@ -272,6 +290,10 @@ func (r *Resource) Delete(
 				data.OrganizationID = types.StringNull()
 				return
 			}
+			resp.Diagnostics.AddError(
+				"Failed to delete member",
+				err.Error(),
+			)
 			return
 		}
 	} else if member.isInvitation() {
@@ -283,6 +305,11 @@ func (r *Resource) Delete(
 				data.OrganizationID = types.StringNull()
 				return
 			}
+
+			resp.Diagnostics.AddError(
+				"Failed to delete invitation",
+				err.Error(),
+			)
 			return
 		}
 	}
@@ -295,3 +322,18 @@ func (r *Resource) Update(
 ) {
 	// This resource does not support updates
 }
+
+func findMember(invitee string, members []packngo.Member, invitations []packngo.Invitation) (*member, error) {
+	for _, mbr := range members {
+		if mbr.User.Email == invitee {
+			return &member{Member: &mbr}, nil
+		}
+	}
+
+	for _, inv := range invitations {
+		if inv.Invitee == invitee {
+			return &member{Invitation: &inv}, nil
+		}
+	}
+	return nil, fmt.Errorf("member not found")
+}
diff --git a/internal/resources/metal/organization_member/resource_test.go b/internal/resources/metal/organization_member/resource_test.go
index 98a466bcd..41f53942d 100644
--- a/internal/resources/metal/organization_member/resource_test.go
+++ b/internal/resources/metal/organization_member/resource_test.go
@@ -47,7 +47,7 @@ func TestAccResourceMetalOrganizationMember_basic(t *testing.T) {
 	resource.ParallelTest(t, resource.TestCase{
 		PreCheck:                 func() { acceptance.TestAccPreCheckMetal(t); acceptance.TestAccPreCheckProviderConfigured(t) },
 		ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories,
-		CheckDestroy:             nil,
+		CheckDestroy:             testAccMetalOrganizationCheckDestroyed,
 		Steps: []resource.TestStep{
 			{
 				Config: testAccResourceMetalOrganizationMember_basic(rInt),
@@ -96,7 +96,7 @@ resource "equinix_metal_project" "test" {
 func testAccResourceMetalOrganizationMember_owner() string {
 	return `
 	resource "equinix_metal_organization_member" "owner" {
-		invitee = "tfacc.testing.member@equinixmetal.com"
+		invitee = "/* TODO: Add org owner email or token owner email here */"
 		roles = ["owner"]
 		projects_ids = []
 		organization_id = equinix_metal_organization.test.id
@@ -116,8 +116,6 @@ resource "equinix_metal_organization_member" "member" {
 `
 }
 
-// invitee = "/* TODO: Add org owner email or token owner email here */"
-// invitee = "tfacc.testing.member@equinixmetal.com"
 func testAccMetalOrganizationCheckDestroyed(s *terraform.State) error {
 	client := acceptance.TestAccProvider.Meta().(*config.Config).Metal
 

From 5a947d4438bb8c5200a2009d4ed5fb09756decd5 Mon Sep 17 00:00:00 2001
From: Charles Treatman <ctreatman@equinix.com>
Date: Tue, 19 Mar 2024 14:36:02 -0500
Subject: [PATCH 3/3] convert invitation/member projects to lists of project
 IDs

---
 .../metal/organization_member/models.go          | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/internal/resources/metal/organization_member/models.go b/internal/resources/metal/organization_member/models.go
index a09e6ec03..9a9b16ce8 100644
--- a/internal/resources/metal/organization_member/models.go
+++ b/internal/resources/metal/organization_member/models.go
@@ -27,7 +27,12 @@ func (m *ResourceModel) parse(ctx context.Context, member *member) diag.Diagnost
 	var diags diag.Diagnostics
 
 	if member.isMember() {
-		projectsList, diag := types.SetValueFrom(ctx, types.StringType, member.Member.Projects)
+		projectIDs := []string{}
+		for _, project := range member.Member.Projects {
+			projectIDs = append(projectIDs, path.Base(project.URL))
+		}
+
+		projectsList, diag := types.SetValueFrom(ctx, types.StringType, projectIDs)
 		if diag.HasError() {
 			return diag
 		}
@@ -42,8 +47,11 @@ func (m *ResourceModel) parse(ctx context.Context, member *member) diag.Diagnost
 		m.OrganizationID = types.StringValue(member.Member.Organization.URL)
 
 	} else if member.isInvitation() {
-
-		projectsList, diag := types.SetValueFrom(ctx, types.StringType, member.Invitation.Projects)
+		projectIDs := []string{}
+		for _, project := range member.Invitation.Projects {
+			projectIDs = append(projectIDs, path.Base(project.Href))
+		}
+		projectsList, diag := types.SetValueFrom(ctx, types.StringType, projectIDs)
 		if diag.HasError() {
 			return diag
 		}
@@ -57,13 +65,11 @@ func (m *ResourceModel) parse(ctx context.Context, member *member) diag.Diagnost
 		}
 		m.Roles = rolesList
 
-		//m.OrganizationID = types.StringValue(member.Invitation.Organization.Href)
 		m.OrganizationID = types.StringValue(path.Base(member.Invitation.Organization.Href))
 		m.Created = types.StringValue(member.Invitation.CreatedAt.String())
 		m.Updated = types.StringValue(member.Invitation.UpdatedAt.String())
 		m.Nonce = types.StringValue(member.Invitation.Nonce)
 
-		//m.InvitedBy = types.StringValue(member.Invitation.InvitedBy.Href)
 		m.InvitedBy = types.StringValue(path.Base(member.Invitation.InvitedBy.Href))
 		m.ID = types.StringValue(member.Invitation.ID)
 	}