defmodule DaProductApp.MerchantRegistration.NpciRegistration do @moduledoc """ NPCI (UPI) merchant registration and integration logic. Handles transformation and submission of onboarded merchants to external NPCI API. """ require Logger alias HTTPoison alias DaProductApp.Repo @doc """ Register onboarded merchant with external NPCI API """ def register(params) do Logger.info("NPCI registration called with params: #{inspect(params)}") # Extract partner_id from config partner_id = get_partner_id() # Transform onboarded merchant data to external API format external_api_payload = transform_to_external_format(params) Logger.info("Transformed payload for NPCI API: #{inspect(external_api_payload)}") # Call external NPCI API case call_external_api(external_api_payload, partner_id) do {:ok, response} -> Logger.info("NPCI registration successful: #{inspect(response)}") save_registration_response(params, response, "success") {:ok, response} {:error, reason} -> Logger.error("NPCI registration failed: #{inspect(reason)}") save_registration_response(params, reason, "failed") {:error, reason} end end @doc """ Inquire NPCI merchant registration status """ def inquire(params) do Logger.info("NPCI inquiry called with params: #{inspect(params)}") merchant_code = params["merchant_code"] partner_id = get_partner_id() base_url = get_api_base_url() url = "#{base_url}/partners/#{partner_id}/merchants/#{merchant_code}" headers = [ {"Accept", "application/json"}, {"Content-Type", "application/json"} ] Logger.debug("Calling NPCI inquiry GET: #{url}") case HTTPoison.get(url, headers) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("NPCI inquiry successful: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.warning("Non-200 inquiry response from NPCI: #{response_body}") {:error, %{status_code: status_code, response: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("HTTPoison error during inquiry: #{inspect(reason)}") {:error, "Network error: #{inspect(reason)}"} end end @doc """ Update NPCI merchant details """ def update_merchant(params) do Logger.info("NPCI merchant update called with params: #{inspect(params)}") merchant_code = params["merchant_code"] partner_id = get_partner_id() # Transform update data to external API format update_payload = transform_merchant_update_format(params) Logger.info("Transformed update payload for NPCI API: #{inspect(update_payload)}") # Call external NPCI API case call_update_api(update_payload, partner_id, merchant_code) do {:ok, response} -> Logger.info("NPCI merchant update successful: #{inspect(response)}") save_update_response(params, response, "success", "update") {:ok, response} {:error, reason} -> Logger.error("NPCI merchant update failed: #{inspect(reason)}") save_update_response(params, reason, "failed", "update") {:error, reason} end end @doc """ Update NPCI merchant status """ def update_merchant_status(params) do Logger.info("NPCI merchant status update called with params: #{inspect(params)}") merchant_code = params["merchant_code"] new_status = params["status"] partner_id = get_partner_id() if is_nil(new_status) do Logger.error("Status field is required for status update") {:error, "Status field is required"} else status_payload = %{"status" => new_status} Logger.info("Status update payload: #{inspect(status_payload)}") # Call external NPCI API case call_status_update_api(status_payload, partner_id, merchant_code) do {:ok, response} -> Logger.info("NPCI status update successful: #{inspect(response)}") save_update_response(params, response, "success", "status_update") {:ok, response} {:error, reason} -> Logger.error("NPCI status update failed: #{inspect(reason)}") save_update_response(params, reason, "failed", "status_update") {:error, reason} end end end # Transform onboarded merchant data to external NPCI API format (flat structure) defp transform_to_external_format(params) do Logger.info("Starting transformation from onboarded to NPCI external format") # Build flat payload matching external API expectations payload = %{ "merchant_code" => params["merchant_code"], "brand_name" => params["brand_name"], "merchant_vpa" => params["merchant_vpa"], "business_type" => params["business_type"] || "RETAIL", "merchant_type" => params["merchant_type"] || "SMALL", "corridor" => params["corridor"] || "DOMESTIC", "settlement_frequency" => params["settlement_frequency"] || "T+1", "settlement_account_number" => params["settlement_account_number"], "settlement_account_ifsc" => params["settlement_account_ifsc"], "max_transaction_limit" => params["max_transaction_limit"], "daily_transaction_limit" => params["daily_transaction_limit"] } # Add optional fields if present payload = if Map.has_key?(params, "tid"), do: Map.put(payload, "tid", params["tid"]), else: payload payload = if Map.has_key?(params, "mid"), do: Map.put(payload, "mid", params["mid"]), else: payload payload = if Map.has_key?(params, "legal_name"), do: Map.put(payload, "legal_name", params["legal_name"]), else: payload payload = if Map.has_key?(params, "merchant_genre"), do: Map.put(payload, "merchant_genre", params["merchant_genre"]), else: payload payload = if Map.has_key?(params, "contact_email"), do: Map.put(payload, "contact_email", params["contact_email"]), else: payload payload = if Map.has_key?(params, "contact_phone"), do: Map.put(payload, "contact_phone", params["contact_phone"]), else: payload payload = if Map.has_key?(params, "address"), do: Map.put(payload, "address", params["address"]), else: payload payload = if Map.has_key?(params, "city"), do: Map.put(payload, "city", params["city"]), else: payload payload = if Map.has_key?(params, "state"), do: Map.put(payload, "state", params["state"]), else: payload payload = if Map.has_key?(params, "pincode"), do: Map.put(payload, "pincode", params["pincode"]), else: payload Logger.info("Flat payload for NPCI API: #{inspect(payload)}") payload end # Call external NPCI API defp call_external_api(payload, partner_id) do base_url = get_api_base_url() url = "#{base_url}/partners/#{partner_id}/merchants" api_key = get_api_key() timeout = get_timeout() headers = [ {"Content-Type", "application/json"}, {"Authorization", "Bearer #{api_key}"}, {"Accept", "application/json"} ] body = Jason.encode!(payload) Logger.debug("Calling NPCI external API: #{url}") Logger.debug("Headers: #{inspect(headers)}") Logger.debug("Body: #{body}") case HTTPoison.post(url, body, headers, [timeout: timeout]) do {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} when status_code in 200..299 -> Logger.info("NPCI API request successful with status #{status_code}") case Jason.decode(response_body) do {:ok, decoded_response} -> {:ok, decoded_response} {:error, json_error} -> Logger.error("Failed to decode NPCI response: #{inspect(json_error)}") {:ok, %{"raw_response" => response_body}} end {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("NPCI API error - Status: #{status_code}, Body: #{response_body}") {:error, %{status_code: status_code, response: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Network error calling NPCI API: #{inspect(reason)}") {:error, "Network error: #{inspect(reason)}"} end end # Call external NPCI Update API (PUT) defp call_update_api(payload, partner_id, merchant_code) do update_base_url = get_update_api_base_url() url = "#{update_base_url}/partners/#{partner_id}/merchants/#{merchant_code}" api_key = get_api_key() timeout = get_timeout() headers = [ {"Content-Type", "application/json"}, {"Authorization", "Bearer #{api_key}"}, {"Accept", "application/json"} ] body = Jason.encode!(payload) Logger.debug("Calling NPCI update API via PUT: #{url}") Logger.debug("Headers: #{inspect(headers)}") Logger.debug("Body: #{body}") case HTTPoison.put(url, body, headers, [timeout: timeout]) do {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} when status_code in 200..299 -> Logger.info("NPCI update API request successful with status #{status_code}") case Jason.decode(response_body) do {:ok, decoded_response} -> {:ok, decoded_response} {:error, json_error} -> Logger.error("Failed to decode NPCI update response: #{inspect(json_error)}") {:ok, %{"raw_response" => response_body}} end {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("NPCI update API error - Status: #{status_code}, Body: #{response_body}") {:error, %{status_code: status_code, response: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Network error calling NPCI update API: #{inspect(reason)}") {:error, "Network error: #{inspect(reason)}"} end end # Call external NPCI Status Update API (PATCH) defp call_status_update_api(payload, partner_id, merchant_code) do update_base_url = get_update_api_base_url() url = "#{update_base_url}/partners/#{partner_id}/merchants/#{merchant_code}/status" api_key = get_api_key() timeout = get_timeout() headers = [ {"Content-Type", "application/json"}, {"Authorization", "Bearer #{api_key}"}, {"Accept", "application/json"} ] body = Jason.encode!(payload) Logger.debug("Calling NPCI status update API via PATCH: #{url}") Logger.debug("Headers: #{inspect(headers)}") Logger.debug("Body: #{body}") # Use HTTPoison.patch for PATCH request case HTTPoison.patch(url, body, headers, [timeout: timeout]) do {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} when status_code in 200..299 -> Logger.info("NPCI status update API request successful with status #{status_code}") case Jason.decode(response_body) do {:ok, decoded_response} -> {:ok, decoded_response} {:error, json_error} -> Logger.error("Failed to decode NPCI status update response: #{inspect(json_error)}") {:ok, %{"raw_response" => response_body}} end {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("NPCI status update API error - Status: #{status_code}, Body: #{response_body}") {:error, %{status_code: status_code, response: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Network error calling NPCI status update API: #{inspect(reason)}") {:error, "Network error: #{inspect(reason)}"} end end # Save update response to database defp save_update_response(merchant_data, response, status, update_type) do alias DaProductApp.Groups.Group merchant_code = merchant_data["merchant_code"] Logger.info("Saving NPCI #{update_type} response for merchant: #{merchant_code}") # Find the group by merchant code case Repo.get_by(Group, code: merchant_code) do nil -> Logger.warning("Group not found for merchant_code: #{merchant_code}") group -> # Update group params with update response updated_params = (group.params || %{}) |> Map.put("npci_#{update_type}", %{ "status" => status, "response" => response, "updatedAt" => DateTime.utc_now() |> DateTime.to_iso8601() }) changeset = Group.changeset(group, %{ params: updated_params }) case Repo.update(changeset) do {:ok, _updated_group} -> Logger.info("NPCI #{update_type} response saved for merchant: #{merchant_code}") {:error, error} -> Logger.error("Failed to save NPCI #{update_type} response: #{inspect(error)}") end end end # Save registration response to database defp save_registration_response(merchant_data, response, status) do alias DaProductApp.Groups.Group merchant_code = merchant_data["merchant_code"] Logger.info("Saving NPCI registration response for merchant: #{merchant_code}") # Find the group by merchant code case Repo.get_by(Group, code: merchant_code) do nil -> Logger.warning("Group not found for merchant_code: #{merchant_code}") group -> # Update group params with registration response updated_params = (group.params || %{}) |> Map.put("npci_registration", %{ "status" => status, "response" => response, "registeredAt" => DateTime.utc_now() |> DateTime.to_iso8601() }) changeset = Group.changeset(group, %{ params: updated_params }) case Repo.update(changeset) do {:ok, _updated_group} -> Logger.info("NPCI registration response saved for merchant: #{merchant_code}") {:error, error} -> Logger.error("Failed to save NPCI registration response: #{inspect(error)}") end end end # Transform merchant update payload to external format (flat structure) defp transform_merchant_update_format(params) do Logger.info("Transforming merchant update payload to external format") # Build flat payload with only fields that are present payload = %{} payload = if Map.has_key?(params, "brand_name"), do: Map.put(payload, "brand_name", params["brand_name"]), else: payload payload = if Map.has_key?(params, "merchant_vpa"), do: Map.put(payload, "merchant_vpa", params["merchant_vpa"]), else: payload payload = if Map.has_key?(params, "contact_email"), do: Map.put(payload, "contact_email", params["contact_email"]), else: payload payload = if Map.has_key?(params, "contact_phone"), do: Map.put(payload, "contact_phone", params["contact_phone"]), else: payload payload = if Map.has_key?(params, "address"), do: Map.put(payload, "address", params["address"]), else: payload payload = if Map.has_key?(params, "city"), do: Map.put(payload, "city", params["city"]), else: payload payload = if Map.has_key?(params, "state"), do: Map.put(payload, "state", params["state"]), else: payload payload = if Map.has_key?(params, "pincode"), do: Map.put(payload, "pincode", params["pincode"]), else: payload payload = if Map.has_key?(params, "max_transaction_limit"), do: Map.put(payload, "max_transaction_limit", params["max_transaction_limit"]), else: payload payload = if Map.has_key?(params, "daily_transaction_limit"), do: Map.put(payload, "daily_transaction_limit", params["daily_transaction_limit"]), else: payload payload = if Map.has_key?(params, "settlement_frequency"), do: Map.put(payload, "settlement_frequency", params["settlement_frequency"]), else: payload payload = if Map.has_key?(params, "settlement_account_number"), do: Map.put(payload, "settlement_account_number", params["settlement_account_number"]), else: payload payload = if Map.has_key?(params, "settlement_account_ifsc"), do: Map.put(payload, "settlement_account_ifsc", params["settlement_account_ifsc"]), else: payload payload = if Map.has_key?(params, "compliance_status"), do: Map.put(payload, "compliance_status", params["compliance_status"]), else: payload Logger.info("Flat update payload for NPCI API: #{inspect(payload)}") payload end # Get API base URL from config defp get_api_base_url do Application.get_env(:da_product_app, :npci_external_api, []) |> Enum.into(%{}) |> Map.get(:base_url, "http://demo.ctrmv.com:4041/api/v1") end # Get Update API base URL from config (typically different from registration API) defp get_update_api_base_url do Application.get_env(:da_product_app, :npci_external_api, []) |> Enum.into(%{}) |> Map.get(:update_base_url, "http://demo.ctrmv.com:4040/api/v1") end # Get partner ID from config defp get_partner_id do Application.get_env(:da_product_app, :npci_external_api, []) |> Enum.into(%{}) |> Map.get(:partner_id, "partner_default") end # Get API key from config defp get_api_key do Application.get_env(:da_product_app, :npci_external_api, []) |> Enum.into(%{}) |> Map.get(:api_key, "") end # Get timeout from config defp get_timeout do Application.get_env(:da_product_app, :npci_external_api, []) |> Enum.into(%{}) |> Map.get(:timeout, 30_000) end end