cover/Elixir.DaProductApp.Adapters.NpciAdapter.html

1 defmodule DaProductApp.Adapters.NpciAdapter do
2 @moduledoc """
3 NPCI communication adapter for handling all UPI XML-based API communications.
4
5 Provides functions for:
6 - Generating ACK responses for immediate NPCI acknowledgment
7 - Sending async RespPay, RespValQr, and other responses to NPCI endpoints
8 - Handling NPCI endpoint communication with proper error handling and logging
9 - XML formatting and response parsing per NPCI specification
10 """
11
12 require Logger
13 import SweetXml
14
15 # NPCI endpoints configuration
16 @npci_base_url "https://precert.nfinite.in/iupi"
17 @request_timeout 30_000
18
19 # ================================
20 # ACK GENERATION FUNCTIONS
21 # ================================
22
23 @doc """
24 Generate ACK XML response for ReqPay requests.
25 Returns immediate acknowledgment to NPCI that request was received.
26 """
27 @spec generate_reqpay_ack(String.t(), String.t()) :: String.t()
28 def generate_reqpay_ack(msg_id, org_txn_id) do
29
:-(
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
30
31
:-(
ack_xml = """
32 <?xml version="1.0" encoding="UTF-8"?>
33 <ns2:Ack xmlns:ns2="http://npci.org/upi/schema/"
34 api="ReqPay"
35
:-(
reqMsgId="#{msg_id}"
36
:-(
orgTxnId="#{org_txn_id}"
37
:-(
ts="#{timestamp}"/>
38 """
39
40
:-(
Logger.info("[NPCI ACK] Generated ReqPay ACK for msgId=#{msg_id}, orgTxnId=#{org_txn_id}")
41
:-(
Logger.debug("[NPCI ACK] ACK XML: #{ack_xml}")
42
43
:-(
ack_xml
44 end
45
46 @doc """
47 Generate ACK XML response for ReqValQr requests.
48 """
49 @spec generate_reqvalqr_ack(String.t(), String.t()) :: String.t()
50 def generate_reqvalqr_ack(msg_id, txn_id) do
51
:-(
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
52
53
:-(
ack_xml = """
54 <?xml version="1.0" encoding="UTF-8"?>
55 <ns2:Ack xmlns:ns2="http://npci.org/upi/schema/"
56 api="ReqValQr"
57
:-(
reqMsgId="#{msg_id}"
58
:-(
txnId="#{txn_id}"
59
:-(
ts="#{timestamp}"/>
60 """
61
62
:-(
Logger.info("[NPCI ACK] Generated ReqValQr ACK for msgId=#{msg_id}, txnId=#{txn_id}")
63
:-(
Logger.debug("[NPCI ACK] ACK XML: #{ack_xml}")
64
65
:-(
ack_xml
66 end
67
68 @doc """
69 Generate ACK XML response for ReqChkTxn requests.
70 """
71 @spec generate_reqchktxn_ack(String.t(), String.t()) :: String.t()
72 def generate_reqchktxn_ack(msg_id, org_txn_id) do
73
:-(
timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
74
75
:-(
ack_xml = """
76 <?xml version="1.0" encoding="UTF-8"?>
77 <ns2:Ack xmlns:ns2="http://npci.org/upi/schema/"
78 api="ReqChkTxn"
79
:-(
reqMsgId="#{msg_id}"
80
:-(
orgTxnId="#{org_txn_id}"
81
:-(
ts="#{timestamp}"/>
82 """
83
84
:-(
Logger.info("[NPCI ACK] Generated ReqChkTxn ACK for msgId=#{msg_id}, orgTxnId=#{org_txn_id}")
85
:-(
Logger.debug("[NPCI ACK] ACK XML: #{ack_xml}")
86
87
:-(
ack_xml
88 end
89
90 # ================================
91 # ASYNC NPCI COMMUNICATION FUNCTIONS
92 # ================================
93
94 @doc """
95 Send RespPay XML asynchronously to NPCI after ACK has been sent.
96 Handles the complete payment response flow with proper error handling.
97 """
98 @spec send_async_resppay(String.t(), String.t(), String.t(), map()) :: :ok
99 def send_async_resppay(resp_pay_xml, txn_id, org_txn_id, req_pay_data) do
100
:-(
Logger.info("[ASYNC RespPay] Starting async RespPay transmission for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
101
:-(
Logger.debug("[ASYNC RespPay] RespPay XML: #{resp_pay_xml}")
102
103 # Construct NPCI callback URL using txn_id from original ReqPay
104
:-(
npci_endpoint = "#{@npci_base_url}/RespPay/2.0/urn:txnid:#{txn_id}"
105
106 # # headers = [
107 # {"content-type", "application/xml"},
108 # {"accept", "application/xml"},
109 # {"user-agent", "Mercury-UPI-PSP/1.0"}
110 # ]
111
:-(
headers = [
112 {"Content-Type", "application/xml; charset=UTF-8"},
113 {"Accept", "application/xml"},
114 {"User-Agent", "Mercury-UPI-PSP/1.0"},
115 {"X-API-Version", "2.0"}
116 ]
117
118
:-(
Logger.info("[ASYNC RespPay] Sending to NPCI URL: #{npci_endpoint}")
119
:-(
Logger.info("[ASYNC RespPay] Request headers: #{inspect(headers)}")
120
121
:-(
case Req.post(npci_endpoint, body: resp_pay_xml, headers: headers, receive_timeout: @request_timeout) do
122 {:ok, %Req.Response{status: 200, body: response_body}} ->
123
:-(
Logger.info("[ASYNC RespPay] Successfully sent to NPCI for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
124
125 # Process NPCI's ACK response
126
:-(
if response_body && String.trim(response_body) != "" do
127
:-(
Logger.info("[ASYNC RespPay] Received ACK from NPCI for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
128
:-(
Logger.debug("[ASYNC RespPay] NPCI ACK response: #{response_body}")
129
130
:-(
case parse_npci_ack(response_body) do
131 {:ok, ack_data} ->
132
:-(
Logger.info("[ASYNC RespPay] Successfully parsed NPCI ACK: #{inspect(ack_data)}")
133
:-(
Logger.info("[ASYNC RespPay] RespPay flow completed successfully")
134
135 # TODO: Update transaction status to indicate successful NPCI response
136
137 {:error, parse_error} ->
138
:-(
Logger.error("[ASYNC RespPay] Failed to parse NPCI ACK: #{parse_error}")
139
:-(
Logger.debug("[ASYNC RespPay] Raw ACK response: #{response_body}")
140
141 # Handle specific error cases
142
:-(
case parse_error do
143 "NPCI endpoint timeout" ->
144
:-(
Logger.warning("[ASYNC RespPay] NPCI timeout - transaction may need retry or manual reconciliation")
145 # TODO: Update transaction status to timeout/pending_reconciliation
146
147 "NPCI endpoint error" ->
148
:-(
Logger.error("[ASYNC RespPay] NPCI returned error - transaction failed")
149 # TODO: Update transaction status to failed
150
151 _ ->
152
:-(
Logger.error("[ASYNC RespPay] Unexpected NPCI response format")
153 # TODO: Update transaction status to pending_review
154 end
155 end
156 else
157
:-(
Logger.warning("[ASYNC RespPay] NPCI sent 200 but no ACK body for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
158 end
159
160 {:ok, %Req.Response{status: 405}} ->
161
:-(
Logger.error("[ASYNC RespPay] NPCI returned 405 Method Not Allowed for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
162
:-(
Logger.error("[ASYNC RespPay] This indicates:")
163
:-(
Logger.error("[ASYNC RespPay] - URL endpoint issue: #{npci_endpoint}")
164
:-(
Logger.error("[ASYNC RespPay] - POST method not accepted by NPCI")
165
:-(
Logger.error("[ASYNC RespPay] - NPCI test environment configuration issue")
166
167 {:ok, %Req.Response{status: status, body: error_body}} ->
168
:-(
Logger.error("[ASYNC RespPay] NPCI returned status #{status} for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
169
:-(
Logger.error("[ASYNC RespPay] Error response: #{inspect(error_body)}")
170
171 # TODO: Handle different error statuses appropriately
172
173 {:error, request_error} ->
174
:-(
Logger.error("[ASYNC RespPay] Network error sending to NPCI for txnId=#{txn_id}, orgTxnId=#{org_txn_id}")
175
:-(
Logger.error("[ASYNC RespPay] Request error: #{inspect(request_error)}")
176
177 # TODO: Implement retry logic or queuing for failed requests
178 end
179
180 :ok
181 end
182
183 @doc """
184 Send RespValQr XML asynchronously to NPCI after ACK has been sent.
185 """
186 @spec send_async_respvalqr(String.t(), String.t(), map()) :: :ok
187 def send_async_respvalqr(resp_val_qr_xml, txn_id, req_val_qr_data) do
188
:-(
Logger.info("[ASYNC RespValQr] Starting async RespValQr transmission for txnId=#{txn_id}")
189
:-(
Logger.debug("[ASYNC RespValQr] RespValQr XML: #{resp_val_qr_xml}")
190
191 # Construct NPCI callback URL
192
:-(
npci_endpoint = "#{@npci_base_url}/RespValQr/2.0/urn:txnid:#{txn_id}"
193
194
:-(
headers = [
195 {"content-type", "application/xml"},
196 {"accept", "application/xml"},
197 {"user-agent", "Mercury-UPI-PSP/1.0"}
198 ]
199
200
:-(
Logger.info("[ASYNC RespValQr] Sending to NPCI URL: #{npci_endpoint}")
201
202
:-(
case Req.post(npci_endpoint, body: resp_val_qr_xml, headers: headers, receive_timeout: @request_timeout) do
203 {:ok, %Req.Response{status: 200, body: response_body}} ->
204
:-(
Logger.info("[ASYNC RespValQr] Successfully sent to NPCI for txnId=#{txn_id}")
205
:-(
handle_npci_ack_response(response_body, "RespValQr", txn_id)
206
207 {:ok, %Req.Response{status: status, body: error_body}} ->
208
:-(
Logger.error("[ASYNC RespValQr] NPCI returned status #{status} for txnId=#{txn_id}")
209
:-(
Logger.error("[ASYNC RespValQr] Error response: #{inspect(error_body)}")
210
211 {:error, request_error} ->
212
:-(
Logger.error("[ASYNC RespValQr] Network error for txnId=#{txn_id}: #{inspect(request_error)}")
213 end
214
215 :ok
216 end
217
218 @doc """
219 Send RespChkTxn XML asynchronously to NPCI after ACK has been sent.
220 """
221 @spec send_async_respchktxn(String.t(), String.t(), map()) :: :ok
222 def send_async_respchktxn(resp_chk_txn_xml, org_txn_id, req_chk_txn_data) do
223
:-(
Logger.info("[ASYNC RespChkTxn] Starting async RespChkTxn transmission for orgTxnId=#{org_txn_id}")
224
:-(
Logger.debug("[ASYNC RespChkTxn] RespChkTxn XML: #{resp_chk_txn_xml}")
225
226 # Construct NPCI callback URL
227
:-(
npci_endpoint = "#{@npci_base_url}/RespChkTxn/2.0/urn:txnid:#{org_txn_id}"
228
229
:-(
headers = [
230 {"Content-Type", "application/xml; charset=UTF-8"},
231 {"Accept", "application/xml"},
232 {"User-Agent", "Mercury-UPI-PSP/1.0"}
233 ]
234
235
:-(
Logger.info("[ASYNC RespChkTxn] Sending to NPCI URL: #{npci_endpoint}")
236
:-(
Logger.info("[ASYNC RespChkTxn] Request headers: #{inspect(headers)}")
237
238
:-(
case Req.post(npci_endpoint, body: resp_chk_txn_xml, headers: headers, receive_timeout: @request_timeout) do
239 {:ok, %Req.Response{status: 200, body: response_body}} ->
240
:-(
Logger.info("[ASYNC RespChkTxn] Successfully sent to NPCI for orgTxnId=#{org_txn_id}")
241
:-(
handle_npci_ack_response(response_body, "RespChkTxn", org_txn_id)
242
243 {:ok, %Req.Response{status: status, body: error_body}} ->
244
:-(
Logger.error("[ASYNC RespChkTxn] NPCI returned status #{status} for orgTxnId=#{org_txn_id}")
245
:-(
Logger.error("[ASYNC RespChkTxn] Error response: #{inspect(error_body)}")
246
247 {:error, request_error} ->
248
:-(
Logger.error("[ASYNC RespChkTxn] Network error for orgTxnId=#{org_txn_id}: #{inspect(request_error)}")
249 end
250
251 :ok
252 end
253
254 # ================================
255 # PRIVATE HELPER FUNCTIONS
256 # ================================
257
258 defp handle_npci_ack_response(response_body, api_name, txn_id) do
259
:-(
if response_body && String.trim(response_body) != "" do
260
:-(
Logger.info("[#{api_name}] Received ACK from NPCI for #{txn_id}")
261
:-(
Logger.debug("[#{api_name}] NPCI ACK response: #{response_body}")
262
263
:-(
case parse_npci_ack(response_body) do
264 {:ok, ack_data} ->
265
:-(
Logger.info("[#{api_name}] Successfully parsed NPCI ACK: #{inspect(ack_data)}")
266
:-(
Logger.info("[#{api_name}] Flow completed successfully")
267
268 {:error, parse_error} ->
269
:-(
Logger.error("[#{api_name}] Failed to parse NPCI ACK: #{parse_error}")
270
:-(
Logger.debug("[#{api_name}] Raw ACK response: #{response_body}")
271
272 # Handle specific error cases
273
:-(
case parse_error do
274 "NPCI endpoint timeout" ->
275
:-(
Logger.warning("[#{api_name}] NPCI timeout - transaction may need retry or manual reconciliation")
276
277 "NPCI endpoint error" ->
278
:-(
Logger.error("[#{api_name}] NPCI returned error - transaction failed")
279
280 _ ->
281
:-(
Logger.error("[#{api_name}] Unexpected NPCI response format")
282 end
283 end
284 else
285
:-(
Logger.warning("[#{api_name}] NPCI sent 200 but no ACK body for #{txn_id}")
286 end
287 end
288
289 defp parse_npci_ack(ack_response) do
290 # Handle non-XML responses like "TIMEOUT", "ERROR", etc.
291
:-(
case String.trim(ack_response) do
292
:-(
"TIMEOUT" ->
293 {:error, "NPCI endpoint timeout"}
294
295
:-(
"ERROR" ->
296 {:error, "NPCI endpoint error"}
297
298
:-(
response when byte_size(response) < 10 ->
299
:-(
{:error, "Invalid NPCI response: #{response}"}
300
301 xml_response ->
302 # Only parse if it looks like XML (starts with < and contains XML content)
303
:-(
if String.starts_with?(xml_response, "<") and String.contains?(xml_response, "Ack") do
304
:-(
try do
305
:-(
parsed = xml_response
306 |> xpath(~x"//ns2:Ack"s,
307 api: ~x"./@api"s,
308 req_msg_id: ~x"./@reqMsgId"s,
309 org_txn_id: ~x"./@orgTxnId"s,
310 txn_id: ~x"./@txnId"s,
311 timestamp: ~x"./@ts"s
312 )
313
314 {:ok, parsed}
315 rescue
316
:-(
error ->
317 {:error, "Failed to parse ACK XML: #{inspect(error)}"}
318 end
319 else
320
:-(
{:error, "Non-XML response from NPCI: #{xml_response}"}
321 end
322 end
323 end
324 end
Line Hits Source