From bbc094daaa51b8116ae1ad400baf12ccaa40998a Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Dec 2025 13:31:15 +0100 Subject: [PATCH 01/11] fix: add validation for required custom fields --- lib/membership/member.ex | 71 +++ lib/mv_web/live/member_live/form.ex | 1 + .../member_required_custom_fields_test.exs | 447 ++++++++++++++++++ 3 files changed, 519 insertions(+) create mode 100644 test/membership/member_required_custom_fields_test.exs diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 5816d19..73820f6 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -329,6 +329,63 @@ defmodule Mv.Membership.Member do {:error, field: :email, message: "is not a valid email"} end end + + # Validate required custom fields + validate fn changeset, _ -> + custom_field_values_arg = Ash.Changeset.get_argument(changeset, :custom_field_values) + + # If argument is not provided (nil), check existing values from member data (update scenario) + # If argument is provided (empty list or list with values), use those values + provided_values = + if is_nil(custom_field_values_arg) do + # Update scenario: check existing custom field values from member + case Ash.load(changeset.data, :custom_field_values) do + {:ok, %{custom_field_values: existing_values}} -> + Enum.reduce(existing_values, %{}, fn cfv, acc -> + value = if is_map(cfv.value), do: Map.get(cfv.value, :value), else: nil + Map.put(acc, cfv.custom_field_id, value) + end) + + _ -> + %{} + end + else + # Create or update scenario: use provided argument values + Enum.reduce(custom_field_values_arg, %{}, fn cfv, acc -> + custom_field_id = Map.get(cfv, "custom_field_id") + value_map = Map.get(cfv, "value", %{}) + actual_value = Map.get(value_map, "value") + Map.put(acc, custom_field_id, actual_value) + end) + end + + # Load all required custom fields + case Mv.Membership.list_custom_fields() do + {:ok, all_custom_fields} -> + required_custom_fields = Enum.filter(all_custom_fields, & &1.required) + + # Check each required custom field + missing_fields = + Enum.filter(required_custom_fields, fn cf -> + value = Map.get(provided_values, cf.id) + not value_present?(value, cf.value_type) + end) + + if Enum.empty?(missing_fields) do + :ok + else + missing_names = Enum.map_join(missing_fields, ", ", & &1.name) + + {:error, + field: :custom_field_values, + message: "Required custom fields missing: #{missing_names}"} + end + + {:error, _} -> + # If we can't load custom fields, skip validation (shouldn't happen in normal operation) + :ok + end + end end attributes do @@ -665,4 +722,18 @@ defmodule Mv.Membership.Member do query end 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 is invalid, 0 is valid + # Date: nil is invalid + # Email: nil is 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), do: not is_nil(value) + defp value_present?(value, :date), do: not is_nil(value) + defp value_present?(value, :email), do: not is_nil(value) + defp value_present?(_value, _type), do: false end diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 87148ad..3378504 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -144,6 +144,7 @@ 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, 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, + optional_field: optional_field + } + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => nil} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => ""} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => " "} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => "test value"} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "integer", "value" => nil} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "integer", "value" => 0} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "integer", "value" => 42} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "boolean", "value" => nil} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "boolean", "value" => false} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "boolean", "value" => true} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "date", "value" => nil} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "date", "value" => ~D[2020-01-01]} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => string_field.id, + "value" => %{"type" => "string", "value" => "test"} + }, + %{ + "custom_field_id" => integer_field.id, + "value" => %{"type" => "integer", "value" => 42} + }, + %{ + "custom_field_id" => boolean_field.id, + "value" => %{"type" => "boolean", "value" => true} + } + ] + + 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 + } do + custom_field_values = [ + %{ + "custom_field_id" => string_field.id, + "value" => %{"type" => "string", "value" => "test"} + } + # Missing required_integer_field + ] + + 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", %{optional_field: field} do + attrs = Map.put(@valid_attrs, :custom_field_values, []) + + assert {:ok, _member} = Membership.create_member(attrs) + end + + test "succeeds when optional custom field has nil value", %{optional_field: field} do + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "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 + } do + # Create member with required custom field + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => "test"} + } + ] + + {: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 + } do + # Create member with required custom field + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => "test"} + } + ] + + {: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 + updated_custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => ""} + } + ] + + 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 + } do + # Create member with required custom field + custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => "old value"} + } + ] + + {:ok, member} = + Membership.create_member(%{ + first_name: "John", + last_name: "Doe", + email: "john@example.com", + custom_field_values: custom_field_values + }) + + # Update with new valid value + updated_custom_field_values = [ + %{ + "custom_field_id" => field.id, + "value" => %{"type" => "string", "value" => "new value"} + } + ] + + 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 From 6084827c739eceb37f2ed6ef421a52f0dd45e0d5 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 17 Dec 2025 14:34:10 +0100 Subject: [PATCH 02/11] test: updated --- lib/membership/member.ex | 10 +- .../member_required_custom_fields_test.exs | 460 ++++++++++-------- 2 files changed, 278 insertions(+), 192 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 73820f6..286bb1a 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -354,7 +354,15 @@ defmodule Mv.Membership.Member do Enum.reduce(custom_field_values_arg, %{}, fn cfv, acc -> custom_field_id = Map.get(cfv, "custom_field_id") value_map = Map.get(cfv, "value", %{}) - actual_value = Map.get(value_map, "value") + + # Support both "value" and "_union_value" keys, without using || to preserve false values + actual_value = + cond do + Map.has_key?(value_map, "value") -> Map.get(value_map, "value") + Map.has_key?(value_map, "_union_value") -> Map.get(value_map, "_union_value") + true -> nil + end + Map.put(acc, custom_field_id, actual_value) end) end diff --git a/test/membership/member_required_custom_fields_test.exs b/test/membership/member_required_custom_fields_test.exs index 7467152..747cebd 100644 --- a/test/membership/member_required_custom_fields_test.exs +++ b/test/membership/member_required_custom_fields_test.exs @@ -69,6 +69,33 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do } 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 + }) 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]} + } + ] + end + describe "create_member with required custom fields" do @valid_attrs %{ first_name: "John", @@ -84,15 +111,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "fails when required string custom field has nil value", %{ - required_string_field: field - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => nil} - } - ] + 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) @@ -101,15 +133,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => ""} - } - ] + 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) @@ -118,15 +155,20 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => " "} - } - ] + 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) @@ -135,30 +177,39 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "succeeds when required string custom field has valid value", %{ - required_string_field: field - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => "test value"} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "integer", "value" => nil} - } - ] + 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) @@ -167,45 +218,49 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "succeeds when required integer custom field has zero value", %{ - required_integer_field: field - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "integer", "value" => 0} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "integer", "value" => 42} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "boolean", "value" => nil} - } - ] + 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) @@ -214,45 +269,49 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "succeeds when required boolean custom field has false value", %{ - required_boolean_field: field - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "boolean", "value" => false} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "boolean", "value" => true} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "date", "value" => nil} - } - ] + 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) @@ -261,57 +320,60 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "date", "value" => ~D[2020-01-01]} - } - ] + 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 "succeeds when multiple required custom fields are provided", %{ - required_string_field: string_field, - required_integer_field: integer_field, - required_boolean_field: boolean_field - } do - custom_field_values = [ - %{ - "custom_field_id" => string_field.id, - "value" => %{"type" => "string", "value" => "test"} - }, - %{ - "custom_field_id" => integer_field.id, - "value" => %{"type" => "integer", "value" => 42} - }, - %{ - "custom_field_id" => boolean_field.id, - "value" => %{"type" => "boolean", "value" => true} - } - ] + 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 - } do - custom_field_values = [ - %{ - "custom_field_id" => string_field.id, - "value" => %{"type" => "string", "value" => "test"} - } - # Missing required_integer_field - ] + 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) @@ -320,19 +382,26 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ integer_field.name end - test "succeeds when optional custom field is missing", %{optional_field: field} do - attrs = Map.put(@valid_attrs, :custom_field_values, []) + 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} do - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => nil} - } - ] + 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) @@ -341,16 +410,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do end describe "update_member with required custom fields" do - test "fails when removing a required custom field value", %{ - required_string_field: field - } do - # Create member with required custom field - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => "test"} - } - ] + 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(%{ @@ -368,16 +433,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "fails when setting required custom field value to empty", %{ - required_string_field: field - } do - # Create member with required custom field - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => "test"} - } - ] + 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(%{ @@ -387,13 +448,16 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do custom_field_values: custom_field_values }) - # Try to update with empty value - updated_custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => ""} - } - ] + # 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, %{ @@ -404,16 +468,12 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do assert error_message(errors, :custom_field_values) =~ field.name end - test "succeeds when updating required custom field to valid value", %{ - required_string_field: field - } do - # Create member with required custom field - custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => "old value"} - } - ] + 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(%{ @@ -423,13 +483,31 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do custom_field_values: custom_field_values }) - # Update with new valid value - updated_custom_field_values = [ - %{ - "custom_field_id" => field.id, - "value" => %{"type" => "string", "value" => "new value"} - } - ] + # 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, %{ From 2d2865b5a642c1d9a638f4ad8805a7028743b476 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 23 Dec 2025 17:01:21 +0100 Subject: [PATCH 03/11] feat: improve validation for custom fields --- lib/membership/member.ex | 188 +++++++++++++++++++++++++++------------ 1 file changed, 129 insertions(+), 59 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 286bb1a..ae53116 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -39,6 +39,7 @@ defmodule Mv.Membership.Member do require Ash.Query import Ash.Expr + require Logger # Module constants @member_search_limit 10 @@ -73,6 +74,9 @@ 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 @@ -332,66 +336,27 @@ defmodule Mv.Membership.Member do # Validate required custom fields validate fn changeset, _ -> - custom_field_values_arg = Ash.Changeset.get_argument(changeset, :custom_field_values) + provided_values = provided_custom_field_values(changeset) - # If argument is not provided (nil), check existing values from member data (update scenario) - # If argument is provided (empty list or list with values), use those values - provided_values = - if is_nil(custom_field_values_arg) do - # Update scenario: check existing custom field values from member - case Ash.load(changeset.data, :custom_field_values) do - {:ok, %{custom_field_values: existing_values}} -> - Enum.reduce(existing_values, %{}, fn cfv, acc -> - value = if is_map(cfv.value), do: Map.get(cfv.value, :value), else: nil - Map.put(acc, cfv.custom_field_id, value) - end) - - _ -> - %{} - end - else - # Create or update scenario: use provided argument values - Enum.reduce(custom_field_values_arg, %{}, fn cfv, acc -> - custom_field_id = Map.get(cfv, "custom_field_id") - value_map = Map.get(cfv, "value", %{}) - - # Support both "value" and "_union_value" keys, without using || to preserve false values - actual_value = - cond do - Map.has_key?(value_map, "value") -> Map.get(value_map, "value") - Map.has_key?(value_map, "_union_value") -> Map.get(value_map, "_union_value") - true -> nil - end - - Map.put(acc, custom_field_id, actual_value) - end) - end - - # Load all required custom fields - case Mv.Membership.list_custom_fields() do - {:ok, all_custom_fields} -> - required_custom_fields = Enum.filter(all_custom_fields, & &1.required) - - # Check each required custom field - missing_fields = - Enum.filter(required_custom_fields, fn cf -> - value = Map.get(provided_values, cf.id) - not value_present?(value, cf.value_type) - end) + 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 - missing_names = Enum.map_join(missing_fields, ", ", & &1.name) - - {:error, - field: :custom_field_values, - message: "Required custom fields missing: #{missing_names}"} + build_custom_field_validation_error(missing_fields) end - {:error, _} -> - # If we can't load custom fields, skip validation (shouldn't happen in normal operation) - :ok + {: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 @@ -731,17 +696,122 @@ defmodule Mv.Membership.Member do 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: "Required custom fields missing: #{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 is invalid, 0 is valid - # Date: nil is invalid - # Email: nil is 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), do: not is_nil(value) - defp value_present?(value, :date), do: not is_nil(value) - defp value_present?(value, :email), do: not is_nil(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 From e3ff3e610cccc6e199b27a805c6ad8f75e6db1d5 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 23 Dec 2025 17:01:50 +0100 Subject: [PATCH 04/11] feat: optimize required custom fields query --- lib/membership/membership.ex | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex index f5a708b..4917c7c 100644 --- a/lib/membership/membership.ex +++ b/lib/membership/membership.ex @@ -21,6 +21,9 @@ defmodule Mv.Membership do use Ash.Domain, extensions: [AshAdmin.Domain, AshPhoenix] + require Ash.Query + import Ash.Expr + admin do show? true end @@ -125,6 +128,29 @@ 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. From 4e101ea36eb065a67d818966deeeef50a97df892 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 23 Dec 2025 17:02:07 +0100 Subject: [PATCH 05/11] feat: Add WCAG-compliant handling for boolean custom fields --- lib/mv_web/components/core_components.ex | 25 +++++++++++++++++++++--- lib/mv_web/live/member_live/form.ex | 3 ++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index a1020ef..ccec5a5 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -333,7 +333,8 @@ 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 capture cols disabled form list max maxlength min minlength + include: + ~w(accept autocomplete aria-required 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 @@ -353,6 +354,24 @@ 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 3378504..dc32824 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -144,7 +144,8 @@ defmodule MvWeb.MemberLive.Form do field={value_form[:value]} label={cf.name} type={custom_field_input_type(cf.value_type)} - required={cf.required} + required={if cf.value_type == :boolean, do: false, else: cf.required} + aria-required={if cf.required, do: "true", else: nil} /> Date: Tue, 23 Dec 2025 17:02:23 +0100 Subject: [PATCH 06/11] test: Add tests for empty string validation in custom fields --- .../member_required_custom_fields_test.exs | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/test/membership/member_required_custom_fields_test.exs b/test/membership/member_required_custom_fields_test.exs index 747cebd..ec8ebe3 100644 --- a/test/membership/member_required_custom_fields_test.exs +++ b/test/membership/member_required_custom_fields_test.exs @@ -51,6 +51,15 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do }) |> 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, %{ @@ -65,6 +74,7 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 @@ -74,7 +84,8 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do required_string_field: string_field, required_integer_field: integer_field, required_boolean_field: boolean_field, - required_date_field: date_field + required_date_field: date_field, + required_email_field: email_field }) do [ %{ @@ -92,6 +103,10 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do %{ "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 @@ -218,6 +233,27 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 @@ -320,6 +356,27 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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 @@ -331,6 +388,59 @@ defmodule Mv.Membership.MemberRequiredCustomFieldsTest do 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, From ca702cf2c10ac4e101a1f68a3cde7645a531f29b Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 23 Dec 2025 17:02:30 +0100 Subject: [PATCH 07/11] i18n: Update translations for custom field validation --- priv/gettext/de/LC_MESSAGES/default.po | 5 +++++ priv/gettext/default.pot | 5 +++++ priv/gettext/en/LC_MESSAGES/default.po | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 3a83ecf..d5a85dc 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -1433,6 +1433,11 @@ msgstr "Benutzerdefiniertes Feld speichern" msgid "Save Custom Field Value" msgstr "Benutzerdefinierten Feldwert speichern" +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "Dieses Feld ist erforderlich" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 8bb080e..8e211dd 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -1433,3 +1433,8 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Save Custom Field Value" msgstr "" + +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index e1c4cc0..0d43e13 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -1434,6 +1434,11 @@ msgstr "" msgid "Save Custom Field Value" msgstr "" +#: lib/mv_web/components/core_components.ex +#, elixir-autogen, elixir-format +msgid "This field is required" +msgstr "" + #~ #: lib/mv_web/live/custom_field_live/show.ex #~ #, elixir-autogen, elixir-format #~ msgid "Auto-generated identifier (immutable)" From 1dd68bcaf222a986d9dcbc36352d79bec5bb6219 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 23 Dec 2025 18:08:31 +0100 Subject: [PATCH 08/11] feat: coherent required boolean handling --- lib/membership/member.ex | 3 ++- lib/mv_web/live/member_live/form.ex | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index ae53116..ad13439 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -782,7 +782,8 @@ defmodule Mv.Membership.Member do missing_names = Enum.map_join(sorted_missing_fields, ", ", & &1.name) {:error, - field: :custom_field_values, message: "Required custom fields missing: #{missing_names}"} + 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 diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index dc32824..3378504 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -144,8 +144,7 @@ defmodule MvWeb.MemberLive.Form do field={value_form[:value]} label={cf.name} type={custom_field_input_type(cf.value_type)} - required={if cf.value_type == :boolean, do: false, else: cf.required} - aria-required={if cf.required, do: "true", else: nil} + required={cf.required} /> Date: Thu, 1 Jan 2026 00:22:14 +0000 Subject: [PATCH 09/11] chore(deps): update mix dependencies --- mix.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.lock b/mix.lock index 1dd3d48..9f14072 100644 --- a/mix.lock +++ b/mix.lock @@ -1,27 +1,27 @@ %{ - "ash": {:hex, :ash, "3.11.1", "9794620bffeb83d1803d92a64e7803f70b57372eb4addba5c12a24343cd04e1a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e0074302bb88d667635fcbfdacbf8a641c53973a3902d0e744f567a49ec808fc"}, + "ash": {:hex, :ash, "3.11.3", "0ba9bb36ed6ee3141a7c08a37850b8c80ff7cccbc50e1e44222dd165ae05550a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e219c4e2c975523efa0af209dea27eb38384054e191ce3334e0b5ca3e5bdcebe"}, "ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"}, - "ash_authentication": {:hex, :ash_authentication, "4.13.3", "4d7a2e96b5a8fe68797ba0124cf40e6897c82b9fb69182fc5fdaac529b72d436", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "03d95b68766b28cda241e68217f6d1d839be350f7e8f20923162b163fb521b91"}, - "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.12.2", "a4646498a7e21fbdbe372f0d8afab08b5d7125b629f91bfcf8f4d1961bc9d57b", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "1dd6fa3a8f7d2563a53cf22aeda31770c855e927421af4d8bfaf480332acf721"}, + "ash_authentication": {:hex, :ash_authentication, "4.13.4", "8c94bb54c46a677ad03725997f48d3ce6e136c401397eae3a41f25eddbcfeb3c", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "92ae2edea459aace26038ca8441cbf7fca9c965b1492eb4e0de5c909cf798751"}, + "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.14.0", "ede30e98529a1a62babc85dff6b6e378d855e256f02d4054c90b0392cd7bb41e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "30dd5710fa62f62b60a5eb9e2ae986ac6a5ab2eaece61118b89c33d1d791226a"}, "ash_phoenix": {:hex, :ash_phoenix, "2.3.18", "fad1b8af1405758888086de568f08650c2911ee97074cfe2e325b14854bc43dd", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7ec28f9216221e83b90d9c3605e9d1cdd228984e09a1a86c9b9d393cebf25222"}, - "ash_postgres": {:hex, :ash_postgres, "2.6.26", "f995bac8762ae039d4fb94cf2b628430aa69b0b30bf4366b96b3543dbd679ae7", [:mix], [{:ash, "~> 3.9", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.12 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7050b3169d5a31d73f7e69a6564d1102cb2bc185e67ea428e78fda3da46a69fc"}, - "ash_sql": {:hex, :ash_sql, "0.3.15", "8b8daae1870ab37b4fb2f980e323194caf23cdb4218fef126c49cc11a01fa243", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "97432507b6f406eb2461e5d0fbf2e5104a8c61a2570322d11de2f124d822d8ff"}, + "ash_postgres": {:hex, :ash_postgres, "2.6.27", "7aa119cc420909573a51802f414a49a9fb21a06ee78769efd7a4db040e748f5c", [:mix], [{:ash, ">= 3.11.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.16 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "f5e71dc3f77bc0c52374869df4b66493e13c0e27507c3d10ff13158ef7ea506f"}, + "ash_sql": {:hex, :ash_sql, "0.3.16", "a4e62d2cf9b2f4a451067e5e3de28349a8d0e69cf50fc1861bad85f478ded046", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "f3d5a810b23e12e3e102799c68b1e934fa7f909ccaa4bd530f10c7317cfcfe56"}, "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, - "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, + "bandit": {:hex, :bandit, "1.10.0", "f8293b4a4e6c06b31655ae10bd3462f59d8c5dbd1df59028a4984f10c5961147", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "43ebceb7060a4d8273e47d83e703d01b112198624ba0826980caa3f5091243c4"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, - "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, @@ -56,7 +56,7 @@ "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.18", "b5410017b3d4edf261d9c98ebc334e0637d7189457c730720cfc13e206443d43", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f189b759595feff0420e9a1d544396397f9cf9e2d5a8cb98ba5b6cab01927da0"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "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"}, @@ -74,14 +74,14 @@ "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, - "swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"}, + "swoosh": {:hex, :swoosh, "1.20.0", "b04134c2b302da74c3a95ca4ddde191e4854d2847d6687783fecb023a9647598", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13e610f709bae54851d68afb6862882aa646e5c974bf49e3bf5edd84a73cf213"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, - "tidewave": {:hex, :tidewave, "0.5.2", "f549acffe9daeed8b6b547c232c60de987770da7f827f9b3300140dfc465b102", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "34ab3ffee7e402f05cd1eae68d0e77ed0e0d1925677971ef83634247553e8afd"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, + "tidewave": {:hex, :tidewave, "0.5.3", "1378aefa93dbf887c2df60842be4cf312c57fdf99dbf91c5227cd4344050876e", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "605a1b912b7a8b56498077b3426be96b7129c4ac06d166311d408dccd0e5e0d3"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, From 850f00fe226e062e353f7f4276672f8337823534 Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 2 Jan 2026 13:53:24 +0100 Subject: [PATCH 10/11] formatting --- lib/membership/member.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 27ebb5f..1d6d96e 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -1250,7 +1250,10 @@ defmodule Mv.Membership.Member do {:error, field: :custom_field_values, - message: Gettext.dgettext(MvWeb.Gettext, "default", "Required custom fields missing: %{fields}", fields: missing_names)} + 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 From 4c22657d16654ceb447dbbcad577da3c4abf7981 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Fri, 2 Jan 2026 13:22:12 +0000 Subject: [PATCH 11/11] chore(deps): update mix dependencies --- mix.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.lock b/mix.lock index 1dd3d48..9f14072 100644 --- a/mix.lock +++ b/mix.lock @@ -1,27 +1,27 @@ %{ - "ash": {:hex, :ash, "3.11.1", "9794620bffeb83d1803d92a64e7803f70b57372eb4addba5c12a24343cd04e1a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e0074302bb88d667635fcbfdacbf8a641c53973a3902d0e744f567a49ec808fc"}, + "ash": {:hex, :ash, "3.11.3", "0ba9bb36ed6ee3141a7c08a37850b8c80ff7cccbc50e1e44222dd165ae05550a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e219c4e2c975523efa0af209dea27eb38384054e191ce3334e0b5ca3e5bdcebe"}, "ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"}, - "ash_authentication": {:hex, :ash_authentication, "4.13.3", "4d7a2e96b5a8fe68797ba0124cf40e6897c82b9fb69182fc5fdaac529b72d436", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "03d95b68766b28cda241e68217f6d1d839be350f7e8f20923162b163fb521b91"}, - "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.12.2", "a4646498a7e21fbdbe372f0d8afab08b5d7125b629f91bfcf8f4d1961bc9d57b", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "1dd6fa3a8f7d2563a53cf22aeda31770c855e927421af4d8bfaf480332acf721"}, + "ash_authentication": {:hex, :ash_authentication, "4.13.4", "8c94bb54c46a677ad03725997f48d3ce6e136c401397eae3a41f25eddbcfeb3c", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "92ae2edea459aace26038ca8441cbf7fca9c965b1492eb4e0de5c909cf798751"}, + "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.14.0", "ede30e98529a1a62babc85dff6b6e378d855e256f02d4054c90b0392cd7bb41e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "30dd5710fa62f62b60a5eb9e2ae986ac6a5ab2eaece61118b89c33d1d791226a"}, "ash_phoenix": {:hex, :ash_phoenix, "2.3.18", "fad1b8af1405758888086de568f08650c2911ee97074cfe2e325b14854bc43dd", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7ec28f9216221e83b90d9c3605e9d1cdd228984e09a1a86c9b9d393cebf25222"}, - "ash_postgres": {:hex, :ash_postgres, "2.6.26", "f995bac8762ae039d4fb94cf2b628430aa69b0b30bf4366b96b3543dbd679ae7", [:mix], [{:ash, "~> 3.9", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.12 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7050b3169d5a31d73f7e69a6564d1102cb2bc185e67ea428e78fda3da46a69fc"}, - "ash_sql": {:hex, :ash_sql, "0.3.15", "8b8daae1870ab37b4fb2f980e323194caf23cdb4218fef126c49cc11a01fa243", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "97432507b6f406eb2461e5d0fbf2e5104a8c61a2570322d11de2f124d822d8ff"}, + "ash_postgres": {:hex, :ash_postgres, "2.6.27", "7aa119cc420909573a51802f414a49a9fb21a06ee78769efd7a4db040e748f5c", [:mix], [{:ash, ">= 3.11.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.16 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "f5e71dc3f77bc0c52374869df4b66493e13c0e27507c3d10ff13158ef7ea506f"}, + "ash_sql": {:hex, :ash_sql, "0.3.16", "a4e62d2cf9b2f4a451067e5e3de28349a8d0e69cf50fc1861bad85f478ded046", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "f3d5a810b23e12e3e102799c68b1e934fa7f909ccaa4bd530f10c7317cfcfe56"}, "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, - "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, + "bandit": {:hex, :bandit, "1.10.0", "f8293b4a4e6c06b31655ae10bd3462f59d8c5dbd1df59028a4984f10c5961147", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "43ebceb7060a4d8273e47d83e703d01b112198624ba0826980caa3f5091243c4"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, - "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, @@ -56,7 +56,7 @@ "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.18", "b5410017b3d4edf261d9c98ebc334e0637d7189457c730720cfc13e206443d43", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f189b759595feff0420e9a1d544396397f9cf9e2d5a8cb98ba5b6cab01927da0"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "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"}, @@ -74,14 +74,14 @@ "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, - "swoosh": {:hex, :swoosh, "1.19.9", "4eb2c471b8cf06adbdcaa1d57a0ad53c0ed9348ce8586a06cc491f9f0dbcb553", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "516898263a64925c31723c56bc7999a26e97b04e869707f681f4c9bca7ee1688"}, + "swoosh": {:hex, :swoosh, "1.20.0", "b04134c2b302da74c3a95ca4ddde191e4854d2847d6687783fecb023a9647598", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "13e610f709bae54851d68afb6862882aa646e5c974bf49e3bf5edd84a73cf213"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, - "tidewave": {:hex, :tidewave, "0.5.2", "f549acffe9daeed8b6b547c232c60de987770da7f827f9b3300140dfc465b102", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "34ab3ffee7e402f05cd1eae68d0e77ed0e0d1925677971ef83634247553e8afd"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, + "tidewave": {:hex, :tidewave, "0.5.3", "1378aefa93dbf887c2df60842be4cf312c57fdf99dbf91c5227cd4344050876e", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "605a1b912b7a8b56498077b3426be96b7129c4ac06d166311d408dccd0e5e0d3"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},