feat: add service skeleton and tests
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
22d50d6c46
commit
cc6d72b6b1
2 changed files with 286 additions and 0 deletions
158
lib/mv/membership/import/member_csv.ex
Normal file
158
lib/mv/membership/import/member_csv.ex
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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 `%Error{}` structs containing:
|
||||
- `csv_line_number` - The physical line number in the CSV file
|
||||
- `field` - The field name (atom) or `nil` if not field-specific
|
||||
- `message` - Human-readable error message
|
||||
|
||||
## 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 `%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: integer(),
|
||||
field: atom() | nil,
|
||||
message: String.t()
|
||||
}
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue