defmodule DaProductApp.TerminalManagement.TerminalGroupRule do use Ecto.Schema import Ecto.Changeset import Ecto.Query @derive {Jason.Encoder, only: [ :id, :group_id, :rule_name, :rule_type, :field_name, :operator, :value, :priority, :is_active, :conditions, :inserted_at, :updated_at ]} schema "terminal_group_rules" do field :rule_name, :string field :rule_type, :string # field_match, merchant_type, location, custom, composite field :field_name, :string # vendor, model, area, merchant_id, status, etc. field :operator, :string # equals, contains, in, starts_with, ends_with, regex, range field :value, :string # The value to match against field :priority, :integer, default: 100 # Lower = higher priority field :is_active, :boolean, default: true field :conditions, :map, default: %{} # Complex conditions as JSON belongs_to :group, DaProductApp.TerminalManagement.TerminalGroup, foreign_key: :group_id has_many :group_memberships, DaProductApp.TerminalManagement.TerminalGroupMembership, foreign_key: :rule_id timestamps() end @required_fields [:group_id, :rule_name, :rule_type, :operator, :value] @optional_fields [:field_name, :priority, :is_active, :conditions] @all_fields @required_fields ++ @optional_fields @valid_rule_types ["field_match", "merchant_type", "location", "custom", "composite"] @valid_operators ["equals", "contains", "in", "starts_with", "ends_with", "regex", "range", "not_equals", "not_in"] # Common terminal fields that can be used in rules @terminal_fields [ "vendor", "model", "area", "status", "group", "imei", "serial_number", "app_version", "system_version", "hardware_version", "deployment_type", "tier", "location_code", "merchant_id" ] def changeset(rule, attrs) do rule |> cast(attrs, @all_fields) |> validate_required(@required_fields) |> validate_length(:rule_name, min: 2, max: 100) |> validate_inclusion(:rule_type, @valid_rule_types) |> validate_inclusion(:operator, @valid_operators) |> validate_field_name_for_rule_type() |> validate_value_format() |> validate_priority_range() end defp validate_field_name_for_rule_type(changeset) do rule_type = get_change(changeset, :rule_type) || get_field(changeset, :rule_type) field_name = get_change(changeset, :field_name) || get_field(changeset, :field_name) case rule_type do "field_match" when field_name in @terminal_fields -> changeset "field_match" when is_nil(field_name) -> add_error(changeset, :field_name, "is required for field_match rule type") "field_match" -> add_error(changeset, :field_name, "must be a valid terminal field") "merchant_type" -> if field_name && field_name not in ["merchant_type_id", "business_category", "tier"] do add_error(changeset, :field_name, "must be merchant_type_id, business_category, or tier for merchant_type rules") else changeset end _ -> changeset end end defp validate_value_format(changeset) do operator = get_change(changeset, :operator) || get_field(changeset, :operator) value = get_change(changeset, :value) || get_field(changeset, :value) case operator do "in" -> case parse_in_values(value) do {:ok, _} -> changeset {:error, _} -> add_error(changeset, :value, "must be comma-separated values for 'in' operator") end "range" -> case parse_range_values(value) do {:ok, _} -> changeset {:error, _} -> add_error(changeset, :value, "must be 'min,max' format for range operator") end "regex" -> case Regex.compile(value) do {:ok, _} -> changeset {:error, _} -> add_error(changeset, :value, "must be a valid regular expression") end _ -> changeset end end defp validate_priority_range(changeset) do validate_number(changeset, :priority, greater_than: 0, less_than: 1000) end # Parse comma-separated values for 'in' operator def parse_in_values(value) when is_binary(value) do values = value |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) case values do [] -> {:error, "no values provided"} list -> {:ok, list} end end # Parse range values for 'range' operator def parse_range_values(value) when is_binary(value) do case String.split(value, ",") do [min_str, max_str] -> with {min_val, ""} <- Integer.parse(String.trim(min_str)), {max_val, ""} <- Integer.parse(String.trim(max_str)), true <- min_val <= max_val do {:ok, {min_val, max_val}} else _ -> {:error, "invalid range format"} end _ -> {:error, "range must have exactly two values"} end end # Check if a terminal matches this rule def matches_terminal?(rule, terminal) do case rule.rule_type do "field_match" -> matches_field_rule?(rule, terminal) "merchant_type" -> matches_merchant_rule?(rule, terminal) "location" -> matches_location_rule?(rule, terminal) "custom" -> matches_custom_rule?(rule, terminal) "composite" -> matches_composite_rule?(rule, terminal) _ -> false end end defp matches_field_rule?(rule, terminal) do field_value = get_terminal_field_value(terminal, rule.field_name) matches_operator?(rule.operator, field_value, rule.value) end defp matches_merchant_rule?(rule, terminal) do # This would require loading merchant data # Implementation depends on how merchant relationship is structured false # Placeholder end defp matches_location_rule?(rule, terminal) do # Implementation for location-based rules area_value = terminal.area || "" location_value = terminal.location_code || "" matches_operator?(rule.operator, area_value, rule.value) or matches_operator?(rule.operator, location_value, rule.value) end defp matches_custom_rule?(rule, terminal) do # Custom rules would be implemented based on conditions map # This allows for complex, programmatic rule definitions case rule.conditions do %{"custom_function" => function_name} -> # Call custom matching function apply_custom_function(function_name, terminal, rule) _ -> false end end defp matches_composite_rule?(rule, terminal) do # Composite rules allow combining multiple conditions # Implementation would parse conditions map for logical operators false # Placeholder for complex implementation end defp get_terminal_field_value(terminal, field_name) do case field_name do "vendor" -> terminal.vendor "model" -> terminal.model "area" -> terminal.area "status" -> terminal.status "group" -> terminal.group "imei" -> terminal.imei "serial_number" -> terminal.serial_number "app_version" -> terminal.app_version "system_version" -> terminal.system_version "hardware_version" -> terminal.hardware_version "deployment_type" -> terminal.deployment_type "tier" -> terminal.tier "location_code" -> terminal.location_code "merchant_id" -> terminal.merchant_id _ -> nil end end defp matches_operator?(operator, field_value, rule_value) do field_value = field_value || "" case operator do "equals" -> field_value == rule_value "not_equals" -> field_value != rule_value "contains" -> String.contains?(field_value, rule_value) "starts_with" -> String.starts_with?(field_value, rule_value) "ends_with" -> String.ends_with?(field_value, rule_value) "in" -> case parse_in_values(rule_value) do {:ok, values} -> field_value in values _ -> false end "not_in" -> case parse_in_values(rule_value) do {:ok, values} -> field_value not in values _ -> true end "regex" -> case Regex.compile(rule_value) do {:ok, regex} -> Regex.match?(regex, field_value) _ -> false end "range" -> case {parse_range_values(rule_value), Integer.parse(field_value)} do {{:ok, {min_val, max_val}}, {field_int, ""}} -> field_int >= min_val and field_int <= max_val _ -> false end _ -> false end end defp apply_custom_function(function_name, terminal, rule) do # Placeholder for custom function implementations # You can add specific business logic functions here case function_name do "high_volume_terminal" -> check_high_volume_terminal(terminal) "new_terminal" -> check_new_terminal(terminal) _ -> false end end defp check_high_volume_terminal(terminal) do # Example: check if terminal processes high volume # This would require additional data/queries false end defp check_new_terminal(terminal) do # Example: check if terminal was added recently case terminal.inserted_at do nil -> false date -> days_ago = Date.diff(Date.utc_today(), Date.from_erl!(date)) days_ago <= 30 end end # Scope for active rules def active_rules(query \\ __MODULE__) do from r in query, where: r.is_active == true end # Scope for rules by type def by_type(query \\ __MODULE__, type) do from r in query, where: r.rule_type == ^type end # Scope for rules by priority def by_priority(query \\ __MODULE__) do from r in query, order_by: [asc: r.priority, desc: r.inserted_at] end end