defmodule DaProductApp.Settlements.Refund.Processor do @moduledoc """ Processor for refund settlement CSV files. This module handles: 1. Parsing CSV files for refund transactions 2. Matching refund transactions with database records 3. Creating settlement summaries and detailed records 4. Error handling and logging """ require Logger import Ecto.Query, warn: false alias DaProductApp.Repo alias DaProductApp.Settlements.Refund.CsvParser alias DaProductApp.Settlements.Settlement alias DaProductApp.Settlements.SettlementTransaction alias DaProductApp.Transactions.TransactionOperation @type processing_result :: %{ settlement_id: String.t() | nil, status: :success | :error, matched_count: integer(), total_refunds: integer(), total_amount: Decimal.t(), unmatched_transactions: [String.t()], errors: [String.t()] } @doc """ Processes a refund settlement CSV file. Steps: 1. Parse CSV to extract refund transactions 2. Match transactions with database records 3. Create settlement summary 4. Create settlement transaction details """ @spec process_refund_file(String.t(), binary(), map()) :: {:ok, processing_result()} | {:error, any()} def process_refund_file(filename, content, options \\ %{}) do Logger.info("Processing refund settlement file: #{filename}") Repo.transaction(fn -> with {:ok, parsed_data} <- CsvParser.parse_file(filename, content), {:ok, matched_data} <- match_refund_transactions(parsed_data), {:ok, settlement} <- create_refund_settlement(matched_data, options), {:ok, _} <- insert_settlement_transactions(matched_data, settlement) do Logger.info("Successfully processed refund settlement file #{filename}") %{ settlement_id: settlement.settlement_id, status: :success, matched_count: length(matched_data.matched_transactions), total_refunds: parsed_data.total_count, total_amount: parsed_data.total_amount, unmatched_transactions: matched_data.unmatched_request_ids, errors: [] } else {:error, reason} -> Logger.error("Failed to process refund settlement file #{filename}: #{inspect(reason)}") Repo.rollback(reason) end end) end @doc """ Matches refund transactions with database records. Uses transactionRequestId to match with operation_request_id in transaction_operations table. """ def match_refund_transactions(parsed_data) do Logger.info("Matching #{length(parsed_data.transactions)} refund transactions") # Extract all transaction request IDs request_ids = Enum.map(parsed_data.transactions, & &1.transaction_request_id) # Query database for matching transaction operations matching_operations = from(op in TransactionOperation, where: op.operation_request_id in ^request_ids, select: {op.operation_request_id, op} ) |> Repo.all() |> Enum.into(%{}) # Separate matched and unmatched transactions {matched_transactions, unmatched_request_ids} = Enum.reduce(parsed_data.transactions, {[], []}, fn refund, {matched, unmatched} -> case Map.get(matching_operations, refund.transaction_request_id) do nil -> {matched, [refund.transaction_request_id | unmatched]} operation -> matched_refund = Map.put(refund, :matched_operation, operation) {[matched_refund | matched], unmatched} end end) matched_amount = matched_transactions |> Enum.map(& &1.transaction_amount) |> Enum.reduce(Decimal.new(0), &Decimal.add/2) Logger.info("Matched #{length(matched_transactions)} refunds, unmatched: #{length(unmatched_request_ids)}") if length(unmatched_request_ids) > 0 do Logger.warn("Unmatched refund transaction IDs: #{inspect(unmatched_request_ids)}") end matched_data = %{ original_data: parsed_data, matched_transactions: matched_transactions, matched_amount: matched_amount, unmatched_request_ids: unmatched_request_ids } {:ok, matched_data} rescue error -> Logger.error("Error matching refund transactions: #{inspect(error)}") {:error, "Failed to match transactions: #{inspect(error)}"} end defp create_refund_settlement(matched_data, options) do Logger.info("Creating refund settlement record") settlement_id = generate_settlement_id("REFUND") settlement_date = options[:settlement_date] || Date.utc_today() settlement_attrs = %{ settlement_id: settlement_id, date: settlement_date, status: "COMPLETED", amount: matched_data.matched_amount, details: %{ file_name: matched_data.original_data.filename, total_refunds_in_file: matched_data.original_data.total_count, matched_refunds: length(matched_data.matched_transactions), unmatched_refunds: length(matched_data.unmatched_request_ids), unmatched_request_ids: matched_data.unmatched_request_ids, processing_timestamp: DateTime.utc_now() }, total_transaction_count: length(matched_data.matched_transactions), gross_settlement_amount: matched_data.matched_amount, gross_settlement_currency: "AED", net_settlement_amount: matched_data.matched_amount, net_settlement_currency: "AED", settlement_timestamp: DateTime.utc_now(), merchant_id: options[:merchant_id], provider_id: options[:provider_id] } case Repo.insert(Settlement.changeset(%Settlement{}, settlement_attrs)) do {:ok, settlement} -> Logger.info("Created refund settlement with ID: #{settlement.settlement_id}") {:ok, settlement} {:error, changeset} -> Logger.error("Failed to create refund settlement: #{inspect(changeset)}") {:error, "Failed to create settlement: #{inspect(changeset)}"} end end defp insert_settlement_transactions(matched_data, settlement) do Logger.info("Inserting #{length(matched_data.matched_transactions)} settlement transaction records") settlement_transaction_records = matched_data.matched_transactions |> Enum.map(fn refund -> %{ settlement_id: settlement.settlement_id, transaction_id: refund.matched_operation.transaction_id, qr_id: "", # Default empty for refunds terminal_id: "", # Default empty for refunds transaction_amount: refund.transaction_amount, transaction_currency: refund.transaction_currency, transaction_status: "matched", transaction_time: refund.transaction_date || DateTime.utc_now(), mdr_charge: Decimal.new(0), mdr_charge_currency: refund.transaction_currency, tax_on_mdr: Decimal.new(0), tax_on_mdr_currency: refund.transaction_currency, net_received_amount: refund.transaction_amount, net_received_currency: refund.transaction_currency, inserted_at: DateTime.utc_now() |> DateTime.truncate(:second), updated_at: DateTime.utc_now() |> DateTime.truncate(:second) } end) case Repo.insert_all(SettlementTransaction, settlement_transaction_records) do {count, _} when count > 0 -> Logger.info("Successfully created #{count} settlement transaction records") {:ok, count} {0, _} -> Logger.warn("No settlement transaction records were created") {:ok, 0} error -> Logger.error("Failed to insert settlement transactions: #{inspect(error)}") {:error, "Failed to insert settlement transactions"} end end defp generate_settlement_id(prefix \\ "REF") do timestamp = DateTime.utc_now() |> DateTime.to_unix() random = :rand.uniform(9999) |> Integer.to_string() |> String.pad_leading(4, "0") "#{prefix}_#{timestamp}_#{random}" end end