feat: adds row validation

This commit is contained in:
carla 2026-01-19 11:22:11 +01:00
parent 9be5dc8751
commit 8b3cc6a6b2
2 changed files with 300 additions and 27 deletions

View file

@ -76,6 +76,8 @@ defmodule Mv.Membership.Import.MemberCSV do
alias Mv.Membership.Import.CsvParser
alias Mv.Membership.Import.HeaderMapper
use Gettext, backend: MvWeb.Gettext
@doc """
Prepares CSV content for import by parsing, mapping headers, and validating limits.
@ -295,38 +297,122 @@ defmodule Mv.Membership.Import.MemberCSV do
{:ok, %{inserted: inserted, failed: failed, errors: Enum.reverse(errors)}}
end
@doc """
Validates a single CSV row before database insertion.
This function:
1. Trims all string values in the member map
2. Validates that email is present and not empty after trimming
3. Validates email format using EctoCommons.EmailValidator
4. Returns structured errors with Gettext-backed messages
## Parameters
- `row_map` - Map with `:member` and `:custom` keys containing field values
- `csv_line_number` - Physical line number in CSV (1-based, header is line 1)
- `opts` - Optional keyword list (for future extensions)
## Returns
- `{:ok, trimmed_row_map}` - Successfully validated row with trimmed values
- `{:error, %Error{}}` - Validation error with structured error information
## Examples
iex> row_map = %{member: %{email: " john@example.com "}, custom: %{}}
iex> MemberCSV.validate_row(row_map, 2, [])
{:ok, %{member: %{email: "john@example.com"}, custom: %{}}}
iex> row_map = %{member: %{}, custom: %{}}
iex> MemberCSV.validate_row(row_map, 3, [])
{:error, %MemberCSV.Error{csv_line_number: 3, field: :email, message: "Email is required."}}
"""
@spec validate_row(map(), pos_integer(), keyword()) ::
{:ok, map()} | {:error, Error.t()}
def validate_row(row_map, csv_line_number, _opts \\ []) do
# Safely get member map (handle missing key)
member_attrs = Map.get(row_map, :member, %{})
custom_attrs = Map.get(row_map, :custom, %{})
# Trim all string values in member map
trimmed_member = trim_string_values(member_attrs)
# Validate email presence (after trim)
email = Map.get(trimmed_member, :email)
cond do
is_nil(email) or email == "" ->
{:error,
%Error{
csv_line_number: csv_line_number,
field: :email,
message: gettext("Email is required.")
}}
not valid_email_format?(email) ->
{:error,
%Error{
csv_line_number: csv_line_number,
field: :email,
message: gettext("Email is invalid.")
}}
true ->
{:ok, %{member: trimmed_member, custom: custom_attrs}}
end
end
# Validates email format using EctoCommons.EmailValidator
defp valid_email_format?(email) when is_binary(email) do
changeset =
{%{}, %{email: :string}}
|> Ecto.Changeset.cast(%{email: email}, [:email])
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
changeset.valid?
end
defp valid_email_format?(_), do: false
# Processes a single row and creates member with custom field values
defp process_row(
%{member: member_attrs, custom: custom_attrs},
row_map,
line_number,
custom_field_lookup
) do
# Prepare custom field values for Ash
custom_field_values = prepare_custom_field_values(custom_attrs, custom_field_lookup)
# Create member with custom field values
member_attrs_with_cf =
member_attrs
|> Map.put(:custom_field_values, custom_field_values)
|> trim_string_values()
# Only include custom_field_values if not empty
final_attrs =
if Enum.empty?(custom_field_values) do
Map.delete(member_attrs_with_cf, :custom_field_values)
else
member_attrs_with_cf
end
case Mv.Membership.create_member(final_attrs) do
{:ok, member} ->
{:ok, member}
{:error, %Ash.Error.Invalid{} = error} ->
{:error, format_ash_error(error, line_number)}
# Validate row before database insertion
case validate_row(row_map, line_number, []) do
{:error, error} ->
{:error, %Error{csv_line_number: line_number, field: nil, message: inspect(error)}}
# Return validation error immediately, no DB insert attempted
{:error, error}
{:ok, %{member: trimmed_member_attrs, custom: custom_attrs}} ->
# Prepare custom field values for Ash
custom_field_values = prepare_custom_field_values(custom_attrs, custom_field_lookup)
# Create member with custom field values
member_attrs_with_cf =
trimmed_member_attrs
|> Map.put(:custom_field_values, custom_field_values)
# Only include custom_field_values if not empty
final_attrs =
if Enum.empty?(custom_field_values) do
Map.delete(member_attrs_with_cf, :custom_field_values)
else
member_attrs_with_cf
end
case Mv.Membership.create_member(final_attrs) do
{:ok, member} ->
{:ok, member}
{:error, %Ash.Error.Invalid{} = error} ->
{:error, format_ash_error(error, line_number)}
{:error, error} ->
{:error, %Error{csv_line_number: line_number, field: nil, message: inspect(error)}}
end
end
rescue
e ->