diff --git a/lib/mv/accounts/user/changes/override_member_email_on_link.ex b/lib/mv/accounts/user/changes/override_member_email_on_link.ex index 7361718..b142a96 100644 --- a/lib/mv/accounts/user/changes/override_member_email_on_link.ex +++ b/lib/mv/accounts/user/changes/override_member_email_on_link.ex @@ -1,47 +1,30 @@ defmodule Mv.Accounts.User.Changes.OverrideMemberEmailOnLink do @moduledoc """ - Overrides member email with user email when linking a user to a member. - - When a user is linked to a member (either during creation or update), - this change ensures that the member's email is updated to match the user's email. - + Overrides member email with user email when linking. User.email is the source of truth when a link is established. - - Uses `around_transaction` to guarantee atomicity - both the user - creation/update and member email override happen in the SAME database transaction. """ use Ash.Resource.Change - alias Mv.EmailSync.Helpers + alias Mv.EmailSync.{Helpers, Loader} @impl true def change(changeset, _opts, context) do - # Skip if already syncing to avoid recursion if Map.get(context, :syncing_email, false) do changeset else - # around_transaction receives the changeset (cs) from Ash - # and a callback that executes the actual database operation - Ash.Changeset.around_transaction(changeset, fn cs, callback -> - result = callback.(cs) - - with {:ok, user} <- Helpers.extract_record(result), - linked_member <- get_linked_member(user) do - Helpers.sync_email_to_linked_record(result, linked_member, user.email) - else - _ -> result - end - end) + override_email(changeset) end end - # Pattern match on nil member_id - no member linked - defp get_linked_member(%{member_id: nil}), do: nil + defp override_email(changeset) do + Ash.Changeset.around_transaction(changeset, fn cs, callback -> + result = callback.(cs) - # Load linked member by ID - defp get_linked_member(%{member_id: id}) do - case Ash.get(Mv.Membership.Member, id) do - {:ok, member} -> member - {:error, _} -> nil - end + with {:ok, user} <- Helpers.extract_record(result), + linked_member <- Loader.get_linked_member(user) do + Helpers.sync_email_to_linked_record(result, linked_member, user.email) + else + _ -> result + end + end) end end diff --git a/lib/mv/accounts/user/changes/sync_email_to_member.ex b/lib/mv/accounts/user/changes/sync_email_to_member.ex index 553ca91..a007dae 100644 --- a/lib/mv/accounts/user/changes/sync_email_to_member.ex +++ b/lib/mv/accounts/user/changes/sync_email_to_member.ex @@ -1,55 +1,32 @@ defmodule Mv.Accounts.User.Changes.SyncEmailToMember do @moduledoc """ Synchronizes user email changes to the linked member. - - When a user's email is updated and the user is linked to a member, - this change automatically updates the member's email to match. - - This ensures bidirectional email synchronization with User.email - as the source of truth. - - Uses `around_transaction` to guarantee atomicity - both the user - and member updates happen in the SAME database transaction. + Uses `around_transaction` for atomicity - both updates in the same transaction. """ use Ash.Resource.Change - alias Mv.EmailSync.Helpers + alias Mv.EmailSync.{Helpers, Loader} @impl true def change(changeset, _opts, context) do cond do - # Skip if already syncing to avoid recursion - Map.get(context, :syncing_email, false) -> - changeset - - # Only proceed if email is actually changing - not Ash.Changeset.changing_attribute?(changeset, :email) -> - changeset - - # Apply the sync logic - true -> - new_email = Ash.Changeset.get_attribute(changeset, :email) - - # around_transaction receives the changeset (cs) from Ash - # and a callback that executes the actual database operation - Ash.Changeset.around_transaction(changeset, fn cs, callback -> - result = callback.(cs) - - with {:ok, user} <- Helpers.extract_record(result), - linked_member <- get_linked_member(user) do - Helpers.sync_email_to_linked_record(result, linked_member, new_email) - else - _ -> result - end - end) + Map.get(context, :syncing_email, false) -> changeset + not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset + true -> sync_email(changeset) end end - defp get_linked_member(%{member_id: nil}), do: nil + defp sync_email(changeset) do + new_email = Ash.Changeset.get_attribute(changeset, :email) - defp get_linked_member(%{member_id: member_id}) do - case Ash.get(Mv.Membership.Member, member_id) do - {:ok, member} -> member - {:error, _} -> nil - end + Ash.Changeset.around_transaction(changeset, fn cs, callback -> + result = callback.(cs) + + with {:ok, user} <- Helpers.extract_record(result), + linked_member <- Loader.get_linked_member(user) do + Helpers.sync_email_to_linked_record(result, linked_member, new_email) + else + _ -> result + end + end) end end diff --git a/lib/mv/email_sync/loader.ex b/lib/mv/email_sync/loader.ex new file mode 100644 index 0000000..ecb1038 --- /dev/null +++ b/lib/mv/email_sync/loader.ex @@ -0,0 +1,40 @@ +defmodule Mv.EmailSync.Loader do + @moduledoc """ + Helper functions for loading linked records in email synchronization. + Centralizes the logic for retrieving related User/Member entities. + """ + + @doc """ + Loads the member linked to a user, returns nil if not linked or on error. + """ + def get_linked_member(%{member_id: nil}), do: nil + + def get_linked_member(%{member_id: id}) do + case Ash.get(Mv.Membership.Member, id) do + {:ok, member} -> member + {:error, _} -> nil + end + end + + @doc """ + Loads the user linked to a member, returns nil if not linked or on error. + """ + def get_linked_user(member) do + case Ash.load(member, :user) do + {:ok, %{user: user}} -> user + {:error, _} -> nil + end + end + + @doc """ + Loads the user linked to a member, returning an error tuple if not linked. + Useful when a link is required for the operation. + """ + def load_linked_user!(member) do + case Ash.load(member, :user) do + {:ok, %{user: user}} when not is_nil(user) -> {:ok, user} + {:ok, _} -> {:error, :no_linked_user} + {:error, _} = error -> error + end + end +end diff --git a/lib/mv/membership/member/changes/override_email_from_user_on_link.ex b/lib/mv/membership/member/changes/override_email_from_user_on_link.ex index b55a696..f8ccd98 100644 --- a/lib/mv/membership/member/changes/override_email_from_user_on_link.ex +++ b/lib/mv/membership/member/changes/override_email_from_user_on_link.ex @@ -1,45 +1,30 @@ defmodule Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink do @moduledoc """ - Overrides member email with user email when linking a member to a user. - - When a member is linked to a user (either during creation or update), - this change ensures that the member's email is updated to match the user's email. - + Overrides member email with user email when linking. User.email is the source of truth when a link is established. - - Uses `around_transaction` to guarantee atomicity - both the member - creation/update and email override happen in the SAME database transaction. """ use Ash.Resource.Change - alias Mv.EmailSync.Helpers + alias Mv.EmailSync.{Helpers, Loader} @impl true def change(changeset, _opts, context) do - # Skip if already syncing to avoid recursion if Map.get(context, :syncing_email, false) do changeset else - # around_transaction receives the changeset (cs) from Ash - # and a callback that executes the actual database operation - Ash.Changeset.around_transaction(changeset, fn cs, callback -> - result = callback.(cs) - - with {:ok, member} <- Helpers.extract_record(result), - {:ok, user} <- load_linked_user(member) do - Helpers.override_with_linked_email(result, user.email) - else - _ -> result - end - end) + override_email(changeset) end end - # Load the linked user, returning error tuple if not linked - defp load_linked_user(member) do - case Ash.load(member, :user) do - {:ok, %{user: user}} when not is_nil(user) -> {:ok, user} - {:ok, _} -> {:error, :no_linked_user} - {:error, _} = error -> error - end + defp override_email(changeset) do + Ash.Changeset.around_transaction(changeset, fn cs, callback -> + result = callback.(cs) + + with {:ok, member} <- Helpers.extract_record(result), + {:ok, user} <- Loader.load_linked_user!(member) do + Helpers.override_with_linked_email(result, user.email) + else + _ -> result + end + end) end end diff --git a/lib/mv/membership/member/changes/sync_email_to_user.ex b/lib/mv/membership/member/changes/sync_email_to_user.ex index eac41d5..8363584 100644 --- a/lib/mv/membership/member/changes/sync_email_to_user.ex +++ b/lib/mv/membership/member/changes/sync_email_to_user.ex @@ -1,53 +1,32 @@ defmodule Mv.Membership.Member.Changes.SyncEmailToUser do @moduledoc """ Synchronizes member email changes to the linked user. - - When a member's email is updated and the member is linked to a user, - this change automatically updates the user's email to match. - - This ensures bidirectional email synchronization. - - Uses `around_transaction` to guarantee atomicity - both the member - and user updates happen in the SAME database transaction. + Uses `around_transaction` for atomicity - both updates in the same transaction. """ use Ash.Resource.Change - alias Mv.EmailSync.Helpers + alias Mv.EmailSync.{Helpers, Loader} @impl true def change(changeset, _opts, context) do cond do - # Skip if already syncing to avoid recursion - Map.get(context, :syncing_email, false) -> - changeset - - # Only proceed if email is actually changing - not Ash.Changeset.changing_attribute?(changeset, :email) -> - changeset - - # Apply the sync logic - true -> - new_email = Ash.Changeset.get_attribute(changeset, :email) - - # around_transaction receives the changeset (cs) from Ash - # and a callback that executes the actual database operation - Ash.Changeset.around_transaction(changeset, fn cs, callback -> - result = callback.(cs) - - with {:ok, member} <- Helpers.extract_record(result), - linked_user <- get_linked_user(member) do - Helpers.sync_email_to_linked_record(result, linked_user, new_email) - else - _ -> result - end - end) + Map.get(context, :syncing_email, false) -> changeset + not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset + true -> sync_email(changeset) end end - # Load the linked user relationship (returns nil if not linked) - defp get_linked_user(member) do - case Ash.load(member, :user) do - {:ok, %{user: user}} -> user - {:error, _} -> nil - end + defp sync_email(changeset) do + new_email = Ash.Changeset.get_attribute(changeset, :email) + + Ash.Changeset.around_transaction(changeset, fn cs, callback -> + result = callback.(cs) + + with {:ok, member} <- Helpers.extract_record(result), + linked_user <- Loader.get_linked_user(member) do + Helpers.sync_email_to_linked_record(result, linked_user, new_email) + else + _ -> result + end + end) end end