diff --git a/apps/upi_core/lib/upi_core/npci_format.ex b/apps/upi_core/lib/upi_core/npci_format.ex
index d0fa58a..627fd51 100644
--- a/apps/upi_core/lib/upi_core/npci_format.ex
+++ b/apps/upi_core/lib/upi_core/npci_format.ex
@@ -58,6 +58,28 @@ defmodule UpiCore.NpciFormat do
 
   def qr_param(_qr_string, _key), do: nil
 
+  @doc """
+  Like `qr_param/2` but percent-decodes the value (`%20` -> space), for free-text
+  QR fields that NPCI expects as readable text (`pn`, `brand`, `invoiceName`).
+
+  Only `%XX` sequences are decoded; a literal `+` is kept as `+` (it is NOT turned
+  into a space). A malformed `%` sequence leaves the raw value untouched.
+  """
+  @spec qr_text(String.t() | nil, String.t()) :: String.t() | nil
+  def qr_text(qr_string, key) do
+    case qr_param(qr_string, key) do
+      nil ->
+        nil
+
+      raw ->
+        try do
+          URI.decode(raw)
+        rescue
+          ArgumentError -> raw
+        end
+    end
+  end
+
   @doc """
   Formats an amount with exactly 2 decimals (`"150.0"` -> `"150.00"`).
 
diff --git a/apps/upi_core/lib/upi_core/upi_xml_schema.ex b/apps/upi_core/lib/upi_core/upi_xml_schema.ex
index 1fdca7b..6b9a49b 100644
--- a/apps/upi_core/lib/upi_core/upi_xml_schema.ex
+++ b/apps/upi_core/lib/upi_core/upi_xml_schema.ex
@@ -1,19 +1,21 @@
 ## removed duplicate defmodule declaration
 defmodule UpiCore.UpiXmlSchema do
-  defp extract_base_amount_from_qr(qr_payload) do
-    # Extract bAm (baseAmount) from QR string, e.g. ...&bAm=123.45&...
-    case Regex.run(~r/(?:bAm)[=:]([^&\s]+)/i, qr_payload || "") do
-      [_, base_amount] -> base_amount
-      _ -> nil
-    end
-  end
-
-  defp extract_base_curr_from_qr(qr_payload) do
-    case Regex.run(~r/(?:bCurr)[=:]([^&\s]+)/i, qr_payload || "") do
-      [_, base_curr] -> base_curr
-      _ -> nil
-    end
-  end
+  # [RespValQr Payee.Amount] REMOVED: extract_base_amount_from_qr is unused now - bAm is read with NpciFormat.qr_param + format_amount
+  # OLD:   defp extract_base_amount_from_qr(qr_payload) do
+  # OLD:     # Extract bAm (baseAmount) from QR string, e.g. ...&bAm=123.45&...
+  # OLD:     case Regex.run(~r/(?:bAm)[=:]([^&\s]+)/i, qr_payload || "") do
+  # OLD:       [_, base_amount] -> base_amount
+  # OLD:       _ -> nil
+  # OLD:     end
+  # OLD:   end
+
+  # [RespValQr Fx.baseCurr] REMOVED: extract_base_curr_from_qr is unused now - bCurr is read with NpciFormat.qr_param
+  # OLD:   defp extract_base_curr_from_qr(qr_payload) do
+  # OLD:     case Regex.run(~r/(?:bCurr)[=:]([^&\s]+)/i, qr_payload || "") do
+  # OLD:       [_, base_curr] -> base_curr
+  # OLD:       _ -> nil
+  # OLD:     end
+  # OLD:   end
   @moduledoc """
   Complete UPI XML Schema handler as per NPCI specification.
   Handles parsing and generation of all UPI XML messages with full field validation.
@@ -22,6 +24,7 @@ defmodule UpiCore.UpiXmlSchema do
 
   import SweetXml
   alias Timex
+  alias UpiCore.NpciFormat
   require Logger
 
   # UPI Standard Error Codes as per specification
@@ -92,31 +95,33 @@ defmodule UpiCore.UpiXmlSchema do
     end
   end
 
-  defp determine_initiation_mode(response_data) do
-    # First check if initiation_mode is explicitly provided (e.g., extracted from QR payload)
-    case Map.get(response_data, :initiation_mode) do
-      nil ->
-        # Fallback: Check if this is a dynamic QR or static QR
-        # Dynamic QR: explicitly marked OR contains transaction reference indicators in payload
-        is_dynamic = Map.get(response_data, :is_dynamic_qr, false) ||
-                     (String.contains?(Map.get(response_data, :qr_payload, ""), ["tr=", "tn="]))
-
-        if is_dynamic do
-          "16"  # Dynamic QR
-        else
-          "01"  # Static QR / RespValQr
-        end
-      mode when is_binary(mode) ->
-        # Use the provided initiation_mode (e.g., "15" from QR payload)
-        mode
-      mode when is_integer(mode) ->
-        # Convert integer to zero-padded string
-        String.pad_leading("#{mode}", 2, "0")
-      mode ->
-        # Convert other types to string
-        "#{mode}"
-    end
-  end
+  # [RespValQr determine_initiation_mode] REMOVED: initiationMode is echoed from the request, never guessed (was 16 / 01 from QR contents)
+  # TSD Table 2: Txn initiationMode = value received in ReqValQr; static flow only for 01/02
+  # OLD:   defp determine_initiation_mode(response_data) do
+  # OLD:     # First check if initiation_mode is explicitly provided (e.g., extracted from QR payload)
+  # OLD:     case Map.get(response_data, :initiation_mode) do
+  # OLD:       nil ->
+  # OLD:         # Fallback: Check if this is a dynamic QR or static QR
+  # OLD:         # Dynamic QR: explicitly marked OR contains transaction reference indicators in payload
+  # OLD:         is_dynamic = Map.get(response_data, :is_dynamic_qr, false) ||
+  # OLD:                      (String.contains?(Map.get(response_data, :qr_payload, ""), ["tr=", "tn="]))
+  # OLD:
+  # OLD:         if is_dynamic do
+  # OLD:           "16"  # Dynamic QR
+  # OLD:         else
+  # OLD:           "01"  # Static QR / RespValQr
+  # OLD:         end
+  # OLD:       mode when is_binary(mode) ->
+  # OLD:         # Use the provided initiation_mode (e.g., "15" from QR payload)
+  # OLD:         mode
+  # OLD:       mode when is_integer(mode) ->
+  # OLD:         # Convert integer to zero-padded string
+  # OLD:         String.pad_leading("#{mode}", 2, "0")
+  # OLD:       mode ->
+  # OLD:         # Convert other types to string
+  # OLD:         "#{mode}"
+  # OLD:     end
+  # OLD:   end
 
   @doc false
   def parse_req_val_qr(xml_string) do
@@ -132,6 +137,41 @@ defmodule UpiCore.UpiXmlSchema do
     end
   end
 
+    # [RespValQr <RespValQr> template] OLD TEMPLATE kept for reference - fields changed: Head.msgId, Txn (note/refId/ts/custRef/initiationMode/purpose echo),
+    # QR (qVer/qrMedium/expireTs/ts from QR string), Payee (addr=pa, name=pn, code=mc, Ac from config), Merchant.Fx removed, Name brand=brand,
+    # Invoice from QR, Institution conCode=cc, FxList only when Fx exists
+    # OLD:     xml = """
+    # OLD:     <?xml version="1.0" encoding="UTF-8"?>
+    # OLD:     <ns2:RespValQr xmlns:ns2="http://npci.org/upi/schema/">
+    # OLD:         <Head ver="2.0" ts="#{get_timestamp()}" orgId="#{Map.get(response_data, :org_id, "MER101")}" msgId="#{generate_fixed_length_msg_id(Map.get(response_data, :msg_id))}"/>
+    # OLD:         <Resp reqMsgId="#{Map.get(response_data, :req_msg_id)}" result="#{Map.get(response_data, :result, "SUCCESS")}"#{if Map.get(response_data, :result, "SUCCESS") != "SUCCESS", do: " errCode=\"#{Map.get(response_data, :err_code)}\"", else: ""}/>
+    # OLD:       <Txn id="#{Map.get(response_data, :txn_id)}" note="#{validate_and_preserve_note(Map.get(response_data, :note))}" refId="#{Map.get(response_data, :ref_id)}" refUrl="https://mercurypay.aritic.app" ts="#{Map.get(response_data, :txn_timestamp) || get_timestamp()}" type="IntlQr"#{cust_ref_attr} initiationMode="#{initiation_mode}" purpose="#{format_purpose(Map.get(response_data, :purpose))}">
+    # OLD:           <QR qVer="#{Map.get(response_data, :qr_version, "01")}" qrMedium="#{Map.get(response_data, :qr_medium, "03")}" expireTs="#{Map.get(response_data, :expire_ts) || get_expiry_timestamp()}"/>
+    # OLD:       </Txn>
+    # OLD:       <Payee addr="#{extract_vpa_from_qr_string(Map.get(response_data, :qr_payload)) || Map.get(response_data, :payee_addr)}" name="#{Map.get(response_data, :payee_name)}" seqNum="1" type="#{Map.get(response_data, :payee_type, "ENTITY")}" code="#{Map.get(response_data, :merchant_code) || Map.get(response_data, :sub_code, "0000")}">
+    # OLD:           <Ac addrType="ACCOUNT">
+    # OLD:         <Detail name="ACTYPE" value="#{Map.get(response_data, :account_type, "SAVINGS")}"/>
+    # OLD:         <Detail name="ACNUM" value="#{Map.get(response_data, :account_number, "1234567890")}"/>
+    # OLD:         <Detail name="IFSC" value="#{Map.get(response_data, :ifsc_code, "MERC0000001")}"/>
+    # OLD:           </Ac>
+    # OLD:           #{amount_tag}
+    # OLD:           <Merchant>
+    # OLD:            #{identifier_tag}
+    # OLD:               #{merchant_fx_tag}
+    # OLD:         <Name brand="#{Map.get(response_data, :merchant_brand) || Map.get(response_data, :payee_name)}"/>
+    # OLD:         <Ownership type="#{Map.get(response_data, :ownership_type, "PARTNERSHIP")}"/>
+    # OLD:         <Invoice name="#{Map.get(response_data, :invoice_name) || Map.get(response_data, :payee_name)}" num="#{extract_invoice_number_from_qr(response_data) || Map.get(response_data, :invoice_number) || generate_invoice_number()}" date="#{invoice_date_for_xml}"/>
+    # OLD:           </Merchant>
+    # OLD:           <Institution netInstId="#{Map.get(response_data, :net_inst_id, "MER1010001")}" QrPayLoad="#{escape_xml_entities(Map.get(response_data, :qr_payload, ""))}" conCode="#{extract_country_code_from_qr(Map.get(response_data, :qr_payload), response_data)}"/>
+    # OLD:           <FxList>
+    # OLD:         #{fxlist_tag}
+    # OLD:           </FxList>
+    # OLD:       </Payee>
+    # OLD:         </ns2:RespValQr>
+    # OLD:     """
+          # [RespValQr Institution.netInstId] CHANGED: no hard-coded "MER1010001" default; the caller passes the ReqValQr netInstId or the app config
+          # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+          # OLD:           <Institution netInstId="#{NpciFormat.xml_attr(Map.get(response_data, :net_inst_id, "MER1010001"))}" QrPayLoad="#{NpciFormat.xml_attr(qr_payload)}" conCode="#{NpciFormat.xml_attr(qr.("cc"))}"/>
   @doc """
   Generate RespValQR XML response with proper NPCI namespace and structure
   Fully compliant with NPCI specification including all mandatory fields
@@ -140,7 +180,25 @@ defmodule UpiCore.UpiXmlSchema do
     require Logger
 
     # Determine initiation mode based on QR type
-    initiation_mode = determine_initiation_mode(response_data)
+    # [RespValQr Txn.initiationMode] CHANGED: echo the request's initiationMode (fall back to QR mode=), no 16/01 guessing
+    # TSD Table 2: Txn initiationMode is the value received in ReqValQr
+    # OLD:     initiation_mode = determine_initiation_mode(response_data)
+    qr_payload = Map.get(response_data, :qr_payload)
+
+    # [RespValQr QR string] ADDED: every QR-derived field below is read raw from the QR string NPCI sent (no URL-decode, "+" kept)
+    qr = fn key -> NpciFormat.qr_param(qr_payload, key) end
+    # free text (pn / brand / invoiceName) is percent-decoded so "Dubai%20Fine%20Dining" is sent as "Dubai Fine Dining"
+    qr_text = fn key -> NpciFormat.qr_text(qr_payload, key) end
+    # attribute is left out entirely when the value is missing (never send a made-up value)
+    attr = fn name, value ->
+      if value in [nil, ""], do: "", else: " #{name}=\"#{NpciFormat.xml_attr(value)}\""
+    end
+
+    initiation_mode =
+      case Map.get(response_data, :initiation_mode) do
+        mode when mode in [nil, ""] -> qr.("mode")
+        mode -> to_string(mode)
+      end
 
     # Extract merchant type with debugging
     merchant_type = case Map.get(response_data, :merchant_type) do
@@ -160,152 +218,233 @@ defmodule UpiCore.UpiXmlSchema do
     Logger.info("Final merchant type for XML: #{merchant_type}")
     Logger.info("QR payload in response_data: #{inspect(Map.get(response_data, :qr_payload))}")
 
-    # Normalize invoice_date but preserve original local timestamp+offset when present
-    raw_inv = Map.get(response_data, :invoice_date)
-    invoice_date_for_xml =
+    # [RespValQr Merchant.Invoice date] CHANGED: invoiceDate from the QR string as-is on all paths (no repair, no 'now' fallback)
+    # TSD Table 2: Invoice date = invoiceDate in QR string
+    # OLD:     # Normalize invoice_date but preserve original local timestamp+offset when present
+    # OLD:     raw_inv = Map.get(response_data, :invoice_date)
+    # OLD:     invoice_date_for_xml =
+    # OLD:       cond do
+    # OLD:         is_binary(raw_inv) and raw_inv != "" ->
+    # OLD:           repaired = Regex.replace(~r/(\d{2}:\d{2}:\d{2})\s([+-]?\d{2}:\d{2})$/, raw_inv, "\\1+\\2")
+    # OLD:           # If repaired already contains explicit timezone offset or Z, keep it verbatim
+    # OLD:           if Regex.match?(~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/, repaired) do
+    # OLD:             repaired
+    # OLD:           else
+    # OLD:             # Fallback: try parsing and formatting, otherwise use current timestamp
+    # OLD:             case DateTime.from_iso8601(repaired) do
+    # OLD:               {:ok, _dt, _offset} -> repaired
+    # OLD:               _ -> get_timestamp()
+    # OLD:             end
+    # OLD:           end
+    # OLD:         true ->
+    # OLD:           get_timestamp()
+    # OLD:       end
+    invoice_date_for_xml = qr.("invoiceDate")
+
+    # [RespValQr is_static_qr] CHANGED: no longer drives Amount / Fx / Identifier; those depend on the QR string and initiationMode now
+    # OLD:     is_static_qr = Map.get(response_data, :is_static_qr, false)
+    _is_static_qr = Map.get(response_data, :is_static_qr, false)
+
+    # [RespValQr Txn.custRef] CHANGED: echo the request custRef (no padding to 12, no generated value)
+    # TSD Table 2: Txn custRef = value received in ReqValQr
+    # OLD:     cust_ref_attr = " custRef=\"#{format_cust_ref(Map.get(response_data, :cust_ref))}\""
+    cust_ref_attr = attr.("custRef", Map.get(response_data, :cust_ref))
+    # [RespValQr Txn.note / refId / ts / purpose / initiationMode] CHANGED: echo from the request (no PAYMENT / REF... / 11 / now defaults)
+    txn_note_attr = attr.("note", Map.get(response_data, :note))
+    txn_ref_id_attr = attr.("refId", Map.get(response_data, :ref_id))
+    txn_ts_attr = attr.("ts", Map.get(response_data, :txn_timestamp))
+    purpose_attr = attr.("purpose", Map.get(response_data, :purpose))
+    initiation_mode_attr = attr.("initiationMode", initiation_mode)
+    # [RespValQr QR qVer / qrMedium / expireTs / ts] CHANGED: all four come from the QR string
+    # TSD Table 2: QR expireTs = QRexpire, ts = QRts (dynamic QR), qVer = ver, qrMedium = qrMedium
+    qr_attrs =
+      attr.("qVer", qr.("ver")) <>
+        attr.("qrMedium", qr.("qrMedium")) <>
+        attr.("expireTs", qr.("QRexpire")) <>
+        attr.("ts", qr.("QRts"))
+
+    # [RespValQr Payee.Amount / Fx] CHANGED: amount and currency are the QR's bAm / bCurr, not response_data amount/currency
+    # TSD Table 2: Amount = bAm + bCurr (2 decimals) for modes 15/16/22/23
+    # OLD:     amount = Map.get(response_data, :amount)
+    # OLD:     currency = Map.get(response_data, :currency)
+    # OLD:     base_currency = Map.get(response_data, :base_currency) || currency
+    base_amount_str = NpciFormat.format_amount(qr.("bAm"))
+    base_curr_str = qr.("bCurr")
+
+    # [RespValQr Payee.Amount] CHANGED: modes 15/16/22/23 always carry Amount = format_amount(bAm) + bCurr, also on error paths; never empty
+    # OLD:     amount_tag =
+    # OLD:       if is_static_qr,
+    # OLD:         do: "",
+    # OLD:         else: "<Amount value=\"#{amount}\" curr=\"#{currency}\"/>"
+    amount_tag =
       cond do
-        is_binary(raw_inv) and raw_inv != "" ->
-          repaired = Regex.replace(~r/(\d{2}:\d{2}:\d{2})\s([+-]?\d{2}:\d{2})$/, raw_inv, "\\1+\\2")
-          # If repaired already contains explicit timezone offset or Z, keep it verbatim
-          if Regex.match?(~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/, repaired) do
-            repaired
-          else
-            # Fallback: try parsing and formatting, otherwise use current timestamp
-            case DateTime.from_iso8601(repaired) do
-              {:ok, _dt, _offset} -> repaired
-              _ -> get_timestamp()
-            end
-          end
-        true ->
-          get_timestamp()
-      end
+        initiation_mode not in ["15", "16", "22", "23"] ->
+          ""
 
-    is_static_qr = Map.get(response_data, :is_static_qr, false)
+        base_amount_str != nil and base_curr_str not in [nil, ""] ->
+          "<Amount value=\"#{NpciFormat.xml_attr(base_amount_str)}\" curr=\"#{NpciFormat.xml_attr(base_curr_str)}\"/>"
 
-    cust_ref_attr = " custRef=\"#{format_cust_ref(Map.get(response_data, :cust_ref))}\""
-
-    amount = Map.get(response_data, :amount)
-    currency = Map.get(response_data, :currency)
-    base_currency = Map.get(response_data, :base_currency) || currency
+        true ->
+          Logger.error("[RespValQr Payee.Amount] bAm / bCurr missing in QR for mode #{initiation_mode} - Amount not sent")
+          ""
+      end
 
-    amount_tag =
-      if is_static_qr,
-        do: "",
-        else: "<Amount value=\"#{amount}\" curr=\"#{currency}\"/>"
-
-    fxlist_tag =
-      if is_static_qr do
-        # Prefer exact QR payload bAm (string) to satisfy NPCI strict string comparison.
-        qr_bam = extract_base_amount_from_qr(Map.get(response_data, :qr_payload))
-        qr_bcurr = extract_base_curr_from_qr(Map.get(response_data, :qr_payload))
-        base_curr_final = qr_bcurr || base_currency
-
-        base_amount_xml =
-          cond do
-            is_binary(qr_bam) and qr_bam != "" -> qr_bam
-            true ->
-              case Map.get(response_data, :base_amount) do
-                nil -> ""
-                v ->
-                  # Ensure formatting with two decimal places (string) to match expected payload format
-                  try do
-                    dec = Decimal.new(to_string(v))
-                    Decimal.to_string(Decimal.round(dec, 2), :fixed)
-                  rescue
-                    _ -> to_string(v)
-                  end
-              end
-          end
+    # [RespValQr FxList/Fx] CHANGED: baseAmount = format_amount(bAm), baseCurr = bCurr, taken from the QR on every path; never an empty Fx
+    # TSD Table 2: FxList/Fx baseAmount / baseCurr
+    # OLD:     fxlist_tag =
+    # OLD:       if is_static_qr do
+    # OLD:         # Prefer exact QR payload bAm (string) to satisfy NPCI strict string comparison.
+    # OLD:         qr_bam = extract_base_amount_from_qr(Map.get(response_data, :qr_payload))
+    # OLD:         qr_bcurr = extract_base_curr_from_qr(Map.get(response_data, :qr_payload))
+    # OLD:         base_curr_final = qr_bcurr || base_currency
+    # OLD:
+    # OLD:         base_amount_xml =
+    # OLD:           cond do
+    # OLD:             is_binary(qr_bam) and qr_bam != "" -> qr_bam
+    # OLD:             true ->
+    # OLD:               case Map.get(response_data, :base_amount) do
+    # OLD:                 nil -> ""
+    # OLD:                 v ->
+    # OLD:                   # Ensure formatting with two decimal places (string) to match expected payload format
+    # OLD:                   try do
+    # OLD:                     dec = Decimal.new(to_string(v))
+    # OLD:                     Decimal.to_string(Decimal.round(dec, 2), :fixed)
+    # OLD:                   rescue
+    # OLD:                     _ -> to_string(v)
+    # OLD:                   end
+    # OLD:               end
+    # OLD:           end
+    # OLD:
+    # OLD:         if base_amount_xml != "" do
+    # OLD:           "<Fx baseAmount=\"#{base_amount_xml}\" baseCurr=\"#{base_curr_final}\"/>"
+    # OLD:         else
+    # OLD:           "<Fx baseCurr=\"#{base_curr_final}\"/>"
+    # OLD:         end
+    # OLD:       else
+    # OLD:         base_amount = Map.get(response_data, :base_amount) || amount
+    # OLD:         # Format non-static base amount to 2 decimal places as well
+    # OLD:         formatted =
+    # OLD:           try do
+    # OLD:             dec = Decimal.new(to_string(base_amount))
+    # OLD:             Decimal.to_string(Decimal.round(dec, 2), :fixed)
+    # OLD:           rescue
+    # OLD:             _ -> to_string(base_amount)
+    # OLD:           end
+    # OLD:
+    # OLD:         "<Fx baseAmount=\"#{formatted}\" baseCurr=\"#{base_currency}\"/>"
+    # OLD:       end
+    fxlist_section =
+      cond do
+        base_amount_str != nil and base_curr_str not in [nil, ""] ->
+          "<FxList><Fx baseAmount=\"#{NpciFormat.xml_attr(base_amount_str)}\" baseCurr=\"#{NpciFormat.xml_attr(base_curr_str)}\"/></FxList>"
 
-        if base_amount_xml != "" do
-          "<Fx baseAmount=\"#{base_amount_xml}\" baseCurr=\"#{base_curr_final}\"/>"
-        else
-          "<Fx baseCurr=\"#{base_curr_final}\"/>"
-        end
-      else
-        base_amount = Map.get(response_data, :base_amount) || amount
-        # Format non-static base amount to 2 decimal places as well
-        formatted =
-          try do
-            dec = Decimal.new(to_string(base_amount))
-            Decimal.to_string(Decimal.round(dec, 2), :fixed)
-          rescue
-            _ -> to_string(base_amount)
-          end
+        base_curr_str not in [nil, ""] ->
+          Logger.error("[RespValQr FxList/Fx] bAm missing in QR - Fx sent with baseCurr only")
+          "<FxList><Fx baseCurr=\"#{NpciFormat.xml_attr(base_curr_str)}\"/></FxList>"
 
-        "<Fx baseAmount=\"#{formatted}\" baseCurr=\"#{base_currency}\"/>"
+        true ->
+          Logger.error("[RespValQr FxList/Fx] bAm / bCurr missing in QR - FxList not sent")
+          ""
       end
 
+    # [RespValQr Merchant.Identifier] CHANGED: subCode=mc, mid, sid=msid, tid=mtid, merchantType=mType, merchantGenre=mGr, onBoardingType=mOnboarding, merchantLoc=mLoc
+    # all straight from the QR string; no RETAIL / IN / International / 0000 / SMALL defaults, and the static and dynamic branches are one now
+    # TSD Table 2: Merchant/Identifier
+    # OLD:     identifier_tag =
+    # OLD:       if is_static_qr do
+    # OLD:         sub_code = Map.get(response_data, :identifier_sub_code, "0000")
+    # OLD:         mid = Map.get(response_data, :identifier_mid, "")
+    # OLD:         sid = Map.get(response_data, :identifier_sid, "")
+    # OLD:         tid = Map.get(response_data, :identifier_tid, "")
+    # OLD:         m_type = Map.get(response_data, :identifier_merchant_type, "SMALL")
+    # OLD:         m_genre = Map.get(response_data, :identifier_merchant_genre, "OFFLINE")
+    # OLD:         m_onboarding = Map.get(response_data, :identifier_onboarding_type, "AGGREGATOR")
+    # OLD:         m_loc = Map.get(response_data, :identifier_merchant_loc, "IN")
+    # OLD:         inst_code = Map.get(response_data, :identifier_inst_code, "")
+    # OLD:         "<Identifier subCode=\"#{sub_code}\" mid=\"#{mid}\" sid=\"#{sid}\" tid=\"#{tid}\" merchantType=\"#{m_type}\" merchantGenre=\"#{m_genre}\" onBoardingType=\"#{m_onboarding}\" merchantLoc=\"#{m_loc}\" merchantInstCode=\"#{inst_code}\"/>"
+    # OLD:       else
+    # OLD:         # For dynamic QRs, extract merchant identifiers from QR payload
+    # OLD:         sub_code = Map.get(response_data, :sub_code) || Map.get(response_data, :identifier_sub_code, "0000")
+    # OLD:         mid = extract_mid_from_qr(response_data) || Map.get(response_data, :identifier_mid, "")
+    # OLD:         sid = extract_msid_from_qr(response_data) || Map.get(response_data, :identifier_sid, "")
+    # OLD:         tid = extract_mtid_from_qr(response_data) || Map.get(response_data, :identifier_tid, "")
+    # OLD:         m_type = extract_merchant_type_from_qr(response_data) || Map.get(response_data, :identifier_merchant_type, "SMALL")
+    # OLD:         m_genre = extract_merchant_genre_from_qr(response_data) || Map.get(response_data, :identifier_merchant_genre, "RETAIL")
+    # OLD:         m_onboarding = extract_merchant_onboarding_from_qr(response_data) || Map.get(response_data, :identifier_onboarding_type, "AGGREGATOR")
+    # OLD:         m_loc = extract_merchant_loc_from_qr(response_data) || Map.get(response_data, :identifier_merchant_loc, "IN")
+    # OLD:         inst_code = Map.get(response_data, :identifier_inst_code, "")
+    # OLD:         "<Identifier subCode=\"#{sub_code}\" mid=\"#{mid}\" sid=\"#{sid}\" tid=\"#{tid}\" merchantType=\"#{m_type}\" merchantGenre=\"#{m_genre}\" onBoardingType=\"#{m_onboarding}\" merchantLoc=\"#{m_loc}\" merchantInstCode=\"#{inst_code}\"/>"
+    # OLD:       end
     identifier_tag =
-      if is_static_qr do
-        sub_code = Map.get(response_data, :identifier_sub_code, "0000")
-        mid = Map.get(response_data, :identifier_mid, "")
-        sid = Map.get(response_data, :identifier_sid, "")
-        tid = Map.get(response_data, :identifier_tid, "")
-        m_type = Map.get(response_data, :identifier_merchant_type, "SMALL")
-        m_genre = Map.get(response_data, :identifier_merchant_genre, "OFFLINE")
-        m_onboarding = Map.get(response_data, :identifier_onboarding_type, "AGGREGATOR")
-        m_loc = Map.get(response_data, :identifier_merchant_loc, "IN")
-        inst_code = Map.get(response_data, :identifier_inst_code, "")
-        "<Identifier subCode=\"#{sub_code}\" mid=\"#{mid}\" sid=\"#{sid}\" tid=\"#{tid}\" merchantType=\"#{m_type}\" merchantGenre=\"#{m_genre}\" onBoardingType=\"#{m_onboarding}\" merchantLoc=\"#{m_loc}\" merchantInstCode=\"#{inst_code}\"/>"
-      else
-        # For dynamic QRs, extract merchant identifiers from QR payload
-        sub_code = Map.get(response_data, :sub_code) || Map.get(response_data, :identifier_sub_code, "0000")
-        mid = extract_mid_from_qr(response_data) || Map.get(response_data, :identifier_mid, "")
-        sid = extract_msid_from_qr(response_data) || Map.get(response_data, :identifier_sid, "")
-        tid = extract_mtid_from_qr(response_data) || Map.get(response_data, :identifier_tid, "")
-        m_type = extract_merchant_type_from_qr(response_data) || Map.get(response_data, :identifier_merchant_type, "SMALL")
-        m_genre = extract_merchant_genre_from_qr(response_data) || Map.get(response_data, :identifier_merchant_genre, "RETAIL")
-        m_onboarding = extract_merchant_onboarding_from_qr(response_data) || Map.get(response_data, :identifier_onboarding_type, "AGGREGATOR")
-        m_loc = extract_merchant_loc_from_qr(response_data) || Map.get(response_data, :identifier_merchant_loc, "IN")
-        inst_code = Map.get(response_data, :identifier_inst_code, "")
-        "<Identifier subCode=\"#{sub_code}\" mid=\"#{mid}\" sid=\"#{sid}\" tid=\"#{tid}\" merchantType=\"#{m_type}\" merchantGenre=\"#{m_genre}\" onBoardingType=\"#{m_onboarding}\" merchantLoc=\"#{m_loc}\" merchantInstCode=\"#{inst_code}\"/>"
-      end
-
-    merchant_fx_tag =
-      if is_static_qr do
-        ""
-      else
-        base_amt =
-          cond do
-            Map.get(response_data, :initiation_mode) == "02" && Map.get(response_data, :qr_payload) ->
-              extract_base_amount_from_qr(Map.get(response_data, :qr_payload)) ||
-                Map.get(response_data, :base_amount) ||
-                Map.get(response_data, :amount)
-            true ->
-              Map.get(response_data, :base_amount) || Map.get(response_data, :amount)
-          end
+      "<Identifier" <>
+        attr.("subCode", qr.("mc")) <>
+        attr.("mid", qr.("mid")) <>
+        attr.("sid", qr.("msid")) <>
+        attr.("tid", qr.("mtid")) <>
+        attr.("merchantType", qr.("mType")) <>
+        attr.("merchantGenre", qr.("mGr")) <>
+        attr.("onBoardingType", qr.("mOnboarding")) <>
+        attr.("merchantLoc", qr.("mLoc")) <>
+        " merchantInstCode=\"#{NpciFormat.xml_attr(Map.get(response_data, :identifier_inst_code, ""))}\"/>"
+
+    # [RespValQr Payee.Ac] CHANGED: our Indian settlement account from config (:upi_core, :psp_settlement_account); dummy defaults removed
+    # TSD Table 2: Payee Ac Details ACTYPE / ACNUM / IFSC are the PSP's Indian settlement account
+    settlement_ac = Map.new(Application.get_env(:upi_core, :psp_settlement_account, []))
+    ac_type = Map.get(settlement_ac, :ac_type)
+    ac_num = Map.get(settlement_ac, :ac_num)
+    ac_ifsc = Map.get(settlement_ac, :ifsc)
+
+    if Enum.any?([ac_type, ac_num, ac_ifsc], &(&1 in [nil, ""])) do
+      Logger.error(
+        "[RespValQr Payee.Ac] :psp_settlement_account is not fully configured (PSP_SETTLEMENT_AC_TYPE / PSP_SETTLEMENT_AC_NUM / PSP_SETTLEMENT_IFSC) - sending empty Ac values"
+      )
+    end
 
-        base_curr = Map.get(response_data, :base_currency) || Map.get(response_data, :currency)
-        "<Fx baseAmount=\"#{base_amt}\" baseCurr=\"#{base_curr}\"/>"
-      end
+    # [RespValQr Merchant.Fx] REMOVED: <Fx> inside <Merchant> is not in TSD Table 2 (Fx is only under FxList)
+    # OLD:     merchant_fx_tag =
+    # OLD:       if is_static_qr do
+    # OLD:         ""
+    # OLD:       else
+    # OLD:         base_amt =
+    # OLD:           cond do
+    # OLD:             Map.get(response_data, :initiation_mode) == "02" && Map.get(response_data, :qr_payload) ->
+    # OLD:               extract_base_amount_from_qr(Map.get(response_data, :qr_payload)) ||
+    # OLD:                 Map.get(response_data, :base_amount) ||
+    # OLD:                 Map.get(response_data, :amount)
+    # OLD:             true ->
+    # OLD:               Map.get(response_data, :base_amount) || Map.get(response_data, :amount)
+    # OLD:           end
+    # OLD:
+    # OLD:         base_curr = Map.get(response_data, :base_currency) || Map.get(response_data, :currency)
+    # OLD:         "<Fx baseAmount=\"#{base_amt}\" baseCurr=\"#{base_curr}\"/>"
+    # OLD:       end
+
+    if Map.get(response_data, :net_inst_id) in [nil, ""],
+      do: Logger.error("[RespValQr Institution.netInstId] no netInstId (ReqValQr value or :psp_net_inst_id config) - sending it empty")
 
     xml = """
     <?xml version="1.0" encoding="UTF-8"?>
     <ns2:RespValQr xmlns:ns2="http://npci.org/upi/schema/">
-        <Head ver="2.0" ts="#{get_timestamp()}" orgId="#{Map.get(response_data, :org_id, "MER101")}" msgId="#{generate_fixed_length_msg_id(Map.get(response_data, :msg_id))}"/>
-        <Resp reqMsgId="#{Map.get(response_data, :req_msg_id)}" result="#{Map.get(response_data, :result, "SUCCESS")}"#{if Map.get(response_data, :result, "SUCCESS") != "SUCCESS", do: " errCode=\"#{Map.get(response_data, :err_code)}\"", else: ""}/>
-      <Txn id="#{Map.get(response_data, :txn_id)}" note="#{validate_and_preserve_note(Map.get(response_data, :note))}" refId="#{Map.get(response_data, :ref_id)}" refUrl="https://mercurypay.aritic.app" ts="#{Map.get(response_data, :txn_timestamp) || get_timestamp()}" type="IntlQr"#{cust_ref_attr} initiationMode="#{initiation_mode}" purpose="#{format_purpose(Map.get(response_data, :purpose))}">
-          <QR qVer="#{Map.get(response_data, :qr_version, "01")}" qrMedium="#{Map.get(response_data, :qr_medium, "03")}" expireTs="#{Map.get(response_data, :expire_ts) || get_expiry_timestamp()}"/>
+        <Head ver="2.0" ts="#{get_timestamp()}" orgId="#{NpciFormat.xml_attr(Map.get(response_data, :org_id, "MER101"))}" msgId="#{NpciFormat.new_msg_id()}"/>
+        <Resp reqMsgId="#{NpciFormat.xml_attr(Map.get(response_data, :req_msg_id))}" result="#{NpciFormat.xml_attr(Map.get(response_data, :result, "SUCCESS"))}"#{if Map.get(response_data, :result, "SUCCESS") != "SUCCESS", do: " errCode=\"#{NpciFormat.xml_attr(Map.get(response_data, :err_code))}\"", else: ""}/>
+      <Txn id="#{NpciFormat.xml_attr(Map.get(response_data, :txn_id))}"#{txn_note_attr}#{txn_ref_id_attr} refUrl="https://mercurypay.aritic.app"#{txn_ts_attr} type="IntlQr"#{cust_ref_attr}#{initiation_mode_attr}#{purpose_attr}>
+          <QR#{qr_attrs}/>
       </Txn>
-      <Payee addr="#{extract_vpa_from_qr_string(Map.get(response_data, :qr_payload)) || Map.get(response_data, :payee_addr)}" name="#{Map.get(response_data, :payee_name)}" seqNum="1" type="#{Map.get(response_data, :payee_type, "ENTITY")}" code="#{Map.get(response_data, :merchant_code) || Map.get(response_data, :sub_code, "0000")}">
+      <Payee addr="#{NpciFormat.xml_attr(qr.("pa"))}" name="#{NpciFormat.xml_attr(qr_text.("pn"))}" seqNum="1" type="#{NpciFormat.xml_attr(Map.get(response_data, :payee_type, "ENTITY"))}" code="#{NpciFormat.xml_attr(qr.("mc"))}">
           <Ac addrType="ACCOUNT">
-        <Detail name="ACTYPE" value="#{Map.get(response_data, :account_type, "SAVINGS")}"/>
-        <Detail name="ACNUM" value="#{Map.get(response_data, :account_number, "1234567890")}"/>
-        <Detail name="IFSC" value="#{Map.get(response_data, :ifsc_code, "MERC0000001")}"/>
+        <Detail name="ACTYPE" value="#{NpciFormat.xml_attr(ac_type)}"/>
+        <Detail name="ACNUM" value="#{NpciFormat.xml_attr(ac_num)}"/>
+        <Detail name="IFSC" value="#{NpciFormat.xml_attr(ac_ifsc)}"/>
           </Ac>
           #{amount_tag}
           <Merchant>
            #{identifier_tag}
-              #{merchant_fx_tag}
-        <Name brand="#{Map.get(response_data, :merchant_brand) || Map.get(response_data, :payee_name)}"/>
-        <Ownership type="#{Map.get(response_data, :ownership_type, "PARTNERSHIP")}"/>
-        <Invoice name="#{Map.get(response_data, :invoice_name) || Map.get(response_data, :payee_name)}" num="#{extract_invoice_number_from_qr(response_data) || Map.get(response_data, :invoice_number) || generate_invoice_number()}" date="#{invoice_date_for_xml}"/>
+        <Name brand="#{NpciFormat.xml_attr(qr_text.("brand"))}"/>
+        <Ownership type="#{NpciFormat.xml_attr(Map.get(response_data, :ownership_type, "PARTNERSHIP"))}"/>
+        <Invoice#{attr.("name", qr_text.("invoiceName"))}#{attr.("num", qr.("invoiceNo"))}#{attr.("date", invoice_date_for_xml)}/>
           </Merchant>
-          <Institution netInstId="#{Map.get(response_data, :net_inst_id, "MER1010001")}" QrPayLoad="#{escape_xml_entities(Map.get(response_data, :qr_payload, ""))}" conCode="#{extract_country_code_from_qr(Map.get(response_data, :qr_payload), response_data)}"/>
-          <FxList>
-        #{fxlist_tag}
-          </FxList>
+          <Institution netInstId="#{NpciFormat.xml_attr(Map.get(response_data, :net_inst_id))}" QrPayLoad="#{NpciFormat.xml_attr(qr_payload)}" conCode="#{NpciFormat.xml_attr(qr.("cc"))}"/>
+          #{fxlist_section}
       </Payee>
         </ns2:RespValQr>
     """
@@ -314,6 +453,39 @@ defmodule UpiCore.UpiXmlSchema do
     {:ok, xml}
   end
 
+  @doc """
+  [RespValQr QM1] ADDED: validate the QR's mc (merchant category code) before answering ReqValQr.
+  TSD Table 2: a QR whose mc is missing, not 4 digits, not the merchant's registered MCC or in the
+  blocked list must be answered with FAILURE errCode="QM1".
+
+  `merchant_mcc` is the MCC registered for the merchant (nil / "" when unknown - then only the
+  format and blocked-list checks apply). Blocked MCCs come from `config :upi_core, :blocked_mccs`
+  (default `["7995"]`).
+
+  Returns `:ok` or `{:error, "QM1"}`.
+  """
+  def validate_qr_mcc(qr_payload, merchant_mcc) do
+    mc = NpciFormat.qr_param(qr_payload, "mc")
+    blocked = Application.get_env(:upi_core, :blocked_mccs, ["7995"])
+
+    cond do
+      not (is_binary(mc) and Regex.match?(~r/^\d{4}$/, mc)) ->
+        Logger.warning("[RespValQr QM1] mc missing or not 4 digits: #{inspect(mc)}")
+        {:error, "QM1"}
+
+      mc in blocked ->
+        Logger.warning("[RespValQr QM1] mc #{mc} is in the blocked MCC list")
+        {:error, "QM1"}
+
+      is_binary(merchant_mcc) and merchant_mcc != "" and merchant_mcc != mc ->
+        Logger.warning("[RespValQr QM1] mc #{mc} does not match the merchant's MCC #{merchant_mcc}")
+        {:error, "QM1"}
+
+      true ->
+        :ok
+    end
+  end
+
   @doc """
   Parse International Credit Request (ReqPay) XML with full UPI specification compliance
   Handles enhanced international UPI structure with merchant details, risk scores, and device info
@@ -1495,6 +1667,9 @@ end
           initiation_mode: doc |> xpath(~x"//Txn/@initiationMode"s) |> fallback_element(doc, "//Txn/initiationMode/text()"),
           purpose: doc |> xpath(~x"//Txn/@purpose"s) |> fallback_element(doc, "//Txn/purpose/text()"),
           cust_ref: doc |> xpath(~x"//Txn/@custRef"s) |> fallback_element(doc, "//Txn/custRef/text()"),
+          # [ReqValQr parse Txn.ts] ADDED: keep the request's Txn@ts so RespValQr can echo it (today Head ts / now / UTC is sent)
+          # TSD Table 2: Txn ts in RespValQr is the Txn ts received in ReqValQr
+          txn_ts: doc |> xpath(~x"//Txn/@ts"s) |> fallback_element(doc, "//Txn/ts/text()"),
 
           # QR Payload (most important)
           qr_payload: doc |> xpath(~x"//Institution/@QrPayLoad"s) |> fallback_element(doc, "//QrPayload/text()"),
@@ -2414,148 +2589,151 @@ end
     result
   end
 
-  defp extract_invoice_number_from_qr(data) do
-    # Extract invoice number from QR payload for mode 17 (mandate)
-    # QR format typically contains invoiceNo=VALUE in query parameters
-    require Logger
-
-    qr_payload = case data do
-      %{qr_payload: payload} when is_binary(payload) -> payload
-      %{qr_string: payload} when is_binary(payload) -> payload
-      _ -> ""
-    end
-
-    Logger.debug("Parsing QR payload for invoiceNo: #{inspect(qr_payload)}")
-
-    result = cond do
-      # Try standard query parameter format: invoiceNo=VALUE
-      match = Regex.run(~r/[&?]invoiceNo=([^&\s]+)/i, qr_payload) ->
-        [_, invoice_no] = match
-        inv = URI.decode(String.trim(invoice_no))
-        Logger.debug("Found invoiceNo via query param: #{inv}")
-        inv
-
-      # Try colon separator: invoiceNo:VALUE
-      match = Regex.run(~r/invoiceNo:([^:&\s]+)/i, qr_payload) ->
-        [_, invoice_no] = match
-        inv = URI.decode(String.trim(invoice_no))
-        Logger.debug("Found invoiceNo via colon: #{inv}")
-        inv
-
-      # Default fallback - return nil so we can use generate_invoice_number()
-      true ->
-        Logger.debug("No invoiceNo found in QR payload")
-        nil
-    end
-
-    Logger.debug("Final extracted invoiceNo: #{result}")
-    result
-  end
-
-  defp extract_vpa_from_qr_string(qr_payload) do
-    # Parse QR payload to extract VPA (Virtual Payment Address)
-    # QR format can be: upi://pay?pa=VPA&... or pa=VPA in query parameters
-    require Logger
-    Logger.debug("Parsing QR payload for VPA: #{inspect(qr_payload)}")
-
-    result = cond do
-      # Try standard query parameter format: pa=VPA
-      match = Regex.run(~r/[&?]pa=([^&\s]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via query param: #{vpa}")
-        vpa
-
-      # Try XML-like format: <pa>VPA</pa>
-      match = Regex.run(~r/<pa>([^<]+)<\/pa>/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via XML: #{vpa}")
-        vpa
-
-      # Try colon separator: pa:VPA
-      match = Regex.run(~r/pa:([^:&\s]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via colon: #{vpa}")
-        vpa
-
-      # Try simple format: paVPA (less common)
-      match = Regex.run(~r/pa([a-zA-Z0-9@.-]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via simple: #{vpa}")
-        vpa
-
-      # Default fallback - extract anything that looks like a VPA
-      match = Regex.run(~r/([a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via email pattern: #{vpa}")
-        vpa
-
-      # Last resort fallback
-      true ->
-        Logger.warning("No VPA found in QR payload, using default")
-        "merchant@defaultbank"
-    end
-
-    Logger.debug("Final extracted VPA: #{result}")
-    result
-  end
-
-
-  defp extract_vpa_from_qr_string(qr_payload) do
-    # Parse QR payload to extract VPA (Virtual Payment Address)
-    # QR format can be: upi://pay?pa=VPA&... or pa=VPA in query parameters
-    require Logger
-    Logger.debug("Parsing QR payload for VPA: #{inspect(qr_payload)}")
-
-    result = cond do
-      # Try standard query parameter format: pa=VPA
-      match = Regex.run(~r/[&?]pa=([^&\s]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via query param: #{vpa}")
-        vpa
-
-      # Try XML-like format: <pa>VPA</pa>
-      match = Regex.run(~r/<pa>([^<]+)<\/pa>/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via XML: #{vpa}")
-        vpa
-
-      # Try colon separator: pa:VPA
-      match = Regex.run(~r/pa:([^:&\s]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via colon: #{vpa}")
-        vpa
-
-      # Try simple format: paVPA (less common)
-      match = Regex.run(~r/pa([a-zA-Z0-9@.-]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via simple: #{vpa}")
-        vpa
-
-      # Default fallback - extract anything that looks like a VPA
-      match = Regex.run(~r/([a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+)/i, qr_payload) ->
-        [_, vpa] = match
-        vpa = String.trim(vpa)
-        Logger.debug("Found VPA via email pattern: #{vpa}")
-        vpa
-
-      # Last resort fallback
-      true ->
-        Logger.warning("No VPA found in QR payload, using default")
-        "merchant@defaultbank"
-    end
-
-    Logger.debug("Final extracted VPA: #{result}")
-    result
-  end
+  # [RespValQr Merchant.Invoice num] REMOVED: extract_invoice_number_from_qr is unused now - invoiceNo is read with NpciFormat.qr_param
+  # OLD:   defp extract_invoice_number_from_qr(data) do
+  # OLD:     # Extract invoice number from QR payload for mode 17 (mandate)
+  # OLD:     # QR format typically contains invoiceNo=VALUE in query parameters
+  # OLD:     require Logger
+  # OLD:
+  # OLD:     qr_payload = case data do
+  # OLD:       %{qr_payload: payload} when is_binary(payload) -> payload
+  # OLD:       %{qr_string: payload} when is_binary(payload) -> payload
+  # OLD:       _ -> ""
+  # OLD:     end
+  # OLD:
+  # OLD:     Logger.debug("Parsing QR payload for invoiceNo: #{inspect(qr_payload)}")
+  # OLD:
+  # OLD:     result = cond do
+  # OLD:       # Try standard query parameter format: invoiceNo=VALUE
+  # OLD:       match = Regex.run(~r/[&?]invoiceNo=([^&\s]+)/i, qr_payload) ->
+  # OLD:         [_, invoice_no] = match
+  # OLD:         inv = URI.decode(String.trim(invoice_no))
+  # OLD:         Logger.debug("Found invoiceNo via query param: #{inv}")
+  # OLD:         inv
+  # OLD:
+  # OLD:       # Try colon separator: invoiceNo:VALUE
+  # OLD:       match = Regex.run(~r/invoiceNo:([^:&\s]+)/i, qr_payload) ->
+  # OLD:         [_, invoice_no] = match
+  # OLD:         inv = URI.decode(String.trim(invoice_no))
+  # OLD:         Logger.debug("Found invoiceNo via colon: #{inv}")
+  # OLD:         inv
+  # OLD:
+  # OLD:       # Default fallback - return nil so we can use generate_invoice_number()
+  # OLD:       true ->
+  # OLD:         Logger.debug("No invoiceNo found in QR payload")
+  # OLD:         nil
+  # OLD:     end
+  # OLD:
+  # OLD:     Logger.debug("Final extracted invoiceNo: #{result}")
+  # OLD:     result
+  # OLD:   end
+
+  # [RespValQr Payee.addr] REMOVED: extract_vpa_from_qr_string (1st clause) is unused now - pa is read with NpciFormat.qr_param (it also fell back to a made-up merchant@defaultbank)
+  # OLD:   defp extract_vpa_from_qr_string(qr_payload) do
+  # OLD:     # Parse QR payload to extract VPA (Virtual Payment Address)
+  # OLD:     # QR format can be: upi://pay?pa=VPA&... or pa=VPA in query parameters
+  # OLD:     require Logger
+  # OLD:     Logger.debug("Parsing QR payload for VPA: #{inspect(qr_payload)}")
+  # OLD:
+  # OLD:     result = cond do
+  # OLD:       # Try standard query parameter format: pa=VPA
+  # OLD:       match = Regex.run(~r/[&?]pa=([^&\s]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via query param: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try XML-like format: <pa>VPA</pa>
+  # OLD:       match = Regex.run(~r/<pa>([^<]+)<\/pa>/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via XML: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try colon separator: pa:VPA
+  # OLD:       match = Regex.run(~r/pa:([^:&\s]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via colon: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try simple format: paVPA (less common)
+  # OLD:       match = Regex.run(~r/pa([a-zA-Z0-9@.-]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via simple: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Default fallback - extract anything that looks like a VPA
+  # OLD:       match = Regex.run(~r/([a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via email pattern: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Last resort fallback
+  # OLD:       true ->
+  # OLD:         Logger.warning("No VPA found in QR payload, using default")
+  # OLD:         "merchant@defaultbank"
+  # OLD:     end
+  # OLD:
+  # OLD:     Logger.debug("Final extracted VPA: #{result}")
+  # OLD:     result
+  # OLD:   end
+
+
+  # [RespValQr Payee.addr] REMOVED: extract_vpa_from_qr_string (duplicate definition) is unused now - see above
+  # OLD:   defp extract_vpa_from_qr_string(qr_payload) do
+  # OLD:     # Parse QR payload to extract VPA (Virtual Payment Address)
+  # OLD:     # QR format can be: upi://pay?pa=VPA&... or pa=VPA in query parameters
+  # OLD:     require Logger
+  # OLD:     Logger.debug("Parsing QR payload for VPA: #{inspect(qr_payload)}")
+  # OLD:
+  # OLD:     result = cond do
+  # OLD:       # Try standard query parameter format: pa=VPA
+  # OLD:       match = Regex.run(~r/[&?]pa=([^&\s]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via query param: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try XML-like format: <pa>VPA</pa>
+  # OLD:       match = Regex.run(~r/<pa>([^<]+)<\/pa>/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via XML: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try colon separator: pa:VPA
+  # OLD:       match = Regex.run(~r/pa:([^:&\s]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via colon: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Try simple format: paVPA (less common)
+  # OLD:       match = Regex.run(~r/pa([a-zA-Z0-9@.-]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via simple: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Default fallback - extract anything that looks like a VPA
+  # OLD:       match = Regex.run(~r/([a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+)/i, qr_payload) ->
+  # OLD:         [_, vpa] = match
+  # OLD:         vpa = String.trim(vpa)
+  # OLD:         Logger.debug("Found VPA via email pattern: #{vpa}")
+  # OLD:         vpa
+  # OLD:
+  # OLD:       # Last resort fallback
+  # OLD:       true ->
+  # OLD:         Logger.warning("No VPA found in QR payload, using default")
+  # OLD:         "merchant@defaultbank"
+  # OLD:     end
+  # OLD:
+  # OLD:     Logger.debug("Final extracted VPA: #{result}")
+  # OLD:     result
+  # OLD:   end
 
   defp extract_merchant_location_from_qr(data) do
     # Extract merchant location from QR payload or other data sources
@@ -2815,29 +2993,31 @@ end
   defp format_txn_note(_), do: "MT_VALQRDEP"
 
   # T12: Format TXN.CUSTREF - must be present, length 12
-  defp format_cust_ref(nil), do: generate_12_char_custref()
-  defp format_cust_ref(""), do: generate_12_char_custref()
-  defp format_cust_ref(cust_ref) when is_binary(cust_ref) do
-    # Ensure exactly 12 characters
-    cust_ref
-      |> String.pad_trailing(12, "0")
-      |> String.slice(0, 12)
-  end
-  defp format_cust_ref(_), do: generate_12_char_custref()
+  # [RespValQr Txn.custRef] REMOVED: format_cust_ref (padded to 12 / generated a custRef) is unused now - custRef is echoed from the request
+  # OLD:   defp format_cust_ref(nil), do: generate_12_char_custref()
+  # OLD:   defp format_cust_ref(""), do: generate_12_char_custref()
+  # OLD:   defp format_cust_ref(cust_ref) when is_binary(cust_ref) do
+  # OLD:     # Ensure exactly 12 characters
+  # OLD:     cust_ref
+  # OLD:       |> String.pad_trailing(12, "0")
+  # OLD:       |> String.slice(0, 12)
+  # OLD:   end
+  # OLD:   defp format_cust_ref(_), do: generate_12_char_custref()
 
   # T14: Format TXN.PURPOSE - must be present/valid
-  defp format_purpose(nil), do: "11"
-  defp format_purpose(""), do: "11"
-  defp format_purpose(purpose) when is_binary(purpose) do
-    # Validate against NPCI purpose codes
-    valid_purposes = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15"]
-    if purpose in valid_purposes do
-      purpose
-    else
-      "11"  # Default to 11 (person to merchant)
-    end
-  end
-  defp format_purpose(_), do: "11"
+  # [RespValQr Txn.purpose] REMOVED: format_purpose (turned 62 / 66 into 11) is unused now - purpose is echoed from the request
+  # OLD:   defp format_purpose(nil), do: "11"
+  # OLD:   defp format_purpose(""), do: "11"
+  # OLD:   defp format_purpose(purpose) when is_binary(purpose) do
+  # OLD:     # Validate against NPCI purpose codes
+  # OLD:     valid_purposes = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15"]
+  # OLD:     if purpose in valid_purposes do
+  # OLD:       purpose
+  # OLD:     else
+  # OLD:       "11"  # Default to 11 (person to merchant)
+  # OLD:     end
+  # OLD:   end
+  # OLD:   defp format_purpose(_), do: "11"
 
   # T03: Validate and preserve TXN.NOTE - must be alphanumeric, length 1-50
   # CRITICAL: Preserve original note value as-is to avoid NPCI T03 validation errors
@@ -2856,14 +3036,15 @@ end
   end
   defp validate_and_preserve_note(_), do: "PAYMENT"
 
-  defp generate_12_char_custref do
-    # Generate a 12-character customer reference
-    timestamp = System.system_time(:millisecond) |> Integer.to_string()
-    random = :crypto.strong_rand_bytes(4) |> Base.encode16() |> String.slice(0, 8)
-    (timestamp <> random)
-      |> String.pad_trailing(12, "0")
-      |> String.slice(0, 12)
-  end
+  # [RespValQr Txn.custRef] REMOVED: generate_12_char_custref is unused now - no generated custRef
+  # OLD:   defp generate_12_char_custref do
+  # OLD:     # Generate a 12-character customer reference
+  # OLD:     timestamp = System.system_time(:millisecond) |> Integer.to_string()
+  # OLD:     random = :crypto.strong_rand_bytes(4) |> Base.encode16() |> String.slice(0, 8)
+  # OLD:     (timestamp <> random)
+  # OLD:       |> String.pad_trailing(12, "0")
+  # OLD:       |> String.slice(0, 12)
+  # OLD:   end
 
   # Helper functions for generating default QR values when not provided
   defp generate_default_expire_ts do
diff --git a/apps/upi_core/test/upi_core/npci_format_test.exs b/apps/upi_core/test/upi_core/npci_format_test.exs
index ae62ee8..4ed0301 100644
--- a/apps/upi_core/test/upi_core/npci_format_test.exs
+++ b/apps/upi_core/test/upi_core/npci_format_test.exs
@@ -66,6 +66,19 @@ defmodule UpiCore.NpciFormatTest do
     end
   end
 
+  describe "qr_text/2" do
+    test "percent-decodes free text but keeps '+'" do
+      assert NpciFormat.qr_text(@dynamic_qr, "pn") == "Dubai Fine Dining"
+      assert NpciFormat.qr_text(@dynamic_qr, "brand") == "Dubai Fine Dining"
+      assert NpciFormat.qr_text("upi://pay?pn=A+B%26C", "pn") == "A+B&C"
+    end
+
+    test "returns nil for a missing key and raw value for a malformed escape" do
+      assert NpciFormat.qr_text(@dynamic_qr, "nope") == nil
+      assert NpciFormat.qr_text("upi://pay?pn=100%zz", "pn") == "100%zz"
+    end
+  end
+
   describe "format_amount/1" do
     test "pads to 2 decimals" do
       assert NpciFormat.format_amount("150.0") == "150.00"
diff --git a/apps/upi_core/test/upi_core/resp_val_qr_test.exs b/apps/upi_core/test/upi_core/resp_val_qr_test.exs
new file mode 100644
index 0000000..8ecd6a0
--- /dev/null
+++ b/apps/upi_core/test/upi_core/resp_val_qr_test.exs
@@ -0,0 +1,269 @@
+defmodule UpiCore.RespValQrTest do
+  use ExUnit.Case, async: false
+
+  import SweetXml
+
+  alias UpiCore.UpiXmlSchema
+
+  # Test A: dynamic QR (mode 16, precert ACC_IP_01)
+  @dynamic_qr "upiGlobal://pay?ver=01&mode=16&purpose=11&orgid=MER1010001&tr=MER2df3f02447089a34f29d4c8cf14de915&tn=MT_1_5439&pa=restaurant@mercury&pn=Dubai%20Fine%20Dining&mc=5812&cu=INR&mid=AEREST001&msid=S001&mtid=T001&mType=SMALL&mGr=OFFLINE&mOnboarding=AGGREGATOR&mLoc=AE&brand=Dubai%20Fine%20Dining&cc=AE&bAm=5.00&bCurr=AED&am=120.66&qrMedium=03&invoiceNo=INV5903547760c1cc&invoiceDate=2026-07-09T14:46:11+05:30&invoiceName=Dubai Fine Dining&QRexpire=2026-07-09T15:16:11+05:30&QRts=2026-07-09T14:46:11+05:30&sign=X"
+
+  # Test C: static QR (mode 02) with a "+" in QRexpire
+  @static_qr "upi://pay?ver=01&mode=02&pa=shop@mercury&pn=Shop%20One&mc=5812&mid=M1&mtid=T1&mType=SMALL&mGr=OFFLINE&mOnboarding=AGGREGATOR&mLoc=AE&brand=Shop%20One&cc=AE&qrMedium=03&invoiceNo=INV1&invoiceDate=2027-04-29T14:15:05+05:30&QRexpire=2027-04-29T14:45:05+05:30&sign=X"
+
+  setup do
+    previous = Application.get_env(:upi_core, :psp_settlement_account)
+
+    Application.put_env(:upi_core, :psp_settlement_account,
+      ac_type: "CURRENT",
+      ac_num: "TESTACNUM001",
+      ifsc: "TEST0000001"
+    )
+
+    on_exit(fn ->
+      if previous,
+        do: Application.put_env(:upi_core, :psp_settlement_account, previous),
+        else: Application.delete_env(:upi_core, :psp_settlement_account)
+    end)
+
+    :ok
+  end
+
+  # What a caller passes: the request echo fields + the raw QR string
+  defp response_data(qr, overrides \\ %{}) do
+    Map.merge(
+      %{
+        org_id: "MER101",
+        req_msg_id: "NPCI0000000000000000000000000000001",
+        result: "SUCCESS",
+        err_code: "00",
+        txn_id: "NPCITXN0001",
+        note: "ACC_IP_01",
+        ref_id: "REFNPCI0001",
+        purpose: "11",
+        cust_ref: "CUSTREF01",
+        initiation_mode: nil,
+        txn_timestamp: "2026-07-09T14:46:30+05:30",
+        qr_payload: qr,
+        # values a caller may still pass that must NOT reach the XML
+        payee_addr: "db@mercury",
+        payee_name: "tester",
+        merchant_code: "5411",
+        merchant_brand: "Mercury PSP",
+        invoice_name: "Mercury PSP",
+        account_number: "1234567890",
+        ifsc_code: "MERC0000001",
+        expire_ts: "2020-01-01T00:00:00+05:30",
+        qr_version: "02",
+        qr_medium: "04"
+      },
+      overrides
+    )
+  end
+
+  defp attr(xml, path), do: xml |> xpath(~x"#{path}"s)
+
+  describe "generate_resp_val_qr/1 - dynamic QR (test A)" do
+    setup do
+      {:ok, xml} = UpiXmlSchema.generate_resp_val_qr(response_data(@dynamic_qr, %{initiation_mode: "16"}))
+      %{xml: xml}
+    end
+
+    test "QR expireTs / ts / qVer / qrMedium come from the QR string", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2026-07-09T15:16:11+05:30"
+      assert attr(xml, "//*[local-name()='QR']/@ts") == "2026-07-09T14:46:11+05:30"
+      assert attr(xml, "//*[local-name()='QR']/@qVer") == "01"
+      assert attr(xml, "//*[local-name()='QR']/@qrMedium") == "03"
+    end
+
+    test "Txn echoes the request", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Txn']/@id") == "NPCITXN0001"
+      assert attr(xml, "//*[local-name()='Txn']/@note") == "ACC_IP_01"
+      assert attr(xml, "//*[local-name()='Txn']/@refId") == "REFNPCI0001"
+      assert attr(xml, "//*[local-name()='Txn']/@custRef") == "CUSTREF01"
+      assert attr(xml, "//*[local-name()='Txn']/@purpose") == "11"
+      assert attr(xml, "//*[local-name()='Txn']/@initiationMode") == "16"
+      assert attr(xml, "//*[local-name()='Txn']/@ts") == "2026-07-09T14:46:30+05:30"
+    end
+
+    test "purpose / note / custRef are not rewritten", %{xml: _} do
+      {:ok, xml} =
+        UpiXmlSchema.generate_resp_val_qr(
+          response_data(@dynamic_qr, %{initiation_mode: "16", purpose: "62", note: "A-B.C D", cust_ref: "ABC"})
+        )
+
+      assert attr(xml, "//*[local-name()='Txn']/@purpose") == "62"
+      assert attr(xml, "//*[local-name()='Txn']/@note") == "A-B.C D"
+      assert attr(xml, "//*[local-name()='Txn']/@custRef") == "ABC"
+    end
+
+    test "Payee comes from the QR, not the DB / caller", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Payee']/@addr") == "restaurant@mercury"
+      assert attr(xml, "//*[local-name()='Payee']/@name") == "Dubai Fine Dining"
+      assert attr(xml, "//*[local-name()='Payee']/@code") == "5812"
+    end
+
+    test "Amount and Fx are bAm / bCurr with 2 decimals", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@value") == "5.00"
+      assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@curr") == "AED"
+      assert attr(xml, "//*[local-name()='FxList']/*[local-name()='Fx']/@baseAmount") == "5.00"
+      assert attr(xml, "//*[local-name()='FxList']/*[local-name()='Fx']/@baseCurr") == "AED"
+    end
+
+    test "there is no <Fx> inside <Merchant>", %{xml: xml} do
+      assert attr(xml, "count(//*[local-name()='Merchant']/*[local-name()='Fx'])") == "0"
+    end
+
+    test "Merchant Identifier / Name / Invoice come from the QR", %{xml: xml} do
+      id = "//*[local-name()='Identifier']"
+      assert attr(xml, id <> "/@mid") == "AEREST001"
+      assert attr(xml, id <> "/@sid") == "S001"
+      assert attr(xml, id <> "/@tid") == "T001"
+      assert attr(xml, id <> "/@merchantType") == "SMALL"
+      assert attr(xml, id <> "/@merchantGenre") == "OFFLINE"
+      assert attr(xml, id <> "/@onBoardingType") == "AGGREGATOR"
+      assert attr(xml, id <> "/@merchantLoc") == "AE"
+      assert attr(xml, id <> "/@subCode") == "5812"
+      assert attr(xml, "//*[local-name()='Name']/@brand") == "Dubai Fine Dining"
+      assert attr(xml, "//*[local-name()='Invoice']/@name") == "Dubai Fine Dining"
+      assert attr(xml, "//*[local-name()='Invoice']/@num") == "INV5903547760c1cc"
+      assert attr(xml, "//*[local-name()='Invoice']/@date") == "2026-07-09T14:46:11+05:30"
+    end
+
+    test "Institution netInstId is the value the caller passes (no hard-coded MER1010001)" do
+      {:ok, with_net} = UpiXmlSchema.generate_resp_val_qr(response_data(@dynamic_qr, %{initiation_mode: "16", net_inst_id: "NPCINET0001"}))
+      assert attr(with_net, "//*[local-name()='Institution']/@netInstId") == "NPCINET0001"
+
+      {:ok, without_net} = UpiXmlSchema.generate_resp_val_qr(response_data(@dynamic_qr, %{initiation_mode: "16"}))
+      assert attr(without_net, "//*[local-name()='Institution']/@netInstId") == ""
+      refute without_net =~ ~s(netInstId="MER1010001")
+    end
+
+    test "Institution conCode is cc and QrPayLoad is the QR", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Institution']/@conCode") == "AE"
+      assert attr(xml, "//*[local-name()='Institution']/@QrPayLoad") == @dynamic_qr
+    end
+
+    test "Payee Ac is the configured settlement account, no dummy values", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Detail'][@name='ACTYPE']/@value") == "CURRENT"
+      assert attr(xml, "//*[local-name()='Detail'][@name='ACNUM']/@value") == "TESTACNUM001"
+      assert attr(xml, "//*[local-name()='Detail'][@name='IFSC']/@value") == "TEST0000001"
+      refute xml =~ "1234567890"
+      refute xml =~ "MERC0000001"
+      refute xml =~ "Mercury PSP"
+    end
+
+    test "Head: orgId as given, msgId is a fresh 35 char id", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Head']/@orgId") == "MER101"
+      msg_id = attr(xml, "//*[local-name()='Head']/@msgId")
+      assert msg_id =~ ~r/^MER[0-9a-f]{32}$/
+
+      {:ok, xml2} = UpiXmlSchema.generate_resp_val_qr(response_data(@dynamic_qr, %{initiation_mode: "16"}))
+      refute msg_id == attr(xml2, "//*[local-name()='Head']/@msgId")
+    end
+
+    test "Resp has no errCode on SUCCESS", %{xml: xml} do
+      assert attr(xml, "//*[local-name()='Resp']/@result") == "SUCCESS"
+      assert attr(xml, "//*[local-name()='Resp']/@errCode") == ""
+    end
+  end
+
+  describe "generate_resp_val_qr/1 - failure paths" do
+    test "QM1 failure still carries Amount, Fx, Payee and Merchant from the QR (test B)" do
+      qr = String.replace(@dynamic_qr, "mc=5812", "mc=7995")
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(qr, "5812")
+
+      {:ok, xml} =
+        UpiXmlSchema.generate_resp_val_qr(
+          response_data(qr, %{initiation_mode: "16", result: "FAILURE", err_code: "QM1"})
+        )
+
+      assert attr(xml, "//*[local-name()='Resp']/@result") == "FAILURE"
+      assert attr(xml, "//*[local-name()='Resp']/@errCode") == "QM1"
+      assert attr(xml, "//*[local-name()='Payee']/@code") == "7995"
+      assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@value") == "5.00"
+      assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@curr") == "AED"
+      assert attr(xml, "//*[local-name()='Fx']/@baseAmount") == "5.00"
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2026-07-09T15:16:11+05:30"
+      assert attr(xml, "//*[local-name()='Txn']/@refId") == "REFNPCI0001"
+    end
+
+    test "PE failure keeps the request orgId given by the caller and the QR expiry" do
+      {:ok, xml} =
+        UpiXmlSchema.generate_resp_val_qr(
+          response_data(@dynamic_qr, %{initiation_mode: "16", result: "FAILURE", err_code: "PE", org_id: "MER101"})
+        )
+
+      assert attr(xml, "//*[local-name()='Head']/@orgId") == "MER101"
+      assert attr(xml, "//*[local-name()='Resp']/@errCode") == "PE"
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2026-07-09T15:16:11+05:30"
+    end
+
+    test "a failure with no refId in the request does not invent one" do
+      {:ok, xml} =
+        UpiXmlSchema.generate_resp_val_qr(
+          response_data(@dynamic_qr, %{initiation_mode: "16", result: "FAILURE", err_code: "ZR", ref_id: nil})
+        )
+
+      refute xml =~ "refId="
+    end
+  end
+
+  describe "generate_resp_val_qr/1 - static QR mode 02 (test C)" do
+    test "the + of QRexpire is kept and there is no Amount" do
+      {:ok, xml} = UpiXmlSchema.generate_resp_val_qr(response_data(@static_qr, %{initiation_mode: "02", org_id: "MER102"}))
+
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2027-04-29T14:45:05+05:30"
+      assert attr(xml, "//*[local-name()='QR']/@ts") == ""
+      assert attr(xml, "count(//*[local-name()='Amount'])") == "0"
+      assert attr(xml, "//*[local-name()='Head']/@orgId") == "MER102"
+    end
+
+    test "missing invoiceName does not crash and does not invent a name" do
+      assert {:ok, xml} = UpiXmlSchema.generate_resp_val_qr(response_data(@static_qr, %{initiation_mode: "02"}))
+      assert attr(xml, "//*[local-name()='Invoice']/@name") == ""
+      assert attr(xml, "//*[local-name()='Invoice']/@num") == "INV1"
+    end
+  end
+
+  describe "validate_qr_mcc/2 (QM1)" do
+    test "ok when mc is 4 digits and equals the merchant MCC" do
+      assert :ok = UpiXmlSchema.validate_qr_mcc(@dynamic_qr, "5812")
+    end
+
+    test "ok when the merchant MCC is unknown but format / blocked list pass" do
+      assert :ok = UpiXmlSchema.validate_qr_mcc(@dynamic_qr, nil)
+      assert :ok = UpiXmlSchema.validate_qr_mcc(@dynamic_qr, "")
+    end
+
+    test "QM1 when mc is missing, malformed, blocked or different from the merchant's" do
+      no_mc = String.replace(@dynamic_qr, "&mc=5812", "")
+      short = String.replace(@dynamic_qr, "mc=5812", "mc=581")
+      blocked = String.replace(@dynamic_qr, "mc=5812", "mc=7995")
+
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(no_mc, "5812")
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(short, "5812")
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(blocked, "7995")
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(@dynamic_qr, "5411")
+      assert {:error, "QM1"} = UpiXmlSchema.validate_qr_mcc(nil, "5812")
+    end
+  end
+
+  describe "parse_req_val_qr/1" do
+    test "keeps the request's Txn ts" do
+      xml = """
+      <ns2:ReqValQr xmlns:ns2="http://npci.org/upi/schema/">
+        <Head ver="2.0" ts="2026-07-09T14:46:20+05:30" orgId="NPCI" msgId="NPCI0000000000000000000000000000001"/>
+        <Txn id="NPCITXN0001" note="ACC_IP_01" refId="REFNPCI0001" refUrl="http://x" ts="2026-07-09T14:46:21+05:30" type="IntlQr" custRef="CUSTREF01" initiationMode="16" purpose="11"/>
+        <Payer addr="payer@npci" name="tester" seqNum="1" type="PERSON" code="0000"/>
+        <Institution netInstId="MER1010001" QrPayLoad="#{@dynamic_qr}" conCode="AE"/>
+      </ns2:ReqValQr>
+      """
+
+      assert {:ok, parsed} = UpiXmlSchema.parse_req_val_qr(xml)
+      assert parsed.txn_ts == "2026-07-09T14:46:21+05:30"
+      assert parsed.timestamp == "2026-07-09T14:46:20+05:30"
+      assert parsed.qr_payload == @dynamic_qr
+    end
+  end
+end
diff --git a/apps/upi_dynamic/lib/upi_dynamic/controllers/api/v1/upi_controller.ex b/apps/upi_dynamic/lib/upi_dynamic/controllers/api/v1/upi_controller.ex
index 74e850f..a003a00 100644
--- a/apps/upi_dynamic/lib/upi_dynamic/controllers/api/v1/upi_controller.ex
+++ b/apps/upi_dynamic/lib/upi_dynamic/controllers/api/v1/upi_controller.ex
@@ -44,7 +44,10 @@ defmodule UpiDynamic.Api.V1.UpiController do
         # --- Early QRts expiry check ---
         # If QR payload contains QRts and it's older than allowed window, send a PE RespValQr
         # to NPCI asynchronously but still reply to NPCI with the ACK immediately.
-        qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
+        # [RespValQr PE check] CHANGED: expiry is decided by the QR's QRexpire (was QRts + qr_validity_minutes). qr_ts_string/qr_ts_dt/diff_seconds below now hold QRexpire.
+        # TSD Table 2: a QR is expired (PE) when now > QRexpire
+        # OLD:         qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
+        qr_ts_string = UpiCore.NpciFormat.qr_param(Map.get(parsed_data, :qr_payload), "QRexpire")
         qr_validity_minutes = Application.get_env(:upi_dynamic, :qr_validity_minutes, 30)
 
         Logger.info(
@@ -99,7 +102,9 @@ defmodule UpiDynamic.Api.V1.UpiController do
               )
 
               # Consider expired if either QRts is older than the transaction ts or older than configured window
-              expired_by_window = diff_seconds > qr_validity_minutes * 60
+              # [RespValQr PE check] CHANGED: expired as soon as now is past QRexpire (diff_seconds = now - QRexpire), not after a 30 minute window
+              # OLD:               expired_by_window = diff_seconds > qr_validity_minutes * 60
+              expired_by_window = diff_seconds > 0
 
               expired_by_txn_ts =
                 if txn_ts_dt, do: DateTime.compare(qr_ts_dt, txn_ts_dt) == :lt, else: false
@@ -111,26 +116,41 @@ defmodule UpiDynamic.Api.V1.UpiController do
               if expired_by_window do
                 # Build RespValQr error with Payload Expired
                 resp_data = %{
-                  org_id: Map.get(parsed_data, :org_id, "MER101"),
+                  # [RespValQr Head.orgId] CHANGED: PSP orgId (MER101), the request's orgId "NPCI" was echoed before
+                  # OLD:                   org_id: Map.get(parsed_data, :org_id, "MER101"),
+                  org_id: Application.get_env(:upi_dynamic, :psp_org_id, "MER101"),
                   msg_id: generate_message_id(),
                   req_msg_id: parsed_data.msg_id,
                   result: "FAILURE",
                   err_code: "PE",
                   txn_id: parsed_data.txn_id,
-                  note: parsed_data.note || "QR Payload expired",
-                  ref_id: parsed_data.ref_id || generate_message_id(),
+                  # [RespValQr Txn.note / Txn.refId] CHANGED: echo the request (no "QR Payload expired" note, no generated refId)
+                  # OLD:                   note: parsed_data.note || "QR Payload expired",
+                  # OLD:                   ref_id: parsed_data.ref_id || generate_message_id(),
+                  note: parsed_data.note,
+                  ref_id: parsed_data.ref_id,
                   ref_url: parsed_data.ref_url || "",
-                  purpose: parsed_data.purpose || "11",
-                  cust_ref: parsed_data.cust_ref || "",
-                  initiation_mode: parsed_data.initiation_mode || "16",
-                  txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                  # [RespValQr Txn.purpose / custRef / initiationMode / ts] CHANGED: echo the request (no 11 / "" / 16 defaults, Txn@ts instead of Head ts)
+                  # OLD:                   purpose: parsed_data.purpose || "11",
+                  # OLD:                   cust_ref: parsed_data.cust_ref || "",
+                  # OLD:                   initiation_mode: parsed_data.initiation_mode || "16",
+                  # OLD:                   txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                  purpose: parsed_data.purpose,
+                  cust_ref: parsed_data.cust_ref,
+                  initiation_mode: parsed_data.initiation_mode,
+                  txn_timestamp: Map.get(parsed_data, :txn_ts),
                   qr_payload: parsed_data.qr_payload,
-                  payee_addr: Map.get(parsed_data, :payer_addr) || "",
-                  payee_name: Map.get(parsed_data, :payer_name) || "Mercury PSP",
+                  # [RespValQr Payee.addr / Payee.name] REMOVED: payer address / payer name ("Mercury PSP") used as payee; the generator sends the QR's pa / pn
+                  # OLD:                   payee_addr: Map.get(parsed_data, :payer_addr) || "",
+                  # OLD:                   payee_name: Map.get(parsed_data, :payer_name) || "Mercury PSP",
                   payee_type: "ENTITY",
-                  merchant_code: "0000",
-                  country_code: "IN",
-                  net_inst_id: "MER1010001"
+                  # [RespValQr Payee.code / Institution.conCode] REMOVED: dummy 0000 / IN; the generator sends the QR's mc / cc
+                  # OLD:                   merchant_code: "0000",
+                  # OLD:                   country_code: "IN",
+                  # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+                  # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+                  # OLD:                   net_inst_id: "MER1010001"
+                  net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_dynamic, :psp_net_inst_id)
                 }
 
                 # Generate the error XML now and send asynchronously so we still return ACK to caller
diff --git a/apps/upi_dynamic/lib/upi_dynamic/upi_transaction_manager.ex b/apps/upi_dynamic/lib/upi_dynamic/upi_transaction_manager.ex
index f3c80c1..74720f7 100644
--- a/apps/upi_dynamic/lib/upi_dynamic/upi_transaction_manager.ex
+++ b/apps/upi_dynamic/lib/upi_dynamic/upi_transaction_manager.ex
@@ -11,6 +11,7 @@ defmodule UpiDynamic.UpiTransactionManager do
 
   alias UpiCore.Transactions.UpiInternationalService
   alias UpiCore.UpiXmlSchema
+  alias UpiCore.NpciFormat
   alias UpiCore.QRValidation.Service, as: QRValidationService
   alias UpiCore.QRValidation.StaticQrService
   alias UpiCore.Transactions.{ReqChkTxn, ReqChkTxnService, DynamicQr}
@@ -64,6 +65,9 @@ defmodule UpiDynamic.UpiTransactionManager do
             {:error, reason} -> {:error, reason}
           end
 
+        {:error, {xml, msg}} when is_binary(xml) ->
+          # [RespValQr QM1] ADDED: error XML already built (e.g. QM1) - send it as is instead of wrapping it in a generic 02
+          {:error, {xml, msg}}
         {:error, reason} ->
           Logger.error("Static QR validation failed: #{inspect(reason)}")
           case generate_error_response(parsed_data, "02", "Static QR validation failed: #{inspect(reason)}") do
@@ -115,7 +119,10 @@ end
 
   # Helper function for regular (non-static) QR validation
   defp process_regular_qr_validation(parsed_data, qr_data) do
-      case validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode) do
+      # [RespValQr QM1] CHANGED: merchant lookup now also checks the QR's mc (missing / not 4 digits / not merchant's MCC / blocked e.g. 7995 -> QM1)
+      # TSD Table 2: QM1 = invalid merchant category code
+      # OLD:       case validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode) do
+      case validate_merchant_qr_with_mcc(parsed_data) do
         {:ok, merchant_info} ->
           # Create QR validation record for monitoring
           qr_validation_attrs = %{
@@ -141,7 +148,9 @@ end
             status: "validated",
             validation_type: determine_validation_type(parsed_data),
             corridor: determine_corridor_from_context(parsed_data, extract_currency_from_qr(parsed_data.qr_payload) || "INR"),
-            merchant_category: merchant_info.merchant_category || "5411",
+            # [RespValQr Payee.code] CHANGED: no invented MCC 5411 on the stored validation row
+            # OLD:             merchant_category: merchant_info.merchant_category || "5411",
+            merchant_category: merchant_info.merchant_category,
             initiation_mode: parsed_data.initiation_mode,  # Add initiation mode for static/dynamic QR validation
             raw_xml: qr_data,
             fx_timestamp: DateTime.utc_now(),
@@ -175,51 +184,75 @@ end
                   Logger.info("[RespValQr Amount] Final: amount=#{resp_amount}, currency=#{resp_currency}")
 
                   response_data = %{
-                org_id: get_psp_org_id(),
+                # [RespValQr Head.orgId] CHANGED: dynamic PSP orgId is MER101 (config :upi_dynamic, :psp_org_id)
+                # OLD:                 org_id: get_psp_org_id(),
+                org_id: Application.get_env(:upi_dynamic, :psp_org_id, "MER101"),
                 msg_id: generate_message_id(),
                 req_msg_id: parsed_data.msg_id,
                 result: "SUCCESS",
                 err_code: "00",
                 txn_id: parsed_data.txn_id,  # Use NPCI transaction ID from request
-                note: parsed_data.note || "MT_VALQRDEP",
-                ref_id: parsed_data.ref_id || generate_reference_id(),
+                # [RespValQr Txn.note / Txn.refId] CHANGED: echo the request's note and refId (no MT_VALQRDEP default, no generated REF...)
+                # TSD Table 2: Txn note / refId are copied from ReqValQr
+                # OLD:                 note: parsed_data.note || "MT_VALQRDEP",
+                # OLD:                 ref_id: parsed_data.ref_id || generate_reference_id(),
+                note: parsed_data.note,
+                ref_id: parsed_data.ref_id,
                 ref_url: parsed_data.ref_url || "",
-                purpose: purpose,
-                cust_ref: parsed_data.cust_ref || generate_customer_reference(),
+                # [RespValQr Txn.purpose / Txn.custRef] CHANGED: echo the request's purpose and custRef (no 11 default, no generated custRef)
+                # OLD:                 purpose: purpose,
+                # OLD:                 cust_ref: parsed_data.cust_ref || generate_customer_reference(),
+                purpose: parsed_data.purpose,
+                cust_ref: parsed_data.cust_ref,
                 initiation_mode: parsed_data.initiation_mode,
-                txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                # [RespValQr Txn.ts] CHANGED: Txn ts is the request's Txn@ts (was Head ts / now)
+                # TSD Table 2: Txn ts = ts received in ReqValQr
+                # OLD:                 txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                txn_timestamp: Map.get(parsed_data, :txn_ts),
                 qr_payload: parsed_data.qr_payload,
 
                 # Payee information
                 payee_addr: merchant_info.merchant_vpa,
                 payee_name: merchant_info.brand_name,
                 payee_type: "ENTITY",
-                merchant_code: merchant_info.merchant_category || "5411",
+                # [RespValQr Payee.code] CHANGED: no invented MCC 5411 (generator sends the QR's mc anyway)
+                # OLD:                 merchant_code: merchant_info.merchant_category || "5411",
+                merchant_code: merchant_info.merchant_code,
 
                 # Institution information
                 country_code: merchant_info.country || "IN",
-                net_inst_id: "MER1010001",
+                # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+                # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+                # OLD:                 net_inst_id: "MER1010001",
+                net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_dynamic, :psp_net_inst_id),
 
                 # Merchant identification
-                sub_code: extract_merchant_category_from_qr(parsed_data.qr_payload) || merchant_info.merchant_category || "5411",
+                # [RespValQr Identifier.subCode] REMOVED: dummy 5411 default (generator sends the QR's mc)
+                # OLD:                 sub_code: extract_merchant_category_from_qr(parsed_data.qr_payload) || merchant_info.merchant_category || "5411",
                 merchant_id: merchant_info.mid,  # Use merchant ID
-                store_id: merchant_info.sid || "STORE001",
-                terminal_id: merchant_info.tid || "TERM001",
-                merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.merchant_genre || "RETAIL",
+                # [RespValQr Identifier sid/tid/merchantGenre] REMOVED: STORE001 / TERM001 / RETAIL dummies (generator sends msid / mtid / mGr from the QR)
+                # OLD:                 store_id: merchant_info.sid || "STORE001",
+                # OLD:                 terminal_id: merchant_info.tid || "TERM001",
+                # OLD:                 merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.merchant_genre || "RETAIL",
                 onboarding_type: merchant_info.onboarding_type || "AGGREGATOR",
-                reg_id: merchant_info.mid || "REG001",
-                pin_code: merchant_info.pincode || "560001",
-                tier: merchant_info.tier || "M2",
-                merchant_location: merchant_info.city || "Bangalore",
+                # [RespValQr Merchant dummy values] REMOVED: REG001 / 560001 / M2 / Bangalore are invented and not part of RespValQr
+                # OLD:                 reg_id: merchant_info.mid || "REG001",
+                # OLD:                 pin_code: merchant_info.pincode || "560001",
+                # OLD:                 tier: merchant_info.tier || "M2",
+                # OLD:                 merchant_location: merchant_info.city || "Bangalore",
                 merchant_inst_code: merchant_info.inst_code || "",
 
                 # Identifier fields for <Identifier> tag in RespValQr
-                identifier_sub_code: merchant_info.sub_code || extract_merchant_category_from_qr(parsed_data.qr_payload) || "0000",
+                # [RespValQr Identifier.subCode] CHANGED: dummy "0000" default removed
+                # OLD:                 identifier_sub_code: merchant_info.sub_code || extract_merchant_category_from_qr(parsed_data.qr_payload) || "0000",
+                identifier_sub_code: merchant_info.sub_code || extract_merchant_category_from_qr(parsed_data.qr_payload),
                 identifier_mid: merchant_info.mid || "",
                 identifier_sid: merchant_info.sid || "",
                 identifier_tid: merchant_info.tid || "",
                 identifier_merchant_type: extract_merchant_type_from_qr(parsed_data.qr_payload) || merchant_info.merchant_type || "SMALL",
-                identifier_merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.merchant_genre || "RETAIL",
+                # [RespValQr Identifier.merchantGenre] CHANGED: dummy RETAIL default removed (generator sends mGr from the QR)
+                # OLD:                 identifier_merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.merchant_genre || "RETAIL",
+                identifier_merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.merchant_genre,
                 identifier_onboarding_type: merchant_info.onboarding_type || "AGGREGATOR",
                 identifier_merchant_loc: merchant_info.country || "IN",
                 identifier_inst_code: merchant_info.inst_code || "",
@@ -235,9 +268,13 @@ end
                 invoice_number: extract_invoice_number_from_qr(parsed_data.qr_payload),
                 invoice_date: extract_invoice_date_from_qr(parsed_data.qr_payload) || get_timestamp(),
                 # Account details
-                ifsc_code: merchant_info.ifsc_code || "MERC0000001",
-                account_type: merchant_info.account_type || "CURRENT",
-                account_number: merchant_info.account_number || "1234567890",
+                # [RespValQr Payee.Ac] CHANGED: dummy MERC0000001 / 1234567890 / CURRENT defaults removed; generator sends our settlement account from config
+                # OLD:                 ifsc_code: merchant_info.ifsc_code || "MERC0000001",
+                # OLD:                 account_type: merchant_info.account_type || "CURRENT",
+                # OLD:                 account_number: merchant_info.account_number || "1234567890",
+                ifsc_code: merchant_info.ifsc_code,
+                account_type: merchant_info.account_type,
+                account_number: merchant_info.account_number,
                 # Amount and currency - GLOBAL UPI uses merchant currency, DOMESTIC uses INR
                 amount: resp_amount,
                 currency: resp_currency,
@@ -250,9 +287,14 @@ end
                 markup_percentage: merchant_info.markup_rate || "0.00",
 
                 # QR information
-                qr_version: "01",
-                qr_medium: "03",
-                expire_ts: get_expiry_timestamp(),
+                # [RespValQr QR.qVer / QR.qrMedium / QR.expireTs] CHANGED: taken from the QR string (ver / qrMedium / QRexpire), not 01 / 03 / now+5min
+                # TSD Table 2: QR expireTs = QRexpire value of the QR string
+                # OLD:                 qr_version: "01",
+                # OLD:                 qr_medium: "03",
+                # OLD:                 expire_ts: get_expiry_timestamp(),
+                qr_version: NpciFormat.qr_param(parsed_data.qr_payload, "ver"),
+                qr_medium: NpciFormat.qr_param(parsed_data.qr_payload, "qrMedium"),
+                expire_ts: NpciFormat.qr_param(parsed_data.qr_payload, "QRexpire"),
                 ver_token: qr_validation.ver_token,
                 stan: generate_stan()
               }
@@ -288,6 +330,13 @@ end
           end
       end
 
+    {:error, "QM1"} ->
+      # [RespValQr QM1] ADDED: mc missing / not 4 digits / not the merchant's MCC / blocked (7995) -> FAILURE errCode="QM1"
+      case generate_error_response(parsed_data, "QM1", "Invalid merchant category code in QR") do
+        {:ok, xml} -> {:error, {xml, "Invalid merchant category code (QM1)"}}
+        {:error, reason} -> {:error, reason}
+      end
+
     {:error, "QR_NOT_FOUND"} ->
       case generate_error_response(parsed_data, "ZQ", "QR code not found") do
         {:ok, xml} -> {:error, {xml, "QR code not found"}}
@@ -315,6 +364,15 @@ end
       end
   end
 
+  # [RespValQr QM1] ADDED: merchant lookup followed by the QR mc check (QM1)
+  defp validate_merchant_qr_with_mcc(parsed_data) do
+    with {:ok, merchant_info} <-
+           validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode),
+         :ok <- UpiXmlSchema.validate_qr_mcc(parsed_data.qr_payload, Map.get(merchant_info, :merchant_code)) do
+      {:ok, merchant_info}
+    end
+  end
+
   # Process static QR validation with database lookup
   defp process_static_qr_validation(static_qr, parsed_data, qr_data) do
     Logger.info("=== PROCESSING STATIC QR VALIDATION ===")
@@ -338,8 +396,20 @@ end
       _ -> nil
     end
 
+    # [RespValQr QM1] ADDED: same mc check for a QR found in the static QR table
+    static_mcc_check =
+      UpiXmlSchema.validate_qr_mcc(
+        parsed_data.qr_payload,
+        case merchant_info_from_validation do
+          %{} = info -> Map.get(info, :merchant_code)
+          _ -> nil
+        end
+      )
+
     # Create transaction using static QR data
-    case create_transaction_for_static_qr(static_qr, parsed_data, merchant_info_from_validation) do
+    # [RespValQr QM1] CHANGED: the transaction is only created when the QR's mc passed the QM1 check
+    # OLD:     case create_transaction_for_static_qr(static_qr, parsed_data, merchant_info_from_validation) do
+    case (if static_mcc_check == :ok, do: create_transaction_for_static_qr(static_qr, parsed_data, merchant_info_from_validation), else: {:error, :qm1}) do
       {:ok, transaction} ->
         Logger.info("✅ Transaction created for static QR: #{transaction.id}")
 
@@ -433,6 +503,13 @@ end
             generate_error_response(parsed_data, "02", "QR validation storage failed: #{inspect(qr_validation_error)}")
         end
 
+      {:error, :qm1} ->
+        # [RespValQr QM1] ADDED: FAILURE errCode="QM1" for a QR with a missing / blocked / mismatching mc
+        case generate_error_response(parsed_data, "QM1", "Invalid merchant category code in QR") do
+          {:ok, xml} -> {:error, {xml, "Invalid merchant category code (QM1)"}}
+          {:error, reason} -> {:error, reason}
+        end
+
       {:error, reason} ->
         Logger.error("❌ Transaction creation failed for static QR: #{inspect(reason)}")
         generate_error_response(parsed_data, "02", "Transaction creation failed: #{inspect(reason)}")
@@ -516,21 +593,29 @@ end
 
 
     # --- BEGIN PATCH: Override orgId and NetInstId for static QR (initiation_mode == "02") ---
-    initiation_mode =
+    # [RespValQr Institution.netInstId] CHANGED: initiation_mode no longer picks a literal netInstId, so the variable is unused
+    # OLD:     initiation_mode =
+    _initiation_mode =
       case qr_validation do
         %{initiation_mode: mode} -> mode
         _ -> Map.get(parsed_data, :initiation_mode) || Map.get(parsed_data, "initiation_mode")
       end
 
+    # [RespValQr Head.orgId] CHANGED: dynamic PSP orgId is MER101 on every path (was the request's orgId "NPCI" or MER102)
+    # OLD:     {org_id, network_inst_id} =
+    # OLD:       if initiation_mode == "02" do
+    # OLD:         {"MER102", "MER1010002"}
+    # OLD:       else
+    # OLD:         {
+    # OLD:           Map.get(qr_validation, :org_id) || Map.get(parsed_data, :org_id) || Map.get(parsed_data, "org_id"),
+    # OLD:           Map.get(qr_validation, :network_inst_id) || Map.get(parsed_data, :network_inst_id) || Map.get(parsed_data, "network_inst_id")
+    # OLD:         }
+    # OLD:       end
     {org_id, network_inst_id} =
-      if initiation_mode == "02" do
-        {"MER102", "MER1010002"}
-      else
-        {
-          Map.get(qr_validation, :org_id) || Map.get(parsed_data, :org_id) || Map.get(parsed_data, "org_id"),
-          Map.get(qr_validation, :network_inst_id) || Map.get(parsed_data, :network_inst_id) || Map.get(parsed_data, "network_inst_id")
-        }
-      end
+      {
+        Application.get_env(:upi_dynamic, :psp_org_id, "MER101"),
+        Map.get(qr_validation, :network_inst_id) || Map.get(parsed_data, :network_inst_id) || Map.get(parsed_data, "network_inst_id")
+      }
     # --- END PATCH ---
 
     %{
@@ -541,13 +626,21 @@ end
       result: "SUCCESS",
       err_code: "00",
       txn_id: parsed_data.txn_id,
-      note: parsed_data.note || "Static QR validated",
-      ref_id: parsed_data.ref_id || generate_reference_id(),
+      # [RespValQr Txn.note / Txn.refId] CHANGED: echo the request (no "Static QR validated" note, no generated REF...)
+      # OLD:       note: parsed_data.note || "Static QR validated",
+      # OLD:       ref_id: parsed_data.ref_id || generate_reference_id(),
+      note: parsed_data.note,
+      ref_id: parsed_data.ref_id,
       ref_url: parsed_data.ref_url || "",
-      purpose: parsed_data.purpose || "11",
-      cust_ref: parsed_data.cust_ref || generate_customer_reference(),
-      initiation_mode: static_qr.initiation_mode,
-      txn_timestamp: parsed_data.timestamp || get_timestamp(),
+      # [RespValQr Txn.purpose / custRef / initiationMode / ts] CHANGED: echo the request (no 11 default, no generated custRef, DB initiation mode, Head ts)
+      # OLD:       purpose: parsed_data.purpose || "11",
+      # OLD:       cust_ref: parsed_data.cust_ref || generate_customer_reference(),
+      # OLD:       initiation_mode: static_qr.initiation_mode,
+      # OLD:       txn_timestamp: parsed_data.timestamp || get_timestamp(),
+      purpose: parsed_data.purpose,
+      cust_ref: parsed_data.cust_ref,
+      initiation_mode: parsed_data.initiation_mode,
+      txn_timestamp: Map.get(parsed_data, :txn_ts),
       qr_payload: Map.get(parsed_data, :qr_payload) || Map.get(parsed_data, "qr_payload"),
 
       # Payee information (prefer validated merchant details)
@@ -559,19 +652,23 @@ end
 
       # Institution information
       country_code: get_country_code_from_corridor(static_qr.corridor),
-      net_inst_id:
-        if initiation_mode == "02" do
-          "MER1020002"
-        else
-          "MER1010001"
-        end,
+      # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+      # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+      # OLD:       net_inst_id:
+      # OLD:         if initiation_mode == "02" do
+      # OLD:           "MER1020002"
+      # OLD:         else
+      # OLD:           "MER1010001"
+      # OLD:         end,
+      net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_dynamic, :psp_net_inst_id),
 
       # Merchant identification
       sub_code: static_qr.merchant_category,
       merchant_id: static_qr.merchant_id,
-      store_id: "STORE001",
-      terminal_id: "TERM001",
-      merchant_genre: "RETAIL",
+      # [RespValQr Identifier sid/tid/merchantGenre] REMOVED: STORE001 / TERM001 / RETAIL dummies (generator sends msid / mtid / mGr from the QR)
+      # OLD:       store_id: "STORE001",
+      # OLD:       terminal_id: "TERM001",
+      # OLD:       merchant_genre: "RETAIL",
       onboarding_type: "AGGREGATOR",
       reg_id: "REG001",
       pin_code: "560001",
@@ -606,9 +703,13 @@ end
       markup_percentage: Decimal.to_string(static_qr.markup_percentage),
 
       # QR information
-      qr_version: "02",
-      qr_medium: "03",
-      expire_ts: get_expiry_timestamp(),
+      # [RespValQr QR.qVer / QR.qrMedium / QR.expireTs] CHANGED: taken from the QR string (ver / qrMedium / QRexpire), not 02 / 03 / now+5min
+      # OLD:       qr_version: "02",
+      # OLD:       qr_medium: "03",
+      # OLD:       expire_ts: get_expiry_timestamp(),
+      qr_version: NpciFormat.qr_param(Map.get(parsed_data, :qr_payload), "ver"),
+      qr_medium: NpciFormat.qr_param(Map.get(parsed_data, :qr_payload), "qrMedium"),
+      expire_ts: NpciFormat.qr_param(Map.get(parsed_data, :qr_payload), "QRexpire"),
       ver_token: qr_validation.ver_token,
       stan: generate_stan(),
 
@@ -2690,9 +2791,10 @@ end
     Application.get_env(:upi_dynamic, :psp_ref_url, "https://mercury.upi.in")
   end
 
-  defp generate_customer_reference do
-    "MERC" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
-  end
+  # [RespValQr Txn.custRef] REMOVED: generate_customer_reference is unused now - custRef is echoed from the request
+  # OLD:   defp generate_customer_reference do
+  # OLD:     "MERC" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
+  # OLD:   end
 
   defp generate_reference_id do
     timestamp = System.system_time(:millisecond)
@@ -2926,9 +3028,13 @@ end
           brand_name: merchant.brand_name,
           legal_name: merchant.legal_name,
           merchant_type: merchant.merchant_type,
-          merchant_category: merchant.business_category || "5411",
-          merchant_genre: merchant.merchant_genre || "RETAIL",
-          onboarding_type: merchant.onboarding_type || "AGGREGATOR",
+          # [RespValQr Payee.code / Merchant.Identifier] CHANGED: no invented 5411 / RETAIL / AGGREGATOR defaults in merchant_info
+          # OLD:           merchant_category: merchant.business_category || "5411",
+          # OLD:           merchant_genre: merchant.merchant_genre || "RETAIL",
+          # OLD:           onboarding_type: merchant.onboarding_type || "AGGREGATOR",
+          merchant_category: merchant.business_category,
+          merchant_genre: merchant.merchant_genre,
+          onboarding_type: merchant.onboarding_type,
 
           # Location details
           address: merchant.address,
@@ -3407,7 +3513,9 @@ end
     keys = [
       :msg_id, :txn_id, :org_id, :payer_addr, :payer_name, :net_inst_id, :con_code,
       :note, :ref_id, :ref_url, :purpose, :cust_ref, :initiation_mode, :timestamp,
-      :qr_payload, :amount, :currency, :base_curr, :tx_type, :txn_type
+      # [ReqValQr Txn.ts] ADDED: :txn_ts is kept so RespValQr can echo the request's Txn@ts
+      # OLD:       :qr_payload, :amount, :currency, :base_curr, :tx_type, :txn_type
+      :qr_payload, :amount, :currency, :base_curr, :tx_type, :txn_type, :txn_ts
     ]
 
     Enum.reduce(keys, %{}, fn key, acc ->
@@ -3523,84 +3631,110 @@ end
     "Partner Ref: #{partner_response.partner_txn_id}, Rate: #{partner_response.fx_rate}"
   end
 
-  defp generate_error_response(parsed_data, error_code, error_msg) do
-    error_merchant_code =
-      case Regex.run(~r/(?:[?&]|&amp;)pa=([^&\s]+)/i, parsed_data.qr_payload || "") do
-        [_, vpa] ->
-          case UpiCore.Merchants.get_merchant_by_vpa(URI.decode(vpa)) do
-            %UpiCore.Merchants.Merchant{business_category: bc} when is_binary(bc) and bc != "" -> bc
-            _ -> extract_merchant_code_from_qr(parsed_data.qr_payload) || "0000"
-          end
-        _ -> extract_merchant_code_from_qr(parsed_data.qr_payload) || "0000"
-      end
+  # [RespValQr error message] CHANGED: error_msg is no longer used as Txn.note
+  # OLD:   defp generate_error_response(parsed_data, error_code, error_msg) do
+  defp generate_error_response(parsed_data, error_code, _error_msg) do
+    # [RespValQr Payee.code] REMOVED: error_merchant_code (DB MCC / "0000" fallback); the generator sends the QR's mc on every path
+    # OLD:     error_merchant_code =
+    # OLD:       case Regex.run(~r/(?:[?&]|&amp;)pa=([^&\s]+)/i, parsed_data.qr_payload || "") do
+    # OLD:         [_, vpa] ->
+    # OLD:           case UpiCore.Merchants.get_merchant_by_vpa(URI.decode(vpa)) do
+    # OLD:             %UpiCore.Merchants.Merchant{business_category: bc} when is_binary(bc) and bc != "" -> bc
+    # OLD:             _ -> extract_merchant_code_from_qr(parsed_data.qr_payload) || "0000"
+    # OLD:           end
+    # OLD:         _ -> extract_merchant_code_from_qr(parsed_data.qr_payload) || "0000"
+    # OLD:       end
 
     response_data = %{
-      org_id: get_psp_org_id(),
+      # [RespValQr generate_error_response] CHANGED: error path echoes the request and reads payee / merchant / amount / invoice from the QR string (done in the generator)
+      # REMOVED invented values: Mercury PSP, payer name as payee, 0000 / 00 merchant code, MERCURY001, STORE001, TERM001, RETAIL, INV000000, MERC0000001, 0000000000, INR/0.00 amounts
+      # TSD Table 2: same RespValQr content on success and failure; only Resp result / errCode differ
+      # OLD:       org_id: get_psp_org_id(),
+      # OLD:       msg_id: generate_message_id(),
+      # OLD:       req_msg_id: parsed_data.msg_id || generate_message_id(),
+      # OLD:       result: "FAILURE",
+      # OLD:       err_code: error_code,
+      # OLD:       txn_id: parsed_data.txn_id || generate_transaction_id(),  # Use NPCI txn_id if available
+      # OLD:       # Use the incoming ReqValQr txn note when available (T03 compliance) otherwise fall back to the error message
+      # OLD:       note: Map.get(parsed_data, :note) || error_msg,
+      # OLD:       ref_id: generate_reference_id(),
+      # OLD:       ref_url: "https://mercurypay.aritic.app",
+      # OLD:   purpose: Map.get(parsed_data, :purpose) || extract_purpose(parsed_data),
+      # OLD:       cust_ref: Map.get(parsed_data, :cust_ref) || generate_customer_reference(),
+      # OLD:       qr_payload: parsed_data.qr_payload || "",
+      # OLD:       initiation_mode: parsed_data.initiation_mode || "01",
+      # OLD:
+      # OLD:       # Extract payee information from request data
+      # OLD:       payee_addr: parsed_data.payer_addr || "merchant@mercury",
+      # OLD:       payee_name: extract_payee_name_from_qr(parsed_data.qr_payload) || "Mercury PSP",
+      # OLD:       payee_type: "ENTITY",
+      # OLD:       merchant_code: error_merchant_code,
+      # OLD:
+      # OLD:       # Default institution information
+      # OLD:       country_code: "IN",
+      # OLD:       net_inst_id: "MER1010001",
+      # OLD:
+      # OLD:       # Default merchant identification
+      # OLD:       sub_code: "00",
+      # OLD:       merchant_id: "MERCURY001",
+      # OLD:       store_id: "STORE001",
+      # OLD:       terminal_id: "TERM001",
+      # OLD:       merchant_type: "SMALL",
+      # OLD:       merchant_genre: "RETAIL",
+      # OLD:       onboarding_type: "AGGREGATOR",
+      # OLD:       reg_id: "REG001",
+      # OLD:       pin_code: "000000",
+      # OLD:       tier: "M2",
+      # OLD:       merchant_location: "IN",
+      # OLD:       merchant_inst_code: "MERC",
+      # OLD:
+      # OLD:       # Default merchant names
+      # OLD:       merchant_brand: "Mercury PSP",
+      # OLD:       legal_name: "Mercury Payment Services",
+      # OLD:       franchise_name: "Mercury",
+      # OLD:
+      # OLD:       # Default ownership and invoice
+      # OLD:       ownership_type: "PRIVATE",
+      # OLD:       invoice_name: "Mercury PSP",
+      # OLD:       invoice_number: "INV000000",
+      # OLD:
+      # OLD:       # Default account details
+      # OLD:       ifsc_code: "MERC0000001",
+      # OLD:       account_type: "CURRENT",
+      # OLD:       account_number: "0000000000",
+      # OLD:
+      # OLD:       # Default amount and currency
+      # OLD:       # For error responses, extract bAm (baseAmount) specifically from QR payload
+      # OLD:       currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
+      # OLD:       amount: extract_base_amount_from_qr(parsed_data.qr_payload) || "0.00",
+      # OLD:       split_name: "MAIN",
+      # OLD:
+      # OLD:       # FX information - extract from QR payload for error responses
+      # OLD:       base_amount: extract_base_amount_from_qr(parsed_data.qr_payload) || "0.00",
+      # OLD:       base_currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
+      # OLD:       fx_active: "N",
+      # OLD:       fx_rate: "1.00",
+      # OLD:       markup_percentage: "0.00"
+      org_id: Application.get_env(:upi_dynamic, :psp_org_id, "MER101"),
       msg_id: generate_message_id(),
-      req_msg_id: parsed_data.msg_id || generate_message_id(),
+      req_msg_id: Map.get(parsed_data, :msg_id),
       result: "FAILURE",
       err_code: error_code,
-      txn_id: parsed_data.txn_id || generate_transaction_id(),  # Use NPCI txn_id if available
-      # Use the incoming ReqValQr txn note when available (T03 compliance) otherwise fall back to the error message
-      note: Map.get(parsed_data, :note) || error_msg,
-      ref_id: generate_reference_id(),
+      txn_id: Map.get(parsed_data, :txn_id),
+      note: Map.get(parsed_data, :note),
+      ref_id: Map.get(parsed_data, :ref_id),
       ref_url: "https://mercurypay.aritic.app",
-  purpose: Map.get(parsed_data, :purpose) || extract_purpose(parsed_data),
-      cust_ref: Map.get(parsed_data, :cust_ref) || generate_customer_reference(),
-      qr_payload: parsed_data.qr_payload || "",
-      initiation_mode: parsed_data.initiation_mode || "01",
-
-      # Extract payee information from request data
-      payee_addr: parsed_data.payer_addr || "merchant@mercury",
-      payee_name: extract_payee_name_from_qr(parsed_data.qr_payload) || "Mercury PSP",
+      purpose: Map.get(parsed_data, :purpose),
+      cust_ref: Map.get(parsed_data, :cust_ref),
+      qr_payload: Map.get(parsed_data, :qr_payload) || "",
+      initiation_mode: Map.get(parsed_data, :initiation_mode),
+      txn_timestamp: Map.get(parsed_data, :txn_ts),
       payee_type: "ENTITY",
-      merchant_code: error_merchant_code,
-
-      # Default institution information
-      country_code: "IN",
-      net_inst_id: "MER1010001",
-
-      # Default merchant identification
-      sub_code: "00",
-      merchant_id: "MERCURY001",
-      store_id: "STORE001",
-      terminal_id: "TERM001",
-      merchant_type: "SMALL",
-      merchant_genre: "RETAIL",
-      onboarding_type: "AGGREGATOR",
-      reg_id: "REG001",
-      pin_code: "000000",
-      tier: "M2",
-      merchant_location: "IN",
-      merchant_inst_code: "MERC",
-
-      # Default merchant names
-      merchant_brand: "Mercury PSP",
-      legal_name: "Mercury Payment Services",
-      franchise_name: "Mercury",
-
-      # Default ownership and invoice
-      ownership_type: "PRIVATE",
-      invoice_name: "Mercury PSP",
-      invoice_number: "INV000000",
-
-      # Default account details
-      ifsc_code: "MERC0000001",
-      account_type: "CURRENT",
-      account_number: "0000000000",
-
-      # Default amount and currency
-      # For error responses, extract bAm (baseAmount) specifically from QR payload
-      currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
-      amount: extract_base_amount_from_qr(parsed_data.qr_payload) || "0.00",
-      split_name: "MAIN",
-
-      # FX information - extract from QR payload for error responses
-      base_amount: extract_base_amount_from_qr(parsed_data.qr_payload) || "0.00",
-      base_currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
-      fx_active: "N",
-      fx_rate: "1.00",
-      markup_percentage: "0.00"
+      # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+      # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+      # OLD:       net_inst_id: "MER1010001",
+      net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_dynamic, :psp_net_inst_id),
+      ownership_type: "PRIVATE"
     }
 
     case UpiXmlSchema.generate_resp_val_qr(response_data) do
@@ -3824,14 +3958,15 @@ end
     "INV" <> (:crypto.strong_rand_bytes(4) |> Base.encode16())
   end
 
-  defp generate_customer_reference do
-    # Generate a 12-character customer reference as required by T12
-    timestamp = System.system_time(:millisecond) |> Integer.to_string() |> String.slice(-6, 6)
-    random = :crypto.strong_rand_bytes(3) |> Base.encode16() |> String.slice(0, 6)
-    (timestamp <> random)
-      |> String.pad_trailing(12, "0")
-      |> String.slice(0, 12)
-  end
+  # [RespValQr Txn.custRef] REMOVED: generate_customer_reference (second definition) is unused now - see above
+  # OLD:   defp generate_customer_reference do
+  # OLD:     # Generate a 12-character customer reference as required by T12
+  # OLD:     timestamp = System.system_time(:millisecond) |> Integer.to_string() |> String.slice(-6, 6)
+  # OLD:     random = :crypto.strong_rand_bytes(3) |> Base.encode16() |> String.slice(0, 6)
+  # OLD:     (timestamp <> random)
+  # OLD:       |> String.pad_trailing(12, "0")
+  # OLD:       |> String.slice(0, 12)
+  # OLD:   end
 
   defp extract_base_amount_from_qr(qr_payload) do
     # Extract bAm (baseAmount) from QR string - priority extraction
@@ -3872,26 +4007,27 @@ end
   end
 
   # Try to extract purpose code from parsed_data (field), QR payload (upiGlobal/upi URL) or raw ReqValQr XML
-  defp extract_purpose(parsed_data) do
-    # 1) parsed_data may already have purpose
-    parsed_purpose = Map.get(parsed_data, :purpose)
-    if parsed_purpose && parsed_purpose != "" do
-      parsed_purpose
-    else
-      # 2) try QR payload (upiGlobal url or upi://pay query param)
-      qr = Map.get(parsed_data, :qr_payload) || ""
-      case Regex.run(~r/[?&]purpose=([0-9]{1,3})/, qr) do
-        [_, p] -> p
-        _ ->
-          # 3) try raw XML (ReqValQr <Txn ... purpose="11"/>)
-          raw = Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, :raw_request) || ""
-          case Regex.run(~r/purpose\s*=\s*"(\d{1,3})"/, raw) do
-            [_, p2] -> p2
-            _ -> "00"
-          end
-      end
-    end
-  end
+  # [RespValQr Txn.purpose] REMOVED: extract_purpose is unused now - purpose is echoed from the request
+  # OLD:   defp extract_purpose(parsed_data) do
+  # OLD:     # 1) parsed_data may already have purpose
+  # OLD:     parsed_purpose = Map.get(parsed_data, :purpose)
+  # OLD:     if parsed_purpose && parsed_purpose != "" do
+  # OLD:       parsed_purpose
+  # OLD:     else
+  # OLD:       # 2) try QR payload (upiGlobal url or upi://pay query param)
+  # OLD:       qr = Map.get(parsed_data, :qr_payload) || ""
+  # OLD:       case Regex.run(~r/[?&]purpose=([0-9]{1,3})/, qr) do
+  # OLD:         [_, p] -> p
+  # OLD:         _ ->
+  # OLD:           # 3) try raw XML (ReqValQr <Txn ... purpose="11"/>)
+  # OLD:           raw = Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, :raw_request) || ""
+  # OLD:           case Regex.run(~r/purpose\s*=\s*"(\d{1,3})"/, raw) do
+  # OLD:             [_, p2] -> p2
+  # OLD:             _ -> "00"
+  # OLD:           end
+  # OLD:       end
+  # OLD:     end
+  # OLD:   end
 
   defp extract_currency_from_qr(qr_payload) do
     # Extract currency from UPI QR string - handle different formats
@@ -4222,77 +4358,79 @@ end
     end
   end
 
-  defp extract_payee_name_from_qr(qr_payload) do
-    # Extract payee name from UPI QR string - handle different formats
-    cond do
-      # Look for payee name parameter in QR (pn= parameter)
-      String.contains?(qr_payload || "", "pn=") ->
-        case Regex.run(~r/pn=([^&]+)/, qr_payload) do
-          [_, payee_name] -> URI.decode(payee_name)
-          _ -> nil
-        end
-
-      # Look for merchant name parameter
-      String.contains?(qr_payload || "", "mn=") ->
-        case Regex.run(~r/mn=([^&]+)/, qr_payload) do
-          [_, merchant_name] -> URI.decode(merchant_name)
-          _ -> nil
-        end
-
-      # Look for name parameter
-      String.contains?(qr_payload || "", "name=") ->
-        case Regex.run(~r/name=([^&]+)/, qr_payload) do
-          [_, name] -> URI.decode(name)
-          _ -> nil
-        end
-
-      # For EMV QR codes, merchant name might be in tag 59
-      String.length(qr_payload || "") > 50 ->
-        # EMV QR codes have merchant name in tag 59 (Merchant Name)
-        # For now, return nil and let it fall back to merchant_info
-        nil
-
-      # Fallback
-      true ->
-        nil
-    end
-  end
-
-  defp extract_merchant_code_from_qr(qr_payload) do
-    # Extract merchant code from UPI QR string - handle different formats
-    cond do
-      # Look for merchant code parameter in QR (mc= parameter)
-      String.contains?(qr_payload || "", "mc=") ->
-        case Regex.run(~r/mc=([^&]+)/, qr_payload) do
-          [_, merchant_code] -> merchant_code
-          _ -> nil
-        end
-
-      # Look for business category parameter
-      String.contains?(qr_payload || "", "bc=") ->
-        case Regex.run(~r/bc=([^&]+)/, qr_payload) do
-          [_, business_category] -> business_category
-          _ -> nil
-        end
-
-      # Look for category parameter
-      String.contains?(qr_payload || "", "cat=") ->
-        case Regex.run(~r/cat=([^&]+)/, qr_payload) do
-          [_, category] -> category
-          _ -> nil
-        end
-
-      # For EMV QR codes, merchant code might be in structured data
-      String.length(qr_payload || "") > 50 ->
-        # EMV QR codes might have merchant code in tag 52
-        # For now, return nil and let it fall back to merchant_info
-        nil
-
-      # Fallback
-      true ->
-        nil
-    end
-  end
+  # [RespValQr Payee.name] REMOVED: extract_payee_name_from_qr is unused now - the generator sends pn
+  # OLD:   defp extract_payee_name_from_qr(qr_payload) do
+  # OLD:     # Extract payee name from UPI QR string - handle different formats
+  # OLD:     cond do
+  # OLD:       # Look for payee name parameter in QR (pn= parameter)
+  # OLD:       String.contains?(qr_payload || "", "pn=") ->
+  # OLD:         case Regex.run(~r/pn=([^&]+)/, qr_payload) do
+  # OLD:           [_, payee_name] -> URI.decode(payee_name)
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # Look for merchant name parameter
+  # OLD:       String.contains?(qr_payload || "", "mn=") ->
+  # OLD:         case Regex.run(~r/mn=([^&]+)/, qr_payload) do
+  # OLD:           [_, merchant_name] -> URI.decode(merchant_name)
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # Look for name parameter
+  # OLD:       String.contains?(qr_payload || "", "name=") ->
+  # OLD:         case Regex.run(~r/name=([^&]+)/, qr_payload) do
+  # OLD:           [_, name] -> URI.decode(name)
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # For EMV QR codes, merchant name might be in tag 59
+  # OLD:       String.length(qr_payload || "") > 50 ->
+  # OLD:         # EMV QR codes have merchant name in tag 59 (Merchant Name)
+  # OLD:         # For now, return nil and let it fall back to merchant_info
+  # OLD:         nil
+  # OLD:
+  # OLD:       # Fallback
+  # OLD:       true ->
+  # OLD:         nil
+  # OLD:     end
+  # OLD:   end
+
+  # [RespValQr Payee.code] REMOVED: extract_merchant_code_from_qr is unused now - the generator sends mc
+  # OLD:   defp extract_merchant_code_from_qr(qr_payload) do
+  # OLD:     # Extract merchant code from UPI QR string - handle different formats
+  # OLD:     cond do
+  # OLD:       # Look for merchant code parameter in QR (mc= parameter)
+  # OLD:       String.contains?(qr_payload || "", "mc=") ->
+  # OLD:         case Regex.run(~r/mc=([^&]+)/, qr_payload) do
+  # OLD:           [_, merchant_code] -> merchant_code
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # Look for business category parameter
+  # OLD:       String.contains?(qr_payload || "", "bc=") ->
+  # OLD:         case Regex.run(~r/bc=([^&]+)/, qr_payload) do
+  # OLD:           [_, business_category] -> business_category
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # Look for category parameter
+  # OLD:       String.contains?(qr_payload || "", "cat=") ->
+  # OLD:         case Regex.run(~r/cat=([^&]+)/, qr_payload) do
+  # OLD:           [_, category] -> category
+  # OLD:           _ -> nil
+  # OLD:         end
+  # OLD:
+  # OLD:       # For EMV QR codes, merchant code might be in structured data
+  # OLD:       String.length(qr_payload || "") > 50 ->
+  # OLD:         # EMV QR codes might have merchant code in tag 52
+  # OLD:         # For now, return nil and let it fall back to merchant_info
+  # OLD:         nil
+  # OLD:
+  # OLD:       # Fallback
+  # OLD:       true ->
+  # OLD:         nil
+  # OLD:     end
+  # OLD:   end
 
   # ================================
   # International Payment Processing Functions
@@ -4774,19 +4912,20 @@ end
     end
   end
 
-  defp get_expiry_timestamp do
-    case Application.get_env(:upi_dynamic, :use_timex_for_timestamps, true) do
-      true ->
-        DateTime.utc_now()
-        |> DateTime.add(30, :minute)
-        |> Timex.to_datetime("Asia/Kolkata")
-        |> Timex.format!("{YYYY}-{0M}-{0D}T{h24}:{m}:{s}+05:30")
-      _ ->
-        DateTime.utc_now()
-        |> DateTime.add(30, :minute)
-        |> DateTime.to_iso8601()
-    end
-  end
+  # [RespValQr QR.expireTs] REMOVED: get_expiry_timestamp (now + 5 min) is unused now - expireTs is the QR's QRexpire
+  # OLD:   defp get_expiry_timestamp do
+  # OLD:     case Application.get_env(:upi_dynamic, :use_timex_for_timestamps, true) do
+  # OLD:       true ->
+  # OLD:         DateTime.utc_now()
+  # OLD:         |> DateTime.add(30, :minute)
+  # OLD:         |> Timex.to_datetime("Asia/Kolkata")
+  # OLD:         |> Timex.format!("{YYYY}-{0M}-{0D}T{h24}:{m}:{s}+05:30")
+  # OLD:       _ ->
+  # OLD:         DateTime.utc_now()
+  # OLD:         |> DateTime.add(30, :minute)
+  # OLD:         |> DateTime.to_iso8601()
+  # OLD:     end
+  # OLD:   end
 
   defp generate_stan do
     :rand.uniform(999999) |> Integer.to_string() |> String.pad_leading(6, "0")
diff --git a/apps/upi_dynamic/test/test_helper.exs b/apps/upi_dynamic/test/test_helper.exs
new file mode 100644
index 0000000..339d34c
--- /dev/null
+++ b/apps/upi_dynamic/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(UpiCore.Repo, :manual)
diff --git a/apps/upi_dynamic/test/upi_dynamic/resp_val_qr_flow_test.exs b/apps/upi_dynamic/test/upi_dynamic/resp_val_qr_flow_test.exs
new file mode 100644
index 0000000..f6c7c51
--- /dev/null
+++ b/apps/upi_dynamic/test/upi_dynamic/resp_val_qr_flow_test.exs
@@ -0,0 +1,110 @@
+defmodule UpiDynamic.RespValQrFlowTest do
+  @moduledoc """
+  Drives UpiTransactionManager.process_qr_validation/1 (the dynamic ReqValQr flow) and checks the
+  RespValQr that comes back: echo of the request, QR-derived fields, QM1 and the error paths.
+  """
+  use ExUnit.Case, async: false
+
+  import SweetXml
+
+  alias UpiCore.Repo
+  alias UpiCore.Merchants.Merchant
+  alias UpiDynamic.UpiTransactionManager
+
+  @qr "upiGlobal://pay?ver=01&mode=16&purpose=11&orgid=MER1010001&tr=MER2df3f02447089a34f29d4c8cf14de915&tn=MT_1_5439&pa=restaurant@mercury&pn=Dubai%20Fine%20Dining&mc=5812&cu=INR&mid=AEREST001&msid=S001&mtid=T001&mType=SMALL&mGr=OFFLINE&mOnboarding=AGGREGATOR&mLoc=AE&brand=Dubai%20Fine%20Dining&cc=AE&bAm=5.00&bCurr=AED&am=120.66&qrMedium=03&invoiceNo=INV5903547760c1cc&invoiceDate=2026-07-09T14:46:11+05:30&invoiceName=Dubai Fine Dining&QRexpire=2026-07-09T15:16:11+05:30&QRts=2026-07-09T14:46:11+05:30&sign=X"
+
+  setup do
+    {:ok, _} = Application.ensure_all_started(:upi_core)
+    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo, shared: true)
+    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+    :ok
+  end
+
+  defp insert_merchant(mcc) do
+    {:ok, merchant} =
+      %Merchant{}
+      |> Merchant.changeset(%{
+        mid: "AEREST001",
+        tid: "T001",
+        sid: "S001",
+        merchant_vpa: "restaurant@mercury",
+        brand_name: "Dubai Fine Dining",
+        business_category: mcc,
+        country_code: "AE",
+        merchant_type: "SMALL",
+        merchant_genre: "ONLINE",
+        onboarding_type: "AGGREGATOR"
+      })
+      |> Repo.insert()
+
+    merchant
+  end
+
+  defp req_val_qr(qr) do
+    """
+    <ns2:ReqValQr xmlns:ns2="http://npci.org/upi/schema/">
+      <Head ver="2.0" ts="2026-07-09T14:46:20+05:30" orgId="NPCI" msgId="NPCI0000000000000000000000000000001"/>
+      <Txn id="NPCITXN0001" note="MT_VALQRDEP" refId="REFNPCI0001" refUrl="http://x" ts="2026-07-09T14:46:21+05:30" type="IntlQr" custRef="CUSTREF01" initiationMode="16" purpose="62"/>
+      <Payer addr="payer@npci" name="tester" seqNum="1" type="PERSON" code="0000"/>
+      <Institution netInstId="NPCINET0001" QrPayLoad="#{qr}" conCode="AE"/>
+    </ns2:ReqValQr>
+    """
+  end
+
+  defp attr(xml, path), do: xpath(xml, ~x"#{path}"s)
+
+  defp assert_request_echo(xml) do
+    assert attr(xml, "//*[local-name()='Head']/@orgId") == "MER101"
+    assert attr(xml, "//*[local-name()='Head']/@msgId") =~ ~r/^MER[0-9a-f]{32}$/
+    assert attr(xml, "//*[local-name()='Resp']/@reqMsgId") == "NPCI0000000000000000000000000000001"
+    assert attr(xml, "//*[local-name()='Txn']/@id") == "NPCITXN0001"
+    assert attr(xml, "//*[local-name()='Txn']/@note") == "MT_VALQRDEP"
+    assert attr(xml, "//*[local-name()='Txn']/@refId") == "REFNPCI0001"
+    assert attr(xml, "//*[local-name()='Txn']/@custRef") == "CUSTREF01"
+    assert attr(xml, "//*[local-name()='Txn']/@purpose") == "62"
+    assert attr(xml, "//*[local-name()='Txn']/@initiationMode") == "16"
+    assert attr(xml, "//*[local-name()='Txn']/@ts") == "2026-07-09T14:46:21+05:30"
+    assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2026-07-09T15:16:11+05:30"
+    assert attr(xml, "//*[local-name()='QR']/@ts") == "2026-07-09T14:46:11+05:30"
+    assert attr(xml, "//*[local-name()='Payee']/@addr") == "restaurant@mercury"
+    assert attr(xml, "//*[local-name()='Payee']/@name") == "Dubai Fine Dining"
+    assert attr(xml, "//*[local-name()='Payee']/@code") == "5812"
+    assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@value") == "5.00"
+    assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@curr") == "AED"
+    assert attr(xml, "//*[local-name()='Fx']/@baseAmount") == "5.00"
+    assert attr(xml, "//*[local-name()='Institution']/@conCode") == "AE"
+    assert attr(xml, "//*[local-name()='Institution']/@netInstId") == "NPCINET0001"
+    refute xml =~ "Mercury PSP"
+    refute xml =~ "1234567890"
+    refute xml =~ "MERC0000001"
+  end
+
+  test "unknown merchant -> ZR keeps echo, Payee, Amount and QR expiry from the request / QR" do
+    assert {:error, {xml, _msg}} = UpiTransactionManager.process_qr_validation(req_val_qr(@qr))
+    assert attr(xml, "//*[local-name()='Resp']/@result") == "FAILURE"
+    assert attr(xml, "//*[local-name()='Resp']/@errCode") == "ZR"
+    assert_request_echo(xml)
+  end
+
+  test "mc 7995 (blocked) -> QM1 (test B)" do
+    insert_merchant("5812")
+    qr = String.replace(@qr, "mc=5812", "mc=7995")
+
+    assert {:error, {xml, _msg}} = UpiTransactionManager.process_qr_validation(req_val_qr(qr))
+    assert attr(xml, "//*[local-name()='Resp']/@result") == "FAILURE"
+    assert attr(xml, "//*[local-name()='Resp']/@errCode") == "QM1"
+    assert attr(xml, "//*[local-name()='Payee']/@code") == "7995"
+    assert attr(xml, "//*[local-name()='Payee']/*[local-name()='Amount']/@value") == "5.00"
+  end
+
+  test "mc that is not the merchant's MCC -> QM1" do
+    insert_merchant("5411")
+
+    assert {:error, {xml, _msg}} = UpiTransactionManager.process_qr_validation(req_val_qr(@qr))
+    assert attr(xml, "//*[local-name()='Resp']/@errCode") == "QM1"
+  end
+
+  # The SUCCESS branch of process_qr_validation/1 writes to qr_validations, whose `qr_payload` column is missing
+  # from the current test database (migrations behind the schema). The SUCCESS XML itself is covered by
+  # UpiCore.RespValQrTest (generate_resp_val_qr/1 with the same QR).
+end
diff --git a/apps/upi_static/lib/upi_static/controllers/api/v1/upi_controller.ex b/apps/upi_static/lib/upi_static/controllers/api/v1/upi_controller.ex
index 0ece430..b9d8ed4 100644
--- a/apps/upi_static/lib/upi_static/controllers/api/v1/upi_controller.ex
+++ b/apps/upi_static/lib/upi_static/controllers/api/v1/upi_controller.ex
@@ -43,7 +43,10 @@ defmodule UpiStatic.Api.V1.UpiController do
         # --- Early QRts expiry check ---
         # If QR payload contains QRts and it's older than allowed window, send a PE RespValQr
         # to NPCI asynchronously but still reply to NPCI with the ACK immediately.
-        qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
+        # [RespValQr PE check] CHANGED: expiry is decided by the QR's QRexpire (was QRts + qr_validity_minutes). qr_ts_string/qr_ts_dt/diff_seconds below now hold QRexpire.
+        # TSD Table 2: a QR is expired (PE) when now > QRexpire
+        # OLD:         qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
+        qr_ts_string = UpiCore.NpciFormat.qr_param(Map.get(parsed_data, :qr_payload), "QRexpire")
         qr_validity_minutes = Application.get_env(:upi_static, :qr_validity_minutes, 30)
 
         Logger.info(
@@ -98,7 +101,9 @@ defmodule UpiStatic.Api.V1.UpiController do
               )
 
               # Consider expired if either QRts is older than the transaction ts or older than configured window
-              expired_by_window = diff_seconds > qr_validity_minutes * 60
+              # [RespValQr PE check] CHANGED: expired as soon as now is past QRexpire (diff_seconds = now - QRexpire), not after a 30 minute window
+              # OLD:               expired_by_window = diff_seconds > qr_validity_minutes * 60
+              expired_by_window = diff_seconds > 0
 
               expired_by_txn_ts =
                 if txn_ts_dt, do: DateTime.compare(qr_ts_dt, txn_ts_dt) == :lt, else: false
@@ -110,26 +115,41 @@ defmodule UpiStatic.Api.V1.UpiController do
               if expired_by_window do
                 # Build RespValQr error with Payload Expired
                 resp_data = %{
-                  org_id: Map.get(parsed_data, :org_id, "MER101"),
+                  # [RespValQr Head.orgId] CHANGED: PSP orgId (MER102), the request's orgId "NPCI" was echoed before
+                  # OLD:                   org_id: Map.get(parsed_data, :org_id, "MER101"),
+                  org_id: Application.get_env(:upi_static, :psp_org_id, "MER102"),
                   msg_id: generate_message_id(),
                   req_msg_id: parsed_data.msg_id,
                   result: "FAILURE",
                   err_code: "PE",
                   txn_id: parsed_data.txn_id,
-                  note: parsed_data.note || "QR Payload expired",
-                  ref_id: parsed_data.ref_id || generate_message_id(),
+                  # [RespValQr Txn.note / Txn.refId] CHANGED: echo the request (no "QR Payload expired" note, no generated refId)
+                  # OLD:                   note: parsed_data.note || "QR Payload expired",
+                  # OLD:                   ref_id: parsed_data.ref_id || generate_message_id(),
+                  note: parsed_data.note,
+                  ref_id: parsed_data.ref_id,
                   ref_url: parsed_data.ref_url || "",
-                  purpose: parsed_data.purpose || "11",
-                  cust_ref: parsed_data.cust_ref || "",
-                  initiation_mode: parsed_data.initiation_mode || "16",
-                  txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                  # [RespValQr Txn.purpose / custRef / initiationMode / ts] CHANGED: echo the request (no 11 / "" / 16 defaults, Txn@ts instead of Head ts)
+                  # OLD:                   purpose: parsed_data.purpose || "11",
+                  # OLD:                   cust_ref: parsed_data.cust_ref || "",
+                  # OLD:                   initiation_mode: parsed_data.initiation_mode || "16",
+                  # OLD:                   txn_timestamp: parsed_data.timestamp || get_timestamp(),
+                  purpose: parsed_data.purpose,
+                  cust_ref: parsed_data.cust_ref,
+                  initiation_mode: parsed_data.initiation_mode,
+                  txn_timestamp: Map.get(parsed_data, :txn_ts),
                   qr_payload: parsed_data.qr_payload,
-                  payee_addr: Map.get(parsed_data, :payer_addr) || "",
-                  payee_name: Map.get(parsed_data, :payer_name) || "Mercury PSP",
+                  # [RespValQr Payee.addr / Payee.name] REMOVED: payer address / payer name ("Mercury PSP") used as payee; the generator sends the QR's pa / pn
+                  # OLD:                   payee_addr: Map.get(parsed_data, :payer_addr) || "",
+                  # OLD:                   payee_name: Map.get(parsed_data, :payer_name) || "Mercury PSP",
                   payee_type: "ENTITY",
-                  merchant_code: "0000",
-                  country_code: "IN",
-                  net_inst_id: "MER1010001"
+                  # [RespValQr Payee.code / Institution.conCode] REMOVED: dummy 0000 / IN; the generator sends the QR's mc / cc
+                  # OLD:                   merchant_code: "0000",
+                  # OLD:                   country_code: "IN",
+                  # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+                  # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+                  # OLD:                   net_inst_id: "MER1010001"
+                  net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_static, :psp_net_inst_id)
                 }
 
                 # Generate the error XML now and send asynchronously so we still return ACK to caller
diff --git a/apps/upi_static/lib/upi_static/static_qr_transaction_manager.ex b/apps/upi_static/lib/upi_static/static_qr_transaction_manager.ex
index ddb0f94..fccfda3 100644
--- a/apps/upi_static/lib/upi_static/static_qr_transaction_manager.ex
+++ b/apps/upi_static/lib/upi_static/static_qr_transaction_manager.ex
@@ -41,7 +41,9 @@ defmodule UpiStatic.StaticQrTransactionManager do
 
       # Static QR: initiation_mode "02" or "15"
       # Dynamic QR: initiation_mode "16"
-      is_static = initiation_mode in ["02", "15"]
+      # [RespValQr routing] CHANGED: static flow only for initiationMode 01 / 02; 15 / 16 / 22 / 23 are dynamic QR modes
+      # OLD:       is_static = initiation_mode in ["02", "15"]
+      is_static = initiation_mode in ["01", "02"]
 
       {:ok, is_static}
     rescue
@@ -54,18 +56,32 @@ defmodule UpiStatic.StaticQrTransactionManager do
   def is_static_qr?(parsed_data) do
     initiation_mode = Map.get(parsed_data, :initiation_mode)
 
-    # Static QR ONLY for initiation_mode "02" or "15"
-    # initiation_mode "16" is ALWAYS dynamic, regardless of tr/tn presence
-    # initiation_mode "15" with expired QR will be handled by PE scenario
+    # [RespValQr routing] CHANGED: static flow only for initiationMode 01 / 02 (was 02 / 15); modes 15 / 16 / 22 / 23 always go to the dynamic flow
+    # TSD Table 2: 15 / 16 / 22 / 23 are dynamic QR initiation modes (they carry Amount / QRts)
+    # OLD:     # Static QR ONLY for initiation_mode "02" or "15"
+    # OLD:     # initiation_mode "16" is ALWAYS dynamic, regardless of tr/tn presence
+    # OLD:     # initiation_mode "15" with expired QR will be handled by PE scenario
+    # OLD:     cond do
+    # OLD:       initiation_mode == "02" ->
+    # OLD:         true
+    # OLD:       initiation_mode == "15" ->
+    # OLD:         true
+    # OLD:       initiation_mode == "16" ->
+    # OLD:         # Mode 16 is STRICTLY DYNAMIC - route to UpiTransactionManager
+    # OLD:         Logger.info("[QR Detection] initiation_mode=16 detected - treating as DYNAMIC QR (not static)")
+    # OLD:         false
+    # OLD:       true ->
+    # OLD:         # For any other mode, check if QR has tr/tn (static QR identifiers)
+    # OLD:         Map.has_key?(parsed_data, :qr_payload) and has_tr_tn?(parsed_data.qr_payload)
+    # OLD:     end
     cond do
-      initiation_mode == "02" ->
+      initiation_mode in ["01", "02"] ->
         true
-      initiation_mode == "15" ->
-        true
-      initiation_mode == "16" ->
-        # Mode 16 is STRICTLY DYNAMIC - route to UpiTransactionManager
-        Logger.info("[QR Detection] initiation_mode=16 detected - treating as DYNAMIC QR (not static)")
+
+      initiation_mode in ["15", "16", "22", "23"] ->
+        Logger.info("[QR Detection] initiation_mode=#{initiation_mode} detected - treating as DYNAMIC QR (not static)")
         false
+
       true ->
         # For any other mode, check if QR has tr/tn (static QR identifiers)
         Map.has_key?(parsed_data, :qr_payload) and has_tr_tn?(parsed_data.qr_payload)
@@ -88,7 +104,9 @@ defmodule UpiStatic.StaticQrTransactionManager do
         Logger.info("[StaticQR] Parsed ReqValQr: #{inspect(parsed_data, limit: :infinity)}")
 
         # Step 1: Validate merchant QR payload (TID, MID, MSID checks)
-        case validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode) do
+        # [RespValQr QM1] CHANGED: merchant lookup now also checks the QR's mc (missing / not 4 digits / not merchant's MCC / blocked e.g. 7995 -> QM1)
+        # OLD:         case validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode) do
+        case validate_merchant_qr_with_mcc(parsed_data) do
           {:ok, merchant_info} ->
             Logger.info("[StaticQR] ✅ Merchant validation passed: #{inspect(merchant_info)}")
 
@@ -196,6 +214,11 @@ defmodule UpiStatic.StaticQrTransactionManager do
                 generate_error_response(parsed_data, "U66", "Invalid QR format")
             end
 
+          {:error, "QM1"} ->
+            # [RespValQr QM1] ADDED: mc missing / not 4 digits / not the merchant's MCC / blocked (7995) -> FAILURE errCode="QM1"
+            Logger.error("[StaticQR] ❌ Invalid merchant category code (QM1)")
+            generate_error_response(parsed_data, "QM1", "Invalid merchant category code")
+
           {:error, "TID_MISMATCH"} ->
             Logger.error("[StaticQR] ❌ Terminal ID mismatch")
             generate_error_response(parsed_data, "XW", "Terminal ID mismatch")
@@ -215,6 +238,15 @@ defmodule UpiStatic.StaticQrTransactionManager do
     end
   end
 
+  # [RespValQr QM1] ADDED: merchant lookup followed by the QR mc check (QM1)
+  defp validate_merchant_qr_with_mcc(parsed_data) do
+    with {:ok, merchant_info} <-
+           validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode),
+         :ok <- UpiXmlSchema.validate_qr_mcc(parsed_data.qr_payload, Map.get(merchant_info, :mcc)) do
+      {:ok, merchant_info}
+    end
+  end
+
   # Validate static QR (expiry, usage limits, status, etc.)
   defp validate_static_qr(static_qr, parsed_data) do
     cond do
@@ -269,15 +301,19 @@ defmodule UpiStatic.StaticQrTransactionManager do
             # Log parsing failure but still check if date is clearly in the past
             Logger.warning("[StaticQR] Could not parse QRexpire: #{expire_str}, reason: #{inspect(reason)}")
 
-            # Fallback: Check if year is clearly in the past (e.g., 2025 vs 2026)
-            if String.contains?(expire_str, "2025-") do
-              Logger.warning("[StaticQR] ⚠️ QR EXPIRED (year-based fallback) - QRexpire: #{expire_str}")
-              {:error, "PE", "Payment Expired - QR code has expired"}
-            else
-              # Can't parse and can't determine expiry, proceed without validation
-              Logger.warning("[StaticQR] Cannot determine QR expiry, proceeding without validation")
-              :ok
-            end
+            # [RespValQr PE check] REMOVED: year-based "2025-" hack. PE is decided only by now > QRexpire; an unparsable QRexpire is not guessed
+            # OLD:             # Fallback: Check if year is clearly in the past (e.g., 2025 vs 2026)
+            # OLD:             if String.contains?(expire_str, "2025-") do
+            # OLD:               Logger.warning("[StaticQR] ⚠️ QR EXPIRED (year-based fallback) - QRexpire: #{expire_str}")
+            # OLD:               {:error, "PE", "Payment Expired - QR code has expired"}
+            # OLD:             else
+            # OLD:               # Can't parse and can't determine expiry, proceed without validation
+            # OLD:               Logger.warning("[StaticQR] Cannot determine QR expiry, proceeding without validation")
+            # OLD:               :ok
+            # OLD:             end
+            # Can't parse QRexpire, so expiry can't be decided - proceed without validation
+            Logger.warning("[StaticQR] Cannot determine QR expiry, proceeding without validation")
+            :ok
         end
     end
   rescue
@@ -342,8 +378,12 @@ defmodule UpiStatic.StaticQrTransactionManager do
             Regex.replace(~r/(\d{2}:\d{2}:\d{2})\s([+-]?\d{2}:\d{2})$/, s, "\\1+\\2")
         end
       ),
-      invoice_name: Map.get(params, "invoiceName") |> URI.decode_www_form(),
-      expire_ts: Map.get(params, "QRexpire"),
+      # [RespValQr Merchant.Invoice name / QR.expireTs] CHANGED: invoiceName no longer crashes when absent (nil -> decode_www_form raised and the whole details map fell back to dummies); QRexpire is the raw QR value (URI.decode_query turned '+' into a space)
+      # TSD Table 2: Invoice name = invoiceName, QR expireTs = QRexpire as received
+      # OLD:       invoice_name: Map.get(params, "invoiceName") |> URI.decode_www_form(),
+      # OLD:       expire_ts: Map.get(params, "QRexpire"),
+      invoice_name: UpiCore.NpciFormat.qr_text(qr_payload, "invoiceName"),
+      expire_ts: UpiCore.NpciFormat.qr_param(qr_payload, "QRexpire"),
       base_amount: Map.get(params, "bAm"),
       base_currency: Map.get(params, "bCurr")
     }
@@ -414,7 +454,10 @@ defmodule UpiStatic.StaticQrTransactionManager do
     error_response = %{
       is_static_qr: true,
       org_id: @static_org_id,
-      net_inst_id: @static_net_inst_id,
+      # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+      # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+      # OLD:       net_inst_id: @static_net_inst_id,
+      net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_static, :psp_net_inst_id),
       msg_id: "RESP_#{parsed_data.msg_id}",
       req_msg_id: parsed_data.msg_id,
       result: "FAILURE",
@@ -422,30 +465,42 @@ defmodule UpiStatic.StaticQrTransactionManager do
       txn_id: parsed_data.txn_id,
       # Required fields for error response XML generation
       qr_payload: Map.get(parsed_data, :qr_payload, ""),
-      payee_addr: qr_details[:payee_addr] || "error@merchant",
-      payee_name: qr_details[:payee_name] || "Error Response",
+      # [RespValQr static generate_error_response] CHANGED: the generator reads payee / merchant / amount / invoice / QR attributes from the QR string on every path
+      # REMOVED invented values: error@merchant, "Error Response", 0000, IN, INR amounts, "Error" brand, ERR000 invoice, now as invoice date, fixed qVer / qrMedium
+      # OLD:       payee_addr: qr_details[:payee_addr] || "error@merchant",
+      # OLD:       payee_name: qr_details[:payee_name] || "Error Response",
+      # OLD:       payee_type: "ENTITY",
+      # OLD:       merchant_code: qr_details[:merchant_code] || merchant_details.sub_code || "0000",
+      # OLD:       country_code: merchant_details.country_code || "IN",
+      # OLD:       account_type: merchant_details.account_type,
+      # OLD:       account_number: merchant_details.account_number,
+      # OLD:       ifsc_code: merchant_details.ifsc_code,
+      # OLD:       currency: "INR",
+      # OLD:       base_currency: "INR",
+      # OLD:       merchant_brand: qr_details[:brand] || merchant_details.merchant_type || "Error",
+      # OLD:       invoice_name: qr_details[:invoice_name] || "Error",
+      # OLD:       invoice_number: qr_details[:invoice_no] || "ERR000",
+      # OLD:       invoice_date: DateTime.utc_now() |> DateTime.to_iso8601(),
+      # OLD:       ownership_type: "PRIVATE",
+      # OLD:       qr_version: "01",
+      # OLD:       qr_medium: "03",
       payee_type: "ENTITY",
-      merchant_code: qr_details[:merchant_code] || merchant_details.sub_code || "0000",
-      country_code: merchant_details.country_code || "IN",
-      account_type: merchant_details.account_type,
-      account_number: merchant_details.account_number,
-      ifsc_code: merchant_details.ifsc_code,
-      currency: "INR",
-      base_currency: "INR",
-      merchant_brand: qr_details[:brand] || merchant_details.merchant_type || "Error",
-      invoice_name: qr_details[:invoice_name] || "Error",
-      invoice_number: qr_details[:invoice_no] || "ERR000",
-      invoice_date: DateTime.utc_now() |> DateTime.to_iso8601(),
       ownership_type: "PRIVATE",
-      qr_version: "01",
-      qr_medium: "03",
-      initiation_mode: Map.get(parsed_data, :initiation_mode, "02"),
-      note: Map.get(parsed_data, :note, "Error"),
-      ref_id: Map.get(parsed_data, :ref_id, ""),
+      # [RespValQr Txn.initiationMode / note / refId / purpose / custRef / ts] CHANGED: echo the request (no 02 / "Error" / 11 defaults, Txn@ts instead of now in UTC)
+      # OLD:       initiation_mode: Map.get(parsed_data, :initiation_mode, "02"),
+      # OLD:       note: Map.get(parsed_data, :note, "Error"),
+      # OLD:       ref_id: Map.get(parsed_data, :ref_id, ""),
+      # OLD:       ref_url: Map.get(parsed_data, :ref_url, ""),
+      # OLD:       purpose: Map.get(parsed_data, :purpose, "11"),
+      # OLD:       cust_ref: Map.get(parsed_data, :cust_ref, ""),
+      # OLD:       txn_timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
+      initiation_mode: Map.get(parsed_data, :initiation_mode),
+      note: Map.get(parsed_data, :note),
+      ref_id: Map.get(parsed_data, :ref_id),
       ref_url: Map.get(parsed_data, :ref_url, ""),
-      purpose: Map.get(parsed_data, :purpose, "11"),
-      cust_ref: Map.get(parsed_data, :cust_ref, ""),
-      txn_timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
+      purpose: Map.get(parsed_data, :purpose),
+      cust_ref: Map.get(parsed_data, :cust_ref),
+      txn_timestamp: Map.get(parsed_data, :txn_ts),
       # Identifier fields from merchant DB lookup
       identifier_sub_code: merchant_details.sub_code,
       identifier_mid: merchant_details.mid,
@@ -455,7 +510,9 @@ defmodule UpiStatic.StaticQrTransactionManager do
       identifier_merchant_genre: merchant_details.merchant_genre,
       identifier_onboarding_type: merchant_details.onboarding_type,
       identifier_merchant_loc: merchant_details.country_code,
-      identifier_inst_code: String.slice(@static_net_inst_id, 0, 6)
+      # [RespValQr Identifier.merchantInstCode] CHANGED: merchant's inst_code from the DB (same as the success path), not the first 6 chars of the net inst id
+      # OLD:       identifier_inst_code: String.slice(@static_net_inst_id, 0, 6)
+      identifier_inst_code: merchant_details.inst_code
     }
 
     case UpiXmlSchema.generate_resp_val_qr(error_response) do
@@ -475,22 +532,32 @@ defmodule UpiStatic.StaticQrTransactionManager do
     # Fetch all merchant details from database using VPA (payee_addr)
     merchant_details = fetch_merchant_details_static(payee_addr)
 
+    # [RespValQr static success] NOTE: payee / merchant / amount / invoice / QR attributes are read from the QR string by
+    # UpiXmlSchema.generate_resp_val_qr; the keys below are no longer used for them (TSD Table 2).
     %{
       is_static_qr: true,
       org_id: @static_org_id,
-      net_inst_id: @static_net_inst_id,
+      # [RespValQr Institution.netInstId] CHANGED: echo the ReqValQr netInstId, else the app config; the literal is gone
+      # TSD Table 2: "NIPL assigns the netInstId to IP" - echo the ReqValQr netInstId, else the app config (:psp_net_inst_id), never a literal
+      # OLD:       net_inst_id: @static_net_inst_id,
+      net_inst_id: Map.get(parsed_data, :net_inst_id) || Application.get_env(:upi_static, :psp_net_inst_id),
       msg_id: "RESP_#{parsed_data.msg_id}",
       req_msg_id: parsed_data.msg_id,
       result: "SUCCESS",
       err_code: "00",
       txn_id: parsed_data.txn_id,
-      note: parsed_data.note || "Static QR validated",
+      # [RespValQr Txn.note] CHANGED: echo the request note (no "Static QR validated")
+      # OLD:       note: parsed_data.note || "Static QR validated",
+      note: parsed_data.note,
       ref_id: parsed_data.ref_id,
       ref_url: parsed_data.ref_url,
       purpose: parsed_data.purpose,
       cust_ref: parsed_data.cust_ref,
-      initiation_mode: "02",
-      txn_timestamp: parsed_data.timestamp || DateTime.utc_now() |> DateTime.to_iso8601(),
+      # [RespValQr Txn.initiationMode / Txn.ts] CHANGED: echo the request's initiationMode (was fixed "02") and Txn@ts (was Head ts / now in UTC)
+      # OLD:       initiation_mode: "02",
+      # OLD:       txn_timestamp: parsed_data.timestamp || DateTime.utc_now() |> DateTime.to_iso8601(),
+      initiation_mode: parsed_data.initiation_mode,
+      txn_timestamp: Map.get(parsed_data, :txn_ts),
       qr_payload: parsed_data.qr_payload,
       payee_addr: payee_addr,
       payee_name: qr_details[:payee_name] || static_qr.merchant_name,
@@ -511,9 +578,13 @@ defmodule UpiStatic.StaticQrTransactionManager do
       base_currency: static_qr.foreign_currency,
       fx_rate: static_qr.fx_rate,
       markup_percentage: static_qr.markup_percentage,
-      qr_version: "01",
-      qr_medium: qr_details[:qr_medium] || "03",
-      expire_ts: qr_details[:expire_ts],
+      # [RespValQr QR.qVer / QR.qrMedium / QR.expireTs] CHANGED: taken from the QR string (ver / qrMedium / QRexpire)
+      # OLD:       qr_version: "01",
+      # OLD:       qr_medium: qr_details[:qr_medium] || "03",
+      # OLD:       expire_ts: qr_details[:expire_ts],
+      qr_version: UpiCore.NpciFormat.qr_param(parsed_data.qr_payload, "ver"),
+      qr_medium: UpiCore.NpciFormat.qr_param(parsed_data.qr_payload, "qrMedium"),
+      expire_ts: UpiCore.NpciFormat.qr_param(parsed_data.qr_payload, "QRexpire"),
       invoice_no: qr_details[:invoice_no],
       invoice_date: qr_details[:invoice_date],
       invoice_name: qr_details[:invoice_name],
@@ -669,6 +740,8 @@ defmodule UpiStatic.StaticQrTransactionManager do
       upi_id: merchant.merchant_vpa,
       merchant_code: merchant.merchant_code || "5411",
       merchant_type: merchant.merchant_type || "SMALL",
+      # [RespValQr QM1] ADDED: the merchant's registered MCC (business_category); merchant_code above is an internal id, not an MCC
+      mcc: merchant.business_category,
       tid: merchant.tid,
       qr_data: qr_data
     }
diff --git a/apps/upi_static/test/test_helper.exs b/apps/upi_static/test/test_helper.exs
new file mode 100644
index 0000000..339d34c
--- /dev/null
+++ b/apps/upi_static/test/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(UpiCore.Repo, :manual)
diff --git a/apps/upi_static/test/upi_static/resp_val_qr_flow_test.exs b/apps/upi_static/test/upi_static/resp_val_qr_flow_test.exs
new file mode 100644
index 0000000..777a4ce
--- /dev/null
+++ b/apps/upi_static/test/upi_static/resp_val_qr_flow_test.exs
@@ -0,0 +1,98 @@
+defmodule UpiStatic.RespValQrFlowTest do
+  @moduledoc """
+  Static ReqValQr flow: routing by initiationMode, and the RespValQr that comes back on the
+  QM1 / ZR error paths (echo of the request, raw QRexpire with '+', no dummy values).
+  """
+  use ExUnit.Case, async: false
+
+  import SweetXml
+
+  alias UpiCore.Repo
+  alias UpiCore.Merchants.Merchant
+  alias UpiStatic.StaticQrTransactionManager
+
+  # Test C: static QR, mode 02, QRexpire with '+', no invoiceName
+  @qr "upi://pay?ver=01&mode=02&pa=shop@mercury&pn=Shop%20One&mc=5812&mid=SHOPMID01&mtid=T1&mType=SMALL&mGr=OFFLINE&mOnboarding=AGGREGATOR&mLoc=AE&brand=Shop%20One&cc=AE&qrMedium=03&invoiceNo=INV1&invoiceDate=2027-04-29T14:15:05+05:30&QRexpire=2027-04-29T14:45:05+05:30&sign=X"
+
+  setup do
+    {:ok, _} = Application.ensure_all_started(:upi_core)
+    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo, shared: true)
+    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+    :ok
+  end
+
+  defp req_val_qr(qr) do
+    """
+    <ns2:ReqValQr xmlns:ns2="http://npci.org/upi/schema/">
+      <Head ver="2.0" ts="2026-07-09T14:46:20+05:30" orgId="NPCI" msgId="NPCI0000000000000000000000000000002"/>
+      <Txn id="NPCITXN0002" note="STATIC_NOTE" refId="REFNPCI0002" refUrl="http://x" ts="2026-07-09T14:46:21+05:30" type="IntlQr" custRef="CUSTREF02" initiationMode="02" purpose="11"/>
+      <Payer addr="payer@npci" name="tester" seqNum="1" type="PERSON" code="0000"/>
+      <Institution netInstId="NPCINET0002" QrPayLoad="#{qr}" conCode="AE"/>
+    </ns2:ReqValQr>
+    """
+  end
+
+  defp attr(xml, path), do: xpath(xml, ~x"#{path}"s)
+
+  describe "is_static_qr?/1 routing" do
+    test "01 and 02 are static" do
+      assert StaticQrTransactionManager.is_static_qr?(%{initiation_mode: "01"})
+      assert StaticQrTransactionManager.is_static_qr?(%{initiation_mode: "02"})
+    end
+
+    test "15 / 16 / 22 / 23 go to the dynamic flow" do
+      for mode <- ["15", "16", "22", "23"] do
+        refute StaticQrTransactionManager.is_static_qr?(%{initiation_mode: mode, qr_payload: "upi://pay?tr=A&tn=B"}),
+               "mode #{mode} must not be routed to the static flow"
+      end
+    end
+
+    test "check_if_static_qr_from_xml/1 follows the same rule" do
+      xml = fn mode -> ~s(<ReqValQr><Txn initiationMode="#{mode}"/></ReqValQr>) end
+      assert {:ok, true} = StaticQrTransactionManager.check_if_static_qr_from_xml(xml.("01"))
+      assert {:ok, true} = StaticQrTransactionManager.check_if_static_qr_from_xml(xml.("02"))
+      assert {:ok, false} = StaticQrTransactionManager.check_if_static_qr_from_xml(xml.("15"))
+      assert {:ok, false} = StaticQrTransactionManager.check_if_static_qr_from_xml(xml.("16"))
+    end
+  end
+
+  describe "process_qr_validation/1 error paths" do
+    test "unknown merchant -> ZR with request echo and raw QRexpire" do
+      assert {:ok, xml} = StaticQrTransactionManager.process_qr_validation(req_val_qr(String.replace(@qr, "&mtid=T1", "")))
+
+      assert attr(xml, "//*[local-name()='Resp']/@errCode") == "ZR"
+      assert attr(xml, "//*[local-name()='Head']/@orgId") == "MER102"
+      assert attr(xml, "//*[local-name()='Institution']/@netInstId") == "NPCINET0002"
+      assert attr(xml, "//*[local-name()='Head']/@msgId") =~ ~r/^MER[0-9a-f]{32}$/
+      assert attr(xml, "//*[local-name()='Resp']/@reqMsgId") == "NPCI0000000000000000000000000000002"
+      assert attr(xml, "//*[local-name()='Txn']/@note") == "STATIC_NOTE"
+      assert attr(xml, "//*[local-name()='Txn']/@refId") == "REFNPCI0002"
+      assert attr(xml, "//*[local-name()='Txn']/@custRef") == "CUSTREF02"
+      assert attr(xml, "//*[local-name()='Txn']/@initiationMode") == "02"
+      assert attr(xml, "//*[local-name()='Txn']/@ts") == "2026-07-09T14:46:21+05:30"
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2027-04-29T14:45:05+05:30"
+      assert attr(xml, "//*[local-name()='Payee']/@addr") == "shop@mercury"
+      assert attr(xml, "//*[local-name()='Payee']/@name") == "Shop One"
+      assert attr(xml, "//*[local-name()='Payee']/@code") == "5812"
+      assert attr(xml, "//*[local-name()='Invoice']/@name") == ""
+      assert attr(xml, "//*[local-name()='Invoice']/@num") == "INV1"
+      refute xml =~ "ERR000"
+      refute xml =~ "Error Response"
+      refute xml =~ "error@merchant"
+    end
+
+    test "blocked mc -> QM1" do
+      {:ok, _} =
+        %Merchant{}
+        |> Merchant.changeset(%{mid: "SHOPMID01", tid: "T1", merchant_vpa: "shop@mercury", brand_name: "Shop One", business_category: "5812"})
+        |> Repo.insert()
+
+      qr = String.replace(@qr, "mc=5812", "mc=7995")
+      assert {:ok, xml} = StaticQrTransactionManager.process_qr_validation(req_val_qr(qr))
+      assert attr(xml, "//*[local-name()='Resp']/@result") == "FAILURE"
+      assert attr(xml, "//*[local-name()='Resp']/@errCode") == "QM1"
+      assert attr(xml, "//*[local-name()='Payee']/@code") == "7995"
+      assert attr(xml, "//*[local-name()='QR']/@expireTs") == "2027-04-29T14:45:05+05:30"
+    end
+  end
+end
diff --git a/config/config.exs b/config/config.exs
index a46b85d..814fe28
--- a/config/config.exs
+++ b/config/config.exs
@@ -44,6 +44,9 @@ config :upi_core, :fx_providers,
   primary: %{provider: "RBI_REFERENCE_RATE", refresh_interval: 300},
   fallback: %{provider: "SANDBOX_RATES", enabled: true}
 
+# [RespValQr QM1] ADDED: MCCs that must be answered with errCode="QM1" in RespValQr
+config :upi_core, :blocked_mccs, ["7995"]
+
 # ─── upi_dynamic: Dynamic QR PSP (orgId: MER101) ─────────────────────────────
 config :upi_dynamic, UpiDynamic.Endpoint,
   url: [host: "localhost"],
diff --git a/config/runtime.exs b/config/runtime.exs
index d617ada..93b11c6
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -62,6 +62,13 @@ config :upi_static, :npci_resp_hbt_endpoint, "#{npci_base_url}/RespHbt/2.0/urn:t
 config :upi_core, :npci_base_url, npci_base_url
 config :upi_core, :npci_mode, npci_mode
 
+# [RespValQr Payee.Ac] ADDED: PSP's Indian settlement account sent as Payee/Ac in RespValQr
+# (TSD Table 2: ACTYPE / ACNUM / IFSC). Real values come from the environment - never hard-coded.
+config :upi_core, :psp_settlement_account,
+  ac_type: System.get_env("PSP_SETTLEMENT_AC_TYPE"),
+  ac_num: System.get_env("PSP_SETTLEMENT_AC_NUM"),
+  ifsc: System.get_env("PSP_SETTLEMENT_IFSC")
+
 IO.puts("\n[NPCI_MODE] #{npci_mode} -> #{npci_base_url}\n")
 
 # ## Using releases
