refactor: email sync changes

This commit is contained in:
Moritz 2025-10-17 14:33:25 +02:00
parent 0f0dbe2ed3
commit a602108e4f
Signed by: moritz
GPG key ID: 1020A035E5DD0824
5 changed files with 102 additions and 138 deletions

View file

@ -1,47 +1,30 @@
defmodule Mv.Accounts.User.Changes.OverrideMemberEmailOnLink do defmodule Mv.Accounts.User.Changes.OverrideMemberEmailOnLink do
@moduledoc """ @moduledoc """
Overrides member email with user email when linking a user to a member. Overrides member email with user email when linking.
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.
User.email is the source of truth when a link is established. 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 use Ash.Resource.Change
alias Mv.EmailSync.Helpers alias Mv.EmailSync.{Helpers, Loader}
@impl true @impl true
def change(changeset, _opts, context) do def change(changeset, _opts, context) do
# Skip if already syncing to avoid recursion
if Map.get(context, :syncing_email, false) do if Map.get(context, :syncing_email, false) do
changeset changeset
else else
# around_transaction receives the changeset (cs) from Ash override_email(changeset)
# 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)
end end
end end
# Pattern match on nil member_id - no member linked defp override_email(changeset) do
defp get_linked_member(%{member_id: nil}), do: nil Ash.Changeset.around_transaction(changeset, fn cs, callback ->
result = callback.(cs)
# Load linked member by ID with {:ok, user} <- Helpers.extract_record(result),
defp get_linked_member(%{member_id: id}) do linked_member <- Loader.get_linked_member(user) do
case Ash.get(Mv.Membership.Member, id) do Helpers.sync_email_to_linked_record(result, linked_member, user.email)
{:ok, member} -> member else
{:error, _} -> nil _ -> result
end end
end)
end end
end end

View file

@ -1,55 +1,32 @@
defmodule Mv.Accounts.User.Changes.SyncEmailToMember do defmodule Mv.Accounts.User.Changes.SyncEmailToMember do
@moduledoc """ @moduledoc """
Synchronizes user email changes to the linked member. Synchronizes user email changes to the linked member.
Uses `around_transaction` for atomicity - both updates in the same transaction.
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.
""" """
use Ash.Resource.Change use Ash.Resource.Change
alias Mv.EmailSync.Helpers alias Mv.EmailSync.{Helpers, Loader}
@impl true @impl true
def change(changeset, _opts, context) do def change(changeset, _opts, context) do
cond do cond do
# Skip if already syncing to avoid recursion Map.get(context, :syncing_email, false) -> changeset
Map.get(context, :syncing_email, false) -> not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset
changeset true -> sync_email(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)
end end
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 Ash.Changeset.around_transaction(changeset, fn cs, callback ->
case Ash.get(Mv.Membership.Member, member_id) do result = callback.(cs)
{:ok, member} -> member
{:error, _} -> nil with {:ok, user} <- Helpers.extract_record(result),
end 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
end end

View file

@ -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

View file

@ -1,45 +1,30 @@
defmodule Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink do defmodule Mv.Membership.Member.Changes.OverrideEmailFromUserOnLink do
@moduledoc """ @moduledoc """
Overrides member email with user email when linking a member to a user. Overrides member email with user email when linking.
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.
User.email is the source of truth when a link is established. 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 use Ash.Resource.Change
alias Mv.EmailSync.Helpers alias Mv.EmailSync.{Helpers, Loader}
@impl true @impl true
def change(changeset, _opts, context) do def change(changeset, _opts, context) do
# Skip if already syncing to avoid recursion
if Map.get(context, :syncing_email, false) do if Map.get(context, :syncing_email, false) do
changeset changeset
else else
# around_transaction receives the changeset (cs) from Ash override_email(changeset)
# 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)
end end
end end
# Load the linked user, returning error tuple if not linked defp override_email(changeset) do
defp load_linked_user(member) do Ash.Changeset.around_transaction(changeset, fn cs, callback ->
case Ash.load(member, :user) do result = callback.(cs)
{:ok, %{user: user}} when not is_nil(user) -> {:ok, user}
{:ok, _} -> {:error, :no_linked_user} with {:ok, member} <- Helpers.extract_record(result),
{:error, _} = error -> error {:ok, user} <- Loader.load_linked_user!(member) do
end Helpers.override_with_linked_email(result, user.email)
else
_ -> result
end
end)
end end
end end

View file

@ -1,53 +1,32 @@
defmodule Mv.Membership.Member.Changes.SyncEmailToUser do defmodule Mv.Membership.Member.Changes.SyncEmailToUser do
@moduledoc """ @moduledoc """
Synchronizes member email changes to the linked user. Synchronizes member email changes to the linked user.
Uses `around_transaction` for atomicity - both updates in the same transaction.
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.
""" """
use Ash.Resource.Change use Ash.Resource.Change
alias Mv.EmailSync.Helpers alias Mv.EmailSync.{Helpers, Loader}
@impl true @impl true
def change(changeset, _opts, context) do def change(changeset, _opts, context) do
cond do cond do
# Skip if already syncing to avoid recursion Map.get(context, :syncing_email, false) -> changeset
Map.get(context, :syncing_email, false) -> not Ash.Changeset.changing_attribute?(changeset, :email) -> changeset
changeset true -> sync_email(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)
end end
end end
# Load the linked user relationship (returns nil if not linked) defp sync_email(changeset) do
defp get_linked_user(member) do new_email = Ash.Changeset.get_attribute(changeset, :email)
case Ash.load(member, :user) do
{:ok, %{user: user}} -> user Ash.Changeset.around_transaction(changeset, fn cs, callback ->
{:error, _} -> nil result = callback.(cs)
end
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
end end