defmodule DaProductApp.Switch.ChannelManager do @moduledoc """ Channel configuration and management system. Features: - Maps listening ports to specific packagers and protocols - Manages channel-specific configurations and transformations - Provides runtime channel configuration updates - Handles channel-specific validation rules Configuration Structure: ```elixir config :da_product_app, :channels, %{ 8583 => %{ name: "primary_iso8583", packager: DaProductApp.MercuryISO8583.Packagers.ISO87BPackager, protocol: DaProductApp.Switch.ProtocolRefactored, max_connections: 100, timeout: 30_000, transformations: [], validation_rules: :standard }, 8584 => %{ name: "secondary_iso8583", packager: DaProductApp.MercuryISO8583.Packagers.ISO87BPackager, protocol: DaProductApp.Switch.ProtocolRefactored, max_connections: 50, timeout: 15_000, transformations: [:normalize_amounts], validation_rules: :strict } } ``` """ require Logger @doc """ Get channel configuration for a specific port. During migration, checks both :enhanced_channels and :channels configs. """ def get_channel_config(port) when is_integer(port) do # First check enhanced channels (for new EnhancedProtocol) enhanced_channels = Application.get_env(:da_product_app, :enhanced_channels, %{}) case Map.get(enhanced_channels, port) do nil -> # Fallback to legacy channels configuration channels = Application.get_env(:da_product_app, :channels, %{}) case Map.get(channels, port) do nil -> Logger.warning("No channel configuration found for port #{port}") {:error, {:channel_not_found, port}} config -> Logger.debug("Retrieved legacy channel config for port #{port}") {:ok, Map.put(config, :port, port)} end config -> Logger.debug("Retrieved enhanced channel config for port #{port}: #{config.name}") {:ok, Map.put(config, :port, port)} end end @doc """ Get all configured channels. """ def get_all_channels do channels = Application.get_env(:da_product_app, :channels, %{}) channels |> Enum.map(fn {port, config} -> {port, Map.put(config, :port, port)} end) |> Map.new() end @doc """ Update channel configuration at runtime. """ def update_channel_config(port, updates) when is_integer(port) and is_map(updates) do Logger.info("Updating channel configuration for port #{port}") case get_channel_config(port) do {:ok, current_config} -> updated_config = Map.merge(current_config, updates) # Update application environment channels = Application.get_env(:da_product_app, :channels, %{}) updated_channels = Map.put(channels, port, updated_config) Application.put_env(:da_product_app, :channels, updated_channels) Logger.info("Channel configuration updated for port #{port}") {:ok, updated_config} {:error, reason} -> {:error, reason} end end @doc """ Add a new channel configuration. """ def add_channel(port, config) when is_integer(port) and is_map(config) do Logger.info("Adding new channel configuration for port #{port}") # Validate required fields case validate_channel_config(config) do :ok -> channels = Application.get_env(:da_product_app, :channels, %{}) if Map.has_key?(channels, port) do Logger.warning("Channel already exists for port #{port}") {:error, {:channel_exists, port}} else updated_channels = Map.put(channels, port, config) Application.put_env(:da_product_app, :channels, updated_channels) Logger.info("Channel added for port #{port}") {:ok, Map.put(config, :port, port)} end {:error, reason} -> {:error, reason} end end @doc """ Remove a channel configuration. """ def remove_channel(port) when is_integer(port) do Logger.info("Removing channel configuration for port #{port}") channels = Application.get_env(:da_product_app, :channels, %{}) if Map.has_key?(channels, port) do updated_channels = Map.delete(channels, port) Application.put_env(:da_product_app, :channels, updated_channels) Logger.info("Channel removed for port #{port}") :ok else Logger.warning("No channel found for port #{port}") {:error, {:channel_not_found, port}} end end @doc """ Get the packager for a specific channel. """ def get_channel_packager(port) when is_integer(port) do case get_channel_config(port) do {:ok, config} -> packager = Map.get(config, :packager, DaProductApp.MercuryISO8583.Packagers.ISO87BPackager) {:ok, packager} {:error, reason} -> {:error, reason} end end @doc """ Get validation rules for a specific channel. """ def get_validation_rules(port) when is_integer(port) do case get_channel_config(port) do {:ok, config} -> rules = Map.get(config, :validation_rules, :standard) {:ok, rules} {:error, reason} -> {:error, reason} end end @doc """ Get transformations for a specific channel. """ def get_channel_transformations(port) when is_integer(port) do case get_channel_config(port) do {:ok, config} -> transformations = Map.get(config, :transformations, []) {:ok, transformations} {:error, reason} -> {:error, reason} end end @doc """ Apply channel-specific transformations to a message. """ def apply_channel_transformations(iso_message, port) when is_integer(port) do case get_channel_transformations(port) do {:ok, transformations} -> Logger.debug("Applying #{length(transformations)} transformations for port #{port}") result = Enum.reduce(transformations, iso_message, fn transformation, acc -> apply_transformation(acc, transformation) end) {:ok, result} {:error, reason} -> Logger.warning("Could not get transformations for port #{port}: #{inspect(reason)}") {:ok, iso_message} # Return original message if no transformations end end @doc """ Create channel context for message processing. """ def create_channel_context(port, socket_info \\ %{}) when is_integer(port) do case get_channel_config(port) do {:ok, config} -> context = %{ port: port, name: Map.get(config, :name), packager: Map.get(config, :packager), protocol: Map.get(config, :protocol), validation_rules: Map.get(config, :validation_rules), transformations: Map.get(config, :transformations, []), header_config: Map.get(config, :header_config), socket_info: socket_info, created_at: DateTime.utc_now() } {:ok, context} {:error, reason} -> {:error, reason} end end @doc """ Validate channel configuration. """ def validate_channel_config(config) when is_map(config) do required_fields = [:name, :packager, :protocol] missing_fields = Enum.filter(required_fields, fn field -> not Map.has_key?(config, field) end) if Enum.empty?(missing_fields) do # Validate field values case validate_field_values(config) do :ok -> :ok {:error, reason} -> {:error, reason} end else {:error, {:missing_required_fields, missing_fields}} end end # Private Functions defp validate_field_values(config) do with :ok <- validate_packager(config.packager), :ok <- validate_protocol(config.protocol), :ok <- validate_optional_fields(config) do :ok else {:error, reason} -> {:error, reason} end end defp validate_packager(packager) do # Check if packager module exists and has required functions if Code.ensure_loaded?(packager) do required_functions = [:new, :pack, :unpack] missing_functions = Enum.filter(required_functions, fn func -> not function_exported?(packager, func, 1) end) if Enum.empty?(missing_functions) do :ok else {:error, {:invalid_packager, {:missing_functions, missing_functions}}} end else {:error, {:invalid_packager, :module_not_found}} end end defp validate_protocol(protocol) do # Check if protocol module exists if Code.ensure_loaded?(protocol) do :ok else {:error, {:invalid_protocol, :module_not_found}} end end defp validate_optional_fields(config) do # Validate optional fields if present with :ok <- validate_max_connections(config), :ok <- validate_timeout(config), :ok <- validate_transformations(config) do :ok else {:error, reason} -> {:error, reason} end end defp validate_max_connections(config) do case Map.get(config, :max_connections) do nil -> :ok value when is_integer(value) and value > 0 -> :ok _ -> {:error, {:invalid_max_connections, :must_be_positive_integer}} end end defp validate_timeout(config) do case Map.get(config, :timeout) do nil -> :ok value when is_integer(value) and value > 0 -> :ok _ -> {:error, {:invalid_timeout, :must_be_positive_integer}} end end defp validate_transformations(config) do case Map.get(config, :transformations) do nil -> :ok [] -> :ok transformations when is_list(transformations) -> # Validate each transformation invalid_transformations = Enum.filter(transformations, fn transformation -> not is_atom(transformation) end) if Enum.empty?(invalid_transformations) do :ok else {:error, {:invalid_transformations, invalid_transformations}} end _ -> {:error, {:invalid_transformations, :must_be_list}} end end defp apply_transformation(iso_message, :normalize_amounts) do # Example transformation: normalize amount fields DaProductApp.MercuryISO8583.Transformations.normalize_amounts(iso_message) end defp apply_transformation(iso_message, :add_local_timestamp) do # Example transformation: add local processing timestamp timestamp = DateTime.utc_now() |> DateTime.to_string() DaProductApp.MercuryISO8583.Packagers.ISOMsg.set(iso_message, 12, timestamp) end defp apply_transformation(iso_message, transformation) do Logger.warning("Unknown transformation: #{transformation}") iso_message end end