defmodule DaProductApp.QRProviders.Alipay do @behaviour DaProductApp.QRProvider require Logger alias HTTPoison # 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" @private_key """ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCJOxzYbsgCClFHEV/n1c2uCDuMQyMpw7VedsTFWOZCr/6b5t1Dz8PJXnsMXfrK4mH97UGOBLjGomyA7AszbsZzb2PG8OJeFdIlJGrq3FuOBXfE0WIzveB+H8X0GxCaMAZOM0EJliV8Zg0tPNYTjl9DIc9Zq6gK0u8CfG3IIPugJUPxA6uoWuDYKGq/zSpPazFt+AmLNN7J4fiZNGH/BRZcOfoZhuuhn56eMAcH6x+tub8AKVJLgTIx+KqRrCcsne3IN0ddpMAA745lvcLcvbYkDKGp2qNNtER1lDU/x6ao9h6uLV4CGPgFhOUTm+7lNvRtB11duWatYZY/NjSCGHMTAgMBAAECggEAL5BgiBellSd0UliQUC+HsYlC8nOWrXQa2dn6i5grfvO3INwc1tMdPh9UMM4mDcn3QubH8OxsCtTjHLAzlakQeZQjFiIJo6iWhK8hq7OivA/jGkGkcuCd/bkPiHMVBwwcM2CKa0MyTPKmIIbUgES5efAvCRp5DP9dPhRYjKP58uBUNXNETO+aCUjCZDeT1ciqhCMg53JQ2r299EKNhUcxBBH2OOdhhC2ofQhbpIqxyRG/mSE1uZEFdAmCmNfHYDjOM9PRkRwsWgwLnkyOhtp6ine73P+DG16xl5do/ejbSJ09lw64bxgfQwCN7/RuLq0rtVqujPaBoXkx4SAXU64hCQKBgQDa1BG7gLCI0A2GarBj2QDJbv+on/mYHZYeP53HU3RZl8SPTMA7qrfJ2PwHzvV1R3WGhvdYZVGQqv4r2+8q2X5kI1HA6ejAtNpLdskiVXqWlqUGLb9tWFTgS3oWBMzEVzV/kV6NiLUmWO8HI1GfyHIFPLSVWj2fYwCKCotYmqNFFwKBgQCgirXZTAM1dNGPpjk7S6PYsvUe/mB/bbTwSvQ6LXe2524Tuo6OvsdSGN+789Yd8Wxp2VqPtwYLS94H5Y2PBCEN6ftifXO9UdOJwDGq9bhiUSP16F+vxzZsyGm0a5ErIUviOPfjZhP3apE8UeKT3rZS7zNOFihLVMLEQP9KFUP3ZQKBgHy97UnUp02mRD9+rASPHHq3cre+Ufrbysp9e0S4FxhHgr4pg1/ABrrinXEaEiSD0sQYRgG26BMu1mtMGX90si8FT0JIVO0da18fXLLcxV/4iiQGihwcAW5GuFa677tw90c8KAlIh/NPORr5kDskeZLwswR8h6pHNnR6ZErjA/WLAoGAVAtp2eEuSNzoHGCz03PsybQeGOSole1T7PwAUTieVHVhrhhbKyV66WK2NgoXzMMns14jR9tT4bQM/2tQKU/LEiKtBMmSPslIifPAzLQom+fIgKLu/PG4b0iX9ejeLYsX081pEHXO/BahA8gGas0L++zXmgiFfbJY6C7yttDdLPUCgYEAxfr4KLo+n9QvSopKV04KrVyxHIgKPY9gdBWCE9RqmpYIt2ou29fY1/dh3Vt4EU5TDAN1LiX/zmoAfPAmuQJwYmWbnILczIvCGx0PUGMUn2qCo62qKMuFzdWjvyLT3HW7pbiiIJbD3PvTDnRM0O4+gS+mBaktmv7upz3qlPYh554= -----END PRIVATE KEY----- """ @impl true def generate(%{ 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: additional_data, device_id: device_id, m_ref_num: m_ref_num, transaction_currency: transaction_currency, # <--- add this settlement_currency: settlement_currency # <--- add this }) do Logger.info("Entering Alipay.generate function") Logger.debug(""" Alipay.generate called with: 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} """) # 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) 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}") case HTTPoison.post(url, body, headers) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Inquiry response: #{response_body}") {:ok, Jason.decode!(response_body)} {: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}") case HTTPoison.post(url, body, headers) do {:ok, %HTTPoison.Response{status_code: 200, body: response_body}} -> Logger.info("Cancel response: #{response_body}") {:ok, Jason.decode!(response_body)} {:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} -> Logger.error("Cancel error: #{status_code} - #{response_body}") {:error, %{status_code: status_code, body: response_body}} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("Cancel failed: #{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)}") case HTTPoison.post(url, body, headers) 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)}") case HTTPoison.post(url, body, headers) 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 defp load_private_key do # Logger.debug("Using hardcoded private key: #{@private_key}") @private_key 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 || "USD" amount = params.amount # Always use the original amount as-is (string or float) order_value = "#{amount}" payment_value = amount %{ "paymentNotifyUrl" => "http://demo.ctrmv.com:4001/api/alipay/notify_payment", "paymentRequestId" => "REQUEST_#{System.system_time(:millisecond)}", "paymentFactor" => %{ "isInStorePayment" => "true", "isCashierPayment" => "true", "inStorePaymentScenario" => "OrderCode" }, "order" => %{ "referenceOrderId" => params.m_ref_num, "orderDescription" => "Payment 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.store_mcc }, "merchantAddress" => %{ "region" => "JP", "city" => "xxx" } } }, "paymentAmount" => %{ "currency" => transaction_currency, "value" => payment_value }, "settlementStrategy" => %{ "settlementCurrency" => settlement_currency }, "paymentMethod" => %{ "paymentMethodType" => "CONNECT_WALLET", "paymentMethodId" => "281666021767541779176757" } } 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