#!/usr/bin/env elixir # Migration Test Client - Sends Mercury format messages to both protocols # Run with: elixir migration_test_client.exs # Or run within Mix context: mix run migration_test_client.exs Mix.install([]) defmodule MigrationTestClient do @moduledoc """ Test client for gradual migration validation. Sends identical Mercury format ISO8583 messages to: 1. Existing Protocol.ex (legacy port) 2. New EnhancedProtocol (enhanced ports) Mercury format: Length(4) + TPDU(5) + ISO8583 message Compares responses to ensure identical behavior. """ require Logger import Bitwise # Configuration @legacy_port 5000 # Your existing Protocol.ex port @enhanced_port_1 8585 # New EnhancedProtocol port 1 @enhanced_port_2 8586 # New EnhancedProtocol port 2 @timeout 30_000 def run_migration_tests do IO.puts("=== Migration Validation Test Suite ===\n") # Create test message test_message = create_test_message() IO.puts("šŸ“¦ Created Mercury format test message: #{byte_size(test_message)} bytes") # Display Mercury packet structure if byte_size(test_message) >= 9 do <> = test_message tpdu_hex = Base.encode16(tpdu_binary) IO.puts(" Length prefix: #{inspect(length_prefix)}") IO.puts(" TPDU: #{tpdu_hex}") IO.puts(" ISO8583 length: #{byte_size(iso_part)} bytes") # Show first part of ISO message if byte_size(iso_part) >= 4 do <> = iso_part IO.puts(" ISO MTI: #{mti}") end end # Show hex (first 100 chars) hex_str = Base.encode16(test_message) display_hex = if String.length(hex_str) > 100, do: String.slice(hex_str, 0, 100) <> "...", else: hex_str IO.puts(" Hex: #{display_hex}") # Test existing protocol IO.puts("\nšŸ”¹ Testing Legacy Protocol (port #{@legacy_port})...") legacy_response = send_to_port(test_message, @legacy_port) # Test enhanced protocol port 1 IO.puts("\nšŸ”ø Testing Enhanced Protocol Port 1 (port #{@enhanced_port_1})...") enhanced_response_1 = send_to_port(test_message, @enhanced_port_1) # Test enhanced protocol port 2 IO.puts("\nšŸ”ø Testing Enhanced Protocol Port 2 (port #{@enhanced_port_2})...") enhanced_response_2 = send_to_port(test_message, @enhanced_port_2) # Compare responses IO.puts("\nšŸ” Comparing Responses...") compare_responses(legacy_response, enhanced_response_1, enhanced_response_2) IO.puts("\n=== Migration Test Complete ===") end defp create_test_message do # Create Mercury format message: Length(4) + TPDU(5) + ISO8583 # This creates a sample packet compatible with your Mercury format # Check if we're running in Mix context (with application loaded) if Code.ensure_loaded?(DaProductApp.MercuryISO8583.Packagers.ISOMsg) do IO.puts("āš™ļø Running in Mix context - using ISOMsg for message creation") create_test_message_with_isomsg() else IO.puts("āš ļø Running in standalone mode - creating basic test packet") create_test_message_manual() end end defp create_test_message_with_isomsg do # Use ISOMsg when running in Mix context alias DaProductApp.MercuryISO8583.Packagers.ISOMsg alias DaProductApp.MercuryISO8583.Packagers.ISO87BPackager # Sample TPDU tpdu = "6000782000" # Create new message and set MTI iso_msg = ISOMsg.new() |> ISOMsg.set_mti("0200") |> ISOMsg.set_packager(ISO87BPackager) IO.puts("āœ“ Created ISOMsg with MTI: #{iso_msg.mti}") # Set various fields using Mercury sample data iso_msg = iso_msg |> ISOMsg.set(2, "4854980600736740") # PAN |> ISOMsg.set(3, "000000") # Processing Code |> ISOMsg.set(4, "000000005200") # Amount |> ISOMsg.set(11, "000027") # STAN |> ISOMsg.set(12, "121033") # Local Time |> ISOMsg.set(13, "0916") # Local Date |> ISOMsg.set(14, "3105") # Expiration Date |> ISOMsg.set(22, "0051") # POS Entry Mode |> ISOMsg.set(23, "0000") # Application PAN Number |> ISOMsg.set(24, "0782") # Network International ID |> ISOMsg.set(25, "00") # POS Condition Code |> ISOMsg.set(35, "4854980600736740D310520600117900") # Track 2 Data |> ISOMsg.set(41, "12345671") # Terminal ID (8 chars) |> ISOMsg.set(42, "123456789012345") # Merchant ID (15 chars) |> ISOMsg.set(49, "784") # Transaction Currency Code |> ISOMsg.set(55, "01329F2701809F100706011203A0A8029F3704D3D5ABE49F36020345950500800400009A032509169C01009F02060000000000005F2A020356820238009F1A0203569F03060000000000009F3303E0E0C89F34034203009F3501229F1E0832333733303030318407A00000000310109F090200969F4104000003009F26087E307BB69E1E064D") # EMV Data |> ISOMsg.set(62, "000001") # Private Data |> ISOMsg.set(63, "98250623730009000008A") # Private Data |> ISOMsg.set(64, "1234567812345678") # MAC IO.puts("āœ“ Set fields: #{inspect(ISOMsg.get_field_numbers(iso_msg))}") # Display field values IO.puts("\nField values:") field_numbers = ISOMsg.get_field_numbers(iso_msg) Enum.each(field_numbers, fn field -> value = ISOMsg.getString(iso_msg, field) display_value = if String.length(value || "") > 50 do String.slice(value, 0, 50) <> "..." else value end IO.puts(" Field #{field}: #{inspect(display_value)}") end) # Pack the message to byte array using ISO87BPackager IO.puts("\nšŸ”ø Packing message to byte array...") case ISO87BPackager.pack(iso_msg) do {:ok, iso_bytes} -> IO.puts("āœ… ISO8583 message packed successfully!") IO.puts(" ISO packed length: #{byte_size(iso_bytes)} bytes") # Convert TPDU from hex string to binary tpdu_binary = Base.decode16!(tpdu, case: :mixed) # Combine TPDU + ISO message packet_data = tpdu_binary <> iso_bytes # Calculate total length and create 4-digit ASCII length prefix total_length = byte_size(packet_data) length_str = String.pad_leading(Integer.to_string(total_length), 4, "0") # Final Mercury packet: Length + TPDU + ISO8583 length_str <> packet_data {:error, reason} -> IO.puts("āŒ Pack failed: #{inspect(reason)}") # Return a minimal packet on error "0009" <> Base.decode16!("6000782000", case: :mixed) <> "0200" end end defp create_test_message_manual do # Create a basic Mercury format packet manually (when ISOMsg not available) IO.puts("āš ļø Running in standalone mode - creating basic test packet") # Sample TPDU tpdu = "6000782000" # Simple ISO8583 message structure for testing mti = "0200" # Simple bitmap for basic fields (2, 3, 4, 11, 41, 42) # Bitmap: 70200000000000000000000000000000 (fields 2,3,4,11,41,42 set) bitmap = Base.decode16!("7020000000000000") # Pack basic fields manually field_2 = "164111111111111111" # LLVAR PAN (16 digits with 2-digit length) field_3 = "000000" # Processing Code (6 digits) field_4 = "000000001000" # Amount (12 digits) field_11 = "123456" # STAN (6 digits) field_41 = "TERM0001" # Terminal ID (8 chars) field_42 = "MERCHANT000001" # Merchant ID (15 chars) # Construct basic ISO message iso_message = mti <> bitmap <> field_2 <> field_3 <> field_4 <> field_11 <> field_41 <> field_42 # Convert TPDU from hex string to binary tpdu_binary = Base.decode16!(tpdu, case: :mixed) # Combine TPDU + ISO message packet_data = tpdu_binary <> iso_message # Calculate total length and create 4-digit ASCII length prefix total_length = byte_size(packet_data) length_str = String.pad_leading(Integer.to_string(total_length), 4, "0") IO.puts("āœ“ Created basic test packet:") IO.puts(" Length: #{length_str}") IO.puts(" TPDU: #{tpdu}") IO.puts(" MTI: #{mti}") IO.puts(" Fields: 2, 3, 4, 11, 41, 42") # Final Mercury packet: Length + TPDU + ISO8583 length_str <> packet_data end defp send_to_port(message, port) do #print message in hex IO.puts(Base.encode16(message)) try do case :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, active: false], @timeout) do {:ok, socket} -> case :gen_tcp.send(socket, message) do :ok -> # For Mercury format, expect response with 4-digit ASCII length prefix case :gen_tcp.recv(socket, 4, @timeout) do {:ok, length_str} -> case Integer.parse(length_str) do {response_length, ""} -> case :gen_tcp.recv(socket, response_length, @timeout) do {:ok, response_data} -> :gen_tcp.close(socket) IO.puts(" āœ… Response received: #{response_length} bytes (declared), #{byte_size(response_data)} bytes (actual)") IO.puts(" šŸ“„ Length prefix: #{inspect(length_str)}") # Parse Mercury response format parse_mercury_response(response_data) {:ok, length_str <> response_data} # Return full response including length {:error, reason} -> :gen_tcp.close(socket) IO.puts(" āŒ Failed to receive response data: #{inspect(reason)}") {:error, {:recv_data_failed, reason}} end _ -> :gen_tcp.close(socket) IO.puts(" āŒ Invalid length format: #{inspect(length_str)}") {:error, {:invalid_length_format, length_str}} end {:error, reason} -> :gen_tcp.close(socket) IO.puts(" āŒ Failed to receive response length: #{inspect(reason)}") {:error, {:recv_length_failed, reason}} end {:error, reason} -> :gen_tcp.close(socket) IO.puts(" āŒ Failed to send message: #{inspect(reason)}") {:error, {:send_failed, reason}} end {:error, reason} -> IO.puts(" āŒ Failed to connect to port #{port}: #{inspect(reason)}") {:error, {:connection_failed, reason}} end rescue exception -> IO.puts(" āŒ Exception: #{inspect(exception)}") {:error, {:exception, exception}} end end defp parse_mercury_response(response_data) when is_binary(response_data) do if byte_size(response_data) >= 5 do # Extract TPDU (first 5 bytes) <> = response_data tpdu_hex = Base.encode16(tpdu_binary) IO.puts(" šŸ”ø Response TPDU: #{tpdu_hex}") # Parse ISO8583 portion if byte_size(iso_data) >= 4 do <> = iso_data IO.puts(" šŸ”ø Response MTI: #{mti}") if byte_size(rest) >= 8 do <> = rest IO.puts(" šŸ”ø Response Bitmap: #{Base.encode16(bitmap)}") # Parse fields from bitmap field_list = parse_bitmap_fields(bitmap) IO.puts(" šŸ”ø Response Fields: #{inspect(field_list)}") end end end end defp compare_responses(legacy_response, enhanced_response_1, enhanced_response_2) do # Compare legacy vs enhanced case {legacy_response, enhanced_response_1} do {{:ok, legacy_data}, {:ok, enhanced_data}} -> if legacy_data == enhanced_data do IO.puts(" āœ… Legacy and Enhanced Port 1 responses MATCH") else IO.puts(" āš ļø Legacy and Enhanced Port 1 responses DIFFER") IO.puts(" Legacy length: #{byte_size(legacy_data)} bytes") IO.puts(" Enhanced length: #{byte_size(enhanced_data)} bytes") # Compare hex representations for better analysis legacy_hex = Base.encode16(legacy_data) enhanced_hex = Base.encode16(enhanced_data) if String.length(legacy_hex) <= 100 and String.length(enhanced_hex) <= 100 do IO.puts(" Legacy hex: #{legacy_hex}") IO.puts(" Enhanced hex: #{enhanced_hex}") else IO.puts(" Legacy hex (first 50 chars): #{String.slice(legacy_hex, 0, 50)}...") IO.puts(" Enhanced hex (first 50 chars): #{String.slice(enhanced_hex, 0, 50)}...") end end {legacy, enhanced} -> IO.puts(" āŒ Cannot compare - Legacy: #{inspect(legacy)}, Enhanced: #{inspect(enhanced)}") end # Compare enhanced port 1 vs enhanced port 2 case {enhanced_response_1, enhanced_response_2} do {{:ok, data_1}, {:ok, data_2}} -> if data_1 == data_2 do IO.puts(" āœ… Enhanced Port 1 and Port 2 responses MATCH") else IO.puts(" āš ļø Enhanced Port 1 and Port 2 responses DIFFER") IO.puts(" Port 1 length: #{byte_size(data_1)} bytes") IO.puts(" Port 2 length: #{byte_size(data_2)} bytes") end _ -> IO.puts(" āŒ Cannot compare enhanced ports") end end def test_individual_port(port) do IO.puts("=== Testing Individual Port #{port} ===") test_message = create_test_message() IO.puts("šŸ“¦ Sending test message to port #{port}") response = send_to_port(test_message, port) case response do {:ok, response_data} -> IO.puts("āœ… Port #{port} responded successfully") parse_and_display_response(response_data) {:error, reason} -> IO.puts("āŒ Port #{port} failed: #{inspect(reason)}") end end defp parse_and_display_response(response_data) when is_binary(response_data) do # Parse Mercury format response: Length(4) + TPDU(5) + ISO8583 if byte_size(response_data) >= 9 do case parse_mercury_packet_response(response_data) do {:ok, %{length: length, tpdu: tpdu, iso_data: iso_data}} -> IO.puts(" šŸ“¦ Mercury Response Structure:") IO.puts(" Length: #{length}") IO.puts(" TPDU: #{tpdu}") IO.puts(" ISO Data: #{byte_size(iso_data)} bytes") # Parse ISO8583 portion if byte_size(iso_data) >= 4 do <> = iso_data IO.puts(" MTI: #{mti}") if byte_size(rest) >= 8 do <> = rest IO.puts(" Bitmap: #{Base.encode16(bitmap)}") # Parse fields from bitmap field_list = parse_bitmap_fields(bitmap) IO.puts(" Fields present: #{inspect(field_list)}") end end {:error, reason} -> IO.puts(" āŒ Failed to parse Mercury response: #{inspect(reason)}") IO.puts(" ļæ½ Raw response: #{Base.encode16(response_data)}") end else IO.puts(" āš ļø Response too short for Mercury format") IO.puts(" šŸ“„ Raw response: #{Base.encode16(response_data)}") end end defp parse_mercury_packet_response(packet_binary) when is_binary(packet_binary) do if byte_size(packet_binary) >= 4 do # Extract length (first 4 bytes as ASCII) <> = packet_binary case Integer.parse(length_str) do {length, ""} -> if byte_size(rest) >= length do <> = rest # Extract TPDU (first 5 bytes of packet data) if byte_size(packet_data) >= 5 do <> = packet_data tpdu_hex = Base.encode16(tpdu_binary) {:ok, %{ length: length, tpdu: tpdu_hex, iso_data: iso_data }} else {:error, :packet_too_short_for_tpdu} end else {:error, :packet_shorter_than_declared_length} end _ -> {:error, :invalid_length_format} end else {:error, :packet_too_short} end end defp parse_bitmap_fields(bitmap) when byte_size(bitmap) == 8 do # Parse primary bitmap to show which fields are present bitmap |> :binary.bin_to_list() |> Enum.with_index() |> Enum.flat_map(fn {byte, byte_index} -> 0..7 |> Enum.filter(fn bit -> (byte &&& (1 <<< (7 - bit))) != 0 end) |> Enum.map(fn bit -> byte_index * 8 + bit + 1 end) end) end end # Run tests IO.puts("=== Migration Test Client ===") IO.puts("Run modes:") IO.puts(" Standalone: elixir migration_test_client.exs") IO.puts(" With Mix: mix run migration_test_client.exs") IO.puts("") if Code.ensure_loaded?(DaProductApp.MercuryISO8583.Packagers.ISOMsg) do IO.puts("āœ… Running with full ISOMsg support") else IO.puts("āš ļø Running in basic mode (limited packet creation)") IO.puts(" For full features, run: mix run migration_test_client.exs") end IO.puts("\nChoose test mode:") IO.puts("1. Full migration test (all ports)") IO.puts("2. Test individual port") IO.puts("3. Test enhanced port 1 only") IO.puts("4. Test enhanced port 2 only") case IO.gets("Enter choice (1-4): ") |> String.trim() do "1" -> MigrationTestClient.run_migration_tests() "2" -> port = IO.gets("Enter port number: ") |> String.trim() |> String.to_integer() MigrationTestClient.test_individual_port(port) "3" -> MigrationTestClient.test_individual_port(8585) "4" -> MigrationTestClient.test_individual_port(8586) _ -> IO.puts("Invalid choice, running full test") MigrationTestClient.run_migration_tests() end