defmodule DaProductAppWeb.AaniSettlementController do use DaProductAppWeb, :controller alias DaProductApp.Settlements alias DaProductApp.Settlements.Settlement alias DaProductApp.Settlements.AaniSettlementAudit alias DaProductApp.Settlements.SettlementTransaction alias DaProductApp.Transactions.Transaction alias DaProductApp.Repo import Ecto.Query require Logger @required_fields ~w(type merchantTag bankUserId settlementDate totalTransactionCount grossSettlementAmount mdrCharges taxOnMdr netSettlementAmount settlementTimestamp) @valid_type "qr_payment_settlement_summary" def settlement_summary(conn, params) do start_time = System.monotonic_time(:millisecond) ip_address = get_client_ip(conn) Logger.info("=== AANI Settlement Request Started ===") Logger.info("Request params: #{inspect(params, pretty: true)}") Logger.info("Client IP: #{ip_address}") # Check IP whitelist if configured with :ok <- check_ip_whitelist(ip_address) do result = process_settlement_summary(params) end_time = System.monotonic_time(:millisecond) processing_time = end_time - start_time # Log the request and response for audit audit_params = %{ merchant_tag: params["merchantTag"], bank_user_id: params["bankUserId"], settlement_date: parse_date(params["settlementDate"]), request_payload: params, response_payload: elem(result, 1), status: case elem(result, 0) do :ok -> "SUCCESS" :error -> "ERROR" end, processing_time_ms: processing_time, ip_address: ip_address } case result do {:ok, response} -> Logger.info("=== AANI Settlement SUCCESS ===") Logger.info("Response: #{inspect(response, pretty: true)}") Logger.info("Processing time: #{processing_time}ms") log_audit(audit_params) json(conn, response) {:error, {status, error_code, message}} -> Logger.error("=== AANI Settlement ERROR ===") Logger.error("Status: #{status}, Error Code: #{error_code}, Message: #{message}") Logger.error("Processing time: #{processing_time}ms") audit_params = Map.merge(audit_params, %{ error_code: error_code, error_message: message }) log_audit(audit_params) conn |> put_status(status) |> json(%{ status: "ERROR", errorCode: error_code, errorMessage: message }) {:error, reason} -> # Handle cases where error is not in the expected {status, error_code, message} format Logger.error("=== AANI Settlement UNEXPECTED ERROR ===") Logger.error("Reason: #{inspect(reason)}") Logger.error("Processing time: #{processing_time}ms") audit_params = Map.merge(audit_params, %{ error_code: "ERR_INTERNAL", error_message: "Internal server error" }) log_audit(audit_params) conn |> put_status(:internal_server_error) |> json(%{ status: "ERROR", errorCode: "ERR_INTERNAL", errorMessage: "Internal server error" }) end else :unauthorized -> audit_params = %{ request_payload: params, response_payload: %{status: "ERROR", errorCode: "ERR_UNAUTHORIZED"}, status: "ERROR", error_code: "ERR_UNAUTHORIZED", error_message: "Unauthorized request. Invalid credentials or token", processing_time_ms: 0, ip_address: ip_address } log_audit(audit_params) conn |> put_status(:unauthorized) |> json(%{ status: "ERROR", errorCode: "ERR_UNAUTHORIZED", errorMessage: "Unauthorized request. Invalid credentials or token" }) end end defp process_settlement_summary(params) do try do with :ok <- validate_required_fields(params), :ok <- validate_type(params), :ok <- validate_field_formats(params), {:ok, settlement_date} <- parse_and_validate_date(params["settlementDate"]), {:ok, merchant_tag} <- validate_merchant_exists(params["merchantTag"]) do # First check if transactions are already settled before looking for unmatched batches case check_transactions_already_settled(params, settlement_date) do {:already_settled, existing_settlement_id} -> # Return existing settlement result Logger.info("Transactions already settled with ID: #{existing_settlement_id}") existing_settlement = Settlements.get_settlement_by_id(existing_settlement_id) case existing_settlement do nil -> Logger.error("Settlement #{existing_settlement_id} not found in database") {:error, {:internal_server_error, "ERR_INTERNAL", "Settlement reference not found"}} settlement -> result = %{ "settlementStatus" => "COMPLETED", "mismatchDetected" => false, "settlementReference" => existing_settlement_id, "transactionMismatchDetails" => %{ "mismatchCount" => 0, "discrepancyAmount" => %{ "value" => "0.00", "currency" => "AED" }, "resolutionFileRequired" => false } } Logger.info("Returning existing settlement result: #{inspect(result)}") {:ok, result} end :not_settled -> # Proceed with normal settlement processing with {:ok, unmatched_batches} <- get_all_unmatched_batches(params, settlement_date), {:ok, duplicate_check} <- check_duplicate_request_for_batches( params["merchantTag"], params["bankUserId"], settlement_date, unmatched_batches ) do case duplicate_check do {:duplicate, previous_result} -> Logger.info("Duplicate request detected, returning previous result") {:ok, previous_result} :not_duplicate -> # Proceed with multi-batch settlement processing process_multi_batch_settlements(params, settlement_date, unmatched_batches) end else {:error, reason} -> {:error, reason} end {:error, reason} -> {:error, reason} end else {:error, reason} -> {:error, reason} end rescue e -> # Log the actual error for debugging require Logger Logger.error("Error in process_settlement_summary: #{inspect(e)}") {:error, {:internal_server_error, "ERR_INTERNAL", "Unexpected server error. Please try again later"}} end end defp validate_required_fields(params) do missing_fields = Enum.filter(@required_fields, fn field -> is_nil(params[field]) or params[field] == "" end) case missing_fields do [] -> :ok [field | _] -> {:error, {:unprocessable_entity, "ERR_MISSING_FIELD", "Required field '#{field}' is missing"}} end end defp validate_type(%{"type" => @valid_type}), do: :ok defp validate_type(%{"type" => type}) when not is_nil(type) do {:error, {:unprocessable_entity, "ERR_INVALID_TYPE", "Invalid notification type. Expected '#{@valid_type}'"}} end defp validate_type(_), do: {:error, {:unprocessable_entity, "ERR_MISSING_FIELD", "Required field 'type' is missing"}} defp validate_field_formats(params) do with :ok <- validate_amount_format(params["grossSettlementAmount"]), :ok <- validate_amount_format(params["mdrCharges"]), :ok <- validate_amount_format(params["taxOnMdr"]), :ok <- validate_amount_format(params["netSettlementAmount"]), :ok <- validate_integer_format(params["totalTransactionCount"]), :ok <- validate_datetime_format(params["settlementTimestamp"]) do :ok else {:error, reason} -> {:error, reason} end end defp validate_amount_format(%{"value" => value, "currency" => currency}) when is_binary(value) and is_binary(currency) do case Decimal.parse(value) do {_, ""} -> :ok _ -> {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid amount format"}} end end defp validate_amount_format(_) do {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Amount must have 'value' and 'currency' fields"}} end defp validate_integer_format(value) when is_binary(value) do case Integer.parse(value) do {_, ""} -> :ok _ -> {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid integer format"}} end end defp validate_integer_format(value) when is_integer(value), do: :ok defp validate_integer_format(_) do {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid integer format"}} end defp validate_datetime_format(datetime_str) when is_binary(datetime_str) do case DateTime.from_iso8601(datetime_str) do {:ok, _, _} -> :ok _ -> {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid format for 'settlementTimestamp'. Expected ISO8601"}} end end defp validate_datetime_format(_) do {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid format for 'settlementTimestamp'. Expected ISO8601"}} end defp parse_and_validate_date(date_str) when is_binary(date_str) do case Date.from_iso8601(date_str) do {:ok, date} -> {:ok, date} _ -> {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid format for 'settlementDate'. Expected ISO8601"}} end end defp parse_and_validate_date(_) do {:error, {:unprocessable_entity, "ERR_INVALID_FORMAT", "Invalid format for 'settlementDate'. Expected ISO8601"}} end defp validate_merchant_exists(merchant_tag) do # For now, we'll assume all merchant tags are valid # In a real implementation, you'd check against a merchants table # Uncomment and implement when merchant validation is needed: # case DaProductApp.Merchants.get_by_tag(merchant_tag) do # nil -> {:error, {:not_found, "ERR_UNKNOWN_MERCHANT", "Merchant with tag '#{merchant_tag}' not found"}} # merchant -> {:ok, merchant_tag} # end if merchant_tag && String.length(merchant_tag) > 0 do {:ok, merchant_tag} else {:error, {:unprocessable_entity, "ERR_MISSING_FIELD", "Required field 'merchantTag' is missing"}} end end defp check_duplicate_request(merchant_tag, bank_user_id, settlement_date, batch_number) do # Check if we already have a settlement request for this merchant/bank_user/date/batch combination case Settlements.get_aani_settlement_by_batch( merchant_tag, bank_user_id, settlement_date, batch_number ) do nil -> {:ok, :not_duplicate} existing_settlement -> # Return the previous result if settlement is completed # Allow retries for mismatched settlements (up to 3 times) {:ok, {:duplicate, get_previous_settlement_result(existing_settlement)}} end end defp get_all_unmatched_batches(params, settlement_date) do merchant_tag = params["merchantTag"] bank_user_id = params["bankUserId"] Logger.info("=== GETTING ALL UNMATCHED BATCHES FOR REQUEST ===") Logger.info("Merchant Tag: #{merchant_tag}") Logger.info("Bank User ID: #{bank_user_id}") Logger.info("Settlement Date: #{settlement_date}") # Get all available unmatched batches for this merchant/bank_user/date unmatched_batches = Settlements.get_unmatched_batches_for_request(merchant_tag, bank_user_id, settlement_date) Logger.info("Found unmatched batches: #{inspect(unmatched_batches)}") case unmatched_batches do [] -> Logger.warn( "No unmatched batches found for merchant #{merchant_tag}, bank_user #{bank_user_id}, date #{settlement_date}" ) {:error, {:not_found, "ERR_NO_TRANSACTIONS", "No unmatched transactions found for the specified criteria"}} batches -> Logger.info("Processing #{length(batches)} unmatched batches: #{inspect(batches)}") {:ok, batches} end end defp check_duplicate_request_for_batches( merchant_tag, bank_user_id, settlement_date, unmatched_batches ) do Logger.info("=== CHECKING DUPLICATE REQUESTS FOR ALL BATCHES ===") # Check if any of the batches already have settlements existing_settlements = Enum.reduce(unmatched_batches, [], fn batch_number, acc -> case Settlements.get_aani_settlement_by_batch( merchant_tag, bank_user_id, settlement_date, batch_number ) do nil -> acc settlement -> [settlement | acc] end end) case existing_settlements do [] -> Logger.info("No duplicate settlements found for any batch") {:ok, :not_duplicate} settlements -> Logger.info( "Found existing settlements for some batches: #{Enum.map(settlements, & &1.settlement_id)}" ) # Return the first settlement's result - in practice, this shouldn't happen # as we filter for unmatched batches, but keeping for safety {:ok, {:duplicate, get_previous_settlement_result(hd(settlements))}} end end defp process_multi_batch_settlements(params, settlement_date, unmatched_batches) do bank_user_id = params["bankUserId"] expected_count = parse_integer(params["totalTransactionCount"]) expected_amount = parse_decimal(params["grossSettlementAmount"]["value"]) Logger.info("=== MULTI-BATCH SETTLEMENT PROCESSING START ===") Logger.info("Settlement Date: #{settlement_date}") Logger.info("Bank User ID: #{bank_user_id}") Logger.info("Unmatched Batches: #{inspect(unmatched_batches)}") Logger.info("Expected Total Count: #{expected_count}") Logger.info("Expected Total Amount: #{expected_amount}") # Get transactions for all batches and calculate totals batch_data = Enum.map(unmatched_batches, fn batch_number -> query = build_transaction_query_for_batch( settlement_date, bank_user_id, params["qrId"], batch_number ) transactions = Repo.all(query) actual_count = length(transactions) actual_amount = Enum.reduce(transactions, Decimal.new(0), fn tx, acc -> Decimal.add(acc, tx.transaction_amount || Decimal.new(0)) end) Logger.info( "Batch #{batch_number}: #{actual_count} transactions, #{actual_amount} amount" ) %{ batch_number: batch_number, transactions: transactions, actual_count: actual_count, actual_amount: actual_amount } end) # Calculate total across all batches total_actual_count = Enum.sum(Enum.map(batch_data, & &1.actual_count)) total_actual_amount = Enum.reduce(batch_data, Decimal.new(0), fn batch, acc -> Decimal.add(acc, batch.actual_amount) end) Logger.info("=== TOTAL ACROSS ALL BATCHES ===") Logger.info("Total Actual Count: #{total_actual_count}") Logger.info("Total Actual Amount: #{total_actual_amount}") # Check if totals match expected values total_count_matches = total_actual_count == expected_count total_amount_matches = Decimal.equal?(total_actual_amount, expected_amount) Logger.info("=== TOTAL MATCHING RESULTS ===") Logger.info( "Total count matches: #{total_count_matches} (expected: #{expected_count}, actual: #{total_actual_count})" ) Logger.info( "Total amount matches: #{total_amount_matches} (expected: #{expected_amount}, actual: #{total_actual_amount})" ) # Determine settlement status based on total matching settlement_status = case {total_count_matches, total_amount_matches} do {true, true} -> "COMPLETED" _ -> "MISMATCH_DETECTED" end # Calculate mismatch details if needed {total_mismatch_count, total_discrepancy_amount} = case settlement_status do "COMPLETED" -> {0, Decimal.new(0)} "MISMATCH_DETECTED" -> mismatch_count = abs(total_actual_count - expected_count) discrepancy_amount = Decimal.sub(expected_amount, total_actual_amount) |> Decimal.abs() {mismatch_count, discrepancy_amount} end Logger.info("Settlement status determined: #{settlement_status}") if settlement_status == "MISMATCH_DETECTED" do Logger.warn("Total mismatch count: #{total_mismatch_count}") Logger.warn("Total discrepancy amount: #{total_discrepancy_amount}") end # Create settlements for each batch using actual batch values Repo.transaction(fn -> settlement_results = Enum.map(batch_data, fn batch -> create_settlement_for_batch( params, settlement_date, batch, settlement_status, total_mismatch_count, total_discrepancy_amount, settlement_status == "MISMATCH_DETECTED" ) end) # Check if all settlements were created successfully case Enum.find(settlement_results, fn result -> elem(result, 0) == :error end) do nil -> # All settlements created successfully settlement_references = Enum.map(settlement_results, fn {:ok, settlement_id} -> settlement_id end) Logger.info("=== ALL BATCH SETTLEMENTS CREATED SUCCESSFULLY ===") Logger.info("Settlement references: #{inspect(settlement_references)}") # Return multi-batch settlement response %{ "settlementStatus" => settlement_status, # Array of settlement IDs "settlementReference" => settlement_references, "mismatchDetected" => settlement_status == "MISMATCH_DETECTED", "transactionMismatchDetails" => %{ "mismatchCount" => total_mismatch_count, "discrepancyAmount" => %{ "value" => to_string(total_discrepancy_amount), "currency" => "AED" }, "resolutionFileRequired" => settlement_status == "MISMATCH_DETECTED" } } {:error, reason} -> Logger.error("Failed to create settlement for one or more batches: #{inspect(reason)}") Repo.rollback("Failed to create settlements") end end) end defp create_settlement_for_batch( params, settlement_date, batch_data, overall_status, total_mismatch_count, total_discrepancy_amount, resolution_file_required ) do batch_number = batch_data.batch_number transactions = batch_data.transactions actual_count = batch_data.actual_count actual_amount = batch_data.actual_amount Logger.info("=== CREATING SETTLEMENT FOR BATCH #{batch_number} ===") Logger.info("Transactions in batch: #{actual_count}") Logger.info("Amount in batch: #{actual_amount}") Logger.info("Overall status: #{overall_status}") # Determine final batch number based on configuration final_batch_number = if Settlements.provider_wise_batch_number_enabled?() do merchant_id = params["bankUserId"] current_batch_number = Settlements.get_current_batch_number(merchant_id) Logger.info("Using current batch number: #{current_batch_number} for merchant: #{merchant_id}") current_batch_number else batch_number end # Create settlement record using actual batch values, not expected API values settlement_attrs = %{ # Using bankUserId as merchant_id merchant_id: params["bankUserId"], provider_id: 3, # Use actual batch amount amount: actual_amount, merchant_tag: params["merchantTag"], bank_user_id: params["bankUserId"], qr_id: params["qrId"], # Use final determined batch number as string batch_number: final_batch_number, date: settlement_date, status: overall_status, # Use actual batch count total_transaction_count: actual_count, # Use actual batch amount gross_settlement_amount: actual_amount, gross_settlement_currency: "AED", # Default for now mdr_charges: Decimal.new("0.00"), mdr_charges_currency: "AED", # Default for now tax_on_mdr: Decimal.new("0.00"), tax_on_mdr_currency: "AED", # Use actual batch amount net_settlement_amount: actual_amount, net_settlement_currency: "AED", settlement_timestamp: parse_datetime(params["settlementTimestamp"]), # Global mismatch count mismatch_count: total_mismatch_count, # Global discrepancy amount discrepancy_amount: total_discrepancy_amount, resolution_file_required: resolution_file_required } case Settlements.create_aani_settlement(settlement_attrs) do {:ok, settlement} -> Logger.info( "Settlement created successfully with ID: #{settlement.settlement_id} for batch: #{batch_number}" ) # Update transactions with settlement information case update_transactions_with_settlement(transactions, settlement) do {:ok, _updated_count} -> Logger.info( "Successfully updated transactions for settlement #{settlement.settlement_id}" ) # Increment batch number AFTER successful settlement creation and transaction updates merchant_id = params["bankUserId"] provider_id = if Settlements.provider_wise_batch_number_enabled?(), do: 3, else: nil case Settlements.increment_batch_number_after_success(merchant_id, provider_id) do {:ok, new_batch_number} -> Logger.info( "Incremented merchant batch number to: #{new_batch_number} for merchant: #{merchant_id} after successful settlement" ) {:error, error} -> Logger.warn( "Failed to increment merchant batch number after settlement: #{inspect(error)}" ) end {:ok, settlement.settlement_id} {:error, error} -> Logger.error( "Failed to update transactions for batch #{batch_number}: #{inspect(error)}" ) {:error, "Failed to update transactions"} end {:error, changeset} -> Logger.error( "Failed to create settlement for batch #{batch_number}: #{inspect(changeset)}" ) {:error, "Failed to create settlement"} end end defp check_transactions_already_settled(params, settlement_date) do bank_user_id = params["bankUserId"] merchant_tag = params["merchantTag"] Logger.info("=== CHECKING FOR ALREADY SETTLED TRANSACTIONS (BATCH-AWARE) ===") Logger.info("Bank User ID: #{bank_user_id}") Logger.info("Merchant Tag: #{merchant_tag}") Logger.info("Settlement Date: #{settlement_date}") # With batch-based approach, we first check if there are any unmatched transactions # If no unmatched transactions exist, then everything is already settled unmatched_batches = Settlements.get_unmatched_batches_for_request(merchant_tag, bank_user_id, settlement_date) Logger.info("Found #{length(unmatched_batches)} unmatched batches") if Enum.empty?(unmatched_batches) do # No unmatched batches means all transactions for this merchant/bank_user/date are already settled # Find the most recent settlement for this combination query = from s in Settlement, where: s.merchant_tag == ^merchant_tag and s.bank_user_id == ^bank_user_id and s.date == ^settlement_date, order_by: [desc: s.inserted_at], limit: 1, select: s.settlement_id case Repo.one(query) do nil -> Logger.warn("No settlements found despite no unmatched batches - this is unexpected") :not_settled settlement_id -> Logger.info( "All transactions already settled - returning existing settlement: #{settlement_id}" ) {:already_settled, settlement_id} end else Logger.info( "Found unmatched transactions in batches: #{inspect(unmatched_batches)} - proceeding with settlement" ) :not_settled end end defp get_previous_settlement_result(settlement) do # Force code reload by adding this comment %{ "settlementStatus" => settlement.status, "settlementReference" => settlement.settlement_id, "mismatchDetected" => settlement.status == "MISMATCH_DETECTED", "transactionMismatchDetails" => %{ "mismatchCount" => settlement.mismatch_count || 0, "discrepancyAmount" => %{ "value" => to_string(settlement.discrepancy_amount || 0), "currency" => "AED" }, "resolutionFileRequired" => settlement.resolution_file_required || false } } end defp match_transactions(params, settlement_date, batch_number) do bank_user_id = params["bankUserId"] qr_id = params["qrId"] expected_count = parse_integer(params["totalTransactionCount"]) expected_amount = parse_decimal(params["grossSettlementAmount"]["value"]) Logger.info("=== TRANSACTION MATCHING START ===") Logger.info("Settlement Date: #{settlement_date}") Logger.info("Bank User ID: #{bank_user_id}") Logger.info("QR ID: #{qr_id}") Logger.info("Batch Number: #{batch_number}") Logger.info("Expected Count: #{expected_count}") Logger.info("Expected Amount: #{expected_amount}") # Build query to match transactions for the specific batch query = build_transaction_query_for_batch(settlement_date, bank_user_id, qr_id, batch_number) Logger.info("Generated Query: #{inspect(query)}") transactions = Repo.all(query) actual_count = length(transactions) actual_amount = Enum.reduce(transactions, Decimal.new(0), fn tx, acc -> Decimal.add(acc, tx.transaction_amount || Decimal.new(0)) end) Logger.info("=== TRANSACTION QUERY RESULTS ===") Logger.info("Found #{actual_count} transactions for batch #{batch_number}") Logger.info("Actual Amount: #{actual_amount}") Logger.info("Transactions found:") Enum.each(transactions, fn tx -> Logger.info( " Transaction ID: #{tx.transaction_id}, Amount: #{tx.transaction_amount}, User ID: #{tx.user_id}, Bank User ID: #{tx.bank_user_id}, Batch: #{tx.batch_number}, Date: #{tx.inserted_at}" ) end) # Compare counts and amounts count_matches = actual_count == expected_count amount_matches = Decimal.equal?(actual_amount, expected_amount) Logger.info("=== MATCHING RESULTS ===") Logger.info( "Count matches: #{count_matches} (expected: #{expected_count}, actual: #{actual_count})" ) Logger.info( "Amount matches: #{amount_matches} (expected: #{expected_amount}, actual: #{actual_amount})" ) Repo.transaction(fn -> case {count_matches, amount_matches} do {true, true} -> # Perfect match Logger.info("=== PERFECT MATCH DETECTED ===") # Create settlement record with batch number case create_settlement_record( params, settlement_date, batch_number, "COMPLETED", 0, Decimal.new(0), false ) do {:ok, settlement} -> Logger.info( "Settlement record created successfully with ID: #{settlement.settlement_id} for batch: #{batch_number}" ) result = %{ "settlementStatus" => "COMPLETED", "settlementReference" => settlement.settlement_id, "mismatchDetected" => false, "transactionMismatchDetails" => %{ "mismatchCount" => 0, "discrepancyAmount" => %{ "value" => "0", "currency" => "AED" }, "resolutionFileRequired" => false } } # Update matched transactions with settlement information case update_transactions_with_settlement(transactions, settlement) do {:ok, updated_count} -> Logger.info( "Successfully updated #{updated_count} transactions with settlement ID: #{settlement.settlement_id}" ) # Increment batch number AFTER successful settlement creation and transaction updates merchant_id = params["bankUserId"] provider_id = if Settlements.provider_wise_batch_number_enabled?(), do: 3, else: nil case Settlements.increment_batch_number_after_success(merchant_id, provider_id) do {:ok, new_batch_number} -> Logger.info( "Incremented merchant batch number to: #{new_batch_number} for merchant: #{merchant_id} after successful settlement" ) {:error, error} -> Logger.warn( "Failed to increment merchant batch number after settlement: #{inspect(error)}" ) end result {:error, error} -> Logger.error("Failed to update transactions with settlement: #{inspect(error)}") # Rollback transaction if update fails Repo.rollback( {:error, {:internal_server_error, "ERR_INTERNAL", "Failed to update transactions with settlement"}} ) end {:error, changeset} -> Logger.error("Failed to create settlement record: #{inspect(changeset)}") Repo.rollback( {:error, {:internal_server_error, "ERR_INTERNAL", "Failed to create settlement record"}} ) end _ -> # Mismatch detected mismatch_count = abs(actual_count - expected_count) discrepancy_amount = Decimal.sub(expected_amount, actual_amount) |> Decimal.abs() Logger.warn("=== MISMATCH DETECTED ===") Logger.warn("Mismatch Count: #{mismatch_count}") Logger.warn("Discrepancy Amount: #{discrepancy_amount}") Logger.warn("Count mismatch: expected #{expected_count}, got #{actual_count}") Logger.warn("Amount mismatch: expected #{expected_amount}, got #{actual_amount}") # Create settlement record with mismatch details and batch number case create_settlement_record( params, settlement_date, batch_number, "MISMATCH_DETECTED", mismatch_count, discrepancy_amount, true ) do {:ok, settlement} -> Logger.info( "Mismatch settlement record created successfully with ID: #{settlement.settlement_id} for batch: #{batch_number}" ) result = %{ "settlementStatus" => "MISMATCH_DETECTED", "settlementReference" => settlement.settlement_id, "mismatchDetected" => true, "transactionMismatchDetails" => %{ "mismatchCount" => mismatch_count, "discrepancyAmount" => %{ "value" => to_string(discrepancy_amount), "currency" => "AED" }, "resolutionFileRequired" => true } } # Even for mismatched settlements, update any matched transactions if length(transactions) > 0 do case update_transactions_with_settlement(transactions, settlement) do {:ok, updated_count} -> Logger.info( "Updated #{updated_count} partially matched transactions with settlement ID: #{settlement.settlement_id}" ) # Increment batch number AFTER successful settlement creation and transaction updates merchant_id = params["bankUserId"] provider_id = if Settlements.provider_wise_batch_number_enabled?(), do: 3, else: nil case Settlements.increment_batch_number_after_success( merchant_id, provider_id ) do {:ok, new_batch_number} -> Logger.info( "Incremented merchant batch number to: #{new_batch_number} for merchant: #{merchant_id} after successful mismatch settlement" ) {:error, error} -> Logger.warn( "Failed to increment merchant batch number after mismatch settlement: #{inspect(error)}" ) end {:error, error} -> Logger.error( "Failed to update transactions with mismatch settlement: #{inspect(error)}" ) Repo.rollback( {:error, {:internal_server_error, "ERR_INTERNAL", "Failed to update transactions with mismatch settlement"}} ) end else # No transactions to update, but still increment batch number for successful settlement merchant_id = params["bankUserId"] provider_id = if Settlements.provider_wise_batch_number_enabled?(), do: 3, else: nil case Settlements.increment_batch_number_after_success(merchant_id, provider_id) do {:ok, new_batch_number} -> Logger.info( "Incremented merchant batch number to: #{new_batch_number} for merchant: #{merchant_id} after successful mismatch settlement (no transactions)" ) {:error, error} -> Logger.warn( "Failed to increment merchant batch number after mismatch settlement: #{inspect(error)}" ) end end result {:error, changeset} -> Logger.error("Failed to create mismatch settlement record: #{inspect(changeset)}") Repo.rollback( {:error, {:internal_server_error, "ERR_INTERNAL", "Failed to create settlement record"}} ) end end end) end defp build_transaction_query_for_batch(settlement_date, bank_user_id, qr_id, batch_number) do Logger.info("=== BUILDING TRANSACTION QUERY FOR BATCH ===") Logger.info("Settlement Date for query: #{settlement_date}") Logger.info("Bank User ID for query: #{bank_user_id}") Logger.info("QR ID for query: #{qr_id}") Logger.info("Batch Number for query: #{batch_number}") # Base query for batch-specific transactions base_query = from t in Transaction, where: t.bank_user_id == ^bank_user_id and t.batch_number == ^batch_number and fragment("DATE(?)", t.inserted_at) == ^settlement_date and (is_nil(t.settlement_id) or t.settlement_status == "unmatched") and fragment("LOWER(?) = ?", t.status, "success") # If qrId is provided, filter transactions before the qrId's timestamp if qr_id do Logger.info("Applying QR ID filter: #{qr_id}") qr_transaction = from t in Transaction, where: t.transaction_id == ^qr_id, select: t.inserted_at, limit: 1 case Repo.one(qr_transaction) do nil -> Logger.warn("QR ID #{qr_id} not found, ignoring QR filter") base_query qr_timestamp -> Logger.info("Found QR transaction timestamp: #{qr_timestamp}") from t in base_query, where: t.inserted_at <= ^qr_timestamp end else Logger.info("No QR ID filter applied") base_query end end defp update_transactions_with_settlement(transactions, settlement) do Logger.info("=== UPDATING TRANSACTIONS WITH SETTLEMENT ===") Logger.info("Settlement ID: #{settlement.settlement_id}") Logger.info("Settlement Status: #{settlement.status}") Logger.info("Number of transactions to update: #{length(transactions)}") try do # Get the transaction IDs transaction_ids = Enum.map(transactions, & &1.id) Logger.info("Transaction IDs to update: #{inspect(transaction_ids)}") # Update all matched transactions with settlement information query = from t in Transaction, where: t.id in ^transaction_ids # Determine settlement status based on settlement result transaction_status = case settlement.status do "COMPLETED" -> "matched" "MISMATCH_DETECTED" -> "partially_matched" _ -> "unmatched" end update_params = [ set: [ settlement_id: settlement.settlement_id, settlement_status: transaction_status, settlement_date_time: settlement.settlement_timestamp, updated_at: DateTime.utc_now() ] ] Logger.info("Executing bulk update with params: #{inspect(update_params)}") Logger.info("Transaction status to set: #{transaction_status}") case Repo.update_all(query, update_params) do {count, _} when count > 0 -> Logger.info( "Successfully updated #{count} transactions with settlement_id: #{settlement.settlement_id}" ) # Log individual transaction updates for verification updated_transactions = from(t in Transaction, where: t.id in ^transaction_ids, select: {t.id, t.transaction_id, t.settlement_id, t.settlement_status} ) |> Repo.all() Logger.info("Updated transaction details:") Enum.each(updated_transactions, fn {id, txn_id, sett_id, sett_status} -> Logger.info( " ID: #{id}, TXN_ID: #{txn_id}, Settlement ID: #{sett_id}, Status: #{sett_status}" ) end) # MISSING FUNCTIONALITY: Create settlement_transactions records Logger.info("=== CREATING SETTLEMENT TRANSACTION RECORDS ===") case create_settlement_transaction_records(transactions, settlement) do {:ok, settlement_transaction_count} -> Logger.info( "Successfully created #{settlement_transaction_count} settlement_transaction records" ) {:ok, count} {:error, error_msg} -> Logger.error("Failed to create settlement_transaction records: #{error_msg}") # Continue with success since transactions were updated, but log the issue {:ok, count} end {0, _} -> Logger.warn("No transactions were updated - this might indicate an issue") {:ok, 0} error -> Logger.error("Unexpected update result: #{inspect(error)}") {:error, "Unexpected update result"} end rescue e -> Logger.error("Error updating transactions: #{inspect(e)}") Logger.error("Stacktrace: #{inspect(__STACKTRACE__)}") {:error, "Failed to update transactions: #{inspect(e)}"} end end defp create_settlement_transaction_records(transactions, settlement) do Logger.info("=== CREATING SETTLEMENT TRANSACTION RECORDS ===") Logger.info("Settlement ID: #{settlement.settlement_id}") Logger.info("Number of transactions to create records for: #{length(transactions)}") try do # Prepare settlement transaction records for bulk insert settlement_transaction_records = transactions |> Enum.map(fn transaction -> %{ settlement_id: settlement.id, transaction_id: transaction.transaction_id, # Empty string as default since Transaction doesn't have qr_id field qr_id: "", # Use device_id as terminal_id, handle nil terminal_id: transaction.device_id || "", transaction_amount: transaction.transaction_amount, # Default currency since Transaction doesn't have currency field transaction_currency: "AED", transaction_status: case settlement.status do "COMPLETED" -> "settled" "MISMATCH_DETECTED" -> "partially_settled" _ -> "pending" end, # Use settlement_date_time or fallback to inserted_at transaction_time: transaction.settlement_date_time || transaction.inserted_at, # Default to 0 for now mdr_charge: Decimal.new("0.00"), mdr_charge_currency: "AED", # Default to 0 for now tax_on_mdr: Decimal.new("0.00"), tax_on_mdr_currency: "AED", net_received_amount: transaction.transaction_amount, # Default currency net_received_currency: "AED", inserted_at: DateTime.utc_now() |> DateTime.truncate(:second), updated_at: DateTime.utc_now() |> DateTime.truncate(:second) } end) Logger.info("Settlement transaction records to insert:") Enum.each(settlement_transaction_records, fn record -> Logger.info( " TXN_ID: #{record.transaction_id}, Amount: #{record.transaction_amount}, Status: #{record.transaction_status}" ) end) # Bulk insert settlement transaction records case Repo.insert_all(SettlementTransaction, settlement_transaction_records) do {count, _} when count > 0 -> Logger.info( "Successfully created #{count} settlement_transaction records for settlement #{settlement.settlement_id}" ) {:ok, count} {0, _} -> Logger.warn("No settlement_transaction records were created") {:ok, 0} error -> Logger.error("Unexpected insert result: #{inspect(error)}") {:error, "Unexpected insert result"} end rescue e -> Logger.error("Error creating settlement transaction records: #{inspect(e)}") Logger.error("Stacktrace: #{inspect(__STACKTRACE__)}") {:error, "Failed to create settlement transaction records: #{inspect(e)}"} end end defp create_settlement_record( params, settlement_date, batch_number, status, mismatch_count, discrepancy_amount, resolution_file_required ) do # Use net_settlement_amount as the main amount field for database requirement amount = parse_decimal(params["netSettlementAmount"]["value"]) # Determine final batch number based on configuration final_batch_number = if Settlements.provider_wise_batch_number_enabled?() do # Provider-wise batch number enabled: get current batch number (without incrementing) merchant_id = params["bankUserId"] current_batch_number = Settlements.get_current_batch_number(merchant_id) Logger.info( "Using current batch number: #{current_batch_number} for merchant: #{merchant_id}" ) # Convert to string to_string(current_batch_number) else # Provider-wise batch number disabled: use existing batch number from transaction # Convert to string to_string(batch_number) end settlement_attrs = %{ # Using bankUserId as merchant_id merchant_id: params["bankUserId"], # Default provider ID - you may want to make this configurable provider_id: 3, # Required field for database amount: amount, merchant_tag: params["merchantTag"], bank_user_id: params["bankUserId"], qr_id: params["qrId"], # Use final determined batch number batch_number: final_batch_number, date: settlement_date, status: status, total_transaction_count: parse_integer(params["totalTransactionCount"]), gross_settlement_amount: parse_decimal(params["grossSettlementAmount"]["value"]), gross_settlement_currency: params["grossSettlementAmount"]["currency"], mdr_charges: parse_decimal(params["mdrCharges"]["value"]), mdr_charges_currency: params["mdrCharges"]["currency"], tax_on_mdr: parse_decimal(params["taxOnMdr"]["value"]), tax_on_mdr_currency: params["taxOnMdr"]["currency"], net_settlement_amount: parse_decimal(params["netSettlementAmount"]["value"]), net_settlement_currency: params["netSettlementAmount"]["currency"], settlement_timestamp: parse_datetime(params["settlementTimestamp"]), mismatch_count: mismatch_count, discrepancy_amount: discrepancy_amount, resolution_file_required: resolution_file_required } Settlements.create_aani_settlement(settlement_attrs) end defp parse_integer(value) when is_binary(value) do case Integer.parse(value) do {int, ""} -> int _ -> 0 end end defp parse_integer(value) when is_integer(value), do: value defp parse_integer(_), do: 0 defp parse_decimal(value) when is_binary(value) do case Decimal.parse(value) do {decimal, ""} -> decimal _ -> Decimal.new(0) end end defp parse_decimal(value), do: Decimal.new(value) defp parse_datetime(value) when is_binary(value) do case DateTime.from_iso8601(value) do {:ok, datetime, _} -> datetime _ -> DateTime.utc_now() end end defp parse_date(nil), do: nil defp parse_date(date_str) when is_binary(date_str) do case Date.from_iso8601(date_str) do {:ok, date} -> date _ -> nil end end defp get_client_ip(conn) do case Plug.Conn.get_req_header(conn, "x-forwarded-for") do [ip | _] -> String.split(ip, ",") |> List.first() |> String.trim() [] -> case conn.remote_ip do {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}" _ -> "unknown" end end end defp log_audit(audit_params) do %AaniSettlementAudit{} |> AaniSettlementAudit.changeset(audit_params) |> Repo.insert() end defp check_ip_whitelist(ip_address) do # Get IP whitelist from application config # For now, we'll allow all IPs (development mode) # In production, this should check against a configured whitelist whitelist = Application.get_env(:da_product_app, :aani_ip_whitelist, :all) case whitelist do :all -> :ok [] -> :unauthorized ips when is_list(ips) -> if ip_address in ips do :ok else :unauthorized end _ -> :ok end end end