defmodule DaProductAppWeb.DeviceInitiateController do use DaProductAppWeb, :controller require Logger import Ecto.Query alias DaProductAppWeb.Validators.QRParamsValidator alias DaProductAppWeb.Services.EventLogger alias DaProductAppWeb.Handlers.ResponseHandler @default_provider "alipay" @spec initiate(any(), any()) :: Plug.Conn.t() def initiate(conn, params) do # If externalRefNumber is missing or blank, generate one params = if is_nil(params["externalRefNumber"]) or params["externalRefNumber"] in ["", nil] do Map.put(params, "externalRefNumber", generate_external_ref_number()) else params end amount = parse_amount(params["amount"]) refrence_id = params["externalRefNumber"] txnType = params["txnType"] case txnType do "QR" -> handle_qr_txn(conn, params, amount, refrence_id) "card" -> handle_card_txn(conn, params, amount, refrence_id) _ -> ResponseHandler.error_response( conn, :bad_request, "MERCURY_0000400", "Invalid transaction type" ) end end defp generate_external_ref_number do timestamp = DateTime.utc_now() |> DateTime.to_unix() random = :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower) "REF_#{timestamp}_#{random}" end defp handle_qr_txn(conn, params, amount, refrence_id) do with {:ok, _} <- EventLogger.store_event_and_log( "Initiate QR Txn Request", params, amount, nil, refrence_id, nil, params["username"] ), :ok <- QRParamsValidator.validate_params(params), {:ok, provider_record_id, pos_terminal_id, merchant_refrence_id} <- validate_provider(params["provider"], params["pushTo"]["deviceId"]), {:ok, ref} <- display_qr_code( conn, params, amount, provider_record_id, pos_terminal_id, merchant_refrence_id ) do ResponseHandler.send_success(conn, ref) else {:error, :event_log} -> ResponseHandler.error_response( conn, :internal_server_error, "MERCURY_0000503", "Failed to log initiate event" ) {:error, msg} when is_binary(msg) -> ResponseHandler.error_response(conn, :bad_request, "MERCURY_4000001", msg) {:error, :provider_key} -> ResponseHandler.error_response( conn, :bad_request, "MERCURY_6000001", "Failed to retrieve provider key." ) {:error, :event_log, code, msg} -> ResponseHandler.error_response(conn, :internal_server_error, code, msg) {:error, :unsupported_provider} -> ResponseHandler.error_response( conn, :bad_request, "MERCURY_0000000", "unsupported_provider" ) {:error, :qr_generation, reason} -> ResponseHandler.error_response( conn, :internal_server_error, "MERCURY_0000501", "Unexpected error: #{inspect(reason)}" ) {:error, :not_found, code, msg} -> ResponseHandler.error_response(conn, :service_unavailable, code, msg) {:error, :publish, reason} -> ResponseHandler.error_response( conn, :service_unavailable, "MERCURY_0000623", "Publish failed: #{inspect(reason)}" ) {:error, :provider_not_associated_with_device_id} -> ResponseHandler.error_response( conn, :provider_not_associated_with_device_id, "MERCURY_0000404", "Device not found" ) {:error, :invalid_amount} -> ResponseHandler.error_response( conn, :bad_request, "MERCURY_0000402", "Invalid amount provided" ) {:error, :invalid_params} -> ResponseHandler.error_response( conn, :bad_request, "MERCURY_0000400", "Invalid parameters provided" ) {:error, :provider_not_found} -> ResponseHandler.error_response(conn, :not_found, "MERCURY_0000405", "Provider not found") {:error, code, message} -> ResponseHandler.error_response(conn, :bad_request, code, message) _ -> ResponseHandler.error_response( conn, :internal_server_error, "MERCURY_5000000", "Unknown error." ) end end # Add this new private function defp validate_provider(provider, device_id) do provider = case provider do nil -> "11" "" -> "11" val -> val end provider_str = to_string(provider) Logger.info("Validating provider: #{provider_str} for device_id: #{device_id}") if provider_str == "" do {:error, "MERCURY_0000404", "Provider is missing"} else # Step 1: Get provider by provider_code case DaProductApp.Repo.get_by(DaProductApp.Providers.Provider, provider_code: provider_str) do nil -> {:error, "MERCURY_0000404", "Provider not found"} provider_record -> # Step 2: Get PosTerminal by serial_number and provider_id case DaProductApp.Repo.get_by(DaProductApp.PosTerminals.PosTerminal, serial_number: device_id #provider_id: provider_record.id ) do nil -> {:error, "MERCURY_0000404", "Device not found for this provider"} terminal -> # Continue with your existing logic (e.g., store/brand checks) case DaProductApp.Repo.get_by(DaProductApp.ShukriaTerminal, shukria_terminal_id: to_string(terminal.id), provider_id: to_string(provider_record.id) ) do nil -> {:error, "MERCURY_0000404", "Shukria Terminal not found"} shukria_terminal -> case DaProductApp.Repo.get(DaProductApp.Brands.Brand, shukria_terminal.shukria_mid) do nil -> {:error, "MERCURY_0000404", "Brand not found"} brand -> # Get all merchant_enrollments for this group_id merchant_enrollments = DaProductApp.Repo.all( from(me in DaProductApp.MerchantEnrollment, where: me.group_id == ^brand.group_id ) ) provider_names = Enum.map(merchant_enrollments, & &1.provider) # Find provider(s) where name in provider_names and provider_code == provider_str providers = DaProductApp.Repo.all( from(p in DaProductApp.Providers.Provider, where: p.name in ^provider_names and p.provider_code == ^provider_str ) ) if providers == [] do {:error, "MERCURY_0000404", "Provider not found or not configured properly"} else {:ok, provider_record.id, terminal.id, brand.merchant_reference_id} end end end end end end end defp handle_card_txn(conn, params, amount, refrence_id) do # TODO: Implement card transaction handling logic Logger.info("Processing card transaction for reference: #{refrence_id}") with {:ok, _} <- EventLogger.store_event_and_log( "Initiate Card Txn Request", params, amount, nil, refrence_id, nil, params["username"] ), :ok <- QRParamsValidator.validate_params(params), {:ok, ref} <- do_handle_card_txn(conn, params, amount) do ResponseHandler.send_success(conn, ref) else {:error, msg} when is_binary(msg) -> ResponseHandler.error_response(conn, :bad_request, "MERCURY_4000001", msg) {:error, _} -> ResponseHandler.error_response( conn, :internal_server_error, "MERCURY_5000000", "Failed to log event" ) end end # Handles card transactions. defp do_handle_card_txn(conn, params, amount) do Logger.info(""" Handling card transaction: - Reference: #{params["externalRefNumber"]} - Amount: #{amount} - Params: #{inspect(params)} """) device_id = params["pushTo"]["deviceId"] amount = parse_amount(params["amount"]) case DaProductApp.DeviceRegistry.is_online?(device_id) do online_status -> # Log device status event {:ok, _} = EventLogger.store_event_and_log( "Response From Device Online Status", %{ device_id: device_id, status: if(online_status, do: "online", else: "offline"), ts: System.system_time(:second) }, amount, nil, params["externalRefNumber"], nil, params["username"] ) case online_status do true -> # topic = "cmd/qr-device/#{device_id}" topic = "ota/pos/12345678/#{device_id}/sub" # temprory commented this # payload = Jason.encode!(%{amount: amount, ts: System.system_time(:second)}) payload = Jason.encode!(%{ amount: amount * 100, type: "card", phone: "93437121736", terminalid: "90080005", request_id: params["externalRefNumber"] }) Logger.info("Publish payload #{device_id}: #{inspect(payload)}") # Insert into CloudTransaction cloud_txn_attrs = %{ transaction_ref_number: params["externalRefNumber"], device_id: device_id, transaction_amount: amount # Add more fields as needed from params } DaProductApp.CloudTransactions.CloudTransaction.create_transaction(cloud_txn_attrs) {:ok, _} = EventLogger.store_event_and_log( "Request For Publish Txn", %{ topic: topic, qos: 1, amount: amount, ts: System.system_time(:second), device_id: device_id }, amount, nil, params["externalRefNumber"], nil, params["username"] ) case Tortoise.publish("phoenix_client_node_meg", topic, payload, qos: 1) do {:ok, ref} -> # Log successful publish response with reference {:ok, _} = EventLogger.store_event_and_log( "Response From Publish Txn", %{ device_id: device_id, amount: amount, topic: topic, status: "success", ts: System.system_time(:second), ref: inspect(ref), error: nil }, amount, nil, params["externalRefNumber"], nil, params["username"] ) # conn # |> put_status(200) # |> json(%{status: "sent", device: device_id, ref: inspect(ref)}) Logger.info("Publish successful to #{device_id} with ref: #{inspect(ref)}") # ResponseHandler.send_success(conn, ref: inspect(ref)) # Create new response array new_resp = %{ "transaction_refid" => params["externalRefNumber"], "mRefId" => nil } ResponseHandler.send_success(conn, new_resp) {:error, reason} -> # Log failed publish response {:ok, _} = EventLogger.store_event_and_log( "Response From Publish Txn", %{ device_id: device_id, amount: amount, topic: topic, status: "failed", ts: System.system_time(:second), error: inspect(reason) }, amount, nil, params["externalRefNumber"], nil, params["username"] ) Logger.error("Publish failed to #{device_id}: #{inspect(reason)}") # conn # |> put_status(503) # |> json(%{error: "publish_failed"}) ResponseHandler.error_response( conn, :internal_server_error, "MERCURY_503", "publish_failed" ) end false -> conn |> put_status(404) |> json(%{error: "device_offline"}) end end # TODO: Add actual card transaction processing logic here # For now, return a mock reference {:ok, "CARD_" <> params["externalRefNumber"]} end # Generates QR code, checks device online, logs event, publishes QR, sends response. defp display_qr_code( conn, params, amount, provider_record_id, pos_terminal_id, merchant_refrence_id ) do device_id = params["pushTo"]["deviceId"] Logger.info( "provider_record_id :#{provider_record_id}, pos_terminal_id: #{pos_terminal_id}, merchant_refrence_id: #{merchant_refrence_id}" ) # Insert into CloudTransaction cloud_txn_attrs = %{ transaction_ref_number: params["externalRefNumber"], device_id: device_id, transaction_amount: amount, provider_id: provider_record_id # Add more fields as needed from params } DaProductApp.CloudTransactions.CloudTransaction.create_transaction(cloud_txn_attrs) # Add event logging for QR Code request {:ok, _} = EventLogger.store_event_and_log( "Sending Request To Middle Layer", params, amount, nil, params["externalRefNumber"], nil, params["username"] ) # Prepare parameters for QRMiddleLayerController middle_layer_params = %{ "transaction_refid" => params["externalRefNumber"], "merchantId" => params["appKey"], "merchantName" => params["accountLabel"], "amount" => amount, "additionalData" => %{ "customerMobileNumber" => params["customerMobileNumber"], "externalRefNumber2" => params["externalRefNumber2"], "externalRefNumber4" => params["externalRefNumber4"], "externalRefNumbers" => params["externalRefNumbers"] }, "deviceId" => device_id, "smid" => merchant_refrence_id, "stid" => pos_terminal_id, "provider" => provider_record_id } Logger.info("QR Request - Starting process for device: #{device_id}") Logger.info("QR Request - Parameters: #{inspect(middle_layer_params)}") # Get authorization header from incoming request headers auth_header = case get_req_header(conn, "authorization") do [header | _] -> # Remove surrounding quotes if present String.trim(header, "\"") [] -> # Fallback to params if not in header, or use default config params["authorization"] || "" end Logger.info("Using Authorization header: #{inspect(auth_header)}") # Call QRMiddleLayerController's processTransaction response = case HTTPoison.post( # Replace with your actual API endpoint "http://demo.ctrmv.com:4004/api/processTransaction", Jason.encode!(middle_layer_params), [ {"Content-Type", "application/json"}, {"Authorization", auth_header} ] ) do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> case Jason.decode(body) do {:ok, decoded_response} -> {:ok, decoded_response["data"]} {:error, decode_error} -> Logger.error("Failed to decode response: #{inspect(decode_error)}") {:error, :decode_error, "Failed to decode response"} end {:ok, %HTTPoison.Response{status_code: 404, body: body}} -> case Jason.decode(body) do {:ok, %{"code" => code, "message" => message}} -> {:error, :not_found, code, message} _ -> {:error, :not_found, "MERCURY_0000404", "Resource not found"} end {:ok, %HTTPoison.Response{status_code: status_code, body: body}} -> case Jason.decode(body) do {:ok, %{"code" => code, "message" => message}} -> Logger.error("Error response with code #{code}: #{message}") {:error, :unexpected_response, code, message} _ -> Logger.error("Unexpected status code #{status_code}: #{inspect(body)}") {:error, :unexpected_response, "MERCURY_#{status_code}", "Unexpected response from server"} end {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("HTTP request failed: #{inspect(reason)}") {:error, :http_error, "Failed to make HTTP request"} end Logger.info( "Raw QR Middleware Response: #{inspect(response, pretty: true, limit: :infinity)}" ) case response do {:ok, qr_response} -> Logger.info(""" QR Success Response: - Device ID: #{device_id} - QR Code: #{inspect(qr_response["qrId"])} - Full Response: #{inspect(qr_response, pretty: true)} """) is_device_online = DaProductApp.DeviceRegistry.is_online?(device_id) Logger.info( "Device Status - #{device_id} is #{if is_device_online, do: "online", else: "offline"}" ) qrResponse = qr_response merchantId = qrResponse["mRefId"] transaction_id = qrResponse["transaction_id"] Logger.info("Merchant ID: #{merchantId}") with true <- is_device_online, {:ok, _} <- EventLogger.store_event_and_log( "Response Received From Middle layer", qrResponse, amount, merchantId, params["externalRefNumber"], transaction_id, params["username"] ), {:ok, _} <- DaProductApp.Activity.CustomEventsLog.update_transaction_id( params["externalRefNumber"], transaction_id ), {:ok, ref} <- publish_qr(device_id, amount, qr_response, params) do Logger.info("QR Published - Successfully published QR code with ref: #{inspect(ref)}") {:ok, ref} else false -> Logger.error("Device offline error for device_id: #{device_id}") {:error, :device_offline} {:error, reason} -> Logger.error("Publish error: #{inspect(reason)}") {:error, :publish, reason} end {:error, :not_found, code, message} -> Logger.error(""" QR Error - Not Found: - Device ID: #{device_id} - Error Code: #{code} - Message: #{message} """) {:error, :not_found, code, message} {:error, reason, message} -> Logger.error(""" QR Error: - Device ID: #{device_id} - Error Type: #{reason} - Message: #{message} """) {:error, :qr_generation, message} unexpected -> Logger.error(""" QR Unexpected Error: - Device ID: #{device_id} - Response: #{inspect(unexpected)} """) {:error, :qr_generation, "Unexpected response"} end end def status(conn, params) do Logger.info("Status request params: #{inspect(params)}") ref_number = params["transaction_ref_number"] Logger.info("Transaction reference number: #{inspect(ref_number)}") if is_nil(ref_number) do response = %{ code: "MERCURY_0000400", errorCode: "Txn_Not_CMPT", messageCode: "Missing transaction reference number", status: "failed", time: DateTime.utc_now() |> DateTime.to_unix() } json(conn, response) else case DaProductApp.CloudTransactions.CloudTransaction |> DaProductApp.Repo.get_by(transaction_ref_number: ref_number) do nil -> response = %{ code: "MERCURY_0000404", messageCode: "Transaction not found", status: "failed", errorCode: "Txn_Not_CMPT", time: DateTime.utc_now() |> DateTime.to_unix() } json(conn, response) transaction -> response = if transaction.status == "success" do %{ code: "200", messageCode: "Transaction successful", status: "success", time: DateTime.utc_now() |> DateTime.to_unix(), data: %{ transaction_ref_number: transaction.transaction_ref_number, status: transaction.status, transaction_amount: transaction.transaction_amount, transaction_id: transaction.id, merchant_refrence_number: transaction.m_ref_num, created_at: transaction.inserted_at } } else %{ code: "300", messageCode: "Transaction not successful", status: transaction.status, time: DateTime.utc_now() |> DateTime.to_unix(), data: %{ transaction_ref_number: transaction.transaction_ref_number, status: transaction.status, transaction_amount: transaction.transaction_amount, transaction_id: transaction.id, merchant_refrence_number: transaction.m_ref_num, created_at: transaction.inserted_at } } end json(conn, response) end end end # Publishes QR code payload to MQTT topic. defp publish_qr(device_id, amount, qr_response, params) do # Add event logging before publishing {:ok, _} = EventLogger.store_event_and_log( "Request For Publish Txn In MQTT", %{ device_id: device_id, amount: amount, qrcode: qr_response["qrId"], topic: "/ota/pFppbioOCKlo5c8E/#{device_id}/update", ts: System.system_time(:second) }, amount, qr_response["mRefId"], params["externalRefNumber"], qr_response["transaction_id"], params["username"] ) Logger.info("Publishing QR code to MQTT topic: cmd/qr-device/#{device_id}") topic = "/ota/pFppbioOCKlo5c8E/#{device_id}/update" # payload = Jason.encode!(%{amount: amount, qrcode: qr_response["qrCodeId"], ts: System.system_time(:second)}) datetime = NaiveDateTime.utc_now() |> NaiveDateTime.to_iso8601() |> String.replace(~r/[-:T]/, "") # Trim milliseconds to match expected format |> String.slice(0..13) # request_id = :crypto.strong_rand_bytes(8) |> Base.encode16() request_id = qr_response["transaction_refid"] payload = Jason.encode!(%{ money: amount, order_sn: qr_response["qrCodeId"], datetime: datetime, ctime: System.system_time(:second), request_id: request_id }) Logger.info(""" MQTT Publish Details: - Topic: #{topic} - Payload: #{payload} """) case Tortoise.publish("phoenix_client_node_meg", topic, payload, qos: 1) do {:ok, ref} = response -> # Log successful publish response {:ok, _} = EventLogger.store_event_and_log( "Response From Publish Txn In MQTT", %{ device_id: device_id, amount: amount, qrcode: qr_response["qrId"], topic: topic, ref: inspect(ref), status: "sent", ts: System.system_time(:second), error: nil, reason: nil }, amount, qr_response["mRefId"], params["externalRefNumber"], qr_response["transaction_id"], params["username"] ) Logger.info(""" MQTT Publish Success: - Reference: #{inspect(ref)} - Device ID: #{device_id} - Response: #{inspect(response)} """) {:ok, qr_response} {:error, reason} = error -> # Log failed publish response {:ok, _} = EventLogger.store_event_and_log( "Response From Publish Txn In MQTT", %{ device_id: device_id, amount: amount, qrcode: qr_response["qrId"], topic: topic, ref: nil, status: "unsent", ts: System.system_time(:second), error: inspect(error), reason: inspect(reason) }, amount, qr_response["mRefId"], params["externalRefNumber"], qr_response["transaction_id"], params["username"] ) Logger.error(""" MQTT Publish Error: - Device ID: #{device_id} - Error: #{inspect(error)} - Reason: #{inspect(reason)} """) {:error, reason} end end # Parses amount as integer or float, raises if invalid. defp parse_amount(amount) when is_integer(amount) or is_float(amount), do: amount defp parse_amount(amount) when is_binary(amount) do case Integer.parse(amount) do {int, ""} -> int _ -> case Float.parse(amount) do {flt, ""} -> flt _ -> raise "invalid_amount" end end end defp parse_amount(_), do: raise("invalid_amount") end