defmodule Mv.Membership.Member do use Ash.Resource, domain: Mv.Membership, data_layer: AshPostgres.DataLayer postgres do table "members" repo Mv.Repo end actions do defaults [:read] create :create_member do primary? true argument :properties, {:array, :map} accept [ :first_name, :last_name, :email, :birth_date, :paid, :phone_number, :join_date, :exit_date, :notes, :city, :street, :house_number, :postal_code ] change manage_relationship(:properties, type: :create) end destroy :destroy do primary? true change Ash.Resource.Change.Builtins.cascade_destroy(:properties) end update :update_member do primary? true require_atomic? false argument :properties, {:array, :map} accept [ :first_name, :last_name, :email, :birth_date, :paid, :phone_number, :join_date, :exit_date, :notes, :city, :street, :house_number, :postal_code ] change manage_relationship(:properties, on_match: :update, on_no_match: :create) end end validations do # Required fields are covered by allow_nil? false # 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 validate fn changeset, _ -> birth_date = Ash.Changeset.get_attribute(changeset, :birth_date) if birth_date && Date.compare(birth_date, Date.utc_today()) == :gt do {:error, field: :birth_date, message: "cannot be in the future"} else :ok end end # Join date not in the future validate fn changeset, _ -> join_date = Ash.Changeset.get_attribute(changeset, :join_date) if join_date && Date.compare(join_date, Date.utc_today()) == :gt do {:error, field: :join_date, message: "cannot be in the future"} else :ok end end # Exit date not before join date validate fn changeset, _ -> join_date = Ash.Changeset.get_attribute(changeset, :join_date) exit_date = Ash.Changeset.get_attribute(changeset, :exit_date) if join_date && exit_date && Date.compare(exit_date, join_date) == :lt do {:error, field: :exit_date, message: "cannot be before join date"} else :ok end end # Phone number format (only if set) validate fn changeset, _ -> phone = Ash.Changeset.get_attribute(changeset, :phone_number) if phone && !Regex.match?(~r/^\+?[0-9\- ]{6,20}$/, phone) do {:error, field: :phone_number, message: "is not a valid phone number"} else :ok end end # Postal code format (only if set) validate fn changeset, _ -> postal_code = Ash.Changeset.get_attribute(changeset, :postal_code) if postal_code && !Regex.match?(~r/^\d{5}$/, postal_code) do {:error, field: :postal_code, message: "must consist of 5 digits"} else :ok end end # paid must be boolean if set validate fn changeset, _ -> paid = Ash.Changeset.get_attribute(changeset, :paid) if not is_nil(paid) and not is_boolean(paid) do {:error, field: :paid, message: "must be true or false"} else :ok end end # Email validation with EctoCommons.EmailValidator validate fn changeset, _ -> email = Ash.Changeset.get_attribute(changeset, :email) changeset2 = {%{}, %{email: :string}} |> Ecto.Changeset.cast(%{email: email}, [:email]) |> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow]) if changeset2.valid? do :ok else {:error, field: :email, message: "is not a valid email"} end end end attributes do uuid_v7_primary_key :id attribute :first_name, :string do allow_nil? false constraints min_length: 1 end attribute :last_name, :string do allow_nil? false constraints min_length: 1 end attribute :email, :string do allow_nil? false constraints min_length: 5, max_length: 254 end attribute :birth_date, :date do allow_nil? true end attribute :paid, :boolean do allow_nil? true end attribute :phone_number, :string do allow_nil? true end attribute :join_date, :date do allow_nil? true end attribute :exit_date, :date do allow_nil? true end attribute :notes, :string do allow_nil? true end attribute :city, :string do allow_nil? true end attribute :street, :string do allow_nil? true end attribute :house_number, :string do allow_nil? true end attribute :postal_code, :string do allow_nil? true end end relationships do has_many :properties, Mv.Membership.Property, destination_attribute: :member_id end end