defmodule DaProductApp.Switch.RoutingRules do @moduledoc """ Routing rules engine for determining upstream networks. """ require Logger alias DaProductApp.MercuryISO8583.Packagers.ISOMsg @doc """ Check if a message matches the given routing rules. """ def matches_message?(%ISOMsg{} = iso_message, routing_rules) when is_list(routing_rules) do Enum.any?(routing_rules, fn rule -> matches_rule?(iso_message, rule) end) end @doc """ Detect card type from ISO message based on PAN (field 2) or track data (field 35). """ def detect_card_type_from_message(%ISOMsg{} = iso_message) do case get_pan_from_message(iso_message) do nil -> "unknown" pan -> detect_card_type_from_pan(pan) end end # Private functions defp matches_rule?(%ISOMsg{} = iso_message, {:bin_range, range}) when is_binary(range) do case String.split(range, "-") do [start_bin, end_bin] -> case ISOMsg.get(iso_message, 2) do # PAN field nil -> false pan when is_binary(pan) -> bin = String.slice(pan, 0, 6) bin >= start_bin and bin <= end_bin _ -> false end _ -> Logger.warning("Invalid BIN range format: #{range}") false end end defp matches_rule?(%ISOMsg{} = iso_message, {:card_type, card_types}) when is_list(card_types) do card_type = detect_card_type_from_message(iso_message) card_type in card_types end defp matches_rule?(%ISOMsg{} = iso_message, {:card_type, card_type}) when is_binary(card_type) do detected_type = detect_card_type_from_message(iso_message) detected_type == card_type end defp matches_rule?(%ISOMsg{} = iso_message, {:mti_pattern, pattern}) when is_binary(pattern) do mti = ISOMsg.get_mti(iso_message) matches_pattern?(mti, pattern) end defp matches_rule?(%ISOMsg{} = iso_message, {:field_value, field_number, expected_value}) do case ISOMsg.get(iso_message, field_number) do ^expected_value -> true _ -> false end end defp matches_rule?(%ISOMsg{} = iso_message, {:field_present, field_number}) do case ISOMsg.get(iso_message, field_number) do nil -> false _ -> true end end defp matches_rule?(_iso_message, {:accept_any_bin, true}) do true end defp matches_rule?(_iso_message, {:default, true}) do true end defp matches_rule?(_iso_message, rule) do Logger.warning("Unknown routing rule: #{inspect(rule)}") false end defp matches_pattern?(mti, pattern) do if String.length(mti) == String.length(pattern) do mti |> String.graphemes() |> Enum.zip(String.graphemes(pattern)) |> Enum.all?(fn {mti_char, pattern_char} -> pattern_char == "?" or mti_char == pattern_char end) else false end end # ============================== # CARD TYPE DETECTION HELPER FUNCTIONS # ============================== defp get_pan_from_message(%ISOMsg{} = iso_message) do # Try field 2 (PAN) first case ISOMsg.get(iso_message, 2) do nil -> # Try field 35 (track 2) as fallback case ISOMsg.get(iso_message, 35) do nil -> nil track_data -> extract_pan_from_track_data(track_data) end pan -> pan end end defp extract_pan_from_track_data(track_data) when is_binary(track_data) do # Track 2 format: PAN=YYMM[service_code][additional_data] case String.split(track_data, "=", parts: 2) do [pan, _rest] -> pan _ -> nil end end defp detect_card_type_from_pan(pan) when is_binary(pan) do case String.slice(pan, 0, 2) do # Visa: starts with 4 "4" <> _ -> "visa" # MasterCard: 51-55 or 2221-2720 bin when bin in ["51", "52", "53", "54", "55"] -> "mastercard" # MasterCard: 2221-2720 range (check first 4 digits) "22" <> _rest -> case String.slice(pan, 0, 4) do four_digit when four_digit >= "2221" and four_digit <= "2720" -> "mastercard" _ -> "unknown" end # American Express: 34, 37 bin when bin in ["34", "37"] -> "amex" # Discover: 6011, 622126-622925, 644-649, 65 "60" -> if String.slice(pan, 0, 4) == "6011", do: "discover", else: "unknown" "62" -> if String.slice(pan, 0, 6) in ["622126", "622925"], do: "discover", else: "unknown" "64" -> "discover" "65" -> "discover" # Diners Club: 300-305, 36, 38 "30" -> if String.slice(pan, 0, 3) <= "305", do: "diners", else: "unknown" "36" -> "diners" "38" -> "diners" _ -> "unknown" end end defp detect_card_type_from_pan(_), do: "unknown" end