diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5641528..7b898a8 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -242,4 +242,9 @@ defmodule Mv.Membership.Member do # The relationship is optional (allow_nil? true by default) has_one :user, Mv.Accounts.User end + + # Define identities for upsert operations + identities do + identity :unique_email, [:email] + end end diff --git a/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs new file mode 100644 index 0000000..51b874f --- /dev/null +++ b/priv/repo/migrations/20250926180341_add_unique_email_to_members.exs @@ -0,0 +1,17 @@ +defmodule Mv.Repo.Migrations.AddUniqueEmailToMembers do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + create unique_index(:members, [:email], name: "members_unique_email_index") + end + + def down do + drop_if_exists unique_index(:members, [:email], name: "members_unique_email_index") + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 3ff747e..04f65a8 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -48,7 +48,7 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.update!() -# Create sample members for testing +# Create sample members for testing - use upsert to prevent duplicates for member_attrs <- [ %{ first_name: "Hans", @@ -90,7 +90,8 @@ for member_attrs <- [ house_number: "8" } ] do - Membership.create_member!(member_attrs) + # Use upsert to prevent duplicates based on email + Membership.create_member!(member_attrs, upsert?: true, upsert_identity: :unique_email) end # Create additional users for user-member linking examples @@ -145,18 +146,22 @@ linked_members = [ } ] -# Create the linked members only if the users don't already have members +# Create the linked members - use upsert to prevent duplicates Enum.each(linked_members, fn member_attrs -> user = member_attrs.user member_attrs_without_user = Map.delete(member_attrs, :user) # Check if user already has a member if user.member_id == nil do - # User is free, create member and link - Membership.create_member!(Map.put(member_attrs_without_user, :user, %{id: user.id})) + # User is free, create member and link - use upsert to prevent duplicates + Membership.create_member!( + Map.put(member_attrs_without_user, :user, %{id: user.id}), + upsert?: true, + upsert_identity: :unique_email + ) else - # User already has a member, just create the member without linking - Membership.create_member!(member_attrs_without_user) + # User already has a member, just create the member without linking - use upsert to prevent duplicates + Membership.create_member!(member_attrs_without_user, upsert?: true, upsert_identity: :unique_email) end end) diff --git a/priv/resource_snapshots/repo/members/20250926180341.json b/priv/resource_snapshots/repo/members/20250926180341.json new file mode 100644 index 0000000..3582051 --- /dev/null +++ b/priv/resource_snapshots/repo/members/20250926180341.json @@ -0,0 +1,202 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "first_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "birth_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "paid", + "type": "boolean" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "phone_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "join_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "exit_date", + "type": "date" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "notes", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "city", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "street", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "house_number", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "postal_code", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "5F070A1E5BEE9883AE864FB5A4A5E81F487A1C57D41576C23BAC8D933005D565", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "members_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "members" +} \ No newline at end of file