WIP: feature/119_member_user_relation_refactor closes #119 #145
5 changed files with 300 additions and 10 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
19
lib/membership/member_email_calculation.ex
Normal file
19
lib/membership/member_email_calculation.ex
Normal 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
|
||||||
|
|
@ -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
|
||||||
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"
|
||||||
|
}
|
||||||
53
test/membership/member_email_test.exs
Normal file
53
test/membership/member_email_test.exs
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue