feat: adds row validation
This commit is contained in:
parent
9be5dc8751
commit
8b3cc6a6b2
2 changed files with 300 additions and 27 deletions
|
|
@ -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 ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue