defmodule Mv.Membership.Import.MemberCSVTest do use Mv.DataCase, async: true alias Mv.Membership.Import.MemberCSV describe "Error struct" do test "Error struct exists with required fields" do # This will fail at runtime if the struct doesn't exist # We use struct/2 to create the struct at runtime error = struct(MemberCSV.Error, %{ csv_line_number: 5, field: :email, message: "is not a valid email" }) assert error.csv_line_number == 5 assert error.field == :email assert error.message == "is not a valid email" end test "Error struct allows nil field" do # This will fail at runtime if the struct doesn't exist error = struct(MemberCSV.Error, %{ csv_line_number: 10, field: nil, message: "Row is empty" }) assert error.csv_line_number == 10 assert error.field == nil assert error.message == "Row is empty" end end describe "prepare/2" do test "accepts file_content and opts and returns tagged tuple" do file_content = "email\njohn@example.com" opts = [] result = MemberCSV.prepare(file_content, opts) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "returns {:ok, import_state} on success" do file_content = "email\njohn@example.com" opts = [] assert {:ok, import_state} = MemberCSV.prepare(file_content, opts) # Check that import_state contains expected fields assert Map.has_key?(import_state, :chunks) assert Map.has_key?(import_state, :column_map) assert Map.has_key?(import_state, :custom_field_map) assert Map.has_key?(import_state, :warnings) assert import_state.column_map[:email] == 0 assert import_state.chunks != [] end test "returns {:error, reason} on failure" do file_content = "" opts = [] assert {:error, _reason} = MemberCSV.prepare(file_content, opts) end end describe "process_chunk/4" do setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end test "accepts chunk_rows_with_lines, column_map, custom_field_map, and opts and returns tagged tuple", %{ actor: actor } do chunk_rows_with_lines = [{2, %{member: %{email: "john@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert match?({:ok, _}, result) or match?({:error, _}, result) end test "creates member successfully with valid data", %{actor: actor} do chunk_rows_with_lines = [ {2, %{member: %{email: "john@example.com", first_name: "John"}, custom: %{}}} ] column_map = %{email: 0, first_name: 1} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 1 assert chunk_result.failed == 0 assert chunk_result.errors == [] # Verify member was created system_actor = Mv.Helpers.SystemActor.get_system_actor() members = Mv.Membership.list_members!(actor: system_actor) assert Enum.any?(members, &(&1.email == "john@example.com")) end test "returns error for invalid email", %{actor: actor} do chunk_rows_with_lines = [ {2, %{member: %{email: "invalid-email"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.field == :email # Error message should come from validate_row (Gettext-backed) assert is_binary(error.message) assert error.message != "" end test "returns error for missing email", %{actor: actor} do chunk_rows_with_lines = [ {2, %{member: %{}, custom: %{}}} ] column_map = %{} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.field == :email assert is_binary(error.message) end test "returns error for whitespace-only email", %{actor: actor} do chunk_rows_with_lines = [ {3, %{member: %{email: " "}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 3 assert error.field == :email end test "returns error for duplicate email", %{actor: actor} do # Create existing member first {:ok, _existing} = Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"}, actor: actor ) chunk_rows_with_lines = [ {2, %{member: %{email: "duplicate@example.com", first_name: "New"}, custom: %{}}} ] column_map = %{email: 0, first_name: 1} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.field == :email assert error.message =~ "email" or error.message =~ "duplicate" or error.message =~ "unique" end test "creates member with custom field values", %{actor: actor} do # Create custom field first {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Phone", value_type: :string }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {2, %{ member: %{email: "withcustom@example.com"}, custom: %{to_string(custom_field.id) => "123-456-7890"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 1 assert chunk_result.failed == 0 # Verify member and custom field value were created members = Mv.Membership.list_members!(actor: actor) member = Enum.find(members, &(&1.email == "withcustom@example.com")) assert member != nil {:ok, member_with_cf} = Ash.load(member, :custom_field_values, actor: actor) assert length(member_with_cf.custom_field_values) == 1 cfv = List.first(member_with_cf.custom_field_values) assert cfv.custom_field_id == custom_field.id assert cfv.value.value == "123-456-7890" end test "handles multiple rows with mixed success and failure", %{actor: actor} do chunk_rows_with_lines = [ {2, %{member: %{email: "valid1@example.com"}, custom: %{}}}, {3, %{member: %{email: "invalid-email"}, custom: %{}}}, {4, %{member: %{email: "valid2@example.com"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 2 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 3 assert error.field == :email # Error should come from validate_row, not from DB insert assert is_binary(error.message) end test "preserves CSV line numbers in errors", %{actor: actor} do chunk_rows_with_lines = [ {5, %{member: %{email: "invalid"}, custom: %{}}}, {10, %{member: %{email: "also-invalid"}, custom: %{}}} ] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.failed == 2 assert length(chunk_result.errors) == 2 line_numbers = Enum.map(chunk_result.errors, & &1.csv_line_number) assert 5 in line_numbers assert 10 in line_numbers end test "returns {:ok, chunk_result} on success", %{actor: actor} do chunk_rows_with_lines = [{2, %{member: %{email: "test@example.com"}, custom: %{}}}] column_map = %{email: 0} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) # Check that chunk_result contains expected fields assert Map.has_key?(chunk_result, :inserted) assert Map.has_key?(chunk_result, :failed) assert Map.has_key?(chunk_result, :errors) assert is_integer(chunk_result.inserted) assert is_integer(chunk_result.failed) assert is_list(chunk_result.errors) end test "returns {:ok, _} with zero counts for empty chunk", %{actor: actor} do chunk_rows_with_lines = [] column_map = %{} custom_field_map = %{} opts = [actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 0 assert chunk_result.errors == [] end test "error capping collects exactly 50 errors", %{actor: actor} do # Create 50 rows with invalid emails chunk_rows_with_lines = 1..50 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 0, max_errors: 50, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 50 assert length(chunk_result.errors) == 50 end test "error capping collects only first 50 errors when more than 50 errors occur", %{ actor: actor } do # Create 60 rows with invalid emails chunk_rows_with_lines = 1..60 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 0, max_errors: 50, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 60 assert length(chunk_result.errors) == 50 end test "error capping respects existing_error_count", %{actor: actor} do # Create 30 rows with invalid emails chunk_rows_with_lines = 1..30 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 25, max_errors: 50, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 30 # Should only collect 25 errors (25 existing + 25 new = 50 limit) assert length(chunk_result.errors) == 25 end test "error capping collects no errors when limit already reached", %{actor: actor} do # Create 10 rows with invalid emails chunk_rows_with_lines = 1..10 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 50, max_errors: 50, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 10 assert chunk_result.errors == [] end test "error capping with mixed success and failure", %{actor: actor} do # Create 100 rows: 30 valid, 70 invalid valid_rows = 1..30 |> Enum.map(fn i -> {i + 1, %{member: %{email: "valid#{i}@example.com"}, custom: %{}}} end) invalid_rows = 31..100 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) chunk_rows_with_lines = valid_rows ++ invalid_rows column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 0, max_errors: 50, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 30 assert chunk_result.failed == 70 # Should only collect 50 errors (limit reached) assert length(chunk_result.errors) == 50 end test "error capping with custom max_errors", %{actor: actor} do # Create 20 rows with invalid emails chunk_rows_with_lines = 1..20 |> Enum.map(fn i -> {i + 1, %{member: %{email: "invalid-email-#{i}"}, custom: %{}}} end) column_map = %{email: 0} custom_field_map = %{} opts = [existing_error_count: 0, max_errors: 10, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 20 assert length(chunk_result.errors) == 10 end end describe "validate_row/3" do test "returns error when email is missing" do row_map = %{member: %{}, custom: %{}} csv_line_number = 5 opts = [] assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert %MemberCSV.Error{} = error assert error.csv_line_number == 5 assert error.field == :email assert error.message != nil assert error.message != "" end test "returns error when email is only whitespace" do row_map = %{member: %{email: " "}, custom: %{}} csv_line_number = 3 opts = [] assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert %MemberCSV.Error{} = error assert error.csv_line_number == 3 assert error.field == :email assert error.message != nil end test "returns error when email is nil" do row_map = %{member: %{email: nil}, custom: %{}} csv_line_number = 7 opts = [] assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert %MemberCSV.Error{} = error assert error.csv_line_number == 7 assert error.field == :email end test "returns error when email format is invalid" do row_map = %{member: %{email: "invalid-email"}, custom: %{}} csv_line_number = 4 opts = [] assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert %MemberCSV.Error{} = error assert error.csv_line_number == 4 assert error.field == :email assert error.message != nil end test "returns {:ok, trimmed_row_map} when email is valid with whitespace" do row_map = %{member: %{email: " john@example.com "}, custom: %{}} csv_line_number = 2 opts = [] assert {:ok, trimmed_row_map} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert trimmed_row_map.member.email == "john@example.com" end test "returns {:ok, trimmed_row_map} when email is valid without whitespace" do row_map = %{member: %{email: "john@example.com"}, custom: %{}} csv_line_number = 2 opts = [] assert {:ok, trimmed_row_map} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert trimmed_row_map.member.email == "john@example.com" end test "trims all string values in member map" do row_map = %{ member: %{ email: " john@example.com ", first_name: " John ", last_name: " Doe " }, custom: %{} } csv_line_number = 2 opts = [] assert {:ok, trimmed_row_map} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert trimmed_row_map.member.email == "john@example.com" assert trimmed_row_map.member.first_name == "John" assert trimmed_row_map.member.last_name == "Doe" end test "preserves custom map unchanged" do row_map = %{ member: %{email: "john@example.com"}, custom: %{"field1" => "value1"} } csv_line_number = 2 opts = [] assert {:ok, trimmed_row_map} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert trimmed_row_map.custom == %{"field1" => "value1"} end test "uses Gettext for error messages" do row_map = %{member: %{}, custom: %{}} csv_line_number = 5 opts = [] # Test with default locale (should work) assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert is_binary(error.message) # Test with German locale Gettext.put_locale(MvWeb.Gettext, "de") assert {:error, error_de} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert is_binary(error_de.message) # Test with English locale Gettext.put_locale(MvWeb.Gettext, "en") assert {:error, error_en} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert is_binary(error_en.message) # Reset to default Gettext.put_locale(MvWeb.Gettext, "en") end test "handles empty opts gracefully" do row_map = %{member: %{email: "john@example.com"}, custom: %{}} csv_line_number = 2 opts = [] assert {:ok, _} = MemberCSV.validate_row(row_map, csv_line_number, opts) end test "handles missing member key gracefully" do row_map = %{custom: %{}} csv_line_number = 3 opts = [] assert {:error, error} = MemberCSV.validate_row(row_map, csv_line_number, opts) assert %MemberCSV.Error{} = error assert error.csv_line_number == 3 end end describe "custom field import" do setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() %{actor: system_actor} end test "creates member with valid integer custom field value", %{actor: actor} do # Create integer custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Alter", value_type: :integer }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {2, %{ member: %{email: "withage@example.com"}, custom: %{to_string(custom_field.id) => "25"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 1 assert chunk_result.failed == 0 # Verify member and custom field value were created members = Mv.Membership.list_members!(actor: actor) member = Enum.find(members, &(&1.email == "withage@example.com")) assert member != nil {:ok, member_with_cf} = Ash.load(member, :custom_field_values, actor: actor) assert length(member_with_cf.custom_field_values) == 1 cfv = List.first(member_with_cf.custom_field_values) assert cfv.custom_field_id == custom_field.id assert cfv.value.value == 25 assert cfv.value.type == :integer end test "returns error for invalid integer custom field value", %{actor: actor} do # Create integer custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Alter", value_type: :integer }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {2, %{ member: %{email: "invalidage@example.com"}, custom: %{to_string(custom_field.id) => "abc"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 2 assert error.message =~ "custom_field: Alter" assert error.message =~ "Number" assert error.message =~ "abc" end test "returns error for invalid date custom field value", %{actor: actor} do # Create date custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Geburtstag", value_type: :date }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {3, %{ member: %{email: "invaliddate@example.com"}, custom: %{to_string(custom_field.id) => "not-a-date"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 3 assert error.message =~ "custom_field: Geburtstag" assert error.message =~ "Date" end test "returns error for invalid email custom field value", %{actor: actor} do # Create email custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Work Email", value_type: :email }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {4, %{ member: %{email: "invalidemailcf@example.com"}, custom: %{to_string(custom_field.id) => "not-an-email"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 4 assert error.message =~ "custom_field: Work Email" assert error.message =~ "E-Mail" end test "returns error for invalid boolean custom field value", %{actor: actor} do # Create boolean custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Is Active", value_type: :boolean }) |> Ash.create(actor: actor) chunk_rows_with_lines = [ {5, %{ member: %{email: "invalidbool@example.com"}, custom: %{to_string(custom_field.id) => "maybe"} }} ] column_map = %{email: 0} custom_field_map = %{to_string(custom_field.id) => 1} custom_field_lookup = %{ to_string(custom_field.id) => %{ id: custom_field.id, value_type: custom_field.value_type, name: custom_field.name } } opts = [custom_field_lookup: custom_field_lookup, actor: actor] assert {:ok, chunk_result} = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts) assert chunk_result.inserted == 0 assert chunk_result.failed == 1 assert length(chunk_result.errors) == 1 error = List.first(chunk_result.errors) assert error.csv_line_number == 5 assert error.message =~ "custom_field: Is Active" # Error message should indicate boolean/Yes-No validation failure assert String.contains?(error.message, "Yes/No") || String.contains?(error.message, "true/false") || String.contains?(error.message, "boolean") end end describe "prepare/2 with custom fields" do setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() # Create a custom field {:ok, custom_field} = Mv.Membership.CustomField |> Ash.Changeset.for_create(:create, %{ name: "Membership Number", value_type: :string }) |> Ash.create(actor: system_actor) %{actor: system_actor, custom_field: custom_field} end test "includes custom field in custom_field_map when header matches", %{ custom_field: custom_field } do # CSV with custom field column csv_content = "email;Membership Number\njohn@example.com;12345" assert {:ok, import_state} = MemberCSV.prepare(csv_content) # Check that custom field is mapped assert Map.has_key?(import_state.custom_field_map, to_string(custom_field.id)) assert import_state.column_map[:email] == 0 end test "includes warning for unknown custom field column", %{custom_field: _custom_field} do # CSV with unknown custom field column (not matching any existing custom field) csv_content = "email;NichtExistierend\njohn@example.com;value" assert {:ok, import_state} = MemberCSV.prepare(csv_content) # Check that warning is present assert import_state.warnings != [] warning = List.first(import_state.warnings) assert warning =~ "NichtExistierend" assert warning =~ "ignored" assert warning =~ "custom field" # Check that unknown column is not in custom_field_map assert import_state.custom_field_map == %{} # Member import should still succeed assert import_state.column_map[:email] == 0 end test "import succeeds even with unknown custom field columns", %{custom_field: _custom_field} do # CSV with unknown custom field column csv_content = "email;UnknownField\njohn@example.com;value" assert {:ok, import_state} = MemberCSV.prepare(csv_content) # Import state should be valid assert import_state.column_map[:email] == 0 assert import_state.chunks != [] end end end