defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do @moduledoc """ Tests for full-text search including custom_field_values. Tests verify that custom field values are included in the search_vector and can be found through the fuzzy_search functionality. """ use Mv.DataCase, async: false alias Mv.Membership.{CustomField, CustomFieldValue, Member} setup do # Create test members {:ok, member1} = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Alice", last_name: "Anderson", email: "alice@example.com" }) |> Ash.create() {:ok, member2} = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Bob", last_name: "Brown", email: "bob@example.com" }) |> Ash.create() {:ok, member3} = Member |> Ash.Changeset.for_create(:create_member, %{ first_name: "Charlie", last_name: "Clark", email: "charlie@example.com" }) |> Ash.create() # Create custom fields for different types {:ok, string_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "membership_number", value_type: :string }) |> Ash.create() {:ok, integer_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "member_id_number", value_type: :integer }) |> Ash.create() {:ok, email_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "secondary_email", value_type: :email }) |> Ash.create() {:ok, date_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "birthday", value_type: :date }) |> Ash.create() {:ok, boolean_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "newsletter", value_type: :boolean }) |> Ash.create() %{ member1: member1, member2: member2, member3: member3, string_field: string_field, integer_field: integer_field, email_field: email_field, date_field: date_field, boolean_field: boolean_field } end describe "search with custom field values" do test "finds member by string custom field value", %{ member1: member1, string_field: string_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"} }) |> Ash.create() # Force search_vector update by reloading member {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for the custom field value results = Member |> Member.fuzzy_search(%{query: "MEMBER12345"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by integer custom field value", %{ member1: member1, integer_field: integer_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 42_424} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for the custom field value results = Member |> Member.fuzzy_search(%{query: "42424"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by email custom field value", %{ member1: member1, email_field: email_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for partial custom field value (should work via FTS or custom field filter) results = Member |> Member.fuzzy_search(%{query: "alice.secondary"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id # Search for full email address (should work via custom field filter LIKE) results_full = Member |> Member.fuzzy_search(%{query: "alice.secondary@example.com"}) |> Ash.read!() assert length(results_full) == 1 assert List.first(results_full).id == member1.id # Search for domain part (should work via FTS or custom field filter) # Note: May return multiple results if other members have same domain results_domain = Member |> Member.fuzzy_search(%{query: "example.com"}) |> Ash.read!() # Verify that member1 is in the results (may have other members too) ids = Enum.map(results_domain, & &1.id) assert member1.id in ids end test "finds member by date custom field value", %{ member1: member1, date_field: date_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: date_field.id, value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for the custom field value (date is stored as text in search_vector) results = Member |> Member.fuzzy_search(%{query: "1990-05-15"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "finds member by boolean custom field value", %{ member1: member1, boolean_field: boolean_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: boolean_field.id, value: %{"_union_type" => "boolean", "_union_value" => true} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for the custom field value (boolean is stored as "true" or "false" text) results = Member |> Member.fuzzy_search(%{query: "true"}) |> Ash.read!() # Note: "true" might match other things, so we check that member1 is in results assert Enum.any?(results, fn m -> m.id == member1.id end) end test "custom field value update triggers search_vector update", %{ member1: member1, string_field: string_field } do # Create initial custom field value {:ok, cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Update custom field value {:ok, _updated_cfv} = cfv |> Ash.Changeset.for_update(:update, %{ value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"} }) |> Ash.update() # Search for the new value results = Member |> Member.fuzzy_search(%{query: "NEWVALUE123"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id # Old value should not be found old_results = Member |> Member.fuzzy_search(%{query: "OLDVALUE"}) |> Ash.read!() refute Enum.any?(old_results, fn m -> m.id == member1.id end) end test "custom field value delete triggers search_vector update", %{ member1: member1, string_field: string_field } do # Create custom field value {:ok, cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Verify it's searchable results = Member |> Member.fuzzy_search(%{query: "TOBEDELETED"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id # Delete custom field value assert :ok = Ash.destroy(cfv) # Value should no longer be found deleted_results = Member |> Member.fuzzy_search(%{query: "TOBEDELETED"}) |> Ash.read!() refute Enum.any?(deleted_results, fn m -> m.id == member1.id end) end test "custom field value create triggers search_vector update", %{ member1: member1, string_field: string_field } do # Create custom field value (trigger should update search_vector automatically) {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"} }) |> Ash.create() # Search should find it immediately (trigger should have updated search_vector) results = Member |> Member.fuzzy_search(%{query: "AUTOUPDATE"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "member update includes custom field values in search_vector", %{ member1: member1, string_field: string_field } do # Create custom field value {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"} }) |> Ash.create() # Update member (should trigger search_vector update including custom fields) {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{notes: "Updated notes"}) |> Ash.update() # Search should find the custom field value results = Member |> Member.fuzzy_search(%{query: "MEMBERUPDATE"}) |> Ash.read!() assert length(results) == 1 assert List.first(results).id == member1.id end test "multiple custom field values are all searchable", %{ member1: member1, string_field: string_field, integer_field: integer_field, email_field: email_field } do # Create multiple custom field values {:ok, _cfv1} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "MULTI1"} }) |> Ash.create() {:ok, _cfv2} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: integer_field.id, value: %{"_union_type" => "integer", "_union_value" => 99_999} }) |> Ash.create() {:ok, _cfv3} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: email_field.id, value: %{"_union_type" => "email", "_union_value" => "multi@test.com"} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # All values should be searchable results1 = Member |> Member.fuzzy_search(%{query: "MULTI1"}) |> Ash.read!() assert Enum.any?(results1, fn m -> m.id == member1.id end) results2 = Member |> Member.fuzzy_search(%{query: "99999"}) |> Ash.read!() assert Enum.any?(results2, fn m -> m.id == member1.id end) results3 = Member |> Member.fuzzy_search(%{query: "multi@test.com"}) |> Ash.read!() assert Enum.any?(results3, fn m -> m.id == member1.id end) end test "finds member by custom field value with numbers in text field (e.g. phone number)", %{ member1: member1, string_field: string_field } do # Create custom field value with numbers and text (like phone number or ID) {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: string_field.id, value: %{"_union_type" => "string", "_union_value" => "M-123-456"} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for full value (should work via search_vector) results_full = Member |> Member.fuzzy_search(%{query: "M-123-456"}) |> Ash.read!() assert Enum.any?(results_full, fn m -> m.id == member1.id end), "Full value search should find member via search_vector" # Note: Partial substring search may require additional implementation # For now, we test that the full value is searchable, which is the primary use case # Substring matching for custom fields may need to be implemented separately end test "finds member by phone number in Emergency Contact custom field", %{ member1: member1 } do # Create Emergency Contact custom field {:ok, emergency_contact_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "Emergency Contact", value_type: :string }) |> Ash.create() # Create custom field value with phone number phone_number = "+49 123 456789" {:ok, _cfv} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member1.id, custom_field_id: emergency_contact_field.id, value: %{"_union_type" => "string", "_union_value" => phone_number} }) |> Ash.create() # Force search_vector update {:ok, _updated_member} = member1 |> Ash.Changeset.for_update(:update_member, %{}) |> Ash.update() # Search for full phone number (should work via search_vector) results_full = Member |> Member.fuzzy_search(%{query: phone_number}) |> Ash.read!() assert Enum.any?(results_full, fn m -> m.id == member1.id end), "Full phone number search should find member via search_vector" # Note: Partial substring search may require additional implementation # For now, we test that the full phone number is searchable, which is the primary use case # Substring matching for custom fields may need to be implemented separately end end end