defmodule DaProductAppWeb.AlipayWebhookController do use DaProductAppWeb, :controller require Logger import Ecto.Query alias DaProductApp.Transactions.Transaction alias DaProductApp.Repo alias DaProductAppWeb.Services.EventLogger alias DaProductApp.Services.YspNotificationService alias DaProductApp.Utils.TxnRefGenerator alias DaProductApp.Settlements.MerchantBatchNumber @doc """ Handles Alipay payment notifications. """ def notify_payment(conn, params) do # Log notification received event log_notification_received(params) 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) # Fetch transaction for merchant_id, provider_id, device_id transaction = from(t in Transaction, where: t.payment_reference_id == ^payment_id, select: %{merchant_id: t.merchant_id, provider_id: t.provider_id, device_id: t.device_id} ) |> Repo.one() opts = case transaction do nil -> [] %{merchant_id: merchant_id, provider_id: provider_id, device_id: device_id} -> # Find pos_terminal by serial_number = device_id Logger.info("[YSP Debug] Looking up pos_terminal with serial_number: #{inspect(device_id)}") pos_terminal = from(p in DaProductApp.PosTerminals.PosTerminal, where: p.serial_number == ^device_id, select: %{id: p.id} ) |> Repo.one() Logger.info("[YSP Debug] pos_terminal lookup result: #{inspect(pos_terminal)}") shukria_terminal = case pos_terminal do nil -> nil %{id: stid} -> Logger.info("[YSP Debug] Looking up shukria_terminal with shukria_terminal_id: #{inspect(to_string(stid))}, provider_id: #{inspect(provider_id)}") result = from(s in DaProductApp.ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid) and s.provider_id == ^to_string(provider_id), select: %{ysp_mid: s.ysp_mid, ysp_tid: s.ysp_tid} ) |> Repo.one() Logger.info("[YSP Debug] shukria_terminal lookup result: #{inspect(result)}") result end Logger.info("[YSP Debug] Final shukria_terminal: #{inspect(shukria_terminal)}") case shukria_terminal do %{ysp_mid: ysp_merchant_id, ysp_tid: ysp_terminal_id} when not is_nil(ysp_merchant_id) and ysp_merchant_id != "" -> [ ysp_merchant_id: ysp_merchant_id, ysp_terminal_id: ysp_terminal_id, merchant_id: merchant_id, provider_id: provider_id, device_id: device_id ] _ -> [ merchant_id: merchant_id, provider_id: provider_id, device_id: device_id ] end end # Send YSP notification for successful payment, passing opts YspNotificationService.send_notification(payment_id, params, opts) #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_devteammiddlelayer", 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}") # Log notification sent event before sending response # Extract original params from the connection original_params = conn.params log_notification_sent(original_params, response) 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_devteammiddlelayer", 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) # Fetch the transaction to get bank_user_id transaction = from(t in Transaction, where: t.payment_reference_id == ^payment_id and t.status == "pending", select: %{id: t.id, bank_user_id: t.bank_user_id} ) |> Repo.one() batch_number = case transaction do %{bank_user_id: bank_user_id} when not is_nil(bank_user_id) -> batch_query = from b in MerchantBatchNumber, where: b.merchant_id == ^bank_user_id and b.provider_id == 1, order_by: [desc: b.inserted_at], limit: 1, select: b.batch_number Repo.one(batch_query) _ -> nil end 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, batch_number: batch_number ] ) |> 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" client_id = "SANDBOX_5YEY443048J704194" 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 key_path = Application.get_env(:da_product_app, :alipay_private_key_path) File.read!(key_path) 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 # Helper function to find transaction ID by payment_id defp find_transaction_id_by_payment_id(payment_id) do case from(t in Transaction, where: t.payment_reference_id == ^payment_id, select: t.id ) |> Repo.one() do nil -> Logger.info("[Alipay Webhook] No transaction found with payment_reference_id: #{payment_id}") nil transaction_id -> Logger.info("[Alipay Webhook] Found transaction_id: #{transaction_id} for payment_id: #{payment_id}") transaction_id end end defp log_notification_received(params) do try do payment_id = Map.get(params, "paymentId") payment_result = Map.get(params, "paymentResult", %{}) payment_amount = Map.get(params, "paymentAmount", %{}) qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") # Find the corresponding transaction in the database transaction_id = find_transaction_id_by_payment_id(payment_id) # Determine notification type based on payment result notification_type = cond do Map.get(payment_result, "resultStatus") == "S" -> "payment_success" Map.get(payment_result, "resultStatus") == "F" -> "payment_failure" true -> "payment_notification" end # Build parameters for event logging event_params = %{ "provider_name" => "alipay", "notification_type" => notification_type, "payment_id" => payment_id, "qr_code_id" => qr_code_transaction_id, "notification_data" => params, "status" => Map.get(payment_result, "resultCode", "UNKNOWN"), "transaction_status" => Map.get(payment_result, "resultStatus") } amount = case payment_amount do %{"value" => value} when is_binary(value) -> case Integer.parse(value) do {int_val, _} -> int_val / 100.0 _ -> 0.0 end %{"value" => value} when is_number(value) -> value / 100.0 _ -> 0.0 end # Get merchant reference number and reference ID merchant_ref_number = payment_id reference_id = qr_code_transaction_id || payment_id # Log the notification received event EventLogger.store_event_and_log( "Notification Received from Provider", event_params, amount, merchant_ref_number, reference_id, transaction_id, # Pass the actual transaction ID if found "system" ) Logger.info("[Alipay Webhook] Successfully logged notification received event for payment_id: #{payment_id}, transaction_id: #{transaction_id}") rescue error -> Logger.error("[Alipay Webhook] Failed to log notification received event: #{inspect(error)}") end end # Helper function to log notification sent event defp log_notification_sent(params, response) do try do payment_result = Map.get(params, "paymentResult", %{}) payment_amount = Map.get(params, "paymentAmount", %{}) payment_id = Map.get(params, "paymentId") qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") # Find the corresponding transaction in the database transaction_id = find_transaction_id_by_payment_id(payment_id) notification_type = case Map.get(payment_result, "resultStatus") do "S" -> "payment_success" "F" -> "payment_failure" _ -> "payment_status_update" end result_data = Map.get(response, "result", %{}) event_params = %{ "provider_name" => "alipay", "notification_type" => notification_type, "payment_id" => payment_id, "qr_code_id" => qr_code_transaction_id, "response_data" => response, "response_status" => Map.get(result_data, "resultStatus"), "response_message" => Map.get(result_data, "resultMessage"), "response_code" => Map.get(result_data, "resultCode") } amount = case payment_amount do %{"value" => value} when is_binary(value) -> case Integer.parse(value) do {int_val, _} -> int_val / 100.0 _ -> 0.0 end %{"value" => value} when is_number(value) -> value / 100.0 _ -> 0.0 end # Get merchant reference number and reference ID merchant_ref_number = payment_id reference_id = qr_code_transaction_id || payment_id # Log the notification sent event EventLogger.store_event_and_log( "Notification Response to Provider", event_params, amount, merchant_ref_number, reference_id, transaction_id, # Pass the actual transaction ID if found "system" ) Logger.info("[Alipay Webhook] Successfully logged notification response event for payment_id: #{payment_id}, transaction_id: #{transaction_id}") rescue error -> Logger.error("[Alipay Webhook] Failed to log notification sent event: #{inspect(error)}") end end @doc """ Sends QR payment notification to YSP server when transaction is successful. Only triggers when both resultCode is "SUCCESS" and resultStatus is "S". """ defp send_ysp_notification(payment_id, params) do # Extract payment result details payment_result = Map.get(params, "paymentResult", %{}) result_code = Map.get(payment_result, "resultCode") result_status = Map.get(payment_result, "resultStatus") # Only proceed if both conditions are met if result_code == "SUCCESS" and result_status == "S" do Logger.info("[YSP Notification] Sending notification for successful payment: #{payment_id}") # Delegate to YspNotificationService for notification logic DaProductApp.Services.YspNotificationService.send_notification(payment_id, params) else Logger.debug("[YSP Notification] Not sending notification - conditions not met. resultCode: #{result_code}, resultStatus: #{result_status}") 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://20.233.59.58:4000/api/payment/notify-success" # Use payment_id as the reference for Alipay, matching DB and notification logic 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