defmodule DaProductApp.Acquirer.ReversalConfig do @moduledoc """ Centralized configuration for transaction reversal behavior. Provides access to all reversal-related timeouts, thresholds, and retry parameters. Configuration is read from Application environment (config/*.exs files). ## Configuration Keys All configuration is under `:da_product_app, :reversal` application environment. Example configuration: config :da_product_app, :reversal, stale_transaction_threshold_seconds: 45, response_timeout_seconds: 30, max_retry_attempts: 3, retry_delay_seconds: 60, connection_recovery_timeout_seconds: 30, startup_cleanup_age_threshold_minutes: 5, scheduled_cleanup_interval_minutes: 15, automatic_reversal_enabled: false ## Feature Flags - `automatic_reversal_enabled` - Master switch for automatic reversal functionality - Defaults to `false` for safety, must be explicitly enabled in production ## Usage iex> ReversalConfig.response_timeout() 30 iex> ReversalConfig.automatic_reversal_enabled?() false """ @doc """ Get stale transaction threshold in seconds. Transactions older than this threshold are considered stale and require reversal. Default: 45 seconds """ @spec stale_transaction_threshold :: non_neg_integer() def stale_transaction_threshold do get_config(:stale_transaction_threshold_seconds, 45) end @doc """ Get response timeout in seconds. Maximum time to wait for response from upstream before marking transaction as timed out. Default: 30 seconds """ @spec response_timeout :: non_neg_integer() def response_timeout do get_config(:response_timeout_seconds, 30) end @doc """ Get maximum retry attempts for reversal. After this many failed reversal attempts, transaction is marked for manual review. Default: 3 attempts """ @spec max_retry_attempts :: non_neg_integer() def max_retry_attempts do get_config(:max_retry_attempts, 3) end @doc """ Get base retry delay in seconds. Base delay used for exponential backoff calculation. Default: 60 seconds """ @spec retry_delay_seconds :: non_neg_integer() def retry_delay_seconds do get_config(:retry_delay_seconds, 60) end @doc """ Get connection recovery timeout in seconds. Time to wait for connection recovery before initiating reversal for affected transactions. Default: 30 seconds """ @spec connection_recovery_timeout :: non_neg_integer() def connection_recovery_timeout do get_config(:connection_recovery_timeout_seconds, 30) end @doc """ Get startup cleanup age threshold in minutes. On application startup, transactions older than this threshold are processed for reversal. Default: 5 minutes """ @spec startup_cleanup_age_threshold_minutes :: non_neg_integer() def startup_cleanup_age_threshold_minutes do get_config(:startup_cleanup_age_threshold_minutes, 5) end @doc """ Get scheduled cleanup interval in minutes. How frequently the ReversalCleanupWorker runs to detect stuck transactions. Default: 15 minutes """ @spec scheduled_cleanup_interval_minutes :: non_neg_integer() def scheduled_cleanup_interval_minutes do get_config(:scheduled_cleanup_interval_minutes, 15) end @doc """ Check if automatic reversal is enabled. Master feature flag for automatic reversal functionality. When disabled, reversals must be manually triggered. Default: false (for safety) """ @spec automatic_reversal_enabled? :: boolean() def automatic_reversal_enabled? do get_config(:automatic_reversal_enabled, false) end @doc """ Check if cleanup worker is enabled. Controls whether ReversalCleanupWorker starts as part of supervision tree. Default: false (controlled separately from automatic_reversal_enabled) """ @spec cleanup_worker_enabled? :: boolean() def cleanup_worker_enabled? do get_config(:cleanup_worker_enabled, false) end @doc """ Check if retry worker is enabled. Controls whether retry mechanism is active for failed reversals. Default: false """ @spec retry_worker_enabled? :: boolean() def retry_worker_enabled? do get_config(:retry_worker_enabled, false) end @doc """ Check if connection loss handler is enabled. Controls whether connection loss triggers automatic reversals. Default: false """ @spec connection_loss_handler_enabled? :: boolean() def connection_loss_handler_enabled? do get_config(:connection_loss_handler_enabled, false) end @doc """ Calculate exponential backoff delay for retry attempt. Uses exponential backoff strategy: base_delay * 2^(attempt_number - 1) ## Examples iex> ReversalConfig.calculate_backoff_delay(1) 60 # 60 * 2^0 = 60 seconds iex> ReversalConfig.calculate_backoff_delay(2) 120 # 60 * 2^1 = 120 seconds iex> ReversalConfig.calculate_backoff_delay(3) 240 # 60 * 2^2 = 240 seconds ## Parameters - `attempt_number` - The current retry attempt number (1-based) ## Returns Delay in seconds (integer) """ @spec calculate_backoff_delay(non_neg_integer()) :: non_neg_integer() def calculate_backoff_delay(attempt_number) when is_integer(attempt_number) and attempt_number > 0 do base_delay = retry_delay_seconds() # Exponential backoff: base * 2^(attempt - 1) (base_delay * :math.pow(2, attempt_number - 1)) |> round() end def calculate_backoff_delay(_), do: retry_delay_seconds() @doc """ Get reversal reason code mapping. Maps reversal reason strings to ISO 8583 response codes. ## Parameters - `reason` - Reversal reason string ## Returns ISO response code (string) """ @spec map_reason_to_code(String.t()) :: String.t() def map_reason_to_code(reason) when is_binary(reason) do case reason do "TIMEOUT" -> "68" # Response received too late "RESPONSE_TIMEOUT" -> "68" "CONNECTION_LOST" -> "91" # Issuer or switch inoperative "STALE_TRANSACTION" -> "68" "DB_CREATE_FAILED" -> "96" # System malfunction "DB_DELETE_FAILED" -> "96" "STARTUP_CLEANUP" -> "68" "ERROR_STATE" -> "96" _ -> "68" # Default: Response received too late end end def map_reason_to_code(_), do: "68" @doc """ Get all configuration values as a map. Useful for debugging and logging configuration state. """ @spec all_config :: map() def all_config do %{ stale_transaction_threshold_seconds: stale_transaction_threshold(), response_timeout_seconds: response_timeout(), max_retry_attempts: max_retry_attempts(), retry_delay_seconds: retry_delay_seconds(), connection_recovery_timeout_seconds: connection_recovery_timeout(), startup_cleanup_age_threshold_minutes: startup_cleanup_age_threshold_minutes(), scheduled_cleanup_interval_minutes: scheduled_cleanup_interval_minutes(), automatic_reversal_enabled: automatic_reversal_enabled?(), cleanup_worker_enabled: cleanup_worker_enabled?(), retry_worker_enabled: retry_worker_enabled?(), connection_loss_handler_enabled: connection_loss_handler_enabled?() } end # Private helper to read config with default defp get_config(key, default) do Application.get_env(:da_product_app, :reversal, []) |> Keyword.get(key, default) end end