mitgliederverwaltung/lib/mv/membership/import/member_csv.ex
carla fb71b7ddb1
Some checks failed
continuous-integration/drone/push Build is failing
fix struct inconsistencies
2026-01-14 09:49:40 +01:00

158 lines
5.2 KiB
Elixir

defmodule Mv.Membership.Import.MemberCSV do
@moduledoc """
Service module for importing members from CSV files.
This module provides the core API for CSV member import functionality:
- `prepare/2` - Parses and validates CSV content, returns import state
- `process_chunk/3` - Processes a chunk of rows and creates members
## Error Handling
Errors are returned as `%MemberCSV.Error{}` structs containing:
- `csv_line_number` - The physical line number in the CSV file (or `nil` for general errors)
- `field` - The field name (atom) or `nil` if not field-specific
- `message` - Human-readable error message (or `nil` for general errors)
## Import State
The `import_state` returned by `prepare/2` contains:
- `chunks` - List of row chunks ready for processing
- `column_map` - Map of canonical field names to column indices
- `custom_field_map` - Map of custom field names to column indices
- `warnings` - List of warning messages (e.g., unknown custom field columns)
## Chunk Results
The `chunk_result` returned by `process_chunk/3` contains:
- `inserted` - Number of successfully created members
- `failed` - Number of failed member creations
- `errors` - List of `%MemberCSV.Error{}` structs (capped at 50 per import)
## Examples
# Prepare CSV for import
{:ok, import_state} = MemberCSV.prepare(csv_content)
# Process first chunk
chunk = Enum.at(import_state.chunks, 0)
{:ok, result} = MemberCSV.process_chunk(chunk, import_state.column_map)
"""
defmodule Error do
@moduledoc """
Error struct for CSV import errors.
## Fields
- `csv_line_number` - The physical line number in the CSV file (1-based, header is line 1)
- `field` - The field name as an atom (e.g., `:email`) or `nil` if not field-specific
- `message` - Human-readable error message
"""
defstruct csv_line_number: nil, field: nil, message: nil
@type t :: %__MODULE__{
csv_line_number: pos_integer() | nil,
field: atom() | nil,
message: String.t() | nil
}
end
@type import_state :: %{
chunks: list(list({pos_integer(), map()})),
column_map: %{atom() => non_neg_integer()},
custom_field_map: %{String.t() => non_neg_integer()},
warnings: list(String.t())
}
@type chunk_result :: %{
inserted: non_neg_integer(),
failed: non_neg_integer(),
errors: list(Error.t())
}
@doc """
Prepares CSV content for import by parsing, mapping headers, and validating limits.
This function:
1. Strips UTF-8 BOM if present
2. Detects CSV delimiter (semicolon or comma)
3. Parses headers and data rows
4. Maps headers to canonical member fields
5. Maps custom field columns by name
6. Validates row count limits
7. Chunks rows for processing
## Parameters
- `file_content` - The raw CSV file content as a string
- `opts` - Optional keyword list:
- `:max_rows` - Maximum number of data rows allowed (default: 1000)
- `:chunk_size` - Number of rows per chunk (default: 200)
## Returns
- `{:ok, import_state}` - Successfully prepared import state
- `{:error, reason}` - Error reason (string or error struct)
## Examples
iex> MemberCSV.prepare("email\\njohn@example.com")
{:ok, %{chunks: [...], column_map: %{email: 0}, ...}}
iex> MemberCSV.prepare("")
{:error, "CSV file is empty"}
"""
@spec prepare(String.t(), keyword()) :: {:ok, import_state()} | {:error, String.t()}
def prepare(file_content, opts \\ []) do
# TODO: Implement in Issue #3 (CSV Parsing)
# This is a skeleton implementation that will be filled in later
_ = {file_content, opts}
# Placeholder return - will be replaced with actual implementation
{:error, "Not yet implemented"}
end
@doc """
Processes a chunk of CSV rows and creates members.
This function:
1. Validates each row
2. Creates members via Ash resource
3. Creates custom field values for each member
4. Collects errors with correct CSV line numbers
5. Returns chunk processing results
## Parameters
- `chunk_rows_with_lines` - List of tuples `{csv_line_number, row_map}` where:
- `csv_line_number` - Physical line number in CSV (1-based)
- `row_map` - Map of column names to values
- `column_map` - Map of canonical field names (atoms) to column indices
- `opts` - Optional keyword list for processing options
## Returns
- `{:ok, chunk_result}` - Chunk processing results
- `{:error, reason}` - Error reason (string)
## Examples
iex> chunk = [{2, %{"email" => "john@example.com"}}]
iex> column_map = %{email: 0}
iex> MemberCSV.process_chunk(chunk, column_map)
{:ok, %{inserted: 1, failed: 0, errors: []}}
"""
@spec process_chunk(
list({pos_integer(), map()}),
%{atom() => non_neg_integer()},
keyword()
) :: {:ok, chunk_result()} | {:error, String.t()}
def process_chunk(chunk_rows_with_lines, column_map, opts \\ []) do
# TODO: Implement in Issue #6 (Persistence)
# This is a skeleton implementation that will be filled in later
_ = {chunk_rows_with_lines, column_map, opts}
# Placeholder return - will be replaced with actual implementation
{:ok, %{inserted: 0, failed: 0, errors: []}}
end
end