defmodule DaProductApp.Settlements.Refund.CsvParser do @moduledoc """ Parser for refund settlement CSV files. Handles parsing CSV files to extract refund transactions where: - transactionType = "REFUND" - fundDirection = "DEBIT" """ require Logger @type refund_transaction :: %{ transaction_request_id: String.t(), transaction_type: String.t(), fund_direction: String.t(), transaction_amount: Decimal.t(), transaction_currency: String.t(), transaction_date: DateTime.t() | nil, raw_data: map() } @type parsed_refunds :: %{ filename: String.t(), total_count: integer(), total_amount: Decimal.t(), transactions: [refund_transaction()] } @doc """ Parses a refund CSV file and extracts REFUND transactions with DEBIT fund direction. """ @spec parse_file(String.t(), binary()) :: {:ok, parsed_refunds()} | {:error, any()} def parse_file(filename, content) do with {:ok, lines} <- validate_and_split_content(content), {:ok, transactions} <- parse_refund_transactions(lines) do total_amount = transactions |> Enum.map(& &1.transaction_amount) |> Enum.reduce(Decimal.new(0), &Decimal.add/2) parsed_data = %{ filename: filename, total_count: length(transactions), total_amount: total_amount, transactions: transactions } {:ok, parsed_data} else {:error, reason} -> {:error, reason} end rescue error -> Logger.error("Failed to parse refund CSV file #{filename}: #{inspect(error)}") {:error, "Failed to parse CSV: #{inspect(error)}"} end defp validate_and_split_content(content) do if String.valid?(content) do lines = content |> String.trim() |> String.split(["\\n", "\\r\\n"], trim: true) |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) {:ok, lines} else {:error, "Invalid UTF-8 content"} end end defp parse_refund_transactions([header | data_rows]) when length(data_rows) > 0 do header_fields = parse_csv_row(header) # Find required field indices transaction_request_id_index = find_field_index(header_fields, "transactionRequestId") transaction_type_index = find_field_index(header_fields, "transactionType") fund_direction_index = find_field_index(header_fields, "fundDirection") if transaction_request_id_index && transaction_type_index && fund_direction_index do refund_transactions = data_rows |> Enum.map(fn row -> parse_transaction_row(row, header_fields) end) |> Enum.filter(&is_refund_transaction?/1) |> Enum.reject(&is_nil/1) {:ok, refund_transactions} else {:error, "Missing required fields: transactionRequestId, transactionType, fundDirection"} end end defp parse_refund_transactions(_), do: {:error, "Invalid CSV structure: no data rows found"} defp parse_transaction_row(row, header_fields) do data_fields = parse_csv_row(row) if length(header_fields) == length(data_fields) do row_map = Enum.zip(header_fields, data_fields) |> Enum.into(%{}) %{ transaction_request_id: Map.get(row_map, "transactionRequestId"), transaction_type: Map.get(row_map, "transactionType"), fund_direction: Map.get(row_map, "fundDirection"), transaction_amount: parse_optional_amount(Map.get(row_map, "transactionAmount", "0")), transaction_currency: Map.get(row_map, "transactionCurrency", "AED"), transaction_date: parse_optional_datetime(Map.get(row_map, "transactionDate")), raw_data: row_map } else nil end rescue error -> Logger.warn("Failed to parse transaction row: #{inspect(error)}") nil end defp is_refund_transaction?(%{transaction_type: "REFUND", fund_direction: "DEBIT"}), do: true defp is_refund_transaction?(_), do: false defp find_field_index(fields, field_name) do Enum.find_index(fields, &(&1 == field_name)) end defp parse_csv_row(row) do # Simple CSV parsing - split by comma and handle quoted fields row |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.map(&String.trim(&1, "\"")) end defp parse_optional_amount(nil), do: Decimal.new(0) defp parse_optional_amount(""), do: Decimal.new(0) defp parse_optional_amount(amount_str) when is_binary(amount_str) do case Decimal.parse(amount_str) do {decimal, ""} -> decimal _ -> Decimal.new(0) end end defp parse_optional_amount(_), do: Decimal.new(0) defp parse_optional_datetime(nil), do: nil defp parse_optional_datetime(""), do: nil defp parse_optional_datetime(date_str) when is_binary(date_str) do case DateTime.from_iso8601(date_str) do {:ok, datetime, _} -> datetime _ -> nil end end defp parse_optional_datetime(_), do: nil end