defmodule DaProductApp.QRProviders.Aani do @behaviour DaProductApp.QRProvider require Logger alias HTTPoison alias DaProductApp.Repo alias DaProductApp.Transactions.Transaction alias DaProductAppWeb.Services.EventLogger # Add environment configuration @environment "prod" # Change this to "simulator" for testing # Define URLs and credentials for different environments @config %{ "simulator" => %{ token_url: "https://demo.ctrmv.com/prasanna/aani/aanigenerateqr.php", register_url_base: "https://demo.ctrmv.com/prasanna/aani/statusQR.php", status_url: "https://demo.ctrmv.com/prasanna/aani/statusQR.php", channel_id: "ipp_qr_merchant", client_id: "myclientid", client_secret: "mysecretid" }, "prod" => %{ api_base_url: "https://appsrv-test-nps.mercury-pay.me/UAEIPP_Channel_Merchant_Payments_API/", token_url: "https://appsrv-test-nps.mercury-pay.me/UAEIPP_Jwt_Token_API/auth/get-token", register_url_base: "register-qr-code-channel", status_url: "check-status-qr-code-channel", channel_id: "ipp_qr_merchant", client_id: "myclientid", client_secret: "mysecretid" } } @impl true def generate(provider_params) do # Extract required parameters from the unified provider_params structure transaction_refid = provider_params[:transaction_refid] merchant_id = provider_params[:merchant_id] merchant_name = provider_params[:merchant_name] amount = provider_params[:amount] additional_data = provider_params[:additional_data] device_id = provider_params[:device_id] m_ref_num = provider_params[:m_ref_num] # Use unified parameter mappings bank_user_id = provider_params[:bank_user_id] merchant_tag = provider_params[:merchant_tag] # This is pmid from shukria_terminals cash_desk_id = provider_params[:cash_desk_id] # This is ptid from shukria_terminals Logger.info("Entering Aani.generate function") Logger.debug(""" Aani.generate called with unified provider_params: transaction_refid: #{transaction_refid}, merchant_id: #{merchant_id}, merchant_name: #{merchant_name}, amount: #{amount}, additional_data: #{inspect(additional_data)}, device_id: #{device_id}, m_ref_num: #{m_ref_num}, bank_user_id: #{bank_user_id}, merchant_tag (pmid): #{merchant_tag}, cash_desk_id (ptid): #{cash_desk_id} """) # Build the request body for QR registration register_body = %{ payment: %{ amount: provider_params[:amount], currency: provider_params[:transaction_currency] || "AED", reason: "Payment transaction", shopId: provider_params[:shop_id] || provider_params[:merchant_id], cashDeskId: provider_params[:cash_desk_id] }, paymentCategory: "01", paymentType: "PAG", qrCodeTransactionId: provider_params[:transaction_refid], mcc: provider_params[:merchant_mcc], merchantCompanyName: provider_params[:merchant_name], merchantCity: provider_params[:merchant_city] || "Dubai" } # Log payment request event to Aani with the actual request body if provider_params[:m_ref_num] do {:ok, _} = EventLogger.store_event_and_log( "Payment Request to Aani", %{ "provider_name" => "aani", "request_body" => register_body }, provider_params[:amount], provider_params[:m_ref_num], provider_params[:transaction_refid], nil, nil ) end # Step 1: Get JWT token case get_jwt_token() do {:ok, token} -> Logger.info("Successfully obtained JWT token") # Step 2: Register QR code with token, use unified parameter mappings case register_qr_with_token(token, %{ payment: %{ amount: amount, currency: provider_params[:transaction_currency] || "AED", # Use transaction_currency from provider_params reason: "Payment transaction", # More generic reason shopId: provider_params[:shop_id] || merchant_id, # Use shop_id (pmid from shukria_terminals) cashDeskId: cash_desk_id # Use ptid from shukria_terminals }, paymentCategory: "01", # Could be made dynamic if needed paymentType: "PAG", # Could be made dynamic if needed qrCodeTransactionId: transaction_refid, mcc: provider_params[:merchant_mcc] , # Use merchant_mcc from provider_params merchantCompanyName: merchant_name, merchantCity: provider_params[:merchant_city] || "Dubai" # Use merchant_city from provider_params }, bank_user_id, merchant_tag) do # Use pmid from shukria_terminals {:ok, response} -> Logger.info("QR Code registered successfully in Aani.generate: #{inspect(response)}") {:ok, response} {:error, reason} -> Logger.error("Failed to register QR Code in Aani.generate: #{inspect(reason)}") {:error, reason} end {:error, reason} -> Logger.error("Failed to get JWT token in Aani.generate: #{inspect(reason)}") {:error, reason} end end # Function to get JWT token defp get_jwt_token do Logger.info("Entering Aani.get_jwt_token function") config = @config[@environment] url = config[:token_url] Logger.info("Using #{@environment} environment with token URL: #{url}") headers = [ {"Content-Type", "application/x-www-form-urlencoded"} ] body = URI.encode_query(%{ "channel_id" => config[:channel_id], "client_id" => config[:client_id], "client_secret" => config[:client_secret] }) Logger.info("Sending token request to Aani: #{url}") case HTTPoison.post(url, body, headers) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Received token response from Aani: #{response_body}") case Jason.decode(response_body) do {:ok, %{"accessToken" => token}} -> {:ok, token} {:ok, %{"access_token" => token}} -> {:ok, token} {:ok, %{"token" => token}} -> {:ok, token} {:ok, parsed_response} -> Logger.error("Token not found in response: #{inspect(parsed_response)}") {:error, "Token not found in response"} {:error, reason} -> Logger.error("Failed to parse token response: #{inspect(reason)}") {:error, reason} end {:ok, %HTTPoison.Response{} = resp} -> Logger.error("Aani token API non-200 response: #{inspect(resp)}") Logger.error("Error response from Aani token API: #{resp.status_code} - #{resp.body}") {:error, %{status_code: resp.status_code, body: resp.body, headers: resp.headers}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Failed to connect to Aani token API: #{inspect(reason)}") {:error, reason} end end # Function to register QR code with JWT token defp register_qr_with_token(token, params, bank_user_id, merchant_tag) do Logger.info("Entering Aani.register_qr_with_token function") config = @config[@environment] api_base_url = config[:api_base_url] || "" register_url_base = config[:register_url_base] url = "#{api_base_url}#{register_url_base}/#{bank_user_id}/#{merchant_tag}/Mercury" Logger.info("Using #{@environment} environment with register URL: #{url}") headers = [ {"Content-Type", "application/json"}, {"Authorization", "Bearer #{token}"} ] body = Jason.encode!(params) Logger.info("Sending register QR request to Aani: #{url} with body: #{body}") # Increase timeout to 30 seconds (30000 ms) http_opts = [timeout: 30_000, recv_timeout: 30_000] case HTTPoison.post(url, body, headers, http_opts) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Received response from Aani.register_qr_with_token: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Error response from Aani.register_qr_with_token: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Failed to connect to Aani.register_qr_with_token: #{inspect(reason)}") {:error, reason} end end @doc """ Check QR code status using GET request with URL path parameters Returns {:ok, status_response} or {:error, reason} """ # Provide a 10-argument version for backward compatibility def check_status(qr_code_id, qr_code_transaction_id, requestor_id, bank_user_id, merchant_tag, transaction_id \\ nil, m_ref_num \\ nil, amount \\ nil, payment_id \\ nil, refrence_id \\ nil) do check_status(qr_code_id, qr_code_transaction_id, requestor_id, bank_user_id, merchant_tag, transaction_id, m_ref_num, amount, payment_id, refrence_id, 1) end # Main 11-argument version def check_status(qr_code_id, qr_code_transaction_id, requestor_id, bank_user_id, merchant_tag, transaction_id, m_ref_num, amount, payment_id, refrence_id, iteration) do Logger.info("Entering Aani.check_status function for qrCodeId: #{qr_code_id}") # First get JWT token case get_jwt_token() do {:ok, token} -> check_status_with_token(token, qr_code_id, qr_code_transaction_id, requestor_id, bank_user_id, merchant_tag, transaction_id, m_ref_num, amount, payment_id, refrence_id, iteration) {:error, reason} -> Logger.error("Failed to get JWT token for status check: #{inspect(reason)}") {:error, reason} end end @doc """ Cancel QR code for Aani provider. Params: - bank_user_id: string (16 chars max) - merchant_tag: string (16 chars max) - qr_code_id: string (16 chars max) - qr_code_transaction_id: string (50 chars max) - requestor_id: string (100 chars max, defaults to "GE083") Returns: {:ok, response} | {:error, reason} """ def cancel_payment(bank_user_id, merchant_tag, qr_code_id, qr_code_transaction_id, requestor_id \\ nil) do # requestor_id should be passed from controller as request_id (not hardcoded) Logger.info("=== AANI CANCEL PAYMENT DEBUG START ===") Logger.info("Entering Aani.cancel_payment function") Logger.info("Environment: #{@environment}") Logger.info("Input Parameters:") Logger.info(" - bank_user_id: '#{bank_user_id}' (length: #{String.length(bank_user_id)})") Logger.info(" - merchant_tag: '#{merchant_tag}' (length: #{String.length(merchant_tag)})") Logger.info(" - qr_code_id: '#{qr_code_id}' (length: #{String.length(qr_code_id)})") Logger.info(" - qr_code_transaction_id: '#{qr_code_transaction_id}' (length: #{String.length(qr_code_transaction_id)})") Logger.info(" - requestor_id (should be request_id from controller): '#{inspect(requestor_id)}' (type: #{inspect(requestor_id) |> Kernel.to_string() |> String.slice(0, 20)})") # If requestor_id is nil or empty, fallback to GE083, but log a warning requestor_id = cond do is_nil(requestor_id) or requestor_id == "" -> Logger.warn("[AANI CANCEL] requestor_id is nil or empty, falling back to 'GE083'. This should be set to request_id from controller!") "GE083" true -> requestor_id end Logger.info("[AANI CANCEL] Final requestor_id used in URL: '#{requestor_id}'") config = @config[@environment] api_base_url = config[:api_base_url] || "" Logger.info("API Base URL: #{api_base_url}") # Ensure /api/ is present before delete-qr-code-channel as per Aani documentation url = if String.ends_with?(api_base_url, "/api/") do "#{api_base_url}delete-qr-code-channel/#{bank_user_id}/#{merchant_tag}/#{qr_code_id}/#{qr_code_transaction_id}/#{requestor_id}" else base = String.trim_trailing(api_base_url, "/") "#{base}/delete-qr-code-channel/#{bank_user_id}/#{merchant_tag}/#{qr_code_id}/#{qr_code_transaction_id}/#{requestor_id}" end Logger.info("Complete Cancel URL: #{url}") Logger.info("URL Components breakdown:") Logger.info(" - Base: #{api_base_url}") Logger.info(" - Endpoint: delete-qr-code-channel") Logger.info(" - bankUserId: #{bank_user_id}") Logger.info(" - merchantTag: #{merchant_tag}") Logger.info(" - qrCodeId: #{qr_code_id}") Logger.info(" - qrCodeTransactionId: #{qr_code_transaction_id}") Logger.info(" - requestUserId: #{requestor_id}") # Step 1: Get JWT token Logger.info("Step 1: Getting JWT token...") case get_jwt_token() do {:ok, token} -> Logger.info("✅ JWT token obtained successfully") Logger.debug("Token (first 50 chars): #{String.slice(token, 0, 50)}...") headers = [ {"Authorization", "Bearer #{token}"} ] Logger.info("Request Headers: #{inspect(headers, pretty: true)}") Logger.info("Step 2: Sending DELETE request to Aani...") Logger.info("HTTP Method: DELETE") Logger.info("URL: #{url}") # Log what we're sending to Aani Logger.info("=== AANI CANCEL REQUEST ===") Logger.info("Request Method: DELETE") Logger.info("Request URL: #{url}") Logger.info("Request Headers: #{inspect(headers, pretty: true)}") Logger.info("Request Body: (empty - DELETE method)") Logger.info("===============================") # Send DELETE request as per Aani API specification # Increase timeout to 30 seconds (30000 ms) http_opts = [timeout: 30_000, recv_timeout: 30_000] case HTTPoison.delete(url, headers, http_opts) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("=== AANI CANCEL RESPONSE ===") Logger.info("Response Status: 200 OK") Logger.info("Response Body: #{response_body}") Logger.info("================================") Logger.info("✅ DELETE SUCCESS - Received 200 response from Aani.cancel_payment") case Jason.decode(response_body) do {:ok, decoded_response} -> Logger.info("✅ Successfully decoded DELETE JSON response") Logger.info("Decoded response: #{inspect(decoded_response, pretty: true)}") Logger.info("=== AANI CANCEL PAYMENT DEBUG END (DELETE SUCCESS) ===") {:ok, decoded_response} {:error, decode_error} -> Logger.error("❌ Failed to decode DELETE response") Logger.error("Decode error: #{inspect(decode_error)}") Logger.error("Raw response body: #{response_body}") Logger.info("=== AANI CANCEL PAYMENT DEBUG END (DELETE DECODE ERROR) ===") {:error, decode_error} end {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("=== AANI CANCEL ERROR RESPONSE ===") Logger.error("Response Status: #{status_code}") Logger.error("Response Body: #{response_body}") Logger.error("===================================") Logger.error("❌ DELETE method failed with status: #{status_code}") Logger.info("=== AANI CANCEL PAYMENT DEBUG END (DELETE HTTP ERROR) ===") {:error, %{status_code: status_code, body: response_body, method: "DELETE"}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("=== AANI CANCEL CONNECTION ERROR ===") Logger.error("Connection Error: #{inspect(reason)}") Logger.error("====================================") Logger.error("❌ DELETE request failed to connect") Logger.info("=== AANI CANCEL PAYMENT DEBUG END (DELETE CONNECTION ERROR) ===") {:error, reason} end {:error, reason} -> Logger.error("❌ Failed to get JWT token for cancel") Logger.error("Token error: #{inspect(reason)}") Logger.info("=== AANI CANCEL PAYMENT DEBUG END (TOKEN ERROR) ===") {:error, reason} end end @doc """ Refund payment for Aani provider. Params: - bank_user_id: string - merchant_tag: string - merchant_trx_id: string (refund transaction id) - opts: map with refund details (see below) opts must include: :tran_type, :requestor_id, :header (map), :body (map) """ def refund_payment(bank_user_id, merchant_tag, merchant_trx_id, opts) do Logger.info("Entering Aani.refund_payment function") config = @config[@environment] tran_type = opts[:tran_type] || "QR" requestor_id = opts[:requestor_id] || "GE083" api_base_url = config[:api_base_url] || "" register_url_base = config[:register_url_base] url = "#{api_base_url}refund/#{bank_user_id}/#{merchant_tag}/#{merchant_trx_id}/#{tran_type}/#{requestor_id}" body = Jason.encode!(opts[:body]) # Step 1: Get JWT token case get_jwt_token() do {:ok, token} -> headers = [ {"Content-Type", "application/json"}, {"Authorization", "Bearer #{token}"} ] ++ (opts[:headers] || []) Logger.info("Sending refund request to Aani: #{url} with body: #{body}") Logger.debug("Headers being sent to Aani refund: #{inspect(headers)}") # Increase timeout to 30 seconds (30000 ms) http_opts = [timeout: 30_000, recv_timeout: 30_000] case HTTPoison.post(url, body, headers, http_opts) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Received response from Aani.refund_payment: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Error response from Aani.refund_payment: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Failed to connect to Aani.refund_payment: #{inspect(reason)}") {:error, reason} end {:error, reason} -> Logger.error("Failed to get JWT token for refund: #{inspect(reason)}") {:error, reason} end end # Function to check status with JWT token using GET request defp check_status_with_token(token, qr_code_id, qr_code_transaction_id, requestor_id, bank_user_id, merchant_tag, transaction_id, m_ref_num, amount, payment_id, refrence_id, iteration) do config = @config[@environment] api_base_url = config[:api_base_url] || "" status_url_base = config[:status_url] status_url = "#{api_base_url}#{status_url_base}/#{bank_user_id}/#{merchant_tag}/#{qr_code_id}/#{qr_code_transaction_id}/#{requestor_id}" headers = [ {"Authorization", "Bearer #{token}"} ] Logger.info("Checking QR status for #{qr_code_id} at URL: #{status_url}") # Use transaction_id from argument if present, otherwise look up using qr_code_transaction_id tx_id = case transaction_id do nil -> transaction = Repo.get_by(Transaction, transaction_ref_number: qr_code_transaction_id) case transaction do nil -> nil t -> t.id end id -> id end # Log status enquiry event - QR Middle Layer → Provider {:ok, _} = EventLogger.store_event_and_log( "Status Enquiry to Provider", %{ "provider_name" => "aani", "qr_code_id" => qr_code_id, "qr_code_transaction_id" => qr_code_transaction_id, "bank_user_id" => bank_user_id, "merchant_tag" => merchant_tag, "requestor_id" => requestor_id, "transaction_id" => tx_id, "m_ref_num" => m_ref_num, "amount" => amount, "payment_id" => payment_id, "refrence_id" => refrence_id, "iteration" => iteration, "request_url" => status_url, "request_headers" => headers }, amount, m_ref_num, refrence_id, tx_id, payment_id ) case HTTPoison.get(status_url, headers) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Aani status API raw response: #{response_body}") Logger.info("Received status response from Aani.check_status: #{response_body}") decoded_response = Jason.decode!(response_body) # Log status response event - Provider → QR Middle Layer {:ok, _} = EventLogger.store_event_and_log( "Status Response from Provider", %{ "provider_name" => "aani", "qr_code_id" => qr_code_id, "qr_code_transaction_id" => qr_code_transaction_id, "bank_user_id" => bank_user_id, "merchant_tag" => merchant_tag, "requestor_id" => requestor_id, "transaction_id" => tx_id, "m_ref_num" => m_ref_num, "amount" => amount, "payment_id" => payment_id, "refrence_id" => refrence_id, "response" => decoded_response, "iteration" => iteration, "raw_response_body" => response_body }, amount, m_ref_num, refrence_id, tx_id, payment_id ) {:ok, decoded_response} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.info("Aani status API raw response: #{response_body}") Logger.error("Error response from Aani.check_status: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Failed to connect for status check in Aani.check_status: #{inspect(reason)}") {:error, reason} end end end