defmodule DaProductApp.Settlements.TransactionEodGeneratorTest do use DaProductApp.DataCase alias DaProductApp.Settlements.TransactionEodGenerator alias DaProductApp.Settlements.{Settlement, SettlementTransaction} alias DaProductApp.Repo @test_date ~D[2025-07-05] @test_merchant_id "900111222333444" describe "generate_settlement_files/1" do setup do # Create test settlement data matching the specification test cases settlement = create_test_settlement() transactions = create_test_transactions(settlement) %{settlement: settlement, transactions: transactions} end test "Happy-Path Settlement (Small Batch) - generates both CSV and JSON files", %{settlement: settlement} do result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :both, sequence: 1 ) assert {:ok, %{csv: csv_path, json: json_path}} = result assert File.exists?(csv_path) assert File.exists?(json_path) # Verify CSV content structure csv_content = File.read!(csv_path) assert String.contains?(csv_content, "#FileType,QR_Payment_Settlement") assert String.contains?(csv_content, "#Version,1.1") assert String.contains?(csv_content, "#SettlementDate,#{Date.to_iso8601(@test_date)}") assert String.contains?(csv_content, "#MerchantTag,TAG123") assert String.contains?(csv_content, "#TotalTransactionCount,3") assert String.contains?(csv_content, "#MismatchDetected,NO") # Verify transaction header assert String.contains?(csv_content, "QRTransactionID,QRID,TID,TransactionAmount,Currency,TransactionStatus,TransactionTime,MDRCharge,TaxOnMDR,NetReceived,BatchNumber") # Verify checksum and footer assert String.contains?(csv_content, "#Checksum,") assert String.contains?(csv_content, "#FileGeneratedBy,AaniQR System") assert String.contains?(csv_content, "#EndOfFile") # Verify JSON content structure json_content = File.read!(json_path) {:ok, json_data} = Jason.decode(json_content) assert json_data["fileType"] == "QR_Payment_Settlement" assert json_data["version"] == "1.1" assert json_data["settlementDate"] == Date.to_iso8601(@test_date) assert json_data["merchantTag"] == "TAG123" assert json_data["totalTransactionCount"] == 3 assert json_data["mismatchDetected"] == false assert length(json_data["transactions"]) == 3 assert json_data["footer"]["endOfFile"] == true # Cleanup File.rm!(csv_path) File.rm!(json_path) end test "generates only CSV file when format is :csv", %{settlement: _settlement} do result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :csv, sequence: 1 ) assert {:ok, %{csv: csv_path}} = result assert File.exists?(csv_path) # Cleanup File.rm!(csv_path) end test "generates only JSON file when format is :json", %{settlement: _settlement} do result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :json, sequence: 1 ) assert {:ok, %{json: json_path}} = result assert File.exists?(json_path) # Cleanup File.rm!(json_path) end test "returns error when no settlement found for merchant and date" do result = TransactionEodGenerator.generate_settlement_files( merchant_id: "NONEXISTENT", date: @test_date, format: :csv, sequence: 1 ) assert {:error, "No settlement found for merchant NONEXISTENT on " <> _} = result end end describe "mismatch detection scenario" do test "detects mismatch when transaction count doesn't match header" do # Create settlement with mismatch_count > 0 settlement = create_test_settlement(%{mismatch_count: 1}) _transactions = create_test_transactions(settlement, 4) # Create 4 transactions but header says 5 result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :csv, sequence: 1 ) assert {:ok, %{csv: csv_path}} = result csv_content = File.read!(csv_path) # Should show mismatch detected assert String.contains?(csv_content, "#MismatchDetected,YES") # Should show actual transaction count in footer assert String.contains?(csv_content, "#TotalRowCount,4") # Cleanup File.rm!(csv_path) end end describe "checksum validation" do test "generates valid SHA256 checksum for CSV content", %{settlement: _settlement} do result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :csv, sequence: 1 ) assert {:ok, %{csv: csv_path}} = result csv_content = File.read!(csv_path) # Extract checksum from file [checksum_line] = csv_content |> String.split("\n") |> Enum.filter(&String.starts_with?(&1, "#Checksum,")) [_, checksum] = String.split(checksum_line, ",", parts: 2) # Verify checksum format (64 character uppercase hex) assert String.length(checksum) == 64 assert String.match?(checksum, ~r/^[A-F0-9]{64}$/) # Verify checksum calculation content_without_checksum = csv_content |> String.split("\n") |> Enum.reject(&(String.starts_with?(&1, "#Checksum") or String.starts_with?(&1, "#EndOfFile"))) |> Enum.join("\n") expected_checksum = TransactionEodGenerator.calculate_checksum(content_without_checksum) assert checksum == expected_checksum # Cleanup File.rm!(csv_path) end end describe "filename format validation" do test "generates filenames according to mercury_pay_MMDD_HHMMSS_seq format", %{settlement: _settlement} do result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :both, sequence: 1 ) assert {:ok, %{csv: csv_path, json: json_path}} = result csv_filename = Path.basename(csv_path) json_filename = Path.basename(json_path) # Verify filename format: mercury_pay_MMDD_HHMMSS_seq.ext assert String.match?(csv_filename, ~r/^mercury_pay_\d{4}_\d{6}_\d{2}\.csv$/) assert String.match?(json_filename, ~r/^mercury_pay_\d{4}_\d{6}_\d{2}\.json$/) # Verify MMDD part corresponds to test date (07-05 -> 0705) assert String.contains?(csv_filename, "0705") assert String.contains?(json_filename, "0705") # Cleanup File.rm!(csv_path) File.rm!(json_path) end end describe "zero-transaction settlement" do test "handles settlement with no transactions", %{settlement: settlement} do # Remove all transactions Repo.delete_all(SettlementTransaction) # Update settlement to reflect zero transactions settlement |> Settlement.changeset(%{total_transaction_count: 0}) |> Repo.update!() result = TransactionEodGenerator.generate_settlement_files( merchant_id: @test_merchant_id, date: @test_date, format: :csv, sequence: 1 ) assert {:ok, %{csv: csv_path}} = result csv_content = File.read!(csv_path) # Should contain header and footer but no transaction lines assert String.contains?(csv_content, "#TotalTransactionCount,0") assert String.contains?(csv_content, "#TotalRowCount,0") assert String.contains?(csv_content, "#NetSettlementAmount,0,AED") # Should still have valid structure lines = String.split(csv_content, "\n") transaction_lines = lines |> Enum.reject(&String.starts_with?(&1, "#")) |> Enum.reject(&(&1 == "QRTransactionID,QRID,TID,TransactionAmount,Currency,TransactionStatus,TransactionTime,MDRCharge,TaxOnMDR,NetReceived,BatchNumber")) |> Enum.reject(&(&1 == "")) assert length(transaction_lines) == 0 # Cleanup File.rm!(csv_path) end end # Helper functions for test setup defp create_test_settlement(attrs \\ %{}) do default_attrs = %{ settlement_id: "SETT20250705-001", merchant_id: @test_merchant_id, date: @test_date, status: "settled", amount: Decimal.new("225.00"), merchant_tag: "TAG123", bank_user_id: "MERCURY_AANI123", total_transaction_count: 3, gross_settlement_amount: Decimal.new("225.00"), gross_settlement_currency: "AED", mdr_charges: Decimal.new("4.50"), mdr_charges_currency: "AED", tax_on_mdr: Decimal.new("0.23"), tax_on_mdr_currency: "AED", net_settlement_amount: Decimal.new("220.27"), net_settlement_currency: "AED", mismatch_count: 0, provider_id: 1, batch_number: "000001" } merged_attrs = Map.merge(default_attrs, attrs) {:ok, settlement} = DaProductApp.Settlements.create_settlement(merged_attrs) settlement end defp create_test_transactions(settlement, count \\ 3) do base_time = ~U[2025-07-05 12:30:00Z] transactions = for i <- 1..count do transaction_time = DateTime.add(base_time, i * 5 * 60, :second) # 5 minutes apart attrs = case i do 1 -> %{ # SUCCESS transaction - 100 AED transaction_id: "aani_pay_2030560038715669_20477574507000#{i}", qr_id: "AANI9876543210000000", terminal_id: "90080001", transaction_amount: Decimal.new("100.00"), transaction_currency: "AED", transaction_status: "SUCCESS", transaction_time: transaction_time, mdr_charge: Decimal.new("2.00"), mdr_charge_currency: "AED", tax_on_mdr: Decimal.new("0.10"), tax_on_mdr_currency: "AED", net_received_amount: Decimal.new("97.90"), net_received_currency: "AED" } 2 -> %{ # SUCCESS transaction - 50 AED transaction_id: "aani_pay_2030560038715669_20477574507000#{i}", qr_id: "AANI9876543210000000", terminal_id: "90080001", transaction_amount: Decimal.new("50.00"), transaction_currency: "AED", transaction_status: "SUCCESS", transaction_time: transaction_time, mdr_charge: Decimal.new("1.00"), mdr_charge_currency: "AED", tax_on_mdr: Decimal.new("0.05"), tax_on_mdr_currency: "AED", net_received_amount: Decimal.new("48.95"), net_received_currency: "AED" } 3 -> %{ # FAILED transaction - 75 AED transaction_id: "aani_pay_2030560038715669_20477574507000#{i}", qr_id: "AANI9876543210000000", terminal_id: "90080001", transaction_amount: Decimal.new("75.00"), transaction_currency: "AED", transaction_status: "FAILED", transaction_time: transaction_time, mdr_charge: Decimal.new("0.00"), mdr_charge_currency: "AED", tax_on_mdr: Decimal.new("0.00"), tax_on_mdr_currency: "AED", net_received_amount: Decimal.new("0.00"), net_received_currency: "AED" } _ -> %{ # Additional transactions for larger counts transaction_id: "aani_pay_2030560038715669_20477574507000#{i}", qr_id: "AANI9876543210000000", terminal_id: "90080001", transaction_amount: Decimal.new("25.00"), transaction_currency: "AED", transaction_status: "SUCCESS", transaction_time: transaction_time, mdr_charge: Decimal.new("0.50"), mdr_charge_currency: "AED", tax_on_mdr: Decimal.new("0.03"), tax_on_mdr_currency: "AED", net_received_amount: Decimal.new("24.47"), net_received_currency: "AED" } end changeset = SettlementTransaction.changeset(%SettlementTransaction{}, Map.put(attrs, :settlement_id, settlement.id)) {:ok, transaction} = Repo.insert(changeset) transaction end transactions end end