defmodule DaProductApp.TerminalManagement.AutoPushService do @moduledoc """ Handles automatic parameter/config push when device reports missing versions. Scenario 1: When any version field (parameter_config, emv_config, keys_config, application) is NULL/EMPTY Strategy: - Find best matching template: Active first, then Default - Match by vendor+model - Push parameters from template using MQTTCommandBuilder for device-specific formats - For keys config, generate and store keys.json using KeysConfigService - Log push operations to parameter_push_logs - Send MQTT commands to device """ require Logger import Ecto.Query, only: [from: 2] alias DaProductApp.{Repo, MQTT} alias DaProductApp.ParameterManagement.{ParameterTemplate, ParameterPushLog, ParameterTemplateValue} alias DaProductApp.TerminalManagement.{TmsTerminal, MQTTCommandBuilder, KeysConfigService} @doc """ Triggers auto-push for a device with missing version info. Returns {:ok, message} or {:error, reason} """ def trigger_missing_version_push(serial_number, vendor, model) do Logger.info("AutoPushService: Triggering auto-push for #{serial_number} (#{vendor} #{model}) - missing version") # Find best template: Active first, then Default with template when not is_nil(template) <- find_best_template(vendor, model), terminal when not is_nil(terminal) <- get_terminal(serial_number) do # Push parameters from template push_template_parameters(serial_number, terminal, vendor, model, template) # Push device setup configs (EMV, Keys, Application) push_device_setup_configs(serial_number, terminal, vendor, model) Logger.info("AutoPushService: Auto-push completed for #{serial_number}") {:ok, "Auto-push triggered for missing versions"} else nil -> Logger.warning("AutoPushService: No template found for #{vendor} #{model}") {:error, "No template found for this device"} end end @doc """ Checks if any version is missing in the extracted versions map. """ def has_missing_versions?(versions) when is_map(versions) do Enum.any?(versions, fn {_key, value} -> is_nil(value) or value == "" end) end def has_missing_versions?(_), do: false # Private Functions defp find_best_template(vendor, model) do # First try to find Active template for specific vendor+model active_template = Repo.one( from t in ParameterTemplate, where: t.is_active == true and t.vendor == ^vendor and t.model == ^model, limit: 1 ) case active_template do nil -> # Fallback to Default template for this vendor+model Repo.one( from t in ParameterTemplate, where: t.is_active == true and t.is_default == true and t.vendor == ^vendor and t.model == ^model, limit: 1 ) tmpl -> tmpl end end defp get_terminal(serial_number) do Repo.get_by(TmsTerminal, serial_number: serial_number) end defp push_template_parameters(serial_number, terminal, vendor, model, template) do # Generate unique request ID request_id = System.unique_integer([:positive]) # Create push log entry push_log_attrs = %{ terminal_id: terminal.id, template_id: template.id, request_id: request_id, config_type: "parameter", device_vendor: vendor, device_model: model, trigger_reason: "missing_version", status: "pending", push_type: "full", log_time: DateTime.utc_now() } case Repo.insert(ParameterPushLog.changeset(%ParameterPushLog{}, push_log_attrs)) do {:ok, log} -> Logger.info("AutoPushService: Created push log #{log.id} for parameters (template: #{template.id})") # Build parameters map from template values template_values = Repo.all( from tv in ParameterTemplateValue, where: tv.template_id == ^template.id, preload: [:parameter_definition] ) parameters_map = Enum.into(template_values, %{}, fn tv -> {tv.parameter_definition.key, tv.value} end) # Update push log with parameters_sent Repo.update( ParameterPushLog.changeset(log, %{parameters_sent: parameters_map}) ) # Send MQTT command to push parameters send_mqtt_parameter_push(serial_number, vendor, model, request_id, parameters_map) {:error, reason} -> Logger.error("AutoPushService: Failed to create parameter push log: #{inspect(reason)}") end end defp push_device_setup_configs(serial_number, terminal, vendor, model) do config_types = ["emv_config", "keys_config", "application"] Enum.each(config_types, fn config_type -> case config_type do "keys_config" -> # Special handling for keys config - call RKI endpoint and generate keys.json push_keys_config(serial_number, terminal, vendor, model) _ -> # Regular config push (emv_config, application) push_regular_config(serial_number, terminal, vendor, model, config_type) end end) end defp push_keys_config(serial_number, terminal, vendor, model) do request_id = MQTTCommandBuilder.generate_request_id() case KeysConfigService.generate_and_store_keys_file(serial_number, model) do {:ok, download_url, file_path} -> # Create push log entry push_log_attrs = %{ terminal_id: terminal.id, request_id: request_id, config_type: "keys_config", device_vendor: vendor, device_model: model, file_path: file_path, file_size: get_file_size(file_path), checksum: calculate_checksum(file_path), trigger_reason: "missing_version", status: "pending", log_time: DateTime.utc_now() } case Repo.insert(ParameterPushLog.changeset(%ParameterPushLog{}, push_log_attrs)) do {:ok, _log} -> Logger.info("AutoPushService: Created push log for keys_config") # Update terminal's last_keys_update timestamp Repo.update(TmsTerminal.changeset(terminal, %{last_keys_update: DateTime.utc_now()})) # Send MQTT command with keys.json download URL send_mqtt_keys_config(serial_number, vendor, model, request_id, download_url) {:error, reason} -> Logger.error("AutoPushService: Failed to create keys_config push log: #{inspect(reason)}") end {:error, reason} -> Logger.error("AutoPushService: Failed to generate keys.json for #{serial_number}: #{inspect(reason)}") end end defp push_regular_config(serial_number, terminal, vendor, model, config_type) do request_id = MQTTCommandBuilder.generate_request_id() push_log_attrs = %{ terminal_id: terminal.id, request_id: request_id, config_type: config_type, device_vendor: vendor, device_model: model, trigger_reason: "missing_version", status: "pending", log_time: DateTime.utc_now() } case Repo.insert(ParameterPushLog.changeset(%ParameterPushLog{}, push_log_attrs)) do {:ok, _log} -> Logger.info("AutoPushService: Created push log for #{config_type}") send_mqtt_config_push(serial_number, vendor, model, config_type, request_id) {:error, reason} -> Logger.error("AutoPushService: Failed to create #{config_type} push log: #{inspect(reason)}") end end defp send_mqtt_keys_config(serial_number, vendor, model, request_id, download_url) do # Use MQTTCommandBuilder for consistent MQTT message format command_params = %{ "command_type" => "LOAD_KEYS", "request_id" => request_id, "url_path_from_download" => download_url } case MQTTCommandBuilder.build_command(model, command_params) do {:ok, mqtt_payload} -> product_key = get_product_key(vendor, model) topic = "/ota/#{product_key}/#{serial_number}/update" Logger.info("AutoPushService: Sending keys config to #{serial_number} on topic: #{topic}") case MQTT.publish(topic, mqtt_payload, qos: 1) do :ok -> Logger.info("AutoPushService: Keys config sent successfully to #{serial_number}") {:ok, _ref} -> Logger.info("AutoPushService: Keys config sent successfully to #{serial_number}") {:error, reason} -> Logger.error("AutoPushService: Failed to send keys config to #{serial_number}: #{inspect(reason)}") end {:error, reason} -> Logger.error("AutoPushService: Failed to build keys command: #{inspect(reason)}") end end defp send_mqtt_parameter_push(serial_number, vendor, model, request_id, parameters_map) do # Use MQTTCommandBuilder to build device-specific payload command_params = %{ "command_type" => "UPDATE_PARAMS", "request_id" => request_id, "parameters" => parameters_map } case MQTTCommandBuilder.build_command(model, command_params) do {:ok, mqtt_payload} -> product_key = get_product_key(vendor, model) topic = "/ota/#{product_key}/#{serial_number}/update" Logger.info("AutoPushService: Sending parameter push to #{serial_number} on topic: #{topic}") case MQTT.publish(topic, mqtt_payload, qos: 1) do :ok -> Logger.info("AutoPushService: Parameter push sent successfully to #{serial_number}") {:ok, _ref} -> Logger.info("AutoPushService: Parameter push sent successfully to #{serial_number}") {:error, reason} -> Logger.error("AutoPushService: Failed to send parameter push to #{serial_number}: #{inspect(reason)}") end {:error, reason} -> Logger.error("AutoPushService: Failed to build parameter command: #{inspect(reason)}") end end defp send_mqtt_config_push(serial_number, vendor, model, config_type, request_id) do # Use MQTTCommandBuilder to build device-specific payload command_params = %{ "command_type" => command_type_for_config(config_type), "request_id" => request_id, "config_type" => config_type } case MQTTCommandBuilder.build_command(model, command_params) do {:ok, mqtt_payload} -> product_key = get_product_key(vendor, model) topic = "/ota/#{product_key}/#{serial_number}/update" Logger.info("AutoPushService: Sending #{config_type} push to #{serial_number} on topic: #{topic}") case MQTT.publish(topic, mqtt_payload, qos: 1) do :ok -> Logger.info("AutoPushService: #{config_type} push sent successfully to #{serial_number}") {:ok, _ref} -> Logger.info("AutoPushService: #{config_type} push sent successfully to #{serial_number}") {:error, reason} -> Logger.error("AutoPushService: Failed to send #{config_type} push to #{serial_number}: #{inspect(reason)}") end {:error, reason} -> Logger.error("AutoPushService: Failed to build #{config_type} command: #{inspect(reason)}") end end defp command_type_for_config("emv_config"), do: "UPDATE_L3_CONFIG" defp command_type_for_config("keys_config"), do: "LOAD_KEYS" defp command_type_for_config("application"), do: "UPDATE_APPLICATION" defp command_type_for_config(_), do: "UPDATE_CONFIG" defp get_product_key(_vendor, _model) do # Default product key, can be enhanced to support multiple vendors/models Application.get_env(:da_product_app, :mqtt_product_key, "pFppbioOCKlo5c8E") end defp get_file_size(file_path) do case File.stat(file_path) do {:ok, stat} -> stat.size {:error, _} -> nil end end defp calculate_checksum(file_path) do case File.read(file_path) do {:ok, content} -> :crypto.hash(:sha256, content) |> Base.encode16(case: :lower) {:error, _} -> nil end end end