defmodule DaProductAppWeb.AaniWebhookController do use DaProductAppWeb, :controller require Logger import Ecto.Query alias DaProductApp.Transactions.Transaction alias DaProductApp.Repo alias DaProductApp.Services.YspNotificationService alias DaProductApp.Utils.TxnRefGenerator alias DaProductAppWeb.Services.EventLogger alias DaProductApp.Settlements.MerchantBatchNumber @doc """ Handles Aani payment notifications according to the Aani QR Payment Notification Guide. Supports: - QR Payment Status Notification (type: "qr_payment_status") - QR Payment Initiation Notification (type: "qr_payment_initiation") - QR Payment Failure Notification (type: "qr_payment_failure") - QR Payment Refund Notification (type: "qr_payment_refund") """ def notify_payment(conn, params) do # Log raw request headers and body for event traceability # Update transaction first, then fetch its ID for event logging payment_id = Map.get(params, "paymentId") qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") || payment_id # Improved transaction lookup: check for any transaction with matching ref numbers (any status) qr_id = Map.get(params, "qrId") transaction = cond do not is_nil(qr_code_transaction_id) -> from(t in Transaction, where: t.transaction_ref_number == ^qr_code_transaction_id, select: t ) |> Repo.one() not is_nil(payment_id) -> from(t in Transaction, where: t.transaction_ref_number == ^payment_id or t.payment_reference_id == ^payment_id, select: t ) |> Repo.one() not is_nil(qr_id) -> from(t in Transaction, where: t.transaction_ref_number == ^qr_id or t.payment_reference_id == ^qr_id, select: t ) |> Repo.one() true -> nil end # If found, update status and payload, do not create new row if transaction do Logger.info("[Aani Webhook] Existing transaction found for static QR, updating status instead of creating new row") # Determine status from params payment_result = Map.get(params, "paymentResult", %{}) notification_type = Map.get(params, "type") qr_status = Map.get(params, "qrCodeRequestStatus") result_status = cond do notification_type == "qr_payment_status" -> "success" notification_type == "qr_payment_initiation" -> "pending" notification_type == "qr_payment_failure" -> "failed" notification_type == "qr_payment_refund" -> "refunded" Map.get(payment_result, "resultStatus") == "S" -> "success" Map.get(payment_result, "resultStatus") == "F" -> "failed" qr_status == "EFF" -> "success" qr_status == "ATT" -> "pending" qr_status == "EXP" -> "failed" qr_status == "ANN" -> "failed" true -> "failed" end # Fetch batch_number from merchant_batch_numbers table for provider_id=3 and merchant_id=bank_user_id (using MerchantBatchNumber schema) bank_user_id = transaction.bank_user_id batch_number = case bank_user_id do nil -> nil _ -> batch_query = from b in MerchantBatchNumber, where: b.merchant_id == ^bank_user_id and b.provider_id == 3, order_by: [desc: b.inserted_at], limit: 1, select: b.batch_number Repo.one(batch_query) end update_attrs = %{ status: result_status, payload: params, settlement_date_time: Map.get(params, "paymentTime") || Map.get(params, "timestamp"), batch_number: batch_number } Transaction.changeset(transaction, update_attrs) |> Repo.update() transaction_id = transaction.id else # Also check for existing transactions by looking for pending aani transactions # that might match this notification (to prevent creating duplicates for dynamic QR) bank_user_id = Map.get(params, "bankUserId") merchant_tag = Map.get(params, "merchantTag") transaction = from(t in Transaction, where: t.provider_name == "aani" and t.status == "pending" and (t.merchant_id == ^bank_user_id or t.bank_user_id == ^bank_user_id) and t.merchant_tag == ^merchant_tag, order_by: [desc: t.inserted_at], limit: 1, select: t.id ) |> Repo.one() transaction_id = transaction # Only create new transaction if none found (true static QR case) if is_nil(transaction_id) do Logger.info("[Aani Webhook] No existing transaction found, creating new static QR transaction") # Generate m_ref_num for static QR m_ref_num = "sh_" <> (:crypto.strong_rand_bytes(10) |> Base.encode16() |> binary_part(0, 13)) payment_amount = Map.get(params, "paymentAmount", %{}) shop_id = Map.get(params, "shopId") bank_user_id = Map.get(params, "bankUserId") merchant_tag = Map.get(params, "merchantTag") # Find brand by merchant_reference_id and merchant_tag brand = from(b in DaProductApp.Brands.Brand, where: b.merchant_reference_id == ^bank_user_id and b.merchant_tag == ^merchant_tag, select: b.id ) |> Repo.one() # Get YSP merchant_id logic using dedicated ysp_mid column ysp_merchant_id = if brand do # Look up YSP MID from dedicated ysp_mid column in shukria_terminals where shukria_mid=brand_id # Convert brand ID to string since shukria_mid is a string field # Use limit(1) to handle multiple results, taking the first one case from(st in DaProductApp.ShukriaTerminal, where: st.shukria_mid == ^to_string(brand), select: st.ysp_mid, limit: 1 ) |> Repo.one() do nil -> Logger.debug("No terminal found for brand_id: #{brand}") bank_user_id # fallback to bank_user_id ysp_mid when not is_nil(ysp_mid) and ysp_mid != "" -> Logger.info("Found YSP MID: #{ysp_mid} for brand_id: #{brand}") ysp_mid _ -> Logger.debug("Terminal found but ysp_mid is empty for brand_id: #{brand}") bank_user_id # fallback to bank_user_id end else bank_user_id # fallback to bank_user_id if no brand found end # Find store by brand_id store_id = if brand do from(s in DaProductApp.Stores.Store, where: s.brand_id == ^brand, select: s.id ) |> Repo.one() else nil end # Get batch_number using bank_user_id as merchant_id batch_number = if bank_user_id do batch_query = from b in DaProductApp.BatchNumber, where: b.merchant_id == ^bank_user_id, order_by: [desc: b.id], limit: 1, select: b.batch_number Repo.one(batch_query) else nil end transaction_params = %{ transaction_ref_number: qr_code_transaction_id || payment_id, m_ref_num: m_ref_num, transaction_amount: case payment_amount do %{"value" => value} when is_binary(value) -> case Float.parse(value) do {num, _} -> num _ -> 0.0 end %{"value" => value} when is_number(value) -> value _ -> 0.0 end, status: "success", provider_name: "aani", provider_id: 3, device_id: Map.get(params, "cashDeskId"), merchant_id: ysp_merchant_id, # Use YSP MID from provider_id=4, fallback to bank_user_id additional_data: params, payment_reference_id: payment_id, payload: params, settlement_date_time: Map.get(params, "paymentTime"), bank_user_id: bank_user_id, merchant_tag: merchant_tag, pay_mode: "Static QR", batch_number: batch_number, store_id: store_id } case Repo.insert(Transaction.changeset(%Transaction{}, transaction_params)) do {:ok, new_txn} -> transaction_id = new_txn.id Logger.info("[Aani Webhook] Created new static QR transaction with ID: #{transaction_id}") {:error, changeset} -> Logger.error("[Aani Webhook] Failed to create static QR transaction: #{inspect(changeset.errors)}") transaction_id = nil end else Logger.info("[Aani Webhook] Found existing transaction with ID: #{transaction_id}, updating status") end end # <-- this closes the 'if is_nil(transaction_id)' block try do raw_headers = Enum.into(conn.req_headers, %{}) raw_body = params # Removed event logging for raw request rescue error -> Logger.error("[Aani Webhook] Failed to log raw request event: #{inspect(error)}") end log_notification_received(params) # Log the incoming request for debugging Logger.info("[Aani NotifyPayment] Received params: #{inspect(params)}") # Extract payment details based on Aani documentation notification_type = Map.get(params, "type") # qr_payment_status, qr_payment_initiation, etc. payment_result = Map.get(params, "paymentResult", %{}) # Contains resultCode, resultStatus, resultMessage # Standard identifiers across all notification types merchant_tag = Map.get(params, "merchantTag") # e.g., "UB776WH" bank_user_id = Map.get(params, "bankUserId") # e.g., "MERCURY_AANI123" qr_id = Map.get(params, "qrId") # QR code ID, e.g., "AANI987654321000000" # Different IDs based on notification type payment_id = Map.get(params, "paymentId") # For payment status notifications qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") # For tracking # Payment amounts based on notification type payment_amount = Map.get(params, "paymentAmount", %{}) # For successful payments settlement_amount = Map.get(params, "settlementAmount", %{}) # For successful payments initiated_amount = Map.get(params, "initiatedAmount", %{}) # For initiation failed_amount = Map.get(params, "failedAmount", %{}) # For failures refund_amount = Map.get(params, "refundAmount", %{}) # For refunds # Timestamps and additional details payment_time = Map.get(params, "paymentTime") # ISO8601 timestamp timestamp = Map.get(params, "timestamp") # Generic timestamp shop_id = Map.get(params, "shopId") # Shop identifier cash_desk_id = Map.get(params, "cashDeskId") # Terminal identifier refund_id = Map.get(params, "refundId") # For refund notifications failure_reason = Map.get(params, "failureReason") # For failure notifications # Internal app fields for device payments deviceid = Map.get(params, "deviceid") || Map.get(params, "deviceId") internalapp = Map.get(params, "internalapp") # Status codes from Aani documentation # According to section 2 (QR Payment Status Notification): # - QR Payment Status Notification uses "qr_payment_status" type with paymentResult.resultStatus="S" # # According to section 3 (Additional Notifications): # 3.1 - QR Payment Initiation uses "qr_payment_initiation" type (pending) # 3.2 - QR Payment Failure uses "qr_payment_failure" type (failed) # 3.3 - QR Payment Refund uses "qr_payment_refund" type (refunded) # # Status codes for QR payments in qrCodeRequestStatus field: # "EFF" - Request paid by the buyer # "ATT" - Request in pending status to be paid by the buyer # "EXP" - Request expired # "ANN" - QRCode canceled by Merchant qr_status = Map.get(params, "qrCodeRequestStatus") # Map notification type to our internal status result_status = cond do notification_type == "qr_payment_status" -> "S" # Success notification_type == "qr_payment_initiation" -> "pending" # Initiated notification_type == "qr_payment_failure" -> "F" # Failed notification_type == "qr_payment_refund" -> "refunded" # Refunded true -> # If no type specified, fall back to QR status codes or payment result status cond do qr_status == "EFF" -> "S" # Success - Request paid by buyer qr_status == "ATT" -> "pending" # Pending - Waiting to be paid qr_status == "EXP" -> "F" # Failed - Request expired qr_status == "ANN" -> "F" # Failed - Cancelled by merchant Map.get(payment_result, "resultStatus") == "S" -> "S" # Success from payment result Map.get(payment_result, "resultStatus") == "F" -> "F" # Failure from payment result true -> nil # No status could be determined end end # Update payment_result with the derived status if needed payment_result = if is_nil(Map.get(payment_result, "resultStatus")) && !is_nil(result_status) do Map.put(payment_result, "resultStatus", result_status) else payment_result end cond do deviceid && internalapp == "yes" -> handle_internal_payment(conn, params) payment_result == %{} && is_nil(qr_status) -> Logger.error("[Aani NotifyPayment] Payment result and status are empty") conn |> put_status(:ok) |> json(%{error: "Payment result is empty"}) true -> handle_notify_payment(conn, params, payment_result, payment_id, payment_amount) end end # Helper function to send YSP notification for successful payments @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 payment_result = Map.get(params, "paymentResult", %{}) result_code = Map.get(payment_result, "resultCode") result_status = Map.get(payment_result, "resultStatus") if result_code == "SUCCESS" and result_status == "S" do Logger.info("[YSP Notification] Sending notification for successful payment: #{payment_id}") 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 # Helper function to log notification received event 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") # Prioritize qr_code_transaction_id for transaction lookup transaction_id = cond do not is_nil(qr_code_transaction_id) -> from(t in Transaction, where: t.transaction_ref_number == ^qr_code_transaction_id, select: t.id ) |> Repo.one() not is_nil(payment_id) -> from(t in Transaction, where: t.transaction_ref_number == ^payment_id or t.payment_reference_id == ^payment_id, select: t.id ) |> Repo.one() true -> nil end notification_type = cond do Map.get(payment_result, "resultStatus") == "S" -> "payment_success" Map.get(payment_result, "resultStatus") == "F" -> "payment_failure" true -> "payment_notification" end event_params = %{ "provider_name" => "aani", "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 merchant_ref_number = payment_id reference_id = qr_code_transaction_id || payment_id EventLogger.store_event_and_log( "Notification Received from Provider", event_params, amount, merchant_ref_number, reference_id, transaction_id, "system" ) Logger.info("[Aani Webhook] Successfully logged notification received event for payment_id: #{payment_id}, transaction_id: #{transaction_id}") rescue error -> Logger.error("[Aani 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") # Prioritize qr_code_transaction_id for transaction lookup transaction_id = cond do not is_nil(qr_code_transaction_id) -> from(t in Transaction, where: t.transaction_ref_number == ^qr_code_transaction_id, select: t.id ) |> Repo.one() not is_nil(payment_id) -> from(t in Transaction, where: t.transaction_ref_number == ^payment_id or t.payment_reference_id == ^payment_id, select: t.id ) |> Repo.one() true -> nil end 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" => "aani", "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 merchant_ref_number = payment_id reference_id = qr_code_transaction_id || payment_id EventLogger.store_event_and_log( "Notification Response to Provider", event_params, amount, merchant_ref_number, reference_id, transaction_id, "system" ) Logger.info("[Aani Webhook] Successfully logged notification response event for payment_id: #{payment_id}, transaction_id: #{transaction_id}") rescue error -> Logger.error("[Aani Webhook] Failed to log notification sent event: #{inspect(error)}") end end defp handle_notify_payment(conn, params, payment_result, payment_id, payment_amount) do # Extract fields according to Aani QR Payment Notification Guide notification_type = Map.get(params, "type") settlement_amount = Map.get(params, "settlementAmount", %{}) # Payment result fields result_status = Map.get(payment_result, "resultStatus") # "S" for success result_code = Map.get(payment_result, "resultCode") # "SUCCESS" or other status result_message = Map.get(payment_result, "resultMessage") # "Transaction completed successfully" # Identification fields qr_id = Map.get(params, "qrId") # QR code ID merchant_tag = Map.get(params, "merchantTag") # Merchant identifier bank_user_id = Map.get(params, "bankUserId") # Bank identifier # Status and transaction details qr_code_status = Map.get(params, "qrCodeRequestStatus") # EFF, ATT, EXP, ANN qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") # Transaction ref payment_time = Map.get(params, "paymentTime") # ISO8601 timestamp failure_reason = Map.get(params, "failureReason") # For failures Logger.info("[Aani NotifyPayment] Payment Details: #{inspect(%{ notification_type: notification_type, payment_id: payment_id, qr_id: qr_id, qr_code_transaction_id: qr_code_transaction_id, merchant_tag: merchant_tag, bank_user_id: bank_user_id, result_status: result_status, result_code: result_code, result_message: result_message, qr_code_status: qr_code_status, payment_time: payment_time, failure_reason: failure_reason, payment_result: payment_result, payment_amount: payment_amount })}") # Try multiple sources to find a valid payment identifier # Per requirements, prioritize qrCodeTransactionId and bankUserId for transaction identification # qrCodeTransactionId matches our transaction_ref_number in the database payment_id = qr_code_transaction_id || payment_id || qr_id # Helper to build opts for YSP notification (similar to Alipay) build_ysp_opts = fn payment_id -> transaction = from(t in Transaction, where: t.transaction_ref_number == ^payment_id or t.payment_reference_id == ^payment_id, select: %{merchant_id: t.merchant_id, device_id: t.device_id, provider_id: t.provider_id} ) |> Repo.one() case transaction do nil -> [] %{merchant_id: merchant_id, device_id: device_id, provider_id: provider_id} -> # Find pos_terminal by serial_number = device_id pos_terminal = from(p in DaProductApp.PosTerminals.PosTerminal, where: p.serial_number == ^device_id, select: %{id: p.id} ) |> Repo.one() shukria_terminal = case pos_terminal do nil -> nil %{id: stid} -> 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() end 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 ] _ -> [ merchant_id: merchant_id, provider_id: provider_id, device_id: device_id ] end end end # Handle notification based on type and status case {payment_id, notification_type, result_status, Map.get(params, "qrCodeRequestStatus")} do {nil, _, _, _} -> Logger.error("[Aani NotifyPayment] Missing payment_id and qrCodeId") send_aani_response(conn) {_, "qr_payment_status", _, _} -> update_transaction_status(payment_id, "success", params) opts = build_ysp_opts.(payment_id) YspNotificationService.send_notification(payment_id, params, opts) notify_external_system(payment_id, payment_amount) send_aani_response(conn) {_, "qr_payment_initiation", _, _} -> update_transaction_status(payment_id, "pending", params) send_aani_response(conn) {_, "qr_payment_failure", _, _} -> update_transaction_status(payment_id, "failed", params) send_aani_response(conn) {_, "qr_payment_refund", _, _} -> update_transaction_status(payment_id, "refunded", params) send_aani_response(conn) {_, _, "S", _} -> update_transaction_status(payment_id, "success", params) opts = build_ysp_opts.(payment_id) YspNotificationService.send_notification(payment_id, params, opts) notify_external_system(payment_id, payment_amount) send_aani_response(conn) {_, _, "F", _} -> update_transaction_status(payment_id, "failed", params) send_aani_response(conn) {_, _, _, "EFF"} -> update_transaction_status(payment_id, "success", params) opts = build_ysp_opts.(payment_id) YspNotificationService.send_notification(payment_id, params, opts) notify_external_system(payment_id, payment_amount) send_aani_response(conn) {_, _, _, "ATT"} -> update_transaction_status(payment_id, "pending", params) send_aani_response(conn) {_, _, _, "EXP"} -> update_transaction_status(payment_id, "failed", params) send_aani_response(conn) {_, _, _, "ANN"} -> update_transaction_status(payment_id, "cancelled", params) send_aani_response(conn) {_, type, status, qr_status} -> Logger.warn("[Aani NotifyPayment] Unknown combination: type=#{type || "nil"}, result_status=#{status || "nil"}, qrCodeRequestStatus=#{qr_status || "nil"}") send_aani_response(conn) end end defp handle_internal_payment(conn, params) do deviceid = Map.get(params, "deviceid") |> to_string() payment_amount = Map.get(params, "paymentAmount", %{}) Logger.info("[Aani 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("[Aani Internal Payment] Payment Details: #{inspect(%{ device_id: deviceid, last_transaction_id: last_transaction, payment_amount: payment_amount })}") case last_transaction do nil -> Logger.error("[Aani Internal Payment] No transaction found for device: #{deviceid}") json(conn, %{status: "ACKNOWLEDGED"}) transaction_id -> datetime = formatted_datetime() topic = "/ota/pFppbioOCKlo5c8E/#{deviceid}/update" # Handle payment amount similar to Alipay currency = payment_amount["currency"] || "AED" amount_value = payment_amount["value"] || "0" # Convert amount to proper format if needed money = if is_binary(amount_value) do case Integer.parse(amount_value) do {int_value, _} -> :erlang.float_to_binary(int_value / 100.0, decimals: 2) :error -> amount_value end else "#{amount_value}" end payload = Jason.encode!(%{ broadcast_type: 1, money: money, biz_type: 1, datetime: datetime, ctime: System.system_time(:second), request_id: transaction_id }) Logger.info("[Aani Internal Payment] Publishing to topic: #{topic} with payload: #{payload}") # publish_result = Tortoise.publish("phoenix_client_node_devteammiddlelayer", topic, payload, qos: 1) # Logger.info("[Aani Internal Payment] Publish result: #{inspect(publish_result)}") json(conn, %{status: "ACKNOWLEDGED"}) end end defp update_transaction_status(payment_id, status, params) do current_time = DateTime.utc_now() |> DateTime.truncate(:second) # Extract timestamp from Aani notification if available settlement_time = case Map.get(params, "paymentTime") || Map.get(params, "timestamp") do nil -> current_time timestamp_str -> case DateTime.from_iso8601(timestamp_str) do {:ok, datetime, _} -> datetime _ -> current_time end end # Extract transaction identifiers from params qr_code_transaction_id = Map.get(params, "qrCodeTransactionId") bank_user_id = Map.get(params, "bankUserId") Logger.info("[Aani NotifyPayment] Updating transaction status: payment_id=#{payment_id}, qrCodeTransactionId=#{qr_code_transaction_id || "nil"}, bankUserId=#{bank_user_id || "nil"}, status=#{status}") # First try with qrCodeTransactionId which should match transaction_ref_number query1 = from(t in Transaction, where: t.transaction_ref_number == ^payment_id and t.status == "pending" ) # Try with qrCodeTransactionId as primary identifier result = query1 |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: settlement_time, updated_at: current_time ] ) # If no rows affected, try finding by payment_reference_id as fallback result = case result do {0, nil} -> query2 = from(t in Transaction, where: t.payment_reference_id == ^payment_id and t.status == "pending" ) query2 |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: settlement_time, updated_at: current_time ] ) other -> other end # If still no match and bankUserId is present, try using that in additional_data # Note: This assumes bankUserId might be stored in additional_data JSON field result = if {0, nil} == result && bank_user_id do Logger.info("[Aani NotifyPayment] Trying to update using bankUserId: #{bank_user_id}") # Try to find transactions where additional_data might contain bankUserId # This is a simplistic approach, you might need to adjust based on how additional_data is structured query_bank_id = from(t in Transaction, where: t.status == "pending" and t.merchant_id == ^bank_user_id ) query_bank_id |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: settlement_time, updated_at: current_time ] ) else result end # Finally try with m_ref_num if available in params result = case result do {0, nil} -> m_ref_num = Map.get(params, "m_ref_num") || Map.get(params, "mRefNum") if m_ref_num do query3 = from(t in Transaction, where: t.m_ref_num == ^m_ref_num and t.status == "pending" ) query3 |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: settlement_time, updated_at: current_time ] ) else # As a last resort, try to find most recent pending transaction Logger.info("[Aani NotifyPayment] No direct match, checking for recent pending transactions") query4 = from(t in Transaction, where: t.status == "pending" and t.provider_name == "aani", order_by: [desc: t.inserted_at], limit: 1 ) recent_pending = query4 |> Repo.all() if length(recent_pending) > 0 do Logger.info("[Aani NotifyPayment] Found recent pending transaction to update") ids = Enum.map(recent_pending, fn t -> t.id end) from(t in Transaction, where: t.id in ^ids ) |> Repo.update_all( set: [ status: status, payload: params, settlement_date_time: settlement_time, updated_at: current_time ] ) else {0, nil} end end other -> other end |> case do {1, nil} -> Logger.info("[Aani NotifyPayment] Successfully updated transaction status to #{status}") {0, nil} -> Logger.error("[Aani NotifyPayment] No pending transaction found with payment_id: #{payment_id}") # Get information about recent transactions for debugging recent_transactions = from(t in Transaction, order_by: [desc: t.updated_at], limit: 3, select: %{id: t.id, payment_reference_id: t.payment_reference_id, transaction_ref_number: t.transaction_ref_number, m_ref_num: t.m_ref_num, status: t.status} ) |> Repo.all() if length(recent_transactions) > 0 do Logger.info("[Aani NotifyPayment] Recent transactions: #{inspect(recent_transactions)}") end {n, nil} -> Logger.warn("[Aani NotifyPayment] Multiple pending transactions (#{n}) updated for payment_reference_id: #{payment_id}") end end defp notify_external_system(payment_id, payment_amount) do # First attempt to publish to topic if applicable publish_payment_success(payment_id, payment_amount) # Then notify external system notify_url = "http://20.233.59.58:4000/api/payment/notify-success" payload = %{ payment_id: payment_id, payment_amount: payment_amount } case HTTPoison.post(notify_url, Jason.encode!(payload), [{"Content-Type", "application/json"}]) do {:ok, %{status_code: 200}} -> Logger.info("[Aani NotifyPayment] Successfully notified payment success") {:ok, %{status_code: 404, body: body}} -> # Handle 404 Not Found specifically Logger.warn("[Aani NotifyPayment] External system returned 404: #{body}") {:ok, %{status_code: status_code, body: body}} -> # Handle any other non-200 status code Logger.warn("[Aani NotifyPayment] External system returned status #{status_code}: #{body}") {:error, error} -> Logger.error("[Aani NotifyPayment] Failed to notify payment success: #{inspect(error)}") end end defp publish_payment_success(payment_id, payment_amount) do # Extract qrCodeTransactionId and bankUserId from the payload if available qr_code_transaction_id = get_in(payment_amount, ["qrCodeTransactionId"]) || payment_id bank_user_id = get_in(payment_amount, ["bankUserId"]) # Log what we're looking for Logger.info("[Aani NotifyPayment] Looking for transaction with payment_id: #{payment_id}, qrCodeTransactionId: #{qr_code_transaction_id}") if bank_user_id, do: Logger.info("[Aani NotifyPayment] Also trying bankUserId: #{bank_user_id}") # Try to find transaction by transaction_ref_number first (should match qrCodeTransactionId) transaction = from(t in Transaction, where: t.transaction_ref_number == ^payment_id, select: %{id: t.id, device_id: t.device_id, m_ref_num: t.m_ref_num, transaction_ref_number: t.transaction_ref_number, merchant_id: t.merchant_id} ) |> Repo.one() # If not found, try by payment_reference_id transaction = if is_nil(transaction) do from(t in Transaction, where: t.payment_reference_id == ^payment_id, select: %{id: t.id, device_id: t.device_id, m_ref_num: t.m_ref_num, transaction_ref_number: t.transaction_ref_number, merchant_id: t.merchant_id} ) |> Repo.one() else transaction end # If bank_user_id is available and transaction still not found, try by merchant_id transaction = if is_nil(transaction) && bank_user_id do Logger.info("[Aani NotifyPayment] Trying to find transaction by bankUserId: #{bank_user_id}") from(t in Transaction, where: t.merchant_id == ^bank_user_id and t.status == "success", order_by: [desc: t.updated_at], limit: 1, select: %{id: t.id, device_id: t.device_id, m_ref_num: t.m_ref_num, transaction_ref_number: t.transaction_ref_number, merchant_id: t.merchant_id} ) |> Repo.one() else transaction end # If still not found, try by m_ref_num from recent successful transactions transaction = if is_nil(transaction) do # Try to find by m_ref_num pattern in recently successful transactions recent_transactions = from(t in Transaction, where: t.status == "success", order_by: [desc: t.updated_at], limit: 3, select: %{id: t.id, device_id: t.device_id, m_ref_num: t.m_ref_num, transaction_ref_number: t.transaction_ref_number} ) |> Repo.all() Logger.info("[Aani NotifyPayment] Recent successful transactions: #{inspect(recent_transactions)}") # Just take the most recent successful transaction as a best guess if length(recent_transactions) > 0 do hd(recent_transactions) else nil end else transaction end case transaction do nil -> Logger.error("[Aani NotifyPayment] No transaction found for payment_id: #{payment_id}") # As a fallback, try searching by full m_ref_num directly # This appears to be what's in the database (6D5F7E7E3A1982DD) # Try a more targeted query to find recent successful transaction with non-empty m_ref_num recent_transaction = from(t in Transaction, where: t.status == "success" and not is_nil(t.m_ref_num) and t.m_ref_num != "", order_by: [desc: t.updated_at], limit: 1, select: %{id: t.id, device_id: t.device_id, payment_reference_id: t.payment_reference_id, transaction_ref_number: t.transaction_ref_number, m_ref_num: t.m_ref_num} ) |> Repo.one() if recent_transaction do Logger.info("[Aani NotifyPayment] Found recent successful transaction with m_ref_num: #{inspect(recent_transaction)}") # Try to publish using this transaction as a fallback if recent_transaction.device_id do transaction_id = recent_transaction.id device_id = recent_transaction.device_id datetime = formatted_datetime() topic = "/ota/pFppbioOCKlo5c8E/#{device_id}/update" # Format money value properly currency = payment_amount["currency"] || "AED" value = payment_amount["value"] || "0" money = if is_binary(value) do case Integer.parse(value) do {int_value, _} -> :erlang.float_to_binary(int_value / 100.0, decimals: 2) :error -> value end else "#{value}" end payload = Jason.encode!(%{ broadcast_type: 1, money: money, biz_type: 1, datetime: datetime, ctime: System.system_time(:second), request_id: transaction_id }) Logger.info("[Aani NotifyPayment] Publishing to topic (fallback): #{topic} with payload: #{payload}") publish_result = Tortoise.publish("phoenix_client_node_devteammiddlelayer", topic, payload, qos: 1) Logger.info("[Aani NotifyPayment] Fallback publish result: #{inspect(publish_result)}") else Logger.warn("[Aani NotifyPayment] Found transaction but no device_id in fallback transaction") end else Logger.warn("[Aani NotifyPayment] No recent successful transactions found with m_ref_num") end %{id: transaction_id, device_id: device_id, m_ref_num: m_ref_num, transaction_ref_number: tx_ref} when not is_nil(device_id) -> Logger.info("[Aani NotifyPayment] Found transaction #{transaction_id} with device_id: #{device_id}, m_ref_num: #{m_ref_num}, transaction_ref_number: #{tx_ref}") datetime = formatted_datetime() topic = "/ota/pFppbioOCKlo5c8E/#{device_id}/update" # Format money value properly currency = payment_amount["currency"] || "AED" value = payment_amount["value"] || "0" money = if is_binary(value) do case Integer.parse(value) do {int_value, _} -> :erlang.float_to_binary(int_value / 100.0, decimals: 2) :error -> value end else "#{value}" end payload = Jason.encode!(%{ broadcast_type: 1, money: money, biz_type: 1, datetime: datetime, ctime: System.system_time(:second), request_id: transaction_id }) Logger.info("[Aani NotifyPayment] Publishing to topic: #{topic} with payload: #{payload}") # publish_result = Tortoise.publish("phoenix_client_node_devteammiddlelayer", topic, payload, qos: 1) # Logger.info("[Aani NotifyPayment] Publish result: #{inspect(publish_result)}") %{id: transaction_id, m_ref_num: m_ref_num, transaction_ref_number: tx_ref} -> Logger.info("[Aani NotifyPayment] Transaction found #{transaction_id} with m_ref_num: #{m_ref_num}, transaction_ref_number: #{tx_ref} but no device_id") _ -> Logger.info("[Aani NotifyPayment] Transaction found but no device_id available for MQTT publishing") end end defp formatted_datetime do NaiveDateTime.utc_now() |> NaiveDateTime.to_iso8601() |> String.replace(~r/[-:T]/, "") |> String.slice(0..13) end defp send_aani_response(conn) do # According to Aani documentation, respond with "ACKNOWLEDGED" status response = %{status: "ACKNOWLEDGED"} # Log notification sent event before sending response # Removed event logging for raw response original_params = conn.params log_notification_sent(original_params, response) json(conn, response) end end