diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 1d6d96e..3a0fa5b 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -39,7 +39,6 @@ defmodule Mv.Membership.Member do require Ash.Query import Ash.Expr - require Logger # Module constants @member_search_limit 10 @@ -74,9 +73,6 @@ defmodule Mv.Membership.Member do create :create_member do primary? true - - # Note: Custom validation function cannot be done atomically (queries DB for required custom fields) - # In Ash 3.0, require_atomic? is not available for create actions, but the validation will still work # Custom field values can be created along with member argument :custom_field_values, {:array, :map} # Allow user to be passed as argument for relationship management @@ -421,32 +417,6 @@ defmodule Mv.Membership.Member do {:error, field: :email, message: "is not a valid email"} end end - - # Validate required custom fields - validate fn changeset, _ -> - provided_values = provided_custom_field_values(changeset) - - case Mv.Membership.list_required_custom_fields() do - {:ok, required_custom_fields} -> - missing_fields = missing_required_fields(required_custom_fields, provided_values) - - if Enum.empty?(missing_fields) do - :ok - else - build_custom_field_validation_error(missing_fields) - end - - {:error, error} -> - Logger.error( - "Failed to load custom fields for validation: #{inspect(error)}. Required field validation cannot be performed." - ) - - {:error, - field: :custom_field_values, - message: - "Unable to validate required custom fields. Please try again or contact support."} - end - end end attributes do @@ -1162,127 +1132,4 @@ defmodule Mv.Membership.Member do query end end - - # Extracts provided custom field values from changeset - # Handles both create (from argument) and update (from existing data) scenarios - defp provided_custom_field_values(changeset) do - custom_field_values_arg = Ash.Changeset.get_argument(changeset, :custom_field_values) - - if is_nil(custom_field_values_arg) do - extract_existing_values(changeset.data) - else - extract_argument_values(custom_field_values_arg) - end - end - - # Extracts custom field values from existing member data (update scenario) - defp extract_existing_values(member_data) do - case Ash.load(member_data, :custom_field_values) do - {:ok, %{custom_field_values: existing_values}} -> - Enum.reduce(existing_values, %{}, &extract_value_from_cfv/2) - - _ -> - %{} - end - end - - # Extracts value from a CustomFieldValue struct - defp extract_value_from_cfv(cfv, acc) do - value = extract_union_value(cfv.value) - Map.put(acc, cfv.custom_field_id, value) - end - - # Extracts value from union type (map or direct value) - defp extract_union_value(value) when is_map(value), do: Map.get(value, :value) - defp extract_union_value(value), do: value - - # Extracts custom field values from provided argument (create/update scenario) - defp extract_argument_values(custom_field_values_arg) do - Enum.reduce(custom_field_values_arg, %{}, &extract_value_from_arg/2) - end - - # Extracts value from argument map - defp extract_value_from_arg(cfv, acc) do - custom_field_id = Map.get(cfv, "custom_field_id") - value_map = Map.get(cfv, "value", %{}) - actual_value = extract_value_from_map(value_map) - Map.put(acc, custom_field_id, actual_value) - end - - # Extracts value from map, supporting both "value" and "_union_value" keys - # Also handles Ash.Union structs (which have atom keys :value and :type) - # Uses cond instead of || to preserve false values - defp extract_value_from_map(value_map) do - cond do - # Handle Ash.Union struct - check if it's a struct with __struct__ == Ash.Union - match?({:ok, Ash.Union}, Map.fetch(value_map, :__struct__)) -> - Map.get(value_map, :value) - - # Handle map with string keys - Map.has_key?(value_map, "value") -> - Map.get(value_map, "value") - - Map.has_key?(value_map, "_union_value") -> - Map.get(value_map, "_union_value") - - # Handle map with atom keys - Map.has_key?(value_map, :value) -> - Map.get(value_map, :value) - - true -> - nil - end - end - - # Finds which required custom fields are missing from provided values - defp missing_required_fields(required_custom_fields, provided_values) do - Enum.filter(required_custom_fields, fn cf -> - value = Map.get(provided_values, cf.id) - not value_present?(value, cf.value_type) - end) - end - - # Builds validation error message for missing required custom fields - defp build_custom_field_validation_error(missing_fields) do - # Sort missing fields alphabetically for consistent error messages - sorted_missing_fields = Enum.sort_by(missing_fields, & &1.name) - missing_names = Enum.map_join(sorted_missing_fields, ", ", & &1.name) - - {:error, - field: :custom_field_values, - message: - Gettext.dgettext(MvWeb.Gettext, "default", "Required custom fields missing: %{fields}", - fields: missing_names - )} - end - - # Helper function to check if a value is present for a given custom field type - # Boolean: false is valid, only nil is invalid - # String: nil or empty strings are invalid - # Integer: nil or empty strings are invalid, 0 is valid - # Date: nil or empty strings are invalid - # Email: nil or empty strings are invalid - defp value_present?(nil, _type), do: false - - defp value_present?(value, :boolean), do: not is_nil(value) - - defp value_present?(value, :string), do: is_binary(value) and String.trim(value) != "" - - defp value_present?(value, :integer) when is_integer(value), do: true - - defp value_present?(value, :integer) when is_binary(value), do: String.trim(value) != "" - - defp value_present?(_value, :integer), do: false - - defp value_present?(value, :date) when is_struct(value, Date), do: true - - defp value_present?(value, :date) when is_binary(value), do: String.trim(value) != "" - - defp value_present?(_value, :date), do: false - - defp value_present?(value, :email) when is_binary(value), do: String.trim(value) != "" - - defp value_present?(_value, :email), do: false - - defp value_present?(_value, _type), do: false end diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index 4917c7c..f5a708b 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -21,9 +21,6 @@ defmodule Mv.Membership do use Ash.Domain, extensions: [AshAdmin.Domain, AshPhoenix] - require Ash.Query - import Ash.Expr - admin do show? true end @@ -128,29 +125,6 @@ defmodule Mv.Membership do |> Ash.update(domain: __MODULE__) end - @doc """ - Lists only required custom fields. - - This is an optimized version that filters at the database level instead of - loading all custom fields and filtering in memory. - - ## Returns - - - `{:ok, required_custom_fields}` - List of required custom fields - - `{:error, error}` - Error reading custom fields - - ## Examples - - iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields() - iex> Enum.all?(required_fields, & &1.required) - true - """ - def list_required_custom_fields do - Mv.Membership.CustomField - |> Ash.Query.filter(expr(required == true)) - |> Ash.read(domain: __MODULE__) - end - @doc """ Updates the member field visibility configuration. diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index ccec5a5..a1020ef 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -333,8 +333,7 @@ defmodule MvWeb.CoreComponents do attr :error_class, :string, default: nil, doc: "the input error class to use over defaults" attr :rest, :global, - include: - ~w(accept autocomplete aria-required capture cols disabled form list max maxlength min minlength + include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength multiple pattern placeholder readonly required rows size step) def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do @@ -354,24 +353,6 @@ defmodule MvWeb.CoreComponents do Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end) - # For checkboxes, we don't use HTML required attribute (means "must be checked") - # Instead, we use aria-required for screen readers (WCAG 2.1, Success Criterion 3.3.2) - # Extract required from rest and remove it, but keep aria-required if provided - rest = assigns.rest || %{} - is_required = Map.get(rest, :required, false) - aria_required = Map.get(rest, :aria_required, if(is_required, do: "true", else: nil)) - - # Remove required from rest (we don't want HTML required on checkbox) - rest_without_required = Map.delete(rest, :required) - # Ensure aria-required is set if field is required - rest_final = - if aria_required, - do: Map.put(rest_without_required, :aria_required, aria_required), - else: rest_without_required - - assigns = assign(assigns, :rest, rest_final) - assigns = assign(assigns, :is_required, is_required) - ~H"""
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 53754aa..63ab513 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -148,7 +148,6 @@ defmodule MvWeb.MemberLive.Form do field={value_form[:value]} label={cf.name} type={custom_field_input_type(cf.value_type)} - required={cf.required} /> Ash.Changeset.for_create(:create, %{ - name: "required_string", - value_type: :string, - required: true - }) - |> Ash.create() - - {:ok, required_integer_field} = - Membership.CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "required_integer", - value_type: :integer, - required: true - }) - |> Ash.create() - - {:ok, required_boolean_field} = - Membership.CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "required_boolean", - value_type: :boolean, - required: true - }) - |> Ash.create() - - {:ok, required_date_field} = - Membership.CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "required_date", - value_type: :date, - required: true - }) - |> Ash.create() - - {:ok, required_email_field} = - Membership.CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "required_email", - value_type: :email, - required: true - }) - |> Ash.create() - - {:ok, optional_field} = - Membership.CustomField - |> Ash.Changeset.for_create(:create, %{ - name: "optional_string", - value_type: :string, - required: false - }) - |> Ash.create() - - %{ - required_string_field: required_string_field, - required_integer_field: required_integer_field, - required_boolean_field: required_boolean_field, - required_date_field: required_date_field, - required_email_field: required_email_field, - optional_field: optional_field - } - end - - # Helper function to create all required custom fields with valid default values - defp all_required_custom_fields_with_defaults(%{ - required_string_field: string_field, - required_integer_field: integer_field, - required_boolean_field: boolean_field, - required_date_field: date_field, - required_email_field: email_field - }) do - [ - %{ - "custom_field_id" => string_field.id, - "value" => %{"_union_type" => "string", "_union_value" => "default"} - }, - %{ - "custom_field_id" => integer_field.id, - "value" => %{"_union_type" => "integer", "_union_value" => 0} - }, - %{ - "custom_field_id" => boolean_field.id, - "value" => %{"_union_type" => "boolean", "_union_value" => false} - }, - %{ - "custom_field_id" => date_field.id, - "value" => %{"_union_type" => "date", "_union_value" => ~D[2020-01-01]} - }, - %{ - "custom_field_id" => email_field.id, - "value" => %{"_union_type" => "email", "_union_value" => "test@example.com"} - } - ] - end - - describe "create_member with required custom fields" do - @valid_attrs %{ - first_name: "John", - last_name: "Doe", - email: "john@example.com" - } - - test "fails when required custom field is missing", %{required_string_field: field} do - attrs = Map.put(@valid_attrs, :custom_field_values, []) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required string custom field has nil value", - %{ - required_string_field: field - } = context do - # Start with all required fields having valid values - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => nil}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required string custom field has empty string value", - %{ - required_string_field: field - } = context do - # Start with all required fields having valid values - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => ""}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required string custom field has whitespace-only value", - %{ - required_string_field: field - } = context do - # Start with all required fields having valid values - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => " "}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when required string custom field has valid value", - %{ - required_string_field: field - } = context do - # Start with all required fields having valid values, then update the string field - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test value"}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "fails when required integer custom field has nil value", - %{ - required_integer_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => nil}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required integer custom field has empty string value", - %{ - required_integer_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => ""}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when required integer custom field has zero value", - %{ - required_integer_field: _field - } = context do - custom_field_values = all_required_custom_fields_with_defaults(context) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "succeeds when required integer custom field has positive value", - %{ - required_integer_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => 42}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "fails when required boolean custom field has nil value", - %{ - required_boolean_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => nil}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when required boolean custom field has false value", - %{ - required_boolean_field: _field - } = context do - custom_field_values = all_required_custom_fields_with_defaults(context) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "succeeds when required boolean custom field has true value", - %{ - required_boolean_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => true}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "fails when required date custom field has nil value", - %{ - required_date_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "date", "_union_value" => nil}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required date custom field has empty string value", - %{ - required_date_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "date", "_union_value" => ""}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when required date custom field has valid date value", - %{ - required_date_field: _field - } = context do - custom_field_values = all_required_custom_fields_with_defaults(context) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "fails when required email custom field has nil value", - %{ - required_email_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "email", "_union_value" => nil}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when required email custom field has empty string value", - %{ - required_email_field: field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "email", "_union_value" => ""}} - else - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when required email custom field has valid email value", - %{ - required_email_field: _field - } = context do - custom_field_values = all_required_custom_fields_with_defaults(context) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "succeeds when multiple required custom fields are provided", - %{ - required_string_field: string_field, - required_integer_field: integer_field, - required_boolean_field: boolean_field - } = context do - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - cond do - cfv["custom_field_id"] == string_field.id -> - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test"}} - - cfv["custom_field_id"] == integer_field.id -> - %{cfv | "value" => %{"_union_type" => "integer", "_union_value" => 42}} - - cfv["custom_field_id"] == boolean_field.id -> - %{cfv | "value" => %{"_union_type" => "boolean", "_union_value" => true}} - - true -> - cfv - end - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "fails when one of multiple required custom fields is missing", - %{ - required_string_field: string_field, - required_integer_field: integer_field - } = context do - # Provide only string field, missing integer, boolean, and date - custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.filter(fn cfv -> - cfv["custom_field_id"] == string_field.id - end) - |> Enum.map(fn cfv -> - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => "test"}} - end) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs) - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ integer_field.name - end - - test "succeeds when optional custom field is missing", %{} = context do - # Provide all required fields, but no optional field - custom_field_values = all_required_custom_fields_with_defaults(context) - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - - test "succeeds when optional custom field has nil value", - %{optional_field: field} = context do - # Provide all required fields plus optional field with nil - custom_field_values = - all_required_custom_fields_with_defaults(context) ++ - [ - %{ - "custom_field_id" => field.id, - "value" => %{"_union_type" => "string", "_union_value" => nil} - } - ] - - attrs = Map.put(@valid_attrs, :custom_field_values, custom_field_values) - - assert {:ok, _member} = Membership.create_member(attrs) - end - end - - describe "update_member with required custom fields" do - test "fails when removing a required custom field value", - %{ - required_string_field: field - } = context do - # Create member with all required custom fields - custom_field_values = all_required_custom_fields_with_defaults(context) - - {:ok, member} = - Membership.create_member(%{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }) - - # Try to update without the required custom field - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.update_member(member, %{custom_field_values: []}) - - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "fails when setting required custom field value to empty", - %{ - required_string_field: field - } = context do - # Create member with all required custom fields - custom_field_values = all_required_custom_fields_with_defaults(context) - - {:ok, member} = - Membership.create_member(%{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }) - - # Try to update with empty value for the string field - updated_custom_field_values = - all_required_custom_fields_with_defaults(context) - |> Enum.map(fn cfv -> - if cfv["custom_field_id"] == field.id do - %{cfv | "value" => %{"_union_type" => "string", "_union_value" => ""}} - else - cfv - end - end) - - assert {:error, %Ash.Error.Invalid{errors: errors}} = - Membership.update_member(member, %{ - custom_field_values: updated_custom_field_values - }) - - assert error_message(errors, :custom_field_values) =~ "Required custom fields missing" - assert error_message(errors, :custom_field_values) =~ field.name - end - - test "succeeds when updating required custom field to valid value", - %{ - required_string_field: field - } = context do - # Create member with all required custom fields - custom_field_values = all_required_custom_fields_with_defaults(context) - - {:ok, member} = - Membership.create_member(%{ - first_name: "John", - last_name: "Doe", - email: "john@example.com", - custom_field_values: custom_field_values - }) - - # Load existing custom field values to get their IDs - {:ok, member_with_cfvs} = Ash.load(member, :custom_field_values) - - # Update with new valid value for the string field, using existing IDs - updated_custom_field_values = - member_with_cfvs.custom_field_values - |> Enum.map(fn cfv -> - if cfv.custom_field_id == field.id do - %{ - "id" => cfv.id, - "custom_field_id" => cfv.custom_field_id, - "value" => %{"_union_type" => "string", "_union_value" => "new value"} - } - else - # Keep other fields as they are - value_type = Atom.to_string(cfv.value.type) - actual_value = cfv.value.value - - %{ - "id" => cfv.id, - "custom_field_id" => cfv.custom_field_id, - "value" => %{"_union_type" => value_type, "_union_value" => actual_value} - } - end - end) - - assert {:ok, _updated_member} = - Membership.update_member(member, %{ - custom_field_values: updated_custom_field_values - }) - end - end - - # Helper function for error evaluation - defp error_message(errors, field) do - errors - |> Enum.filter(fn err -> Map.get(err, :field) == field end) - |> Enum.map_join(" ", &Map.get(&1, :message, "")) - end -end