WIP: feature/119_member_user_relation_refactor closes #119 #145
17 changed files with 871 additions and 46 deletions
|
|
@ -21,4 +21,15 @@ defmodule Mv.Accounts do
|
||||||
|
|
||||||
resource Mv.Accounts.Token
|
resource Mv.Accounts.Token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Register a new user with password using AshAuthentication's standard action.
|
||||||
|
This creates a user and the notifier will automatically create a member.
|
||||||
|
"""
|
||||||
|
def register_with_password(params) do
|
||||||
|
# Use AshAuthentication's standard register_with_password action
|
||||||
|
Mv.Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, params)
|
||||||
|
|> Ash.create(domain: __MODULE__)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ defmodule Mv.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Accounts,
|
domain: Mv.Accounts,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
notifiers: [Mv.Accounts.User.MemberCreationNotifier]
|
||||||
|
|
||||||
# authorizers: [Ash.Policy.Authorizer]
|
# authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
|
|
@ -64,11 +65,16 @@ defmodule Mv.Accounts.User do
|
||||||
defaults [:read, :create, :destroy, :update]
|
defaults [:read, :create, :destroy, :update]
|
||||||
|
|
||||||
create :create_user do
|
create :create_user do
|
||||||
accept [:email]
|
accept [:email, :member_id]
|
||||||
|
argument :member, :map
|
||||||
|
change manage_relationship(:member, type: :create)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_user do
|
update :update_user do
|
||||||
accept [:email]
|
require_atomic? false
|
||||||
|
accept [:email, :member_id]
|
||||||
|
argument :member, :map
|
||||||
|
change manage_relationship(:member, on_match: :update, on_no_match: :create)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Admin action for direct password changes in admin panel
|
# Admin action for direct password changes in admin panel
|
||||||
|
|
@ -152,6 +158,7 @@ defmodule Mv.Accounts.User do
|
||||||
identities do
|
identities do
|
||||||
identity :unique_email, [:email]
|
identity :unique_email, [:email]
|
||||||
identity :unique_oidc_id, [:oidc_id]
|
identity :unique_oidc_id, [:oidc_id]
|
||||||
|
identity :unique_member_id, [:member_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
# You can customize this if you wish, but this is a safe default that
|
# You can customize this if you wish, but this is a safe default that
|
||||||
|
|
|
||||||
41
lib/accounts/user/member_creation_notifier.ex
Normal file
41
lib/accounts/user/member_creation_notifier.ex
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
defmodule Mv.Accounts.User.MemberCreationNotifier do
|
||||||
|
@moduledoc """
|
||||||
|
Notifier that automatically creates a member for users who don't have one.
|
||||||
|
This ensures that every user has an associated member profile.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Ash.Notifier
|
||||||
|
|
||||||
|
def notify(%Ash.Notifier.Notification{
|
||||||
|
action: %{name: action_name},
|
||||||
|
resource: Mv.Accounts.User,
|
||||||
|
data: user
|
||||||
|
})
|
||||||
|
when action_name in [:create_user, :register_with_password] do
|
||||||
|
# Only create member if user doesn't have one
|
||||||
|
if is_nil(user.member_id) do
|
||||||
|
create_member_for_user(user)
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify(_), do: :ok
|
||||||
|
|
||||||
|
defp create_member_for_user(user) do
|
||||||
|
member_attrs = %{
|
||||||
|
member_email: user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
case Mv.Membership.create_member(member_attrs) do
|
||||||
|
{:ok, member} ->
|
||||||
|
# Update the user with the new member_id
|
||||||
|
Mv.Accounts.update_user(user, %{member_id: member.id})
|
||||||
|
|
||||||
|
{:error, _error} ->
|
||||||
|
# Log error but don't fail the user creation
|
||||||
|
# In a real application, you might want to handle this differently
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -18,7 +18,7 @@ defmodule Mv.Membership.Member do
|
||||||
accept [
|
accept [
|
||||||
:first_name,
|
:first_name,
|
||||||
:last_name,
|
:last_name,
|
||||||
:email,
|
:member_email,
|
||||||
:birth_date,
|
:birth_date,
|
||||||
:paid,
|
:paid,
|
||||||
:phone_number,
|
:phone_number,
|
||||||
|
|
@ -42,7 +42,7 @@ defmodule Mv.Membership.Member do
|
||||||
accept [
|
accept [
|
||||||
:first_name,
|
:first_name,
|
||||||
:last_name,
|
:last_name,
|
||||||
:email,
|
:member_email,
|
||||||
:birth_date,
|
:birth_date,
|
||||||
:paid,
|
:paid,
|
||||||
:phone_number,
|
:phone_number,
|
||||||
|
|
@ -60,12 +60,7 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
|
|
||||||
validations do
|
validations do
|
||||||
# Required fields are covered by allow_nil? false
|
# All fields are optional - no required validations
|
||||||
|
|
||||||
# First name and last name must not be empty
|
|
||||||
validate present(:first_name)
|
|
||||||
validate present(:last_name)
|
|
||||||
validate present(:email)
|
|
||||||
|
|
||||||
# Birth date not in the future
|
# Birth date not in the future
|
||||||
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||||
|
|
@ -92,38 +87,38 @@ defmodule Mv.Membership.Member do
|
||||||
where: [present(:postal_code)],
|
where: [present(:postal_code)],
|
||||||
message: "must consist of 5 digits"
|
message: "must consist of 5 digits"
|
||||||
|
|
||||||
# Email validation with EctoCommons.EmailValidator
|
# Email validation with EctoCommons.EmailValidator (only for member_email)
|
||||||
validate fn changeset, _ ->
|
validate fn changeset, _ ->
|
||||||
email = Ash.Changeset.get_attribute(changeset, :email)
|
member_email = Ash.Changeset.get_attribute(changeset, :member_email)
|
||||||
|
|
||||||
changeset2 =
|
changeset2 =
|
||||||
{%{}, %{email: :string}}
|
{%{}, %{email: :string}}
|
||||||
|> Ecto.Changeset.cast(%{email: email}, [:email])
|
|> Ecto.Changeset.cast(%{email: member_email}, [:email])
|
||||||
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
|
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
|
||||||
|
|
||||||
if changeset2.valid? do
|
if changeset2.valid? do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
{:error, field: :email, message: "is not a valid email"}
|
{:error, field: :member_email, message: "is not a valid email"}
|
||||||
end
|
end
|
||||||
end
|
end,
|
||||||
|
where: [present(:member_email)]
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_v7_primary_key :id
|
uuid_v7_primary_key :id
|
||||||
|
|
||||||
attribute :first_name, :string do
|
attribute :first_name, :string do
|
||||||
allow_nil? false
|
allow_nil? true
|
||||||
constraints min_length: 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :last_name, :string do
|
attribute :last_name, :string do
|
||||||
allow_nil? false
|
allow_nil? true
|
||||||
constraints min_length: 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :email, :string do
|
# Internal email field for members without users
|
||||||
allow_nil? false
|
attribute :member_email, :string do
|
||||||
|
allow_nil? true
|
||||||
constraints min_length: 5, max_length: 254
|
constraints min_length: 5, max_length: 254
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -170,5 +165,10 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
has_many :properties, Mv.Membership.Property
|
has_many :properties, Mv.Membership.Property
|
||||||
|
has_one :user, Mv.Accounts.User, destination_attribute: :member_id
|
||||||
|
end
|
||||||
|
|
||||||
|
calculations do
|
||||||
|
calculate :email, :string, Mv.Membership.MemberEmailCalculation
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
27
lib/membership/member_email_calculation.ex
Normal file
27
lib/membership/member_email_calculation.ex
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Mv.Membership.MemberEmailCalculation do
|
||||||
|
use Ash.Resource.Calculation
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def load(_query, _opts, _context) do
|
||||||
|
# We need member_email and user.email
|
||||||
|
[:member_email, user: [:email]]
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def calculate(records, _opts, _context) do
|
||||||
|
Enum.map(records, fn record ->
|
||||||
|
case record.user do
|
||||||
|
%{email: user_email} ->
|
||||||
|
# Convert Ash.CiString to string if needed
|
||||||
|
if is_struct(user_email, Ash.CiString) do
|
||||||
|
to_string(user_email)
|
||||||
|
else
|
||||||
|
user_email
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
record.member_email
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -10,6 +10,7 @@ defmodule Mv.Membership do
|
||||||
resource Mv.Membership.Member do
|
resource Mv.Membership.Member do
|
||||||
define :create_member, action: :create_member
|
define :create_member, action: :create_member
|
||||||
define :list_members, action: :read
|
define :list_members, action: :read
|
||||||
|
define :get_member!, action: :read, get_by: [:id]
|
||||||
define :update_member, action: :update_member
|
define :update_member, action: :update_member
|
||||||
define :destroy_member, action: :destroy
|
define :destroy_member, action: :destroy
|
||||||
end
|
end
|
||||||
|
|
|
||||||
2
mix.lock
2
mix.lock
|
|
@ -60,7 +60,7 @@
|
||||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||||
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
"postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
|
||||||
"reactor": {:hex, :reactor, "0.15.6", "d717f9add549b25a089a94c90197718d2d838e35d81dd776b1d81587d4cf2aaa", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "74db98165e3644d86e0f723672d91ceca4339eaa935bcad7e78bf146a46d77b9"},
|
"reactor": {:hex, :reactor, "0.15.6", "d717f9add549b25a089a94c90197718d2d838e35d81dd776b1d81587d4cf2aaa", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "74db98165e3644d86e0f723672d91ceca4339eaa935bcad7e78bf146a46d77b9"},
|
||||||
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
|
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
|
||||||
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
|
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
|
||||||
|
|
|
||||||
17
priv/repo/migrations/20250724161006_add_unique_member_id.exs
Normal file
17
priv/repo/migrations/20250724161006_add_unique_member_id.exs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Mv.Repo.Migrations.AddUniqueMemberId 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(:users, [:member_id], name: "users_unique_member_id_index")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop_if_exists unique_index(:users, [:member_id], name: "users_unique_member_id_index")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Mv.Repo.Migrations.AddMemberEmailToMembers 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
|
||||||
|
rename table(:members), :email, to: :member_email
|
||||||
|
|
||||||
|
alter table(:members) do
|
||||||
|
modify :member_email, :text, null: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:members) do
|
||||||
|
modify :email, :text, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
rename table(:members), :member_email, to: :email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
defmodule Mv.Repo.Migrations.UpdateMemberConstraints 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
|
||||||
|
alter table(:members) do
|
||||||
|
modify :last_name, :text, null: true
|
||||||
|
modify :first_name, :text, null: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:members) do
|
||||||
|
modify :first_name, :text, null: false
|
||||||
|
modify :last_name, :text, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
187
priv/resource_snapshots/repo/members/20250805131958.json
Normal file
187
priv/resource_snapshots/repo/members/20250805131958.json
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
{
|
||||||
|
"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?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "member_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": "F783CF3A9A24DD4D635BD2820236F3DB9A95F7FA6EBA94A3C15A3F054D579999",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mv.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "members"
|
||||||
|
}
|
||||||
187
priv/resource_snapshots/repo/members/20250805151226.json
Normal file
187
priv/resource_snapshots/repo/members/20250805151226.json
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
{
|
||||||
|
"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?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "first_name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "last_name",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_nil?": true,
|
||||||
|
"default": "nil",
|
||||||
|
"generated?": false,
|
||||||
|
"precision": null,
|
||||||
|
"primary_key?": false,
|
||||||
|
"references": null,
|
||||||
|
"scale": null,
|
||||||
|
"size": null,
|
||||||
|
"source": "member_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": "A287EF28CB6E052DB408E8045D54E43CED1252820DD43D261E26E1DDF5CA7966",
|
||||||
|
"identities": [],
|
||||||
|
"multitenancy": {
|
||||||
|
"attribute": null,
|
||||||
|
"global": null,
|
||||||
|
"strategy": null
|
||||||
|
},
|
||||||
|
"repo": "Elixir.Mv.Repo",
|
||||||
|
"schema": null,
|
||||||
|
"table": "members"
|
||||||
|
}
|
||||||
52
test/accounts/empty_member_creation_test.exs
Normal file
52
test/accounts/empty_member_creation_test.exs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
defmodule Mv.Accounts.EmptyMemberCreationTest do
|
||||||
|
use Mv.DataCase, async: true
|
||||||
|
alias Mv.Accounts
|
||||||
|
alias Mv.Membership
|
||||||
|
|
||||||
|
describe "Empty Member Creation" do
|
||||||
|
test "can create completely empty member" do
|
||||||
|
{:ok, member} = Membership.create_member(%{})
|
||||||
|
|
||||||
|
assert member.id
|
||||||
|
assert member.first_name == nil
|
||||||
|
assert member.last_name == nil
|
||||||
|
assert member.member_email == nil
|
||||||
|
assert member.birth_date == nil
|
||||||
|
assert member.paid == nil
|
||||||
|
assert member.phone_number == nil
|
||||||
|
assert member.join_date == nil
|
||||||
|
assert member.exit_date == nil
|
||||||
|
assert member.notes == nil
|
||||||
|
assert member.city == nil
|
||||||
|
assert member.street == nil
|
||||||
|
assert member.house_number == nil
|
||||||
|
assert member.postal_code == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user creation creates empty member automatically" do
|
||||||
|
{:ok, user} = Accounts.create_user(%{email: "test@example.com"})
|
||||||
|
|
||||||
|
# Reload user to get the member_id
|
||||||
|
user = Ash.reload!(user, domain: Mv.Accounts)
|
||||||
|
assert user.member_id
|
||||||
|
|
||||||
|
# Get the created member
|
||||||
|
member = Ash.get!(Mv.Membership.Member, user.member_id, domain: Mv.Membership)
|
||||||
|
|
||||||
|
# Member should be mostly empty except for email
|
||||||
|
assert member.member_email == "test@example.com"
|
||||||
|
assert member.first_name == nil
|
||||||
|
assert member.last_name == nil
|
||||||
|
assert member.birth_date == nil
|
||||||
|
assert member.paid == nil
|
||||||
|
assert member.phone_number == nil
|
||||||
|
assert member.join_date == nil
|
||||||
|
assert member.exit_date == nil
|
||||||
|
assert member.notes == nil
|
||||||
|
assert member.city == nil
|
||||||
|
assert member.street == nil
|
||||||
|
assert member.house_number == nil
|
||||||
|
assert member.postal_code == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
80
test/accounts/password_email_identification_test.exs
Normal file
80
test/accounts/password_email_identification_test.exs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
defmodule Mv.Accounts.PasswordEmailIdentificationTest do
|
||||||
|
use Mv.DataCase, async: true
|
||||||
|
|
||||||
|
describe "Password Email Identification" do
|
||||||
|
test "user can sign in with email and password" do
|
||||||
|
# Get the password strategy
|
||||||
|
strategy = AshAuthentication.Info.strategy!(Mv.Accounts.User, :password)
|
||||||
|
|
||||||
|
# Create a user with password
|
||||||
|
{:ok, user} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :register, %{
|
||||||
|
"email" => "test@example.com",
|
||||||
|
"password" => "password123"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sign in with email
|
||||||
|
{:ok, signed_in_user} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :sign_in, %{
|
||||||
|
"email" => "test@example.com",
|
||||||
|
"password" => "password123"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert signed_in_user.id == user.id
|
||||||
|
assert to_string(signed_in_user.email) == "test@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sign in fails with wrong password" do
|
||||||
|
# Get the password strategy
|
||||||
|
strategy = AshAuthentication.Info.strategy!(Mv.Accounts.User, :password)
|
||||||
|
|
||||||
|
# Create a user with password
|
||||||
|
{:ok, _user} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :register, %{
|
||||||
|
"email" => "test2@example.com",
|
||||||
|
"password" => "password123"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Try to sign in with wrong password
|
||||||
|
{:error, _} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :sign_in, %{
|
||||||
|
"email" => "test2@example.com",
|
||||||
|
"password" => "wrongpassword"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sign in fails with non-existent email" do
|
||||||
|
# Get the password strategy
|
||||||
|
strategy = AshAuthentication.Info.strategy!(Mv.Accounts.User, :password)
|
||||||
|
|
||||||
|
# Try to sign in with non-existent email
|
||||||
|
{:error, _} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :sign_in, %{
|
||||||
|
"email" => "nonexistent@example.com",
|
||||||
|
"password" => "password123"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user gets member automatically created during registration" do
|
||||||
|
# Get the password strategy
|
||||||
|
strategy = AshAuthentication.Info.strategy!(Mv.Accounts.User, :password)
|
||||||
|
|
||||||
|
# Register a user
|
||||||
|
{:ok, user} =
|
||||||
|
AshAuthentication.Strategy.action(strategy, :register, %{
|
||||||
|
"email" => "member@example.com",
|
||||||
|
"password" => "password123"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Reload user to get member_id
|
||||||
|
user = Ash.reload!(user, domain: Mv.Accounts)
|
||||||
|
|
||||||
|
# User should have a member
|
||||||
|
assert user.member_id
|
||||||
|
|
||||||
|
# Member should have the same email
|
||||||
|
member = Ash.get!(Mv.Membership.Member, user.member_id, domain: Mv.Membership)
|
||||||
|
assert member.member_email == "member@example.com"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
71
test/accounts/user_member_integration_test.exs
Normal file
71
test/accounts/user_member_integration_test.exs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
defmodule Mv.Accounts.UserMemberIntegrationTest do
|
||||||
|
use Mv.DataCase, async: true
|
||||||
|
alias Mv.Accounts
|
||||||
|
alias Mv.Membership
|
||||||
|
alias Mv.Accounts.User.MemberCreationNotifier
|
||||||
|
|
||||||
|
describe "User-Member-Relation" do
|
||||||
|
test "ein User kann einem Member zugeordnet werden" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "max@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, user} = Accounts.create_user(%{email: "user1@example.com", member_id: member.id})
|
||||||
|
assert user.member_id == member.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ein Member kann nur einem User zugeordnet werden (unique constraint)" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "anna@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, user1} = Accounts.create_user(%{email: "user2@example.com", member_id: member.id})
|
||||||
|
assert user1.member_id == member.id
|
||||||
|
|
||||||
|
{:error, %Ash.Error.Invalid{errors: errors}} =
|
||||||
|
Accounts.create_user(%{email: "user3@example.com", member_id: member.id})
|
||||||
|
|
||||||
|
assert Enum.any?(errors, fn error ->
|
||||||
|
error.message =~ "already been taken" or error.field == :member_id
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ein User ohne Member ist nicht erlaubt (bei Registrierung/Erstellung)" do
|
||||||
|
# Create user without member first
|
||||||
|
result = Accounts.create_user(%{email: "user4@example.com"})
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, user} ->
|
||||||
|
# User is created but doesn't have member yet
|
||||||
|
assert user.member_id == nil
|
||||||
|
|
||||||
|
# Manually trigger the notifier to simulate automatic member creation
|
||||||
|
notification = %Ash.Notifier.Notification{
|
||||||
|
action: %{name: :create_user},
|
||||||
|
resource: Mv.Accounts.User,
|
||||||
|
data: user
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, _updated_user} = MemberCreationNotifier.notify(notification)
|
||||||
|
|
||||||
|
# Reload user and verify member was created and assigned
|
||||||
|
user = Ash.reload!(user, domain: Mv.Accounts)
|
||||||
|
assert user.member_id, "User should have a member_id assigned after notifier"
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
flunk("User creation should succeed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ein Member kann ohne User existieren" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "lisa@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert member.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
99
test/membership/member_email_test.exs
Normal file
99
test/membership/member_email_test.exs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
defmodule Mv.Membership.MemberEmailTest do
|
||||||
|
use Mv.DataCase, async: true
|
||||||
|
alias Mv.Membership
|
||||||
|
|
||||||
|
describe "member_email and computed email field" do
|
||||||
|
test "email shows member_email when no user is assigned" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "memberonly@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Load the email calculation
|
||||||
|
member = Ash.load!(member, :email, domain: Membership)
|
||||||
|
|
||||||
|
assert member.member_email == "memberonly@example.com"
|
||||||
|
assert member.email == "memberonly@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating member_email updates the computed email when no user is assigned" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "old@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.update_member(member, %{member_email: "new@example.com"})
|
||||||
|
|
||||||
|
# Load the email calculation
|
||||||
|
member = Ash.load!(member, :email, domain: Membership)
|
||||||
|
|
||||||
|
assert member.member_email == "new@example.com"
|
||||||
|
assert member.email == "new@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "member can be created without member_email" do
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{})
|
||||||
|
|
||||||
|
# Load the email calculation with user relationship
|
||||||
|
member = Ash.load!(member, [:email, user: [:email]], domain: Membership)
|
||||||
|
|
||||||
|
assert member.member_email == nil
|
||||||
|
assert member.email == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "email shows user.email when user is assigned" do
|
||||||
|
# Create a member first
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "member@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a user and assign it to the member
|
||||||
|
{:ok, _user} =
|
||||||
|
Mv.Accounts.create_user(%{
|
||||||
|
email: "user@example.com",
|
||||||
|
member_id: member.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Load the email calculation with user relationship
|
||||||
|
member = Ash.load!(member, [:email, user: [:email]], domain: Membership)
|
||||||
|
|
||||||
|
assert member.member_email == "member@example.com"
|
||||||
|
# Should show user email
|
||||||
|
assert member.email == "user@example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "email updates when user email changes" do
|
||||||
|
# Create a member
|
||||||
|
{:ok, member} =
|
||||||
|
Membership.create_member(%{
|
||||||
|
member_email: "member@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a user
|
||||||
|
{:ok, user} =
|
||||||
|
Mv.Accounts.create_user(%{
|
||||||
|
email: "old@example.com",
|
||||||
|
member_id: member.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Load the email calculation initially
|
||||||
|
member = Ash.load!(member, [:email, user: [:email]], domain: Membership)
|
||||||
|
assert member.email == "old@example.com"
|
||||||
|
|
||||||
|
# Update user email
|
||||||
|
{:ok, _updated_user} =
|
||||||
|
Mv.Accounts.update_user(user, %{
|
||||||
|
email: "new@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Reload member and check email calculation
|
||||||
|
member = Ash.reload!(member, domain: Membership)
|
||||||
|
member = Ash.load!(member, [:email, user: [:email]], domain: Membership)
|
||||||
|
|
||||||
|
assert member.email == "new@example.com"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule Mv.Membership.MemberTest do
|
||||||
last_name: "Doe",
|
last_name: "Doe",
|
||||||
birth_date: ~D[1990-01-01],
|
birth_date: ~D[1990-01-01],
|
||||||
paid: true,
|
paid: true,
|
||||||
email: "john@example.com",
|
member_email: "john@example.com",
|
||||||
phone_number: "+49123456789",
|
phone_number: "+49123456789",
|
||||||
join_date: ~D[2020-01-01],
|
join_date: ~D[2020-01-01],
|
||||||
exit_date: nil,
|
exit_date: nil,
|
||||||
|
|
@ -19,28 +19,25 @@ defmodule Mv.Membership.MemberTest do
|
||||||
postal_code: "12345"
|
postal_code: "12345"
|
||||||
}
|
}
|
||||||
|
|
||||||
test "First name is required and must not be empty" do
|
test "First name is optional" do
|
||||||
attrs = Map.put(@valid_attrs, :first_name, "")
|
attrs = Map.delete(@valid_attrs, :first_name)
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
assert {:ok, _member} = Membership.create_member(attrs)
|
||||||
assert error_message(errors, :first_name) =~ "must be present"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Last name is required and must not be empty" do
|
test "Last name is optional" do
|
||||||
attrs = Map.put(@valid_attrs, :last_name, "")
|
attrs = Map.delete(@valid_attrs, :last_name)
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
assert {:ok, _member} = Membership.create_member(attrs)
|
||||||
assert error_message(errors, :last_name) =~ "must be present"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Email is required" do
|
test "Email is optional" do
|
||||||
attrs = Map.put(@valid_attrs, :email, "")
|
attrs = Map.delete(@valid_attrs, :member_email)
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
assert {:ok, _member} = Membership.create_member(attrs)
|
||||||
assert error_message(errors, :email) =~ "must be present"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Email must be valid" do
|
test "Email must be valid if provided" do
|
||||||
attrs = Map.put(@valid_attrs, :email, "test@")
|
attrs = Map.put(@valid_attrs, :member_email, "test@")
|
||||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||||
assert error_message(errors, :email) =~ "is not a valid email"
|
assert error_message(errors, :member_email) =~ "is not a valid email"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Birth date is optional but must not be in the future" do
|
test "Birth date is optional but must not be in the future" do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue