defmodule DaProductApp.TerminalManagement.TerminalGroupService do @moduledoc """ Service module for managing terminal groups, rules, and automatic assignments. """ import Ecto.Query alias DaProductApp.Repo alias DaProductApp.TerminalManagement.{ TerminalGroup, TerminalGroupRule, TerminalGroupMembership, TmsTerminal, MerchantType, TerminalEventDispatcher } # ============================================================================ # Group Management # ============================================================================ @doc "List all terminal groups with optional filters" def list_groups(filters \\ %{}) do TerminalGroup |> apply_group_filters(filters) |> preload_group_associations() |> add_terminal_counts() |> Repo.all() end @doc "Get a specific group by ID with terminal count" def get_group(id) do case Repo.get(TerminalGroup, id) do nil -> {:error, :not_found} group -> group = group |> Repo.preload([:group_rules, :child_groups, :parent_group]) |> add_terminal_count() {:ok, group} end end @doc "Create a new terminal group" def create_group(attrs) do %TerminalGroup{} |> TerminalGroup.changeset(attrs) |> Repo.insert() |> case do {:ok, group} -> # If this is an auto group, create initial rules if group.group_type == "auto" and attrs["initial_rules"] do create_initial_rules(group, attrs["initial_rules"]) end {:ok, group} error -> error end end @doc "Update a terminal group" def update_group(%TerminalGroup{} = group, attrs) do group |> TerminalGroup.changeset(attrs) |> Repo.update() end @doc "Delete a terminal group (only if not system group)" def delete_group(%TerminalGroup{group_type: "system"}) do {:error, :cannot_delete_system_group} end def delete_group(%TerminalGroup{} = group) do Repo.delete(group) end # ============================================================================ # Group Rules Management # ============================================================================ @doc "List rules for a specific group" def list_group_rules(group_id) do TerminalGroupRule |> where([r], r.group_id == ^group_id) |> order_by([r], [asc: r.priority, desc: r.inserted_at]) |> Repo.all() end @doc "Create a new group rule" def create_group_rule(attrs) do result = %TerminalGroupRule{} |> TerminalGroupRule.changeset(attrs) |> Repo.insert() case result do {:ok, rule} -> # Trigger event-driven rule application TerminalEventDispatcher.rule_created(rule) {:ok, rule} error -> error end end @doc "Update a group rule" def update_group_rule(%TerminalGroupRule{} = rule, attrs) do old_rule = rule result = rule |> TerminalGroupRule.changeset(attrs) |> Repo.update() case result do {:ok, updated_rule} -> # Trigger event-driven rule re-application TerminalEventDispatcher.rule_updated(old_rule, updated_rule) {:ok, updated_rule} error -> error end end @doc "Delete a group rule" def delete_group_rule(%TerminalGroupRule{} = rule) do result = Repo.delete(rule) case result do {:ok, deleted_rule} -> # Trigger event-driven cleanup TerminalEventDispatcher.rule_deleted(deleted_rule) {:ok, deleted_rule} error -> error end end # ============================================================================ # Terminal Assignment Management # ============================================================================ @doc "Manually assign a terminal to a group" def assign_terminal_to_group(terminal_id, group_id, assigned_by) do attrs = TerminalGroupMembership.manual_assignment(terminal_id, group_id, assigned_by) %TerminalGroupMembership{} |> TerminalGroupMembership.changeset(attrs) |> Repo.insert() end @doc "Remove terminal from all groups (useful when terminal is updated/deleted)" def remove_terminal_from_all_groups(terminal_id) do TerminalGroupMembership |> where([m], m.terminal_id == ^terminal_id and m.is_active == true) |> Repo.update_all(set: [is_active: false, updated_at: DateTime.utc_now()]) end @doc "Apply a specific rule to all terminals" def apply_rule_to_all_terminals(rule) do terminals = Repo.all(TmsTerminal) apply_rule_to_terminals(rule, terminals) {:ok, length(terminals)} end @doc "Remove terminal from group" def remove_terminal_from_group(terminal_id, group_id) do TerminalGroupMembership |> where([m], m.terminal_id == ^terminal_id and m.group_id == ^group_id and m.is_active == true) |> Repo.update_all(set: [is_active: false, updated_at: DateTime.utc_now()]) end @doc "Get terminals in a specific group" def get_group_terminals(group_id, filters \\ %{}) do from(t in TmsTerminal, join: m in TerminalGroupMembership, on: m.terminal_id == t.id and m.group_id == ^group_id and m.is_active == true, preload: [group_memberships: m] ) |> apply_terminal_filters(filters) |> Repo.all() end @doc "Get groups for a specific terminal" def get_terminal_groups(terminal_id) do from(g in TerminalGroup, join: m in TerminalGroupMembership, on: m.group_id == g.id and m.terminal_id == ^terminal_id and m.is_active == true, preload: [group_memberships: m] ) |> Repo.all() end # ============================================================================ # Automatic Rule Processing # ============================================================================ @doc "Apply all active rules to all terminals" def apply_all_rules do rules = TerminalGroupRule |> TerminalGroupRule.active_rules() |> TerminalGroupRule.by_priority() |> Repo.all() terminals = Repo.all(TmsTerminal) Enum.each(rules, fn rule -> apply_rule_to_terminals(rule, terminals) end) {:ok, length(rules)} end @doc "Apply rules to a specific terminal (useful when terminal is updated)" def apply_rules_to_terminal(terminal_id) do terminal = Repo.get(TmsTerminal, terminal_id) if terminal do rules = TerminalGroupRule |> TerminalGroupRule.active_rules() |> TerminalGroupRule.by_priority() |> Repo.all() applied_count = Enum.reduce(rules, 0, fn rule, acc -> if TerminalGroupRule.matches_terminal?(rule, terminal) do create_rule_assignment(terminal.id, rule.group_id, rule.id) acc + 1 else acc end end) {:ok, applied_count} else {:error, :terminal_not_found} end end @doc "Remove all rule-based assignments and re-apply all rules" def refresh_all_rule_assignments do # Remove all rule-based assignments TerminalGroupMembership |> where([m], m.assignment_type == "rule_based") |> Repo.update_all(set: [is_active: false, updated_at: DateTime.utc_now()]) # Re-apply all rules apply_all_rules() end # ============================================================================ # Statistics and Analytics # ============================================================================ @doc "Get group statistics" def get_group_statistics do total_groups = Repo.aggregate(TerminalGroup, :count, :id) active_groups = Repo.aggregate(TerminalGroup.active_groups(), :count, :id) group_types = TerminalGroup |> group_by([g], g.group_type) |> select([g], {g.group_type, count(g.id)}) |> Repo.all() |> Enum.into(%{}) total_rules = Repo.aggregate(TerminalGroupRule, :count, :id) active_rules = Repo.aggregate(TerminalGroupRule.active_rules(), :count, :id) total_assignments = Repo.aggregate(TerminalGroupMembership, :count, :id) active_assignments = Repo.aggregate(TerminalGroupMembership.active_memberships(), :count, :id) assignment_types = TerminalGroupMembership |> TerminalGroupMembership.active_memberships() |> group_by([m], m.assignment_type) |> select([m], {m.assignment_type, count(m.id)}) |> Repo.all() |> Enum.into(%{}) %{ groups: %{ total: total_groups, active: active_groups, by_type: group_types }, rules: %{ total: total_rules, active: active_rules }, assignments: %{ total: total_assignments, active: active_assignments, by_type: assignment_types } } end @doc "Get terminals without any group assignment" def get_unassigned_terminals do subquery = from m in TerminalGroupMembership, where: m.is_active == true, select: m.terminal_id from(t in TmsTerminal, where: t.id not in subquery(subquery) ) |> Repo.all() end # ============================================================================ # Private Helper Functions # ============================================================================ defp apply_group_filters(query, filters) do Enum.reduce(filters, query, fn {"active_only", true}, q -> TerminalGroup.active_groups(q) {"group_type", type}, q when type != "" -> TerminalGroup.by_type(q, type) {"search", term}, q when term != "" -> search_term = "%#{term}%" from g in q, where: ilike(g.name, ^search_term) or ilike(g.description, ^search_term) _, q -> q end) end defp apply_terminal_filters(query, filters) do Enum.reduce(filters, query, fn {"status", status}, q when status != "" -> from t in q, where: t.status == ^status {"vendor", vendor}, q when vendor != "" -> from t in q, where: t.vendor == ^vendor {"model", model}, q when model != "" -> from t in q, where: t.model == ^model {"search", term}, q when term != "" -> search_term = "%#{term}%" from t in q, where: ilike(t.serial_number, ^search_term) or ilike(t.vendor, ^search_term) _, q -> q end) end defp preload_group_associations(query) do preload(query, [:group_rules, :child_groups, :parent_group]) end defp add_terminal_counts(query) do # For system groups, we need special handling subquery = from m in TerminalGroupMembership, where: m.is_active == true, group_by: m.group_id, select: %{group_id: m.group_id, count: count(m.terminal_id)} # Get total terminal count for system groups total_terminals = Repo.aggregate(TmsTerminal, :count, :id) from(g in query, left_join: tc in subquery(subquery), on: tc.group_id == g.id, select_merge: %{ terminal_count: fragment( "CASE WHEN ? = 'system' AND ? = 'All Terminals' THEN ? ELSE COALESCE(?, 0) END", g.group_type, g.name, ^total_terminals, tc.count ) } ) end defp add_terminal_count(group) do count = case {group.group_type, group.name} do {"system", "All Terminals"} -> # System "All Terminals" group should show total terminal count Repo.aggregate(TmsTerminal, :count, :id) _ -> # Regular groups use membership count TerminalGroupMembership |> where([m], m.group_id == ^group.id and m.is_active == true) |> Repo.aggregate(:count, :terminal_id) end %{group | terminal_count: count} end defp create_initial_rules(group, rules_data) when is_list(rules_data) do Enum.each(rules_data, fn rule_attrs -> attrs = Map.put(rule_attrs, "group_id", group.id) create_group_rule(attrs) end) end defp apply_rule_to_terminals(rule, terminals \\ nil) do terminals = terminals || Repo.all(TmsTerminal) Enum.each(terminals, fn terminal -> if TerminalGroupRule.matches_terminal?(rule, terminal) do create_rule_assignment(terminal.id, rule.group_id, rule.id) end end) end defp create_rule_assignment(terminal_id, group_id, rule_id) do # Check if assignment already exists existing = TerminalGroupMembership |> where([m], m.terminal_id == ^terminal_id and m.group_id == ^group_id and m.is_active == true) |> Repo.one() unless existing do attrs = TerminalGroupMembership.rule_based_assignment(terminal_id, group_id, rule_id) %TerminalGroupMembership{} |> TerminalGroupMembership.changeset(attrs) |> Repo.insert() end end defp remove_rule_assignments(rule_id) do TerminalGroupMembership |> where([m], m.rule_id == ^rule_id and m.is_active == true) |> Repo.update_all(set: [is_active: false, updated_at: DateTime.utc_now()]) end end