defmodule DaProductAppWeb.AlipayWebhookController do use DaProductAppWeb, :controller require Logger import Ecto.Query alias DaProductApp.Transactions.Transaction alias DaProductApp.Repo @doc """ Handles Alipay payment notifications. """ def notify_payment(conn, params) do payment_result = Map.get(params, "paymentResult", %{}) payment_id = Map.get(params, "paymentId") deviceid = Map.get(params, "deviceid") internalapp = Map.get(params, "internalapp") cond do deviceid && internalapp == "yes" -> handle_internal_payment(conn, params) payment_result == %{} -> Logger.error("[Alipay NotifyPayment] Payment result is empty") json(conn, %{error: "Payment result is empty"}) true -> handle_notify_payment(conn, params, payment_result, payment_id) end end defp handle_notify_payment(conn, params, payment_result, payment_id) do settlement_quote = Map.get(params, "settlementQuote", %{}) payment_amount = Map.get(params, "paymentAmount", %{}) result_status = Map.get(payment_result, "resultStatus") Logger.info("[Alipay NotifyPayment] Payment Details: #{inspect(%{ payment_id: payment_id, result_status: result_status, payment_result: payment_result, settlement_quote: settlement_quote })}") case {payment_id, result_status} do {nil, _} -> Logger.error("[Alipay NotifyPayment] Missing payment_id") response = %{ "result" => %{ "resultCode" => "SUCCESS", "resultMessage" => "success", "resultStatus" => "S" } } body = Jason.encode!(response, pretty: false) headers = alipay_response_headers(response) Logger.info("[Alipay NotifyPayment] Response Headers: #{inspect(headers)}") Logger.info("[Alipay NotifyPayment] Response Body: #{body}") conn |> put_resp_headers(headers) |> send_resp(200, body) {_, nil} -> Logger.error("[Alipay NotifyPayment] Missing result_status") json(conn, %{error: "Missing result_status"}) {_, "S"} -> update_transaction_status(payment_id, "success", params) #publish_payment_success(payment_id, payment_amount) # Attempt to notify external system (non-critical operation) notify_external_system_async(payment_id, payment_amount) send_alipay_response(conn, payment_result) {_, "F"} -> update_transaction_status(payment_id, "failed", params) send_alipay_response(conn, payment_result) {_, unknown} -> Logger.warn("[Alipay NotifyPayment] Unknown result status: #{unknown}") send_alipay_response(conn, payment_result) end end defp publish_payment_success(payment_id, payment_amount) do transaction = from(t in Transaction, where: t.payment_reference_id == ^payment_id, select: %{id: t.id, device_id: t.device_id} ) |> Repo.one() case transaction do nil -> Logger.error("[Alipay NotifyPayment] No transaction found for payment_id: #{payment_id}") %{id: transaction_id, device_id: device_id} -> datetime = formatted_datetime() topic = "/ota/pFppbioOCKlo5c8E/#{device_id}/update" currency = payment_amount["currency"] || "AED" value = payment_amount["value"] || "0" money = :erlang.float_to_binary(from_smallest_unit(value, currency), decimals: 2) payload = Jason.encode!(%{ broadcast_type: 1, money: money, # <-- now in "100.00" format biz_type: 1, datetime: datetime, ctime: System.system_time(:second), request_id: transaction_id }) Logger.info("[Alipay NotifyPayment] Publishing to topic: #{topic} with payload: #{payload}") publish_result = Tortoise.publish("phoenix_client_node_devteam", topic, payload, qos: 1) Logger.info("[Alipay NotifyPayment] Publish result: #{inspect(publish_result)}") end end defp send_alipay_response(conn, payment_result) do response = %{ "result" => %{ "resultCode" => Map.get(payment_result, "resultCode", "SUCCESS"), "resultMessage" => Map.get(payment_result, "resultMessage", "success"), "resultStatus" => Map.get(payment_result, "resultStatus", "S") } } body = Jason.encode!(response, pretty: false) headers = alipay_response_headers(response) Logger.info("[Alipay NotifyPayment] Response Headers: #{inspect(headers)}") Logger.info("[Alipay NotifyPayment] Response Body: #{body}") conn |> put_resp_headers(headers) |> send_resp(200, body) end defp handle_internal_payment(conn, params) do deviceid = Map.get(params, "deviceid") |> to_string() payment_amount = Map.get(params, "paymentAmount", %{}) Logger.info("[Alipay Internal Payment] deviceid: #{inspect(deviceid)}") last_transaction = from(t in Transaction, where: t.device_id == ^deviceid, order_by: [desc: t.inserted_at], limit: 1, select: t.id ) |> Repo.one() Logger.info("[Alipay Internal Payment] Payment Details: #{inspect(%{ device_id: deviceid, last_transaction_id: last_transaction, payment_amount: payment_amount })}") case last_transaction do nil -> Logger.error("[Alipay Internal Payment] No transaction found for device: #{deviceid}") json(conn, %{error: "No transaction found for device"}) transaction_id -> datetime = formatted_datetime() topic = "/ota/pFppbioOCKlo5c8E/#{deviceid}/update" payload = Jason.encode!(%{ broadcast_type: 1, money: payment_amount["value"], biz_type: 1, datetime: datetime, ctime: System.system_time(:second), request_id: transaction_id }) Logger.info("[Alipay Internal Payment] Publishing to topic: #{topic} with payload: #{payload}") publish_result = Tortoise.publish("phoenix_client_node_devteam", topic, payload, qos: 1) Logger.info("[Alipay Internal Payment] Publish result: #{inspect(publish_result)}") json(conn, %{ resultCode: "SUCCESS", resultStatus: "S", resultMessage: "success" }) end end defp update_transaction_status(payment_id, status, params) do current_time = DateTime.utc_now() |> DateTime.truncate(:second) from(t in Transaction, where: t.payment_reference_id == ^payment_id and t.status == "pending" ) |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: current_time, updated_at: current_time ] ) |> case do {1, nil} -> Logger.info("[Alipay NotifyPayment] Successfully updated transaction status to #{status}") {0, nil} -> Logger.error("[Alipay NotifyPayment] No pending transaction found with payment_reference_id: #{payment_id}") {n, nil} -> Logger.warn("[Alipay NotifyPayment] Multiple pending transactions (#{n}) updated for payment_reference_id: #{payment_id}") end end defp alipay_response_headers(response_map) do client_id = "SANDBOX_5YEV5L30082Z03013" key_version = 1 algorithm = "RSA256" method = "POST" path = "/api/alipay/notify_payment" timestamp = DateTime.utc_now() |> DateTime.to_iso8601() body_string = Jason.encode!(response_map, pretty: false) signature_payload = "#{method} #{path}\n#{client_id}.#{timestamp}.#{body_string}" signature = generate_signature(signature_payload) [ {"content-type", "application/json; charset=UTF-8"}, {"signature", "algorithm=#{algorithm},keyVersion=#{key_version},signature=#{signature}"}, {"client-id", client_id}, {"response-time", timestamp} ] end defp generate_signature(payload) do private_key = load_private_key() case :public_key.pem_decode(private_key) do [entry] -> rsa_private_key = :public_key.pem_entry_decode(entry) signature = :public_key.sign(payload, :sha256, rsa_private_key) 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 defp load_private_key do """ -----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----- """ end defp put_resp_headers(conn, headers) do Enum.reduce(headers, conn, fn {k, v}, acc -> Plug.Conn.put_resp_header(acc, k, v) end) end defp formatted_datetime do NaiveDateTime.utc_now() |> NaiveDateTime.to_iso8601() |> String.replace(~r/[-:T]/, "") |> String.slice(0..13) end defp from_smallest_unit(amount_str, currency) do value = String.to_integer("#{amount_str}") case currency do "JPY" -> value _ -> value / 100 end end defp notify_external_system_async(payment_id, payment_amount) do # Run the notification in a separate process so it doesn't block the response Task.start(fn -> notify_url = "http://demo.ctrmv.com:4001/api/payment/notify-success" payload = %{ payment_id: payment_id, payment_amount: payment_amount } try do case HTTPoison.post(notify_url, Jason.encode!(payload), [{"Content-Type", "application/json"}], recv_timeout: 5000) do {:ok, %{status_code: 200}} -> Logger.info("[Alipay NotifyPayment] Successfully notified external system") {:ok, %{status_code: status_code, body: body}} -> Logger.warning("[Alipay NotifyPayment] External system returned non-200 status: #{status_code}") Logger.debug("[Alipay NotifyPayment] External system response body: #{String.slice(body, 0, 500)}") {:error, error} -> Logger.warning("[Alipay NotifyPayment] Failed to notify external system: #{inspect(error)}") end rescue exception -> Logger.warning("[Alipay NotifyPayment] Exception while notifying external system: #{inspect(exception)}") end end) end end