refactor: email sync changes
This commit is contained in:
parent
0f0dbe2ed3
commit
a602108e4f
5 changed files with 102 additions and 138 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
40
lib/mv/email_sync/loader.ex
Normal file
40
lib/mv/email_sync/loader.ex
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue