defmodule DaProductApp.QRProviders.Alipay do @behaviour DaProductApp.QRProvider require Logger alias HTTPoison alias DaProductAppWeb.Services.EventLogger # Add environment configuration @environment "simulator" # Change this to "prod" for production # Define URLs for different environments @urls %{ "simulator" => %{ generate: "https://open-sea-global.alipayplus.com/aps/api/v1/payments/pay" }, "prod" => %{ generate: "https://open-sea-global.alipayplus.com/aps/api/v1/payments/pay" } } # Define static configuration values #@client_id "SANDBOX_5YEV5L30082Z03013" @client_id "SANDBOX_5YEY443048J704194" @impl true def generate(provider_params) do # Prepare the request body request_body = prepare_request_body(%{ transaction_refid: provider_params[:transaction_refid], merchant_id: provider_params[:merchant_id], merchant_name: provider_params[:merchant_name], amount: provider_params[:amount], m_ref_num: provider_params[:m_ref_num], merchant_mcc: provider_params[:merchant_mcc], store_id: provider_params[:store_id], store_name: provider_params[:store_name], store_mcc: provider_params[:store_mcc], transaction_currency: provider_params[:transaction_currency] || "AED", settlement_currency: provider_params[:settlement_currency] || "USD" }) Logger.info("Alipay.generate - Sending request body to Alipay: #{inspect(request_body)}") # Log payment request event to Alipay with the actual request body (as JSON string) if provider_params[:m_ref_num] do {:ok, _} = EventLogger.store_event_and_log( "Payment Request to Alipay", %{ "provider_name" => "alipay", "request_body" => Jason.encode!(request_body) }, provider_params[:amount], provider_params[:m_ref_num], provider_params[:transaction_refid], nil, nil ) end # 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] store_id = provider_params[:store_id] store_name = provider_params[:store_name] store_mcc = provider_params[:merchant_mcc] merchant_mcc = provider_params[:merchant_mcc] amount = provider_params[:amount] additional_data = provider_params[:additional_data] device_id = provider_params[:device_id] m_ref_num = provider_params[:m_ref_num] transaction_currency = provider_params[:transaction_currency] settlement_currency = provider_params[:settlement_currency] # Shukria terminal data (available if needed) stid = provider_params[:stid] ptid = provider_params[:ptid] # provider_tid pmid = provider_params[:pmid] # provider_mid Logger.info("Entering Alipay.generate function") Logger.debug(""" Alipay.generate called with unified provider_params: transaction_refid: #{transaction_refid}, merchant_id: #{merchant_id}, merchant_name: #{merchant_name}, store_id: #{store_id}, store_name: #{store_name}, store_mcc: #{store_mcc}, merchant_mcc: #{merchant_mcc}, amount: #{amount}, additional_data: #{inspect(additional_data)}, device_id: #{device_id}, m_ref_num: #{m_ref_num}, transaction_currency: #{transaction_currency}, settlement_currency: #{settlement_currency}, stid: #{stid}, ptid: #{ptid}, pmid: #{pmid} """) # Prepare the request body request_body = prepare_request_body(%{ transaction_refid: transaction_refid, merchant_id: merchant_id, merchant_name: merchant_name, amount: amount, m_ref_num: m_ref_num, merchant_mcc: merchant_mcc, store_id: store_id, store_name: store_name, store_mcc: store_mcc, transaction_currency: transaction_currency || "AED", settlement_currency: settlement_currency || "USD" }) #Logger.debug("Prepared request body for Alipay.generate: #{inspect(request_body)}") # Prepare the headers headers = prepare_headers(request_body) # Send the request case register_qr(request_body, headers) do {:ok, response} -> # Logger.info("QR Code registered successfully in Alipay.generate: #{inspect(response)}") {:ok, response} {:error, reason} -> Logger.error("Failed to register QR Code in Alipay.generate: #{inspect(reason)}") {:error, reason} end end def inquire_payment(payment_id, m_ref_num \\ nil, transaction_refid \\ nil, transaction_id \\ nil, amount \\ nil) do path = "/aps/api/v1/payments/inquiryPayment" url = "https://open-sea-global.alipayplus.com#{path}" timestamp = DateTime.utc_now() |> DateTime.to_iso8601() body_map = %{"paymentId" => payment_id} body = Jason.encode!(body_map) payload = construct_payload("POST", path, @client_id, timestamp, body_map) signature = generate_signature(payload) headers = [ {"Content-Type", "application/json; charset=UTF-8"}, {"signature", "algorithm=RSA256, keyVersion=0, signature=#{signature}"}, {"client-id", @client_id}, {"request-time", timestamp} ] # Logger.info("Sending inquiry to Alipay for paymentId: #{payment_id}") # Log status_enquiry event - QR Middle Layer → Provider if m_ref_num do {:ok, _} = EventLogger.store_event_and_log( "Status Enquiry to Provider", %{ "provider_name" => "alipay", "payment_id" => payment_id, "m_ref_num" => m_ref_num }, amount, m_ref_num, transaction_refid, transaction_id, nil ) end # 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("Inquiry response: #{response_body}") decoded_response = Jason.decode!(response_body) # Log status_response event - Provider → QR Middle Layer if m_ref_num do {:ok, _} = EventLogger.store_event_and_log( "Status Response from Provider", %{ "provider_name" => "alipay", "payment_id" => payment_id, "m_ref_num" => m_ref_num, "response" => decoded_response }, amount, m_ref_num, transaction_refid, transaction_id, nil ) end {:ok, decoded_response} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Inquiry error: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Inquiry failed: #{inspect(reason)}") {:error, reason} end end def cancel_payment(payment_id) do path = "/aps/api/v1/payments/cancelPayment" url = "https://open-sea-global.alipayplus.com#{path}" timestamp = DateTime.utc_now() |> DateTime.to_iso8601() body_map = %{"paymentId" => payment_id} body = Jason.encode!(body_map) payload = construct_payload("POST", path, @client_id, timestamp, body_map) signature = generate_signature(payload) headers = [ {"Content-Type", "application/json; charset=UTF-8"}, {"signature", "algorithm=RSA256, keyVersion=0, signature=#{signature}"}, {"client-id", @client_id}, {"request-time", timestamp} ] Logger.info("Sending cancel request to Alipay for paymentId: #{payment_id}") Logger.info("Cancel request body: #{body}") # Logger.info("Cancel request headers: #{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("Cancel response from Alipay: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Cancel error from Alipay: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Cancel failed to connect to Alipay: #{inspect(reason)}") {:error, reason} end end def refund_payment(payment_id, refund_request_id, refund_value, refund_currency) do path = "/aps/api/v1/payments/refund" # <-- Correct endpoint url = "https://open-sea-global.alipayplus.com#{path}" timestamp = DateTime.utc_now() |> DateTime.to_iso8601() body_map = %{ "paymentId" => payment_id, "refundRequestId" => refund_request_id, "refundAmount" => %{ "value" => "#{refund_value}", "currency" => refund_currency } } body = Jason.encode!(body_map) payload = construct_payload("POST", path, @client_id, timestamp, body_map) signature = generate_signature(payload) headers = [ {"Content-Type", "application/json; charset=UTF-8"}, {"signature", "algorithm=RSA256, keyVersion=0, signature=#{signature}"}, {"client-id", @client_id}, {"request-time", timestamp} ] Logger.info("Sending refund request to Alipay for paymentId: #{payment_id}") Logger.info("Refund request body: #{body}") # Logger.info("Refund request headers: #{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("Refund response from Alipay: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Refund error from Alipay: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Refund failed to connect to Alipay: #{inspect(reason)}") {:error, reason} end end defp register_qr(params, headers) do Logger.info("Entering Alipay.register_qr function") url = get_url() Logger.info("Using #{@environment} environment with URL: #{url}") body = Jason.encode!(params) myts = System.system_time(:millisecond) body = body |> String.replace("{{myts}}", myts |> Integer.to_string()) #Logger.debug("Sending request to Alipay registerQR: #{url} with body: #{body} and headers: #{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 successful response from Alipay.register_qr: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Error response from Alipay.register_qr: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Failed to connect to Alipay.register_qr: #{inspect(reason)}") {:error, reason} end end # Helper function to prepare headers dynamically defp prepare_headers(request_body) do timestamp = DateTime.utc_now() |> DateTime.to_iso8601() path = "/aps/api/v1/payments/pay" payload = construct_payload("POST", path, @client_id, timestamp, request_body) signature = generate_signature(payload) [ {"Content-Type", "application/json; charset=UTF-8"}, {"signature", "algorithm=RSA256, keyVersion=0, signature=#{signature}"}, {"client-id", @client_id}, {"request-time", timestamp} ] end # Helper function to construct the payload defp construct_payload(_method, path, client_id, timestamp, request_body) do body = Jason.encode!(request_body) "POST #{path}\n#{client_id}.#{timestamp}.#{body}" end # Helper function to generate the RSA signature defp generate_signature(payload) do private_key = load_private_key() # Logger.debug("Payload to sign: #{payload}") # Decode the PEM-encoded private key case :public_key.pem_decode(private_key) do [entry] -> rsa_private_key = :public_key.pem_entry_decode(entry) # Sign the payload using RSA-SHA256 signature = :public_key.sign(payload, :sha256, rsa_private_key) # Encode the signature in Base64 URL format Base.url_encode64(signature, padding: false) [] -> Logger.error("Failed to decode private key: PEM format is invalid or missing") raise ArgumentError, "Invalid PEM format for private key" end end # Helper function to load the private key from file path in config defp load_private_key do key_path = Application.get_env(:da_product_app, :alipay_private_key_path) File.read!(key_path) end # Helper function to prepare the request body defp prepare_request_body(params) do transaction_currency = params.transaction_currency || "AED" settlement_currency = params.settlement_currency || "AED" amount = params.amount # Always use the original amount as-is (string or float) order_value = "#{amount}" payment_value = amount request_body = %{ "paymentNotifyUrl" => "http://40.120.104.51:4001/api/alipay/notify_payment", "paymentRequestId" => params.m_ref_num, "paymentFactor" => %{ "isInStorePayment" => "true", "isCashierPayment" => "true", "inStorePaymentScenario" => "OrderCode" }, "order" => %{ "referenceOrderId" => params.m_ref_num, "orderDescription" => "In store transaction", "orderAmount" => %{ "currency" => transaction_currency, "value" => order_value }, "merchant" => %{ "referenceMerchantId" => params.merchant_id, "merchantName" => params.merchant_name, "merchantMCC" => params.merchant_mcc, "store" => %{ "referenceStoreId" => params.store_id, "storeName" => params.store_name, "storeMcc" => params.merchant_mcc }, "merchantAddress" => %{ "region" => "AE", "city" => "xxx" } } }, "paymentAmount" => %{ "currency" => transaction_currency, "value" => payment_value }, "settlementStrategy" => %{ "settlementCurrency" => settlement_currency }, "paymentMethod" => %{ "paymentMethodType" => "CONNECT_WALLET", "paymentMethodId" => "281666021767541779176757" } } Logger.info("prepare_request_body - Final request body: #{inspect(request_body)}") request_body end # Helper function to get the URL based on environment defp get_url do Logger.info("Fetching generate URL for Alipay") @urls[@environment][:generate] end defp from_smallest_unit(amount_str, currency) do value = String.to_integer(amount_str) case currency do "JPY" -> value _ -> value / 100 end end end