diff --git a/lib/mv/membership/import/header_mapper.ex b/lib/mv/membership/import/header_mapper.ex index 709e156..d96d96e 100644 --- a/lib/mv/membership/import/header_mapper.ex +++ b/lib/mv/membership/import/header_mapper.ex @@ -17,15 +17,24 @@ defmodule Mv.Membership.Import.HeaderMapper do ## Member Field Mapping - Maps CSV headers to canonical member fields: - - `email` (required) - - `first_name` (optional) - - `last_name` (optional) - - `street` (optional) - - `postal_code` (optional) - - `city` (optional) + Maps CSV headers to canonical member fields (same as `Mv.Constants.member_fields()` for + importable attributes). All DB-backed member attributes can be imported. - Supports both English and German variants (e.g., "Email" / "E-Mail", "First Name" / "Vorname"). + - `email` (required) + - `first_name`, `last_name` (optional) + - `join_date`, `exit_date` (optional, ISO-8601 date) + - `notes` (optional) + - `country`, `city`, `street`, `house_number`, `postal_code` (optional) + - `membership_fee_start_date` (optional, ISO-8601 date) + + Supports English and German header variants (e.g. "Email" / "E-Mail", "Join Date" / "Beitrittsdatum"). + + ## Fields not supported for import + + - **membership_fee_status** – Computed (calculation from membership fee cycles). Not stored; + cannot be set via CSV. Export can include it. + - **groups** – Many-to-many relationship (through member_groups). Import would require + resolving group names/slugs to IDs and creating associations; not in current import scope. ## Custom Field Detection @@ -75,11 +84,37 @@ defmodule Mv.Membership.Import.HeaderMapper do "nachname", "familienname" ], + join_date: [ + "join date", + "join_date", + "beitrittsdatum", + "beitritts-datum" + ], + exit_date: [ + "exit date", + "exit_date", + "austrittsdatum", + "austritts-datum" + ], + notes: [ + "notes", + "notizen", + "bemerkungen" + ], street: [ "street", "address", "strasse" ], + house_number: [ + "house number", + "house_number", + "house no", + "hausnummer", + "nr", + "nr.", + "nummer" + ], postal_code: [ "postal code", "postal_code", @@ -93,6 +128,18 @@ defmodule Mv.Membership.Import.HeaderMapper do "town", "stadt", "ort" + ], + country: [ + "country", + "land", + "staat" + ], + membership_fee_start_date: [ + "membership fee start date", + "membership_fee_start_date", + "fee start", + "beitragsbeginn", + "beitrags-beginn" ] } diff --git a/lib/mv/membership/import/member_csv.ex b/lib/mv/membership/import/member_csv.ex index c967bf5..23e0d93 100644 --- a/lib/mv/membership/import/member_csv.ex +++ b/lib/mv/membership/import/member_csv.ex @@ -549,9 +549,12 @@ defmodule Mv.Membership.Import.MemberCSV do line_number, actor ) do + # Convert empty strings to nil for date fields so Ash accepts them + member_attrs = sanitize_date_fields(trimmed_member_attrs) + # Create member with custom field values member_attrs_with_cf = - trimmed_member_attrs + member_attrs |> Map.put(:custom_field_values, custom_field_values) # Only include custom_field_values if not empty @@ -793,6 +796,23 @@ defmodule Mv.Membership.Import.MemberCSV do end) end + # Converts empty strings to nil for date fields so Ash can accept them + @date_fields [:join_date, :exit_date, :membership_fee_start_date] + + defp sanitize_date_fields(attrs) when is_map(attrs) do + Enum.reduce(@date_fields, attrs, fn field, acc -> + put_date_field(acc, field, Map.get(acc, field)) + end) + end + + defp put_date_field(acc, field, ""), do: Map.put(acc, field, nil) + + defp put_date_field(acc, field, val) when is_binary(val) do + if String.trim(val) == "", do: Map.put(acc, field, nil), else: acc + end + + defp put_date_field(acc, _field, _), do: acc + # Formats Ash errors into MemberCSV.Error structs defp format_ash_error(%Ash.Error.Invalid{errors: errors}, line_number, email) do # Try to find email-related errors first (for better error messages)