refactor: email sync changes

This commit is contained in:
Moritz 2025-10-17 14:33:25 +02:00
parent 0d74384cd5
commit 7b9985b396
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
@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

View file

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

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

View file

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