alias DaProductApp.PosTerminals.PosTerminal alias DaProductApp.ShukriaTerminal alias DaProductApp.Brands.Brand defmodule DaProductApp.Services.YspNotificationService do @moduledoc """ Service for handling YSP payment notifications with retry logic and proper logging. Features: - Retry logic with exponential backoff (max 3 attempts) - Database logging of all notification attempts - Merchant-specific configuration support - Automatic batch number generation """ require Logger import Ecto.Query alias DaProductApp.Repo alias DaProductApp.Transactions.Transaction alias DaProductApp.YspNotificationLog alias DaProductApp.YspConfiguration alias DaProductApp.BatchNumber alias DaProductApp.Utils.TxnRefGenerator @doc """ Sends YSP notification for a successful payment with retry logic. Only triggers when both resultCode is "SUCCESS" and resultStatus: is "S". """ def send_notification(payment_id, params, opts \\ []) 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") Logger.info("[YSP Notification] Processing payment: #{payment_id}, resultCode: #{result_code}, resultStatus: #{result_status}") Logger.debug("[YSP Notification] Received opts: #{inspect(opts)}") # 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}") # Get transaction details from database case get_transaction_details(payment_id) do {:ok, transaction_data} -> Logger.info("[YSP Notification] Found transaction data: #{inspect(transaction_data)}") # Get merchant configuration merchant_id = transaction_data.merchant_id || "default" config = YspConfiguration.get_config_for_merchant(merchant_id) Logger.info("[YSP Notification] Merchant config loaded for #{merchant_id}: #{inspect(config != nil)}") if config && config.is_active do Logger.info("[YSP Notification] Configuration is active, building payload...") # Build payload with dynamic batch number, pass opts case build_notification_payload(payment_id, params, transaction_data, config, opts) do {:ok, payload} -> Logger.info("[YSP Notification] Payload built successfully: #{inspect(payload)}") # Create notification log entry notification_attrs = %{ transaction_id: transaction_data.transaction_id, payment_id: payment_id, notification_url: config.notification_url, payload: payload, max_attempts: config.max_retry_attempts, status: :pending } case Repo.insert(YspNotificationLog.changeset(%YspNotificationLog{}, notification_attrs)) do {:ok, notification_log} -> Logger.info("[YSP Notification] Notification log created with ID: #{notification_log.id}") # Start async notification with retry logic Task.start(fn -> send_notification_with_retry(notification_log, config) end) {:ok, "Notification queued successfully"} {:error, changeset} -> Logger.error("[YSP Notification] Failed to create notification log: #{inspect(changeset)}") {:error, "Failed to create notification log"} end {:error, reason} -> Logger.error("[YSP Notification] Failed to build payload: #{reason}") {:error, reason} end else Logger.warn("[YSP Notification] No active configuration found for merchant: #{merchant_id}") {:error, "No active configuration found"} end {:error, reason} -> Logger.error("[YSP Notification] Could not get transaction details: #{reason}") {:error, reason} end else Logger.debug("[YSP Notification] Not sending notification - conditions not met. resultCode: #{result_code}, resultStatus: #{result_status}") {:ok, "Conditions not met for notification"} end end @doc """ Sends YSP refund notification for a successful refund. """ def send_refund_notification(payment_id, refund_data, opts \\ []) do Logger.info("[YSP Refund Notification] Processing refund for payment_id: #{payment_id}") Logger.debug("[YSP Refund Notification] Refund data: #{inspect(refund_data)}") Logger.debug("[YSP Refund Notification] Received opts: #{inspect(opts)}") # Get transaction details from database case get_transaction_details(payment_id) do {:ok, transaction_data} -> Logger.info("[YSP Refund Notification] Found transaction data: #{inspect(transaction_data)}") # Get merchant configuration merchant_id = transaction_data.merchant_id || "default" config = YspConfiguration.get_config_for_merchant(merchant_id) Logger.info("[YSP Refund Notification] Merchant config loaded for #{merchant_id}: #{inspect(config != nil)}") if config && config.is_active do Logger.info("[YSP Refund Notification] Configuration is active, building refund payload...") # Build refund payload case build_refund_payload(payment_id, refund_data, transaction_data, config, opts) do {:ok, payload} -> Logger.info("[YSP Refund Notification] Refund payload built successfully: #{inspect(payload)}") # Create notification log entry notification_attrs = %{ transaction_id: transaction_data.transaction_id, payment_id: payment_id, notification_url: config.notification_url, payload: payload, max_attempts: config.max_retry_attempts, status: :pending } case Repo.insert(YspNotificationLog.changeset(%YspNotificationLog{}, notification_attrs)) do {:ok, notification_log} -> Logger.info("[YSP Refund Notification] Notification log created with ID: #{notification_log.id}") # Start async notification with retry logic Task.start(fn -> send_notification_with_retry(notification_log, config) end) {:ok, "Refund notification queued successfully"} {:error, changeset} -> Logger.error("[YSP Refund Notification] Failed to create notification log: #{inspect(changeset)}") {:error, "Failed to create refund notification log"} end {:error, reason} -> Logger.error("[YSP Refund Notification] Failed to build refund payload: #{reason}") {:error, reason} end else Logger.warn("[YSP Refund Notification] No active configuration found for merchant: #{merchant_id}") {:error, "No active configuration found"} end {:error, reason} -> Logger.error("[YSP Refund Notification] Could not get transaction details: #{reason}") {:error, reason} end end @doc """ Sends YSP notification from polling results. """ def send_notification_from_polling(m_ref_num, inquiry_response, payment_result) do # Check if this is actually a success case with the right format result_code = if is_map(payment_result) do Map.get(payment_result, "resultCode", "SUCCESS") else "SUCCESS" end result_status = if is_map(payment_result) do Map.get(payment_result, "resultStatus") else "S" # For the "PAYMENT_SUCCESS" string case end # 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 polling: #{m_ref_num}") # Get transaction details from database case get_transaction_details_by_ref(m_ref_num) do {:ok, transaction_data} -> # Get merchant configuration merchant_id = transaction_data.merchant_id || "default" config = YspConfiguration.get_config_for_merchant(merchant_id) if config && config.is_active do # Build payload for polling result case build_polling_payload(m_ref_num, inquiry_response, payment_result, transaction_data, config) do {:ok, payload} -> # Create notification log entry notification_attrs = %{ transaction_id: transaction_data.transaction_id, payment_id: transaction_data.payment_reference_id, notification_url: config.notification_url, payload: payload, max_attempts: config.max_retry_attempts, status: :pending } case Repo.insert(YspNotificationLog.changeset(%YspNotificationLog{}, notification_attrs)) do {:ok, notification_log} -> # Start async notification with retry logic Task.start(fn -> send_notification_with_retry(notification_log, config) end) {:ok, "Notification queued successfully"} {:error, changeset} -> Logger.error("[YSP Notification] Failed to create notification log: #{inspect(changeset)}") {:error, "Failed to create notification log"} end {:error, reason} -> Logger.error("[YSP Notification] Failed to build polling payload: #{reason}") {:error, reason} end else Logger.warn("[YSP Notification] No active configuration found for merchant: #{merchant_id}") {:error, "No active configuration found"} end {:error, reason} -> Logger.error("[YSP Notification] Could not get transaction details: #{reason}") {:error, reason} end else Logger.debug("[YSP Notification] Not sending notification from polling - conditions not met. resultCode: #{result_code}, resultStatus: #{result_status}") {:ok, "Conditions not met for notification"} end end # Private Functions defp send_notification_with_retry(notification_log, config) do # Logger.info("[YSP Notification] Attempting to send notification ID: #{notification_log.id} to URL: #{notification_log.notification_url}") case make_http_request(notification_log.notification_url, notification_log.payload) do {:ok, status_code, response_body} when status_code in 200..299 -> Logger.info("[YSP Notification] Raw response from YSP: status=#{status_code}, body=#{inspect(response_body)}") # Success - update log and complete update_notification_log(notification_log, %{ status: :success, http_status_code: status_code, response_body: response_body, completed_at: DateTime.utc_now() }) Logger.info("[YSP Notification] Successfully sent notification ID: #{notification_log.id}, Response: #{response_body}") {:ok, status_code, response_body} -> Logger.info("[YSP Notification] Raw response from YSP: status=#{status_code}, body=#{inspect(response_body)}") # HTTP error - retry or fail Logger.warn("[YSP Notification] HTTP error #{status_code} for notification ID: #{notification_log.id}, Response: #{response_body}") handle_retry_or_fail(notification_log, config, "HTTP #{status_code}: #{response_body}") {:error, error} -> # Network/connection error - retry or fail error_message = inspect(error) Logger.error("[YSP Notification] Network error for notification ID: #{notification_log.id}, Error: #{error_message}") handle_retry_or_fail(notification_log, config, error_message) end end defp handle_retry_or_fail(notification_log, config, error_message) do if notification_log.attempt_number < notification_log.max_attempts do # Schedule retry next_attempt = notification_log.attempt_number + 1 retry_delay = calculate_retry_delay(next_attempt, config.retry_interval_seconds) next_retry_at = DateTime.add(DateTime.utc_now(), retry_delay, :second) update_notification_log(notification_log, %{ attempt_number: next_attempt, error_message: error_message, next_retry_at: next_retry_at }) Logger.info("[YSP Notification] Scheduling retry #{next_attempt}/#{notification_log.max_attempts} in #{retry_delay} seconds") # Schedule the retry Process.send_after(self(), {:retry_notification, notification_log.id, config}, retry_delay * 1000) else # Max attempts reached - mark as failed update_notification_log(notification_log, %{ status: :max_attempts_reached, error_message: error_message, completed_at: DateTime.utc_now() }) Logger.error("[YSP Notification] Max attempts reached. Notification failed permanently.") end end defp calculate_retry_delay(attempt_number, base_interval) do # Exponential backoff: base_interval * (2 ^ (attempt - 1)) # For attempt 2: base_interval * 2, for attempt 3: base_interval * 4 base_interval * :math.pow(2, attempt_number - 1) |> trunc() end defp make_http_request(url, payload) do headers = [{"Content-Type", "application/json"}] # Add hackney :insecure option to ignore SSL certificate errors options = [timeout: 30_000, recv_timeout: 30_000, hackney: [:insecure]] encoded_payload = Jason.encode!(payload) # Hardcoded notification URL for testing hardcoded_url = "https://10.211.3.73/mercuryapi/notify_payment" Logger.info("[YSP Notification] Notification URL (HARDCODED): #{hardcoded_url}") # Logger.info("[YSP Notification] Notification URL: #{url}") Logger.info("[YSP Notification] Making HTTP request to: #{hardcoded_url}") Logger.debug("[YSP Notification] Request payload: #{encoded_payload}") case HTTPoison.post(hardcoded_url, encoded_payload, headers, options) do {:ok, %{status_code: status_code, body: body}} -> Logger.info("[YSP Notification] HTTP response received - Status: #{status_code}") Logger.info("[YSP Notification] HTTP response body: #{inspect(body)}") {:ok, status_code, body} {:error, %HTTPoison.Error{reason: reason}} -> Logger.error("[YSP Notification] HTTP request failed - Reason: #{inspect(reason)}") {:error, reason} end end defp update_notification_log(notification_log, attrs) do notification_log |> YspNotificationLog.changeset(attrs) |> Repo.update!() end defp get_transaction_details(payment_id) do Logger.info("[YSP Notification][DB] Querying transactions by payment_reference_id OR transaction_ref_number: #{payment_id}") query = from t in Transaction, where: t.payment_reference_id == ^payment_id or t.transaction_ref_number == ^payment_id, select: %{ transaction_id: t.id, transaction_ref_number: t.transaction_ref_number, amount: t.transaction_amount, m_ref_num: t.m_ref_num, device_id: t.device_id, merchant_id: t.merchant_id, batch_number: t.batch_number, provider_id: t.provider_id, payment_reference_id: t.payment_reference_id, provider_name: t.provider_name, ysp_tid: t.ysp_tid } result = Repo.one(query) Logger.info("[YSP Notification][DB] transactions query result: #{inspect(result)}") case result do nil -> {:error, "Transaction not found"} transaction -> {:ok, transaction} end end defp get_transaction_details_by_ref(m_ref_num) do Logger.info("[YSP Notification][DB] Querying transactions by m_ref_num: #{m_ref_num}") query = from t in Transaction, where: t.m_ref_num == ^m_ref_num, select: %{ transaction_id: t.id, transaction_ref_number: t.transaction_ref_number, amount: t.transaction_amount, payment_reference_id: t.payment_reference_id, device_id: t.device_id, provider_name: t.provider_name, merchant_id: t.merchant_id, batch_number: t.batch_number, provider_id: t.provider_id, ysp_tid: t.ysp_tid } result = Repo.one(query) Logger.info("[YSP Notification][DB] transactions query result: #{inspect(result)}") case result do nil -> {:error, "Transaction not found"} transaction -> {:ok, transaction} end end defp build_notification_payload(payment_id, params, transaction_data, config, opts) do Logger.info("[YSP Notification] transaction_data: #{inspect(transaction_data)} (provider_id: #{Map.get(transaction_data, :provider_id) || Map.get(transaction_data, "provider_id")})") device_id = transaction_data.device_id Logger.info("[YSP Notification][DB] Querying pos_terminals by serial_number: #{device_id}") pos_terminal = from(p in PosTerminal, where: p.serial_number == ^device_id, select: %{id: p.id}) |> Repo.one() Logger.info("[YSP Notification][DB] pos_terminals query result: #{inspect(pos_terminal)}") provider_id = Map.get(transaction_data, :provider_id) || Map.get(transaction_data, "provider_id") Logger.info("[YSP Notification][DB] Querying shukria_terminals for YSP data with shukria_terminal_id=#{pos_terminal && pos_terminal.id} and provider_id=#{provider_id}") shukria_terminal_ysp = case pos_terminal do nil -> nil %{id: stid} when not is_nil(provider_id) -> Logger.info("[YSP Notification][DB] Executing: SELECT ysp_mid, shukria_mid FROM shukria_terminals WHERE shukria_terminal_id='#{stid}' AND provider_id='#{provider_id}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid) and s.provider_id == ^to_string(provider_id), select: %{shukria_mid: s.shukria_mid, ysp_mid: s.ysp_mid} ) result = Repo.one(query) Logger.info("[YSP Notification][DB] shukria_terminals (YSP) query result: #{inspect(result)}") result %{id: stid} -> Logger.warn("[YSP Notification] provider_id is missing, querying without provider filter for shukria_terminal_id='#{stid}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid), select: %{shukria_mid: s.shukria_mid, ysp_mid: s.ysp_mid} ) result = Repo.one(query) Logger.info("[YSP Notification][DB] shukria_terminals (YSP) query result: #{inspect(result)}") result end if is_nil(provider_id) do Logger.warn("[YSP Notification] provider_id is missing in transaction_data. Provider-specific shukria_terminals query will be skipped.") end Logger.info("[YSP Notification][DB] Querying shukria_terminals for provider-specific with shukria_terminal_id=#{pos_terminal && pos_terminal.id} and provider_id=#{provider_id}") shukria_terminal_provider = case pos_terminal do nil -> nil %{id: stid} when not is_nil(provider_id) -> Logger.info("[YSP Notification][DB] Executing: SELECT provider_tid FROM shukria_terminals WHERE shukria_terminal_id='#{stid}' AND provider_id='#{provider_id}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid) and s.provider_id == ^to_string(provider_id), select: %{provider_tid: s.provider_tid} ) result = Repo.one(query) Logger.info("[YSP Notification][DB] shukria_terminals (provider-specific) query result: #{inspect(result)}") result _ -> nil end Logger.info("[YSP Notification][DB] shukria_terminal_ysp response: #{inspect(shukria_terminal_ysp)}") Logger.info("[YSP Notification][DB] shukria_terminal_provider response: #{inspect(shukria_terminal_provider)}") Logger.info("[YSP Notification][DB] Querying Brand by shukria_mid: #{shukria_terminal_ysp && shukria_terminal_ysp.shukria_mid}") brand = case shukria_terminal_ysp do %{shukria_mid: smid} when not is_nil(smid) -> result = Repo.get(Brand, smid) Logger.info("[YSP Notification][DB] Brand query result: #{inspect(result)}") result _ -> nil end merchant_tag = brand && brand.merchant_tag bank_user_id = brand && brand.merchant_reference_id Logger.info("[YSP Notification][DB] Querying Store by brand_id: #{shukria_terminal_ysp && shukria_terminal_ysp.shukria_mid || (brand && brand.id)}") store_code = case shukria_terminal_ysp do %{shukria_mid: smid} when not is_nil(smid) -> query = from(s in DaProductApp.Store, where: s.brand_id == ^smid, select: s.code) result = Repo.one(query) Logger.info("[YSP Notification][DB] Store query result: #{inspect(result)}") result _ -> case brand do %{id: brand_id} -> query = from(s in DaProductApp.Store, where: s.brand_id == ^brand_id, select: s.code) result = Repo.one(query) Logger.info("[YSP Notification][DB] Store query result: #{inspect(result)}") result _ -> nil end end # Use ysp_tid and merchant_id directly from transaction_data merchant_id = transaction_data.merchant_id || (shukria_terminal_ysp && shukria_terminal_ysp.ysp_mid) terminal_id = transaction_data.ysp_tid || opts[:ysp_terminal_id] || config.terminal_id || device_id batch_number = Map.get(transaction_data, :batch_number) || Map.get(transaction_data, "batch_number") || "000001" payment_result = Map.get(params, "paymentResult", %{}) payment_amount = Map.get(params, "paymentAmount", %{}) settlement_amount = Map.get(params, "settlementAmount", payment_amount) m_ref_num = transaction_data.m_ref_num # Trim paymentId to last 16 characters for YSP (right to left) payment_id = transaction_data.payment_reference_id |> to_string() |> (fn s -> len = String.length(s) if len > 16, do: String.slice(s, len - 16, 16), else: s end).() qr_code_transaction_id = m_ref_num payment_time = case Map.get(params, "paymentTime") do nil -> DateTime.now!("Etc/UTC") |> DateTime.to_iso8601(:extended) val when is_binary(val) -> String.replace(val, " ", "T") val -> val end # Provider name from transaction_data, capitalize only first letter, rest lowercase provider_name = transaction_data[:provider_name] || transaction_data["provider_name"] || "Alipay" provider_name = case provider_name do <> -> String.upcase(<>) <> String.downcase(rest) _ -> provider_name end # Convert paymentAmount and settlementAmount to major units {major_payment_str, major_settlement_str} = cond do String.downcase(provider_name) == "aani" -> {payment_amount["value"], settlement_amount["value"]} String.downcase(provider_name) == "alipay" -> # Alipay sends in smallest unit, so divide by 100 payment_value = case payment_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end settlement_value = case settlement_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end major_payment = payment_value / 100 major_settlement = settlement_value / 100 major_payment_str = :erlang.float_to_binary(major_payment, decimals: 2) major_settlement_str = :erlang.float_to_binary(major_settlement, decimals: 2) {major_payment_str, major_settlement_str} true -> payment_value = case payment_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end settlement_value = case settlement_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end # For other providers, do not divide (assume already in major unit) major_payment_str = :erlang.float_to_binary(payment_value, decimals: 2) major_settlement_str = :erlang.float_to_binary(settlement_value, decimals: 2) {major_payment_str, major_settlement_str} end # Generate unique transaction reference number txn_ref_no = TxnRefGenerator.generate_txn_ref_no() payload = %{ "txnRefNo" => txn_ref_no, "type" => "qr_payment_sale", "provider" => provider_name, "merchantTag" => merchant_tag, "bankUserId" => bank_user_id, "merchant_id" => merchant_id, "terminal_id" => terminal_id, "batchNumber" => batch_number, "qrId" => qr_code_transaction_id, "paymentResult" => payment_result, "qrCodeTransactionId" => qr_code_transaction_id, "paymentId" => payment_id, # trimmed to 16 chars "paymentTime" => payment_time, "shopId" => store_code, "cashDeskId" => shukria_terminal_provider && shukria_terminal_provider.provider_tid, "paymentAmount" => %{ "value" => major_payment_str, "currency" => payment_amount["currency"] || "AED" }, "settlementAmount" => %{ "value" => major_settlement_str, "currency" => settlement_amount["currency"] || "AED" } } Logger.info("[YSP Notification] Final payload to YSP: #{inspect(payload)}") {:ok, payload} end defp build_refund_payload(payment_id, refund_data, transaction_data, config, opts) do Logger.info("[YSP Refund Notification] transaction_data: #{inspect(transaction_data)} (provider_id: #{Map.get(transaction_data, :provider_id) || Map.get(transaction_data, "provider_id")})") device_id = transaction_data.device_id Logger.info("[YSP Refund Notification][DB] Querying pos_terminals by serial_number: #{device_id}") pos_terminal = from(p in PosTerminal, where: p.serial_number == ^device_id, select: %{id: p.id}) |> Repo.one() Logger.info("[YSP Refund Notification][DB] pos_terminals query result: #{inspect(pos_terminal)}") provider_id = Map.get(transaction_data, :provider_id) || Map.get(transaction_data, "provider_id") Logger.info("[YSP Refund Notification][DB] Querying shukria_terminals for YSP data with shukria_terminal_id=#{pos_terminal && pos_terminal.id} and provider_id=#{provider_id}") shukria_terminal_ysp = case pos_terminal do nil -> nil %{id: stid} when not is_nil(provider_id) -> Logger.info("[YSP Refund Notification][DB] Executing: SELECT ysp_mid, shukria_mid FROM shukria_terminals WHERE shukria_terminal_id='#{stid}' AND provider_id='#{provider_id}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid) and s.provider_id == ^to_string(provider_id), select: %{shukria_mid: s.shukria_mid, ysp_mid: s.ysp_mid} ) result = Repo.one(query) Logger.info("[YSP Refund Notification][DB] shukria_terminals (YSP) query result: #{inspect(result)}") result %{id: stid} -> Logger.warn("[YSP Refund Notification] provider_id is missing, querying without provider filter for shukria_terminal_id='#{stid}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid), select: %{shukria_mid: s.shukria_mid, ysp_mid: s.ysp_mid} ) result = Repo.one(query) Logger.info("[YSP Refund Notification][DB] shukria_terminals (YSP) query result: #{inspect(result)}") result end if is_nil(provider_id) do Logger.warn("[YSP Refund Notification] provider_id is missing in transaction_data. Provider-specific shukria_terminals query will be skipped.") end Logger.info("[YSP Refund Notification][DB] Querying shukria_terminals for provider-specific with shukria_terminal_id=#{pos_terminal && pos_terminal.id} and provider_id=#{provider_id}") shukria_terminal_provider = case pos_terminal do nil -> nil %{id: stid} when not is_nil(provider_id) -> Logger.info("[YSP Refund Notification][DB] Executing: SELECT provider_tid FROM shukria_terminals WHERE shukria_terminal_id='#{stid}' AND provider_id='#{provider_id}'") query = from(s in ShukriaTerminal, where: s.shukria_terminal_id == ^to_string(stid) and s.provider_id == ^to_string(provider_id), select: %{provider_tid: s.provider_tid} ) result = Repo.one(query) Logger.info("[YSP Refund Notification][DB] shukria_terminals (provider-specific) query result: #{inspect(result)}") result _ -> nil end Logger.info("[YSP Refund Notification][DB] shukria_terminal_ysp response: #{inspect(shukria_terminal_ysp)}") Logger.info("[YSP Refund Notification][DB] shukria_terminal_provider response: #{inspect(shukria_terminal_provider)}") Logger.info("[YSP Refund Notification][DB] Querying Brand by shukria_mid: #{shukria_terminal_ysp && shukria_terminal_ysp.shukria_mid}") brand = case shukria_terminal_ysp do %{shukria_mid: smid} when not is_nil(smid) -> result = Repo.get(Brand, smid) Logger.info("[YSP Refund Notification][DB] Brand query result: #{inspect(result)}") result _ -> nil end merchant_tag = brand && brand.merchant_tag bank_user_id = brand && brand.merchant_reference_id Logger.info("[YSP Refund Notification][DB] Querying Store by brand_id: #{shukria_terminal_ysp && shukria_terminal_ysp.shukria_mid || (brand && brand.id)}") store_code = case shukria_terminal_ysp do %{shukria_mid: smid} when not is_nil(smid) -> query = from(s in DaProductApp.Store, where: s.brand_id == ^smid, select: s.code) result = Repo.one(query) Logger.info("[YSP Refund Notification][DB] Store query result: #{inspect(result)}") result _ -> case brand do %{id: brand_id} -> query = from(s in DaProductApp.Store, where: s.brand_id == ^brand_id, select: s.code) result = Repo.one(query) Logger.info("[YSP Refund Notification][DB] Store query result: #{inspect(result)}") result _ -> nil end end # Use ysp_tid and merchant_id directly from transaction_data merchant_id = transaction_data.merchant_id || (shukria_terminal_ysp && shukria_terminal_ysp.ysp_mid) terminal_id = transaction_data.ysp_tid || opts[:ysp_terminal_id] || config.terminal_id || device_id batch_number = Map.get(transaction_data, :batch_number) || Map.get(transaction_data, "batch_number") || "000001" # Extract refund details from refund_data refund_amount = Map.get(refund_data, :refund_amount, %{}) refund_response = Map.get(refund_data, :refund_response, %{}) refund_request_id = Map.get(refund_data, :refund_request_id) m_ref_num = transaction_data.m_ref_num # Trim paymentId to last 16 characters for YSP (right to left) payment_id = transaction_data.payment_reference_id |> to_string() |> (fn s -> len = String.length(s) if len > 16, do: String.slice(s, len - 16, 16), else: s end).() qr_code_transaction_id = m_ref_num refund_time = DateTime.now!("Etc/UTC") |> DateTime.to_iso8601(:extended) # Provider name from transaction_data, capitalize only first letter, rest lowercase provider_name = transaction_data[:provider_name] || transaction_data["provider_name"] || "Alipay" provider_name = case provider_name do <> -> String.upcase(<>) <> String.downcase(rest) _ -> provider_name end # Convert refund amount to major units {major_refund_str} = cond do String.downcase(provider_name) == "aani" -> {refund_amount["value"] || "0.00"} String.downcase(provider_name) == "alipay" -> # Alipay sends in smallest unit, so divide by 100 refund_value = case refund_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end major_refund = refund_value / 100 major_refund_str = :erlang.float_to_binary(major_refund, decimals: 2) {major_refund_str} true -> refund_value = case refund_amount["value"] do val when is_binary(val) -> case Float.parse(val) do {num, _} -> num _ -> 0 end val when is_number(val) -> val _ -> 0 end # For other providers, do not divide (assume already in major unit) major_refund_str = :erlang.float_to_binary(refund_value, decimals: 2) {major_refund_str} end # Generate unique transaction reference number txn_ref_no = TxnRefGenerator.generate_txn_ref_no() # Build refund result from refund_response refund_result = case refund_response do %{"result" => %{"resultCode" => "SUCCESS"}} -> %{ "resultCode" => "SUCCESS", "resultStatus" => "S", "resultMessage" => "Refund processed successfully" } %{"data" => %{"result" => %{"resultCode" => "SUCCESS"}}} -> %{ "resultCode" => "SUCCESS", "resultStatus" => "S", "resultMessage" => "Refund processed successfully" } _ -> %{ "resultCode" => "SUCCESS", "resultStatus" => "S", "resultMessage" => "Refund processed successfully" } end payload = %{ "txnRefNo" => txn_ref_no, "type" => "qr_payment_refund", "provider" => provider_name, "merchantTag" => merchant_tag, "bankUserId" => bank_user_id, "merchant_id" => merchant_id, "terminal_id" => terminal_id, "batchNumber" => batch_number, "qrId" => refund_request_id, "paymentResult" => refund_result, "qrCodeTransactionId" => refund_request_id, #"paymentId" => payment_id, # trimmed to 16 chars "paymentId" => qr_code_transaction_id, "paymentTime" => refund_time, "shopId" => store_code, "cashDeskId" => shukria_terminal_provider && shukria_terminal_provider.provider_tid, "paymentAmount" => %{ "value" => major_refund_str, "currency" => refund_amount["currency"] || "AED" }, "settlementAmount" => %{ "value" => major_refund_str, "currency" => refund_amount["currency"] || "AED" } } Logger.info("[YSP Refund Notification] Final refund payload to YSP: #{inspect(payload)}") {:ok, payload} end defp build_polling_payload(m_ref_num, _inquiry_response, payment_result, transaction_data, config) do # Use merchant_id and ysp_tid directly from transaction_data merchant_id = transaction_data.merchant_id terminal_id = transaction_data.ysp_tid || config.terminal_id || transaction_data.device_id batch_number = Map.get(transaction_data, :batch_number) || Map.get(transaction_data, "batch_number") || "000001" payment_id = transaction_data.payment_reference_id provider_name = transaction_data.provider_name provider_name = case provider_name do <> -> String.upcase(<>) <> String.downcase(rest) _ -> provider_name end payment_time = DateTime.now!("Etc/UTC") |> DateTime.to_iso8601(:extended) # Build the payment result structure payment_result_map = if is_map(payment_result) do payment_result else %{ "resultCode" => "SUCCESS", "resultStatus" => "S", "resultMessage" => "Transaction completed successfully" } end # Use transaction amount data if available amount_value = case transaction_data.amount do amount when is_number(amount) -> amount amount_str when is_binary(amount_str) -> case Float.parse(amount_str) do {val, _} -> val _ -> 0 end _ -> 0 end # Convert to major unit (divide by 100) major_unit_amount = amount_value / 100 major_unit_str = :erlang.float_to_binary(major_unit_amount, decimals: 2) # Always fetch bankUserId from Brand.merchant_reference_id using merchant_id brand = merchant_id && Repo.get(Brand, merchant_id) bank_user_id = brand && brand.merchant_reference_id # Generate unique transaction reference number txn_ref_no = TxnRefGenerator.generate_txn_ref_no() payload = %{ "txnRefNo" => txn_ref_no, "type" => "qr_payment_sale", "provider" => provider_name, "merchantTag" => config.merchant_tag || "UB776WH", "bankUserId" => bank_user_id, "merchant_id" => merchant_id, "terminal_id" => terminal_id, "batchNumber" => batch_number, "qrId" => transaction_data.transaction_ref_number || payment_id, "paymentResult" => payment_result_map, "qrCodeTransactionId" => transaction_data.transaction_ref_number || payment_id, "paymentId" => payment_id, "paymentTime" => payment_time, "shopId" => config.shop_id || "SHOP001", "cashDeskId" => config.cash_desk_id || transaction_data.device_id, "paymentAmount" => %{ "value" => major_unit_str, "currency" => "AED" }, "settlementAmount" => %{ "value" => major_unit_str, "currency" => "AED" } } {:ok, payload} end end