WIP: feature/119_member_user_relation_refactor closes #119 #145

Closed
moritz wants to merge 6 commits from feature/119_member_user_relation_refactor into main
5 changed files with 300 additions and 10 deletions
Showing only changes of commit b7f0060358 - Show all commits

View file

@ -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,
@ -65,7 +65,6 @@ defmodule Mv.Membership.Member do
# First name and last name must not be empty # First name and last name must not be empty
validate present(:first_name) validate present(:first_name)
validate present(:last_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,21 +91,21 @@ 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
@ -122,8 +121,9 @@ defmodule Mv.Membership.Member do
constraints min_length: 1 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 +170,11 @@ 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
end end
calculations do
calculate :email, :string, Mv.Membership.MemberEmailCalculation
end
end end

View file

@ -0,0 +1,19 @@
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} when is_binary(user_email) -> user_email
_ -> record.member_email
end
end)
end
end

View file

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

View 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"
}

View file

@ -0,0 +1,53 @@
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(%{
first_name: "Test",
last_name: "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(%{
first_name: "Update",
last_name: "Test",
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(%{
first_name: "No",
last_name: "Email"
})
# Load the email calculation
member = Ash.load!(member, :email, domain: Membership)
assert member.member_email == nil
assert member.email == nil
end
end
end