cover/Elixir.DaProductAppWeb.Api.V1.UpiController.html

1
:-(
defmodule DaProductAppWeb.Api.V1.UpiController do
2 @moduledoc """
3 Complete UPI Controller implementing all 8 UPI APIs as per NPCI specification.
4 Handles both NPCI→PSP requests and PSP→NPCI responses with full XML compliance.
5 """
6
7
:-(
use DaProductAppWeb, :controller
8
9 import Ecto.Query
10
11 alias DaProductAppWeb.UpiTransactionManager
12 alias DaProductAppWeb.UpiXmlSchema
13 alias DaProductApp.Transactions.UpiInternationalService
14 alias DaProductApp.Transactions.ReqPayService
15 alias DaProductApp.QRValidation.Service, as: QRValidationService
16 alias DaProductApp.Merchants
17 alias DaProductApp.Adapters.NpciAdapter
18
19 require Logger
20
21 # ================================
22 # API 1: ReqValQR - QR Validation Request (NPCI → PSP)
23 # ================================
24
25 @doc """
26 Handle QR validation request from NPCI.
27 Validates merchant QR code and returns merchant details.
28 """
29 def validate_qr(conn, _params) do
30
:-(
with {:ok, xml_body} <- read_request_body(conn),
31
:-(
{:ok, parsed_data} <- UpiXmlSchema.parse_req_val_qr(xml_body) do
32
33 # --- Early QRts expiry check ---
34 # If QR payload contains QRts and it's older than allowed window, send a PE RespValQr
35 # to NPCI asynchronously but still reply to NPCI with the ACK immediately.
36
:-(
qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
37
:-(
qr_validity_minutes = Application.get_env(:da_product_app, :qr_validity_minutes, 30)
38
39
:-(
Logger.info("[QR EXPIRY CHECK] Starting expiry check for msgId=#{Map.get(parsed_data, :msg_id, "unknown")}")
40
:-(
Logger.debug("[QR EXPIRY CHECK] Extracted note value: #{inspect(Map.get(parsed_data, :note))}")
41
:-(
Logger.debug("[QR EXPIRY CHECK] QR payload: #{inspect(Map.get(parsed_data, :qr_payload))}")
42
:-(
Logger.debug("[QR EXPIRY CHECK] Extracted QRts: #{inspect(qr_ts_string)}")
43
:-(
Logger.debug("[QR EXPIRY CHECK] QR validity window: #{qr_validity_minutes} minutes")
44
45
:-(
if qr_ts_string && qr_ts_string != "" do
46
:-(
Logger.info("[QR EXPIRY CHECK] QRts found: #{qr_ts_string}")
47
:-(
case DateTime.from_iso8601(qr_ts_string) do
48 {:ok, qr_ts_dt, _offset} ->
49
:-(
now = DateTime.utc_now()
50
:-(
diff_seconds = DateTime.diff(now, qr_ts_dt)
51
:-(
diff_minutes = diff_seconds / 60
52
53
:-(
Logger.info("[QR EXPIRY CHECK] QRts parsed: #{DateTime.to_iso8601(qr_ts_dt)}")
54
:-(
Logger.info("[QR EXPIRY CHECK] Current UTC time: #{DateTime.to_iso8601(now)}")
55
:-(
Logger.info("[QR EXPIRY CHECK] Time diff: #{diff_seconds} seconds (#{Float.round(diff_minutes, 2)} minutes)")
56
57
:-(
txn_ts_dt = case Map.get(parsed_data, :timestamp) do
58
:-(
nil -> nil
59
:-(
s -> case DateTime.from_iso8601(s) do
60
:-(
{:ok, tdt, _} -> tdt
61
:-(
_ -> nil
62 end
63 end
64
65
:-(
Logger.debug("[QR EXPIRY CHECK] ReqValQr timestamp: #{inspect(Map.get(parsed_data, :timestamp))}")
66
:-(
Logger.debug("[QR EXPIRY CHECK] Parsed txn timestamp: #{if txn_ts_dt, do: DateTime.to_iso8601(txn_ts_dt), else: "nil"}")
67
68 # Consider expired if either QRts is older than the transaction ts or older than configured window
69
:-(
expired_by_window = diff_seconds > qr_validity_minutes * 60
70
:-(
expired_by_txn_ts = if txn_ts_dt, do: DateTime.compare(qr_ts_dt, txn_ts_dt) == :lt, else: false
71
72
:-(
Logger.info("[QR EXPIRY CHECK] Expiry analysis: expired_by_window=#{expired_by_window}, expired_by_txn_ts=#{expired_by_txn_ts}")
73
74
:-(
if expired_by_window do
75 # Build RespValQr error with Payload Expired
76
:-(
resp_data = %{
77 org_id: Map.get(parsed_data, :org_id, "MER101"),
78 msg_id: generate_message_id(),
79
:-(
req_msg_id: parsed_data.msg_id,
80 result: "FAILURE",
81 err_code: "PE",
82
:-(
txn_id: parsed_data.txn_id,
83
:-(
note: parsed_data.note || "QR Payload expired",
84
:-(
ref_id: parsed_data.ref_id || generate_message_id(),
85
:-(
ref_url: parsed_data.ref_url || "",
86
:-(
purpose: parsed_data.purpose || "11",
87
:-(
cust_ref: parsed_data.cust_ref || "",
88
:-(
initiation_mode: parsed_data.initiation_mode || "16",
89
:-(
txn_timestamp: parsed_data.timestamp || get_timestamp(),
90
:-(
qr_payload: parsed_data.qr_payload,
91
:-(
payee_addr: Map.get(parsed_data, :payer_addr) || "",
92
:-(
payee_name: Map.get(parsed_data, :payer_name) || "Mercury PSP",
93 payee_type: "ENTITY",
94 merchant_code: "0000",
95 country_code: "IN",
96 net_inst_id: "MER1010001"
97 }
98
99 # Generate the error XML now and send asynchronously so we still return ACK to caller
100
:-(
case UpiXmlSchema.generate_resp_val_qr(resp_data) do
101 {:ok, error_xml} ->
102
:-(
Logger.info("[RespValQr] QR payload expired (PE) - scheduling async PE response for msgId=#{parsed_data.msg_id}; expired_by_window=#{expired_by_window}, expired_by_txn_ts=#{expired_by_txn_ts}")
103
:-(
Logger.debug("[RespValQr] Error XML payload: #{error_xml}")
104
105
:-(
Task.start(fn ->
106
:-(
npci_qr_endpoint = Application.get_env(:da_product_app, :npci_qr_validation_endpoint, "https://precert.nfinite.in/iupi/RespValQr/2.0/urn:txnid:")
107
:-(
npci_callback_url = "#{npci_qr_endpoint}#{parsed_data.txn_id}"
108
:-(
headers = [{"content-type", "application/xml"}, {"accept", "application/xml"}]
109
110 # Try to find an existing QR validation record to update (best-effort)
111
:-(
case QRValidationService.find_original_qr_validation(parsed_data.msg_id, parsed_data.org_id) do
112
:-(
nil -> Logger.info("[RespValQr] No existing QR validation row found for msgId=#{parsed_data.msg_id}, proceeding to send error XML")
113 qr ->
114
:-(
Logger.info("[RespValQr] Found existing QR validation row id=#{qr.id} for msgId=#{parsed_data.msg_id}, will store response hash where possible")
115 # Best-effort: store the resp xml hash before sending
116
:-(
try do
117
:-(
_ = QRValidationService.store_response_xml_hash(qr, error_xml)
118 rescue
119
:-(
e -> Logger.error("[RespValQr] store_response_xml_hash failed for qr_id=#{qr.id}: #{inspect(e)}")
120 end
121 end
122
123
:-(
case Req.post(npci_callback_url, body: error_xml, headers: headers, receive_timeout: 30_000) do
124
:-(
{:ok, %Req.Response{status: 200}} -> Logger.info("[RespValQr] Sent PE error response to NPCI for msgId=#{parsed_data.msg_id}")
125
:-(
{:ok, %Req.Response{status: status, body: body}} -> Logger.error("[RespValQr] NPCI returned status #{status} for PE response: #{inspect(body)}")
126
:-(
{:error, reason} -> Logger.error("[RespValQr] Failed to send PE response to NPCI: #{inspect(reason)}")
127 end
128 end)
129
130 # Send ACK immediately and return (do not continue with normal processing)
131
:-(
msg_id_str = parsed_data.msg_id || "unknown"
132
:-(
ack_xml = """
133
:-(
<ns2:Ack xmlns:ns2="http://npci.org/upi/schema/" api="ReqValQr" reqMsgId="#{msg_id_str}" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}"/>
134 """
135
136
:-(
Logger.info("Sending ACK for ReqValQr with msgId=#{msg_id_str}")
137
:-(
Logger.info("ACK XML: #{ack_xml}")
138
139 # Return connection with ACK - do NOT continue processing
140 conn
141 |> put_resp_content_type("application/xml")
142
:-(
|> send_resp(200, ack_xml)
143
144 {:error, reason} ->
145
:-(
Logger.error("[RespValQr] Failed to generate PE RespValQr XML: #{inspect(reason)}")
146
147 # Send ACK and continue with normal flow if PE generation fails
148
:-(
Logger.info("[QR EXPIRY CHECK] PE generation failed, continuing with normal flow")
149
:-(
process_normal_qr_flow(conn, xml_body, parsed_data)
150 end
151 else
152
:-(
Logger.info("[QR EXPIRY CHECK] QR is still valid - proceeding with normal flow")
153
:-(
process_normal_qr_flow(conn, xml_body, parsed_data)
154 end
155 {:error, parse_error} ->
156
:-(
Logger.warn("[QR EXPIRY CHECK] Failed to parse QRts '#{qr_ts_string}': #{inspect(parse_error)}")
157
:-(
process_normal_qr_flow(conn, xml_body, parsed_data)
158 end
159 else
160
:-(
Logger.info("[QR EXPIRY CHECK] No QRts found in payload - proceeding with normal flow")
161
:-(
process_normal_qr_flow(conn, xml_body, parsed_data)
162 end
163 else
164 {:error, :invalid_request} ->
165
:-(
Logger.error("Failed to read request body for ReqValQr")
166
:-(
send_error_response(conn, "96", "System malfunction")
167 {:error, parse_error} ->
168
:-(
Logger.error("Failed to parse ReqValQr XML: #{inspect(parse_error)}")
169
:-(
send_error_response(conn, "96", "System malfunction")
170 end
171 end
172
173 # Normal QR validation processing flow (extracted from main function)
174 defp process_normal_qr_flow(conn, xml_body, parsed_data) do
175 # Step 1: Generate ACK XML
176
:-(
msg_id_str = parsed_data.msg_id || "unknown"
177
:-(
ack_xml = """
178
:-(
<ns2:Ack xmlns:ns2="http://npci.org/upi/schema/" api="ReqValQr" reqMsgId="#{msg_id_str}" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}"/>
179 """
180
181 # Log the ACK being sent
182
:-(
Logger.info("Sending ACK for ReqValQr with msgId=#{msg_id_str}")
183
:-(
Logger.info("ACK XML: #{ack_xml}")
184
185 # Step 2: Send ACK immediately as HTTP response (main thread)
186
:-(
conn =
187 conn
188 |> put_resp_content_type("application/xml")
189 |> send_resp(200, ack_xml)
190
191 # Step 3: In a separate Task, process and send RespValQr to NPCI
192
:-(
Task.start(fn ->
193
:-(
case UpiTransactionManager.process_qr_validation(xml_body) do
194 {:ok, {respvalqr_xml, qr_validation}} ->
195 # Store the RespValQr XML hash before sending to NPCI
196
:-(
case QRValidationService.store_response_xml_hash(qr_validation, respvalqr_xml) do
197 {:ok, updated_qr_validation} ->
198
:-(
Logger.info("[RespValQr] XML hash stored successfully for msgId=#{parsed_data.msg_id || "unknown"}")
199
200 # Success case - send RespValQr to NPCI callback URL
201
:-(
npci_qr_endpoint = Application.get_env(:da_product_app, :npci_qr_validation_endpoint, "https://precert.nfinite.in/iupi/RespValQr/2.0/urn:txnid:")
202
:-(
npci_callback_url = "#{npci_qr_endpoint}#{parsed_data.txn_id}"
203
:-(
headers = [
204 {"content-type", "application/xml"},
205 {"accept", "application/xml"}
206 ]
207
208
:-(
Logger.info("[RespValQr] Ready to send to NPCI for msgId=#{parsed_data.msg_id || "unknown"}")
209
:-(
Logger.info("[RespValQr] Sending to URL: #{npci_callback_url}")
210
:-(
Logger.info("[RespValQr] Request headers: #{inspect(headers)}")
211
:-(
Logger.info("[RespValQr] XML payload: #{respvalqr_xml}")
212
213 # Update QR validation with response sent timestamp
214
:-(
response_sent_at = DateTime.utc_now()
215
:-(
QRValidationService.update_validation(updated_qr_validation, %{
216 npci_response_sent_at: response_sent_at
217 })
218
219 # Send RespValQr to NPCI (handle response/log errors)
220
:-(
case Req.post(npci_callback_url, body: respvalqr_xml, headers: headers, receive_timeout: 30_000) do
221 {:ok, %Req.Response{status: 200, body: body}} ->
222
:-(
msg_id_str = parsed_data.msg_id || "unknown"
223
:-(
Logger.info("[RespValQr] Successfully sent to NPCI for msgId=#{msg_id_str}")
224
225 # Parse and log the ACK response from NPCI
226
:-(
Logger.info("[RespValQr] Received ACK response from NPCI for msgId=#{msg_id_str}")
227
:-(
Logger.debug("[RespValQr] NPCI ACK response: #{body}")
228
229 # Parse ACK response from NPCI
230
:-(
parsed_ack = UpiXmlSchema.parse_ack_response(body)
231
:-(
Logger.info("[RespValQr] Successfully parsed ACK from NPCI: #{inspect(parsed_ack)}")
232
:-(
Logger.info("[RespValQr] QR validation flow completed successfully with ACK")
233
234 {:ok, %Req.Response{status: status, body: body}} ->
235
:-(
Logger.error("[RespValQr] NPCI returned non-200 status #{status} for msgId=#{parsed_data.msg_id || "unknown"}")
236
:-(
Logger.error("[RespValQr] NPCI error response: #{inspect(body)}")
237
238 {:error, reason} ->
239
:-(
Logger.error("[RespValQr] Failed to send RespValQr to NPCI for msgId=#{parsed_data.msg_id || "unknown"}: #{inspect(reason)}")
240 end
241
242 {:error, storage_error} ->
243
:-(
Logger.error("[RespValQr] Failed to store XML hash for msgId=#{parsed_data.msg_id || "unknown"}: #{inspect(storage_error)}")
244 end
245
246 {:ok, respvalqr_xml} when is_binary(respvalqr_xml) ->
247 # Error case - XML response without QR validation record
248
:-(
Logger.error("[RespValQr] Error response generated: #{String.slice(respvalqr_xml, 0, 200)}...")
249
250
:-(
npci_qr_endpoint = Application.get_env(:da_product_app, :npci_qr_validation_endpoint, "https://precert.nfinite.in/iupi/RespValQr/2.0/urn:txnid:")
251
:-(
npci_callback_url = "#{npci_qr_endpoint}#{parsed_data.txn_id}"
252
:-(
headers = [
253 {"content-type", "application/xml"},
254 {"accept", "application/xml"}
255 ]
256
257
:-(
Logger.info("[RespValQr] Sending error response to NPCI for msgId=#{parsed_data.msg_id || "unknown"}")
258
:-(
Logger.info("[RespValQr] Sending to URL: #{npci_callback_url}")
259
:-(
Logger.info("[RespValQr] XML payload: #{respvalqr_xml}")
260
261 # Send error RespValQr to NPCI
262
:-(
case Req.post(npci_callback_url, body: respvalqr_xml, headers: headers, receive_timeout: 30_000) do
263 {:ok, %Req.Response{status: 200, body: body}} ->
264
:-(
msg_id_str = parsed_data.msg_id || "unknown"
265
:-(
Logger.info("[RespValQr] Successfully sent error response to NPCI for msgId=#{msg_id_str}")
266
:-(
Logger.debug("[RespValQr] NPCI ACK response: #{body}")
267
268 # Parse ACK response from NPCI for error responses and log it
269
:-(
case UpiXmlSchema.parse_ack_response(body) do
270 {:ok, parsed_ack} ->
271
:-(
Logger.info("[RespValQr] Successfully parsed ACK from NPCI for error response: #{inspect(parsed_ack)}")
272 {:error, parse_err} ->
273
:-(
Logger.error("[RespValQr] Failed to parse ACK from NPCI for error response: #{inspect(parse_err)}")
274 end
275
276 {:ok, %Req.Response{status: status, body: body}} ->
277
:-(
Logger.error("[RespValQr] NPCI returned non-200 status #{status} for error response msgId=#{parsed_data.msg_id || "unknown"}")
278
:-(
Logger.error("[RespValQr] NPCI error response: #{inspect(body)}")
279
280 {:error, reason} ->
281
:-(
Logger.error("[RespValQr] Failed to send error RespValQr to NPCI for msgId=#{parsed_data.msg_id || "unknown"}: #{inspect(reason)}")
282 end
283
284 {:error, processing_error} ->
285
:-(
Logger.error("[RespValQr] Failed to process QR validation for msgId=#{parsed_data.msg_id || "unknown"}: #{inspect(processing_error)}")
286 end
287 end)
288
289 # Return the connection with ACK already sent
290
:-(
conn
291 end
292
293 # ================================
294 # API 2: RespValQR - QR Validation Response (PSP → NPCI)
295 # ================================
296 # Note: This is automatically generated by validate_qr/2 above
297
298 # ================================
299 # API 3: ReqPay - Payment Request (NPCI → PSP)
300 # ================================
301
302 @doc """
303 Handle OTP request from NPCI.
304 """
305 def otp_request(conn, params) do
306
:-(
Logger.info("OTP request: #{inspect(params)}")
307
308
:-(
with {:ok, xml_body} <- read_request_body(conn),
309
:-(
{:ok, xml_response} <- UpiTransactionManager.process_otp_request(xml_body) do
310
311
:-(
Logger.info("OTP request processed successfully")
312
313 conn
314 |> put_resp_content_type("application/xml")
315
:-(
|> send_resp(200, xml_response)
316 else
317 {:error, reason} ->
318
:-(
Logger.error("OTP request failed: #{reason}")
319
:-(
send_error_response(conn, "Z6", "OTP request failed: #{reason}")
320 end
321 end
322
323 @doc """
324 Handle set credentials request from NPCI.
325 """
326 def set_credentials(conn, params) do
327
:-(
Logger.info("Set credentials request: #{inspect(params)}")
328
329
:-(
with {:ok, xml_body} <- read_request_body(conn),
330
:-(
{:ok, xml_response} <- UpiTransactionManager.process_set_credentials(xml_body) do
331
332
:-(
Logger.info("Set credentials processed successfully")
333
334 conn
335 |> put_resp_content_type("application/xml")
336
:-(
|> send_resp(200, xml_response)
337 else
338 {:error, reason} ->
339
:-(
Logger.error("Set credentials failed: #{reason}")
340
:-(
send_error_response(conn, "Z7", "Set credentials failed: #{reason}")
341 end
342 end
343
344 # ================================
345 # Private Helper Functions
346 # ================================
347
348 defp extract_npci_path_info(params) do
349
:-(
case params do
350 %{"path" => path_parts} when is_list(path_parts) ->
351
:-(
%{
352 version: Enum.at(path_parts, 0, "2.0"),
353 transaction_id: Enum.at(path_parts, 1, ""),
354 full_path: Enum.join(path_parts, "/")
355 }
356 _ ->
357
:-(
%{version: "2.0", transaction_id: "", full_path: ""}
358 end
359 end
360
361 defp generate_utc_timestamp do
362 DateTime.utc_now()
363 |> DateTime.to_iso8601()
364 |> String.replace("Z", "+00:00")
365 end
366
367 defp generate_msg_id do
368 # Generate exactly 35 characters as required by NPCI
369
:-(
prefix = "MERMSG" # 6 chars
370 # Need 29 more chars (35 - 6 = 29)
371 # Use 14 bytes (28 hex chars) + 1 extra char = 29 chars
372
:-(
suffix = (:crypto.strong_rand_bytes(14) |> Base.encode16()) <> "A"
373
:-(
(prefix <> suffix) |> String.slice(0, 35)
374 end
375
376 # ================================
377 # API 1: ReqValQR - QR Validation Request (NPCI → PSP)
378 # ================================
379
380 @doc """
381 Handle QR validation request from NPCI.
382 Validates merchant QR code and returns merchant details.
383 """
384 @doc """
385 Handle QR validation request from NPCI.
386 Validates merchant QR code and returns merchant details.
387
388 This function:
389 - Reads the raw XML request body from the connection.
390 - Passes the XML to UpiTransactionManager.process_qr_validation/1 for business logic and response generation.
391 - On success, logs the event and sends the XML response with HTTP 200.
392 - On error, logs the error and sends a compliant error response with code "ZH".
393
394 # ================================
395 # API 2: RespValQR - QR Validation Response (PSP → NPCI)
396 # ================================
397 # Note: This is automatically generated by validate_qr/2 above
398
399 # ================================
400 # API 3: ReqPay - Payment Request (NPCI → PSP)
401 # ================================
402
403 @doc """
404 Handle payment request from NPCI.
405 Processes both CREDIT and REVERSAL payment requests.
406 """
407 def process_payment(conn, _params) do
408
:-(
with {:ok, xml_body} <- read_request_body(conn) do
409 # Log the complete incoming request at controller level
410
:-(
Logger.info(String.duplicate("=", 60))
411
:-(
Logger.info("CONTROLLER: RECEIVED PAYMENT REQUEST")
412
:-(
Logger.info(String.duplicate("=", 60))
413
:-(
Logger.info("#{xml_body}")
414
:-(
Logger.info(String.duplicate("=", 60))
415
416
:-(
case determine_payment_type_and_process(xml_body) do
417 {:ok, xml_response} ->
418
:-(
Logger.info("Payment request processed successfully")
419 conn
420 |> put_resp_content_type("application/xml")
421
:-(
|> send_resp(200, xml_response)
422 {:error, reason} ->
423
:-(
Logger.error("Payment processing failed: #{reason}")
424
:-(
send_error_response(conn, "ZH", reason)
425 end
426 else
427 {:error, reason} ->
428
:-(
Logger.error("Failed to read request body: #{reason}")
429
:-(
send_error_response(conn, "ZH", "Invalid request body")
430 end
431 end
432
433 # ================================
434 # API 4: RespPay - Payment Response (PSP → NPCI)
435 # ================================
436 # Note: This is automatically generated by process_payment/2 above
437
438 # ================================
439 # API 5: ReqChkTxn - Check Transaction Request (NPCI → PSP)
440 # ================================
441
442 @doc """
443 Handle transaction status check request from NPCI.
444 Returns current status of a transaction.
445 """
446 def check_transaction(conn, _params) do
447 # Log the incoming request details
448
:-(
request_path = conn.request_path
449
:-(
request_url = "#{conn.scheme}://#{conn.host}:#{conn.port}#{request_path}"
450
451
:-(
Logger.info("ReqChkTxn received from NPCI")
452
:-(
Logger.info("Request URL: #{request_url}")
453
:-(
Logger.info("Request Path: #{request_path}")
454
455
:-(
with {:ok, xml_body} <- read_request_body(conn),
456
:-(
{:ok, _xml_response} <- UpiTransactionManager.process_status_check(xml_body) do
457
458
:-(
Logger.info("Transaction status check processed successfully")
459
:-(
Logger.info("RespChkTxn sent to NPCI endpoint directly")
460
461 # Return a simple acknowledgment to NPCI since the actual RespChkTxn
462 # has been sent to the appropriate NPCI endpoint
463 conn
464 |> put_resp_content_type("application/xml")
465
:-(
|> send_resp(200, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ack>SUCCESS</ack>")
466 else
467 {:error, reason} ->
468
:-(
Logger.error("Transaction status check failed: #{reason}")
469
:-(
Logger.error("Failed request URL: #{request_url}")
470
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
471
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
472 end
473 end
474
475 # ================================
476 # API 6: RespChkTxn - Check Transaction Response (PSP → NPCI)
477 # ================================
478 # Note: This is automatically generated by check_transaction/2 above
479
480 # ================================
481 # API 7: ReqHbt - Heartbeat Request (NPCI → PSP)
482 # ================================
483
484 @doc """
485 Handle heartbeat request from NPCI.
486 Used for health monitoring and connectivity checks.
487
488 Expected URL format: /ReqHbt/2.0/urn:txnid:MERfb3b2da8511849259e669b6b356aa660
489 Expected XML format:
490 <ns2:ReqHbt xmlns:ns2="http://npci.org/upi/schema/">
491 <Head ver="2.0" ts="2024-01-29T13:13:55+05:30" orgId="NPCI" msgId="PAM3eaf410eff2349638897034ef263d1e3"/>
492 <Txn id="PAM3aee05cd100641a8b263d58f749abea8" note="ReqHbt" refId="083231151104" refUrl="www.test.co.in" ts="2024-01-29T13:13:55+05:30" type="Hbt" custRef="083231151104"/>
493 <HbtMsg type="ALIVE" value="NA"/>
494 </ns2:ReqHbt>
495
496 Response format:
497 <ns2:Ack api="ReqHbt" reqMsgId="PAM3eaf410eff2349638897034ef263d1e3" ts="2024-01-29T07:43:56+00:00" xmlns:ns2="http://npci.org/upi/schema/"/>
498 """
499 def heartbeat(conn, params) do
500 # Extract path information if present (from NPCI URL pattern)
501
:-(
path_info = extract_npci_path_info(params)
502
:-(
Logger.info("NPCI Heartbeat request received - Version: #{path_info.version}, Path: #{path_info.full_path}")
503
504
:-(
with {:ok, xml_body} <- read_request_body(conn),
505
:-(
{:ok, xml_response} <- UpiTransactionManager.process_heartbeat(xml_body) do
506
507
:-(
Logger.info("Heartbeat processed successfully - responding with Ack")
508
:-(
Logger.info("ACK Response: #{xml_response}")
509
510 conn
511 |> put_resp_content_type("application/xml")
512
:-(
|> send_resp(200, xml_response)
513 else
514 {:error, reason} ->
515
:-(
Logger.error("Heartbeat processing failed: #{reason}")
516
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
517 end
518 end
519
520 # ================================
521 # API 8: RespHbt - Heartbeat Response (PSP → NPCI)
522 # ================================
523 # Note: This is automatically generated by heartbeat/2 above
524
525 # ================================
526 # Additional API Endpoints for Enhanced Functionality
527 # ================================
528
529 @doc """
530 Handle batch transaction status requests.
531 Extension for checking multiple transactions at once.
532 """
533 def batch_check_transactions(conn, _params) do
534
:-(
with {:ok, xml_body} <- read_request_body(conn),
535
:-(
{:ok, batch_responses} <- process_batch_status_check(xml_body) do
536
537
:-(
Logger.info("Batch transaction check processed successfully")
538
539 conn
540 |> put_resp_content_type("application/xml")
541
:-(
|> send_resp(200, batch_responses)
542 else
543 {:error, reason} ->
544
:-(
Logger.error("Batch transaction check failed: #{reason}")
545
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
546 end
547 end
548
549 @doc """
550 Handle reconciliation requests.
551 For end-of-day settlement reconciliation.
552 """
553 def reconciliation(conn, _params) do
554
:-(
with {:ok, xml_body} <- read_request_body(conn),
555
:-(
{:ok, reconciliation_response} <- process_reconciliation(xml_body) do
556
557
:-(
Logger.info("Reconciliation processed successfully")
558
559 conn
560 |> put_resp_content_type("application/xml")
561
:-(
|> send_resp(200, reconciliation_response)
562 else
563 {:error, reason} ->
564
:-(
Logger.error("Reconciliation failed: #{reason}")
565
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
566 end
567 end
568
569 @doc """
570 Handle mandate requests (for recurring payments).
571 Extension for UPI mandate functionality.
572 """
573 def mandate_request(conn, _params) do
574
:-(
with {:ok, xml_body} <- read_request_body(conn),
575
:-(
{:ok, mandate_response} <- process_mandate_request(xml_body) do
576
577
:-(
Logger.info("Mandate request processed successfully")
578
579 conn
580 |> put_resp_content_type("application/xml")
581
:-(
|> send_resp(200, mandate_response)
582 else
583 {:error, reason} ->
584
:-(
Logger.error("Mandate request failed: #{reason}")
585
:-(
send_error_response(conn, "96", "System malfunction: #{reason}")
586 end
587 end
588
589 # ================================
590 # International UPI Query APIs
591 # ================================
592
593 @doc """
594 Get international QR with FX conversion details.
595 GET /api/v1/upi/international-qr
596 """
597 def get_international_qr(conn, params) do
598
:-(
try do
599
:-(
with {:ok, validated_params} <- validate_international_qr_params(params),
600
:-(
{:ok, qr_data} <- generate_international_qr_response(validated_params) do
601
602 conn
603
:-(
|> json(%{
604 success: true,
605 data: qr_data,
606 message: "International QR generated successfully"
607 })
608 else
609 {:error, reason} ->
610
:-(
Logger.error("International QR generation failed: #{inspect(reason)}")
611
612 conn
613 |> put_status(:bad_request)
614
:-(
|> json(%{
615 success: false,
616 error: "qr_generation_failed",
617 message: "Failed to generate international QR: #{inspect(reason)}"
618 })
619 end
620 rescue
621
:-(
e ->
622
:-(
Logger.error("International QR API error: #{Exception.message(e)}")
623
624 conn
625 |> put_status(:internal_server_error)
626
:-(
|> json(%{
627 success: false,
628 error: "internal_server_error",
629 message: "An internal error occurred"
630 })
631 end
632 end
633
634 @doc """
635 Get FX rate for currency conversion.
636 GET /api/v1/upi/fx-rate/:from/:to
637 """
638 def get_fx_rate(conn, %{"from" => from_currency, "to" => to_currency}) do
639
:-(
try do
640 # For now, using static rates with markup - in production this would query live rates
641
:-(
base_rate = get_base_fx_rate(from_currency, to_currency)
642
:-(
markup_percentage = get_corridor_markup(from_currency)
643
:-(
final_rate = base_rate * (1 + markup_percentage / 100)
644
645 conn
646
:-(
|> json(%{
647 success: true,
648 data: %{
649 from_currency: from_currency,
650 to_currency: to_currency,
651 base_rate: Float.to_string(base_rate),
652 markup_percentage: Float.to_string(markup_percentage),
653 final_rate: Float.to_string(Float.round(final_rate, 2)),
654 rate_timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
655 rate_source: "LIVE_MARKET"
656 },
657 message: "FX rate retrieved successfully"
658 })
659 rescue
660
:-(
e ->
661
:-(
Logger.error("FX rate API error: #{Exception.message(e)}")
662
663 conn
664 |> put_status(:internal_server_error)
665
:-(
|> json(%{
666 success: false,
667 error: "rate_unavailable",
668 message: "Unable to retrieve exchange rate"
669 })
670 end
671 end
672
673 # ================================
674 # International QR Helper Functions
675 # ================================
676
677 defp validate_international_qr_params(params) do
678
:-(
required_fields = ["merchant_id", "amount", "currency", "corridor","customer_ref"]
679
680
:-(
case Enum.find(required_fields, fn field -> is_nil(params[field]) or params[field] == "" end) do
681
:-(
nil -> {:ok, params}
682
:-(
missing_field -> {:error, "Missing required field: #{missing_field}"}
683 end
684 end
685
686 defp generate_international_qr_response(params) do
687
:-(
with {:ok, fx_rate} <- get_fx_rate_for_params(params["currency"]),
688
:-(
{:ok, inr_amount} <- calculate_inr_conversion(params["amount"], fx_rate) do
689
690 # Use customer_ref from metadata if available, otherwise fallback to merchant lookup
691
:-(
merchant_vpa = case get_in(params, ["metadata", "customer_ref"]) do
692
:-(
nil -> get_merchant_vpa(params["merchant_id"])
693 customer_ref when is_binary(customer_ref) ->
694 # Replace @sgpayments with @mercury to standardize
695
:-(
String.replace(customer_ref, "@sgpayments", "@mercury")
696 end
697
698
:-(
merchant_name = params["merchant_name"] || "International Merchant"
699
700
:-(
qr_string = "upi://pay?pa=#{merchant_vpa}&pn=#{URI.encode(merchant_name)}&am=#{inr_amount}&cu=INR&mc=#{params["merchant_category"] || "5411"}"
701
702
:-(
qr_data = %{
703 qr_string: qr_string,
704 foreign_amount: params["amount"],
705 foreign_currency: params["currency"],
706 fx_rate: Float.to_string(fx_rate),
707 markup_percentage: Float.to_string(get_corridor_markup(params["currency"])),
708 inr_amount: inr_amount,
709 corridor: params["corridor"],
710
:-(
validity_minutes: String.to_integer(params["validity_minutes"] || "300")
711 }
712
713 {:ok, qr_data}
714 else
715
:-(
error -> error
716 end
717 end
718
719 defp get_fx_rate_for_params(currency) do
720
:-(
rate = get_base_fx_rate(currency, "INR")
721
:-(
markup = get_corridor_markup(currency)
722
:-(
final_rate = rate * (1 + markup / 100)
723 {:ok, Float.round(final_rate, 2)}
724 end
725
726 defp calculate_inr_conversion(amount_str, fx_rate) do
727
:-(
try do
728
:-(
amount = String.to_float(amount_str)
729
:-(
inr_amount = amount * fx_rate
730 {:ok, Float.round(inr_amount, 2) |> Float.to_string()}
731 rescue
732
:-(
_e -> {:error, "Invalid amount format"}
733 end
734 end
735
736 defp get_base_fx_rate(from_currency, _to_currency) do
737
:-(
case from_currency do
738
:-(
"SGD" -> 81.25
739
:-(
"USD" -> 83.50
740
:-(
"AED" -> 22.75
741
:-(
"EUR" -> 89.40
742
:-(
"GBP" -> 104.25
743
:-(
_ -> 80.00 # Default fallback
744 end
745 end
746 defp get_merchant_vpa(merchant_id) do
747
:-(
case Merchants.get_merchant_by_mid(merchant_id) do
748 nil ->
749 # Fallback to constructed VPA if merchant not found
750
:-(
"#{merchant_id}@mercury"
751
752 %Merchants.Merchant{} = merchant ->
753 # Use the merchant's actual VPA, replacing @sgpayments with @mercury if needed
754
:-(
case merchant.merchant_vpa do
755
:-(
nil -> "#{merchant_id}@mercury"
756 vpa when is_binary(vpa) ->
757 # Replace @sgpayments with @mercury to standardize
758
:-(
String.replace(vpa, "@sgpayments", "@mercury")
759 end
760 end
761 end
762 defp get_corridor_markup(currency) do
763
:-(
case currency do
764
:-(
"SGD" -> 2.5
765
:-(
"USD" -> 2.0
766
:-(
"AED" -> 3.0
767
:-(
"EUR" -> 2.8
768
:-(
"GBP" -> 3.2
769
:-(
_ -> 2.5 # Default markup
770 end
771 end
772
773 # ================================
774 # Private Helper Functions
775 # ================================
776
777 defp read_request_body(conn) do
778
:-(
Logger.info("Reading request body...")
779
780 # For XML requests, first check if ConditionalBodyReader captured the body
781
:-(
case conn.assigns[:raw_body] do
782 body when is_binary(body) and body != "" ->
783
:-(
Logger.info("Using body from ConditionalBodyReader, size: #{byte_size(body)}")
784
:-(
Logger.debug("Body content: #{inspect(body)}")
785 {:ok, body}
786
787 _ ->
788
:-(
Logger.info("No body in assigns, trying direct read...")
789 # Fallback: try to read directly from connection
790
:-(
case read_body(conn) do
791 {:ok, body, _conn} when body != "" ->
792
:-(
Logger.info("Successfully read body directly, size: #{byte_size(body)}")
793
:-(
Logger.debug("Body content: #{inspect(body)}")
794 {:ok, body}
795
796 {:ok, "", _conn} ->
797
:-(
Logger.error("Empty request body from direct read")
798 {:error, "Empty request body"}
799
800 {:error, :already_read} ->
801
:-(
Logger.error("Request body already consumed by middleware")
802 {:error, "Request body already consumed by middleware"}
803
804 {:error, reason} ->
805
:-(
Logger.error("Failed to read request body: #{inspect(reason)}")
806 {:error, "Failed to read request body: #{inspect(reason)}"}
807 end
808 end
809 end
810
811 defp determine_payment_type_and_process(xml_body) do
812 # Check payment type based on XML structure - prioritize QR detection
813
:-(
Logger.debug(" Analyzing payment type for XML...")
814
815
:-(
cond do
816 is_qr_payment?(xml_body) ->
817
:-(
Logger.info("Processing QR payment request (with immediate ACK)")
818 # Best-effort: parse and persist ReqPay before processing so the UI and
819 # audit tables reflect the incoming payment even if async processing
820 # later fails. This avoids losing visibility into incoming ReqPay rows.
821
:-(
case UpiXmlSchema.parse_req_pay(xml_body) do
822 {:ok, parsed_reqpay} ->
823
:-(
req_attrs = build_req_pay_attrs(parsed_reqpay)
824 # Call create_req_pay/3 which will create req_pays and initial events
825
:-(
case ReqPayService.create_req_pay(req_attrs, xml_body, parsed_reqpay) do
826 {:ok, stored} ->
827
:-(
Logger.info("[ReqPay] Pre-stored ReqPay id=#{stored.id} for txn_id=#{Map.get(parsed_reqpay, :txn_id)}")
828 {:error, err} ->
829
:-(
Logger.error("[ReqPay] Pre-store failed: #{inspect(err)} - continuing processing")
830 end
831 {:error, _} ->
832
:-(
Logger.debug("[ReqPay] Could not parse ReqPay for pre-storage; proceeding without pre-store")
833 end
834
835
:-(
case UpiTransactionManager.process_qr_payment_request(xml_body) do
836
:-(
xml_response when is_binary(xml_response) -> {:ok, xml_response}
837
:-(
{:ok, xml_response} -> {:ok, xml_response}
838
:-(
error -> error
839 end
840
841
:-(
String.contains?(xml_body, "REVERSAL") or String.contains?(xml_body, "type=\"REVERSAL\"") ->
842
:-(
Logger.info("Processing reversal request")
843
:-(
UpiTransactionManager.process_reversal_request(xml_body)
844
845
:-(
is_international_payment?(xml_body) ->
846
:-(
Logger.info("Processing international payment request")
847
:-(
UpiTransactionManager.process_international_payment_request(xml_body)
848
849
:-(
true ->
850
:-(
Logger.info("Processing regular payment request")
851
:-(
UpiTransactionManager.process_payment_request(xml_body)
852 end
853 end
854
855 defp is_qr_payment?(xml_body) do
856 # Detect QR payment by checking for QR-specific indicators
857 # QR payments should be prioritized over international detection
858
:-(
Logger.debug(" Checking if QR payment...")
859
860
:-(
qr_indicators = [
861 String.contains?(xml_body, "<QR "), # QR section present
862 String.contains?(xml_body, "query="), # QR query parameter
863 String.contains?(xml_body, "upiGlobal://pay"), # UPI Global QR format
864 String.contains?(xml_body, "upi://pay"), # Standard UPI QR format
865 String.contains?(xml_body, "qrMedium="), # QR medium specification
866
:-(
String.contains?(xml_body, "verToken=") or String.contains?(xml_body, "stan="), # QR verification tokens
867
:-(
String.contains?(xml_body, "QRexpire=") or String.contains?(xml_body, "expireTs=") # QR expiry
868 ]
869
870 # Must have QR section and at least 2 other QR indicators
871
:-(
has_qr_section = String.contains?(xml_body, "<QR ")
872
:-(
qr_indicator_count = Enum.count(qr_indicators, & &1)
873
874
:-(
Logger.debug("QR detection: has_qr_section=#{has_qr_section}, indicators=#{qr_indicator_count}")
875
876
:-(
result = has_qr_section and qr_indicator_count >= 3
877
:-(
Logger.debug("QR payment result: #{result}")
878
:-(
result
879 end
880
881 defp is_international_payment?(xml_body) do
882 # Detect international UPI payment by checking for strong international indicators
883 # Exclude QR payments that may have international elements
884
:-(
Logger.debug("Checking if international payment...")
885
886 # Don't treat QR payments as international even if they have foreign currency
887
:-(
if String.contains?(xml_body, "<QR ") do
888
:-(
Logger.debug("Has QR section - not international")
889 false
890 else
891 # Strong international indicators (more specific than QR detection)
892
:-(
strong_intl_indicators = [
893 String.contains?(xml_body, "<InternationalPay>"), # Strongest indicator
894 String.contains?(xml_body, "prodType=\"UPI\""), # Explicit UPI product
895 String.contains?(xml_body, "<Destination>"), # International-specific section
896 String.contains?(xml_body, "<Country code="), # International country codes
897 String.contains?(xml_body, "purpose=\"11\"") # International purpose code
898 ]
899
900 # Namespace requirement
901
:-(
has_namespace = String.contains?(xml_body, "xmlns=\"http://npci.org/upi/schema/\"") or
902
:-(
String.contains?(xml_body, "xmlns:upi=\"http://npci.org/upi/schema/\"")
903
904 # Must have namespace AND either InternationalPay section OR multiple strong indicators
905
:-(
has_intl_section = String.contains?(xml_body, "<InternationalPay>")
906
:-(
strong_indicator_count = Enum.count(strong_intl_indicators, & &1)
907
908
:-(
Logger.debug("International detection: namespace=#{has_namespace}, intl_section=#{has_intl_section}, strong_indicators=#{strong_indicator_count}")
909
910
:-(
result = has_namespace and (has_intl_section or strong_indicator_count >= 2)
911
:-(
Logger.debug("International payment result: #{result}")
912
:-(
result
913 end
914 end
915
916 defp process_batch_status_check(xml_body) do
917 # Parse batch status check request
918 # This is an enhancement for handling multiple status checks
919
920
:-(
case extract_batch_org_txn_ids(xml_body) do
921 {:ok, org_txn_ids} ->
922
:-(
responses = Enum.map(org_txn_ids, fn org_txn_id ->
923
:-(
check_request = build_individual_check_request(org_txn_id)
924
925
:-(
case UpiTransactionManager.process_status_check(check_request) do
926
:-(
{:ok, response} -> response
927
:-(
{:error, _} -> build_error_check_response(org_txn_id)
928 end
929 end)
930
931 {:ok, combine_batch_responses(responses)}
932
933
:-(
{:error, reason} ->
934
:-(
{:error, "Batch processing failed: #{reason}"}
935 end
936 end
937
938 defp process_reconciliation(xml_body) do
939 # Parse reconciliation request
940
:-(
case extract_reconciliation_data(xml_body) do
941 {:ok, recon_data} ->
942 # Get all transactions for the specified date range
943
:-(
case UpiInternationalService.get_transactions_for_reconciliation(recon_data.from_date, recon_data.to_date) do
944 {:ok, transactions} ->
945
:-(
response = build_reconciliation_response(transactions, recon_data)
946 {:ok, response}
947
948
:-(
{:error, reason} ->
949
:-(
{:error, "Reconciliation data retrieval failed: #{reason}"}
950 end
951
952
:-(
{:error, reason} ->
953
:-(
{:error, "Invalid reconciliation request: #{reason}"}
954 end
955 end
956
957 defp process_mandate_request(xml_body) do
958 # Parse mandate request
959
:-(
case extract_mandate_data(xml_body) do
960 {:ok, mandate_data} ->
961 # Process mandate creation/modification
962
:-(
case UpiInternationalService.process_mandate(mandate_data) do
963 {:ok, mandate} ->
964
:-(
response = build_mandate_response(mandate, mandate_data)
965 {:ok, response}
966
967
:-(
{:error, reason} ->
968
:-(
{:error, "Mandate processing failed: #{reason}"}
969 end
970
971
:-(
{:error, reason} ->
972
:-(
{:error, "Invalid mandate request: #{reason}"}
973 end
974 end
975
976 defp extract_batch_org_txn_ids(xml_body) do
977 # Simple regex-based extraction for batch IDs
978
:-(
pattern = ~r/<orgTxnId>([^<]+)<\/orgTxnId>/
979
980
:-(
case Regex.scan(pattern, xml_body) do
981
:-(
[] -> {:error, "No transaction IDs found"}
982
:-(
matches -> {:ok, Enum.map(matches, fn [_, id] -> id end)}
983 end
984 end
985
986 defp extract_reconciliation_data(xml_body) do
987 # Extract reconciliation parameters
988
:-(
from_date_pattern = ~r/<fromDate>([^<]+)<\/fromDate>/
989
:-(
to_date_pattern = ~r/<toDate>([^<]+)<\/toDate>/
990
991
:-(
with [_, from_date] <- Regex.run(from_date_pattern, xml_body),
992
:-(
[_, to_date] <- Regex.run(to_date_pattern, xml_body) do
993 {:ok, %{from_date: from_date, to_date: to_date}}
994 else
995 _ -> {:error, "Invalid reconciliation date range"}
996 end
997 end
998
999 defp extract_mandate_data(xml_body) do
1000 # Extract mandate parameters
1001
:-(
mandate_id_pattern = ~r/<mandateId>([^<]+)<\/mandateId>/
1002
:-(
amount_pattern = ~r/<amount>([^<]+)<\/amount>/
1003
:-(
frequency_pattern = ~r/<frequency>([^<]+)<\/frequency>/
1004
1005
:-(
with [_, mandate_id] <- Regex.run(mandate_id_pattern, xml_body),
1006
:-(
[_, amount] <- Regex.run(amount_pattern, xml_body),
1007
:-(
[_, frequency] <- Regex.run(frequency_pattern, xml_body) do
1008 {:ok, %{mandate_id: mandate_id, amount: amount, frequency: frequency}}
1009 else
1010 _ -> {:error, "Invalid mandate parameters"}
1011 end
1012 end
1013
1014 defp build_individual_check_request(org_txn_id) do
1015
:-(
"""
1016 <?xml version="1.0" encoding="UTF-8"?>
1017 <ReqChkTxn>
1018
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="NPCI" msgId="#{generate_msg_id()}"/>
1019
:-(
<Txn orgTxnId="#{org_txn_id}"/>
1020 </ReqChkTxn>
1021 """
1022 end
1023
1024 defp build_error_check_response(org_txn_id) do
1025
:-(
"""
1026 <RespChkTxn>
1027
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="MER1010001" msgId="#{generate_msg_id()}"/>
1028 <Resp result="FAILURE" errCode="05"/>
1029
:-(
<Txn id="ERROR" orgTxnId="#{org_txn_id}" status="FAILED"/>
1030 <Amount curr="INR" value="0.00"/>
1031 </RespChkTxn>
1032 """
1033 end
1034
1035 defp combine_batch_responses(responses) do
1036
:-(
"""
1037 <?xml version="1.0" encoding="UTF-8"?>
1038 <BatchRespChkTxn>
1039
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="MER1010001" msgId="#{generate_msg_id()}"/>
1040 <Responses>
1041 #{Enum.join(responses, "\n")}
1042 </Responses>
1043 </BatchRespChkTxn>
1044 """
1045 end
1046
1047 defp build_reconciliation_response(transactions, recon_data) do
1048
:-(
transaction_list = Enum.map(transactions, fn txn ->
1049
:-(
"""
1050 <Transaction>
1051
:-(
<txnId>#{txn.id}</txnId>
1052
:-(
<orgTxnId>#{txn.org_txn_id}</orgTxnId>
1053
:-(
<amount>#{txn.amount}</amount>
1054
:-(
<status>#{txn.status}</status>
1055
:-(
<timestamp>#{txn.updated_at}</timestamp>
1056 </Transaction>
1057 """
1058 end)
1059
1060
:-(
"""
1061 <?xml version="1.0" encoding="UTF-8"?>
1062 <RespReconciliation>
1063
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="MER1010001" msgId="#{generate_msg_id()}"/>
1064 <Resp result="SUCCESS" errCode="00"/>
1065
:-(
<ReconciliationData fromDate="#{recon_data.from_date}" toDate="#{recon_data.to_date}">
1066
:-(
<TransactionCount>#{length(transactions)}</TransactionCount>
1067
:-(
<TotalAmount>#{calculate_total_amount(transactions)}</TotalAmount>
1068 <Transactions>
1069 #{Enum.join(transaction_list, "\n")}
1070 </Transactions>
1071 </ReconciliationData>
1072 </RespReconciliation>
1073 """
1074 end
1075
1076 defp build_mandate_response(mandate, mandate_data) do
1077
:-(
"""
1078 <?xml version="1.0" encoding="UTF-8"?>
1079 <RespMandate>
1080
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="MER1010001" msgId="#{generate_msg_id()}"/>
1081 <Resp result="SUCCESS" errCode="00"/>
1082 <Mandate>
1083
:-(
<mandateId>#{mandate.id}</mandateId>
1084
:-(
<status>#{mandate.status}</status>
1085
:-(
<amount>#{mandate_data.amount}</amount>
1086
:-(
<frequency>#{mandate_data.frequency}</frequency>
1087
:-(
<validUntil>#{mandate.valid_until}</validUntil>
1088 </Mandate>
1089 </RespMandate>
1090 """
1091 end
1092
1093 defp calculate_total_amount(transactions) do
1094 transactions
1095
:-(
|> Enum.map(& &1.amount)
1096 |> Enum.map(&Decimal.new/1)
1097 |> Enum.reduce(Decimal.new(0), &Decimal.add/2)
1098
:-(
|> Decimal.to_string()
1099 end
1100
1101 defp send_error_response(conn, error_code, error_message) do
1102 # Generate proper NPCI compliant error response
1103
:-(
error_response_data = %{
1104 org_id: "MER1010001",
1105 msg_id: generate_msg_id(),
1106 req_msg_id: "UNKNOWN",
1107 result: "FAILURE",
1108 err_code: error_code,
1109 txn_id: generate_msg_id(),
1110 note: error_message,
1111 ref_id: "",
1112 cust_ref: "",
1113 ref_url: "",
1114 txn_type: "PAY",
1115 sub_type: "",
1116 initiation_mode: "QR",
1117 org_txn_id: "UNKNOWN",
1118 org_rrn: "",
1119 org_txn_date: Date.to_string(Date.utc_today()),
1120 ref_category: "",
1121 prod_type: "UPI",
1122 sp_risk_score: "0",
1123 npci_risk_score: "0",
1124 add_info: error_message,
1125 expire_ts: DateTime.utc_now() |> DateTime.add(300, :second) |> DateTime.to_iso8601()
1126 }
1127
1128
:-(
case UpiXmlSchema.generate_resp_pay(error_response_data) do
1129 {:ok, xml_response} ->
1130 conn
1131 |> put_resp_content_type("application/xml")
1132
:-(
|> send_resp(500, xml_response)
1133
1134 {:error, _reason} ->
1135 # Fallback to simple error response if XML generation fails
1136
:-(
error_xml = """
1137 <?xml version="1.0" encoding="UTF-8"?>
1138 <ErrorResponse>
1139
:-(
<Head ver="1.0" ts="#{get_timestamp()}" orgId="MER1010001" msgId="#{generate_msg_id()}"/>
1140
:-(
<Resp result="FAILURE" errCode="#{error_code}" />
1141
:-(
<Error>#{error_message}</Error>
1142 </ErrorResponse>
1143 """
1144
1145 conn
1146 |> put_resp_content_type("application/xml")
1147
:-(
|> send_resp(500, error_xml)
1148 end
1149 end
1150 @doc """
1151 Handle UPI credit payment requests.
1152 This implements the proper NPCI ACK + async RespPay flow:
1153 1. Parse incoming ReqPay from NPCI
1154 2. Store ReqPay details in database
1155 3. Send immediate ACK response
1156 4. Process payment asynchronously
1157 5. Send RespPay to NPCI in background task
1158 """
1159 def process_credit_payment(conn, _params) do
1160
:-(
with {:ok, xml_body} <- read_request_body(conn),
1161
:-(
{:ok, parsed_data} <- UpiXmlSchema.parse_req_pay(xml_body) do
1162
1163
:-(
msg_id = parsed_data.msg_id || "unknown"
1164
:-(
org_txn_id = parsed_data.org_txn_id || "unknown"
1165
1166
:-(
Logger.info("[ReqPay] Received credit payment request - msgId=#{msg_id}, orgTxnId=#{org_txn_id}")
1167
:-(
Logger.debug("[ReqPay] Parsed request data: #{inspect(parsed_data)}")
1168
1169 # Step 1: Store ReqPay details in database
1170
:-(
req_pay_attrs = build_req_pay_attrs(parsed_data)
1171
1172
:-(
case ReqPayService.create_req_pay(req_pay_attrs, xml_body, parsed_data) do
1173 {:ok, req_pay} ->
1174
:-(
Logger.info("[ReqPay] Stored ReqPay with ID: #{req_pay.id}")
1175
1176 # Step 2: Generate and send immediate ACK response
1177
:-(
ack_xml = NpciAdapter.generate_reqpay_ack(msg_id, org_txn_id)
1178
1179 # Step 3: Send ACK as HTTP response immediately
1180
:-(
conn_with_ack =
1181 conn
1182 |> put_resp_content_type("application/xml")
1183 |> send_resp(200, ack_xml)
1184
1185
:-(
Logger.info("[ReqPay] ACK sent to NPCI for msgId=#{msg_id}")
1186
1187 # Step 4: Start async task for payment processing and RespPay
1188
:-(
{:ok, _pid} = Task.start(fn ->
1189
:-(
Logger.info("[ASYNC ReqPay] Starting async payment processing for orgTxnId=#{org_txn_id}")
1190
1191
:-(
result = case process_credit_transaction_async(req_pay, parsed_data, xml_body) do
1192 {:ok, {resp_pay_xml, processing_result}} ->
1193
:-(
Logger.info("[ASYNC ReqPay] Payment processed successfully for orgTxnId=#{org_txn_id}")
1194
:-(
Logger.info("[ASYNC ReqPay] Processing result: #{inspect(processing_result)}")
1195
1196 # Update ReqPay with response data
1197
:-(
response_attrs = %{
1198 payment_status: "SUCCESS",
1199 response_code: "00",
1200 response_message: "Transaction successful"
1201 }
1202
:-(
ReqPayService.update_req_pay_with_response(req_pay, response_attrs, resp_pay_xml)
1203
1204 # Send RespPay to NPCI asynchronously using txn_id from ReqPay
1205
:-(
txn_id = parsed_data.txn_id || org_txn_id
1206
:-(
send_async_resppay_to_npci(parsed_data, resp_pay_xml, processing_result)
1207 :ok
1208
1209 {:error, {error_resp_xml, error_reason}} ->
1210
:-(
Logger.error("[ASYNC ReqPay] Payment processing failed for orgTxnId=#{org_txn_id}: #{error_reason}")
1211
1212 # Update ReqPay with error details
1213
:-(
ReqPayService.mark_as_failed(req_pay, error_reason, "Payment processing failed")
1214
1215 # Send error RespPay to NPCI using txn_id from ReqPay
1216
:-(
txn_id = parsed_data.txn_id || org_txn_id
1217
:-(
send_async_resppay_to_npci(parsed_data, error_resp_xml, %{result: "FAILURE", err_code: error_reason, resp_code: error_reason, sett_amount: "0.00"})
1218 :ok
1219
1220 {:error, reason} ->
1221
:-(
Logger.error("[ASYNC ReqPay] Critical error processing payment for orgTxnId=#{org_txn_id}: #{inspect(reason)}")
1222
1223 # Update ReqPay with critical error
1224
:-(
ReqPayService.mark_as_failed(req_pay, "96", "System malfunction")
1225
1226 # Generate critical error response
1227
:-(
error_response_data = build_error_response_data(parsed_data, "96", "System malfunction")
1228
1229
:-(
case UpiXmlSchema.generate_credit_response(error_response_data) do
1230 {:ok, error_xml} ->
1231
:-(
txn_id = parsed_data.txn_id || org_txn_id
1232
:-(
send_async_resppay_to_npci(parsed_data, error_xml, %{result: "FAILURE", err_code: "96", resp_code: "96", sett_amount: "0.00"})
1233 :ok
1234 {:error, xml_error} ->
1235
:-(
Logger.error("[ASYNC ReqPay] Failed to generate error XML for orgTxnId=#{org_txn_id}: #{inspect(xml_error)}")
1236 :ok
1237 end
1238 end
1239
1240 # Task completed successfully
1241
:-(
result
1242 end)
1243
1244 # Return the connection with ACK already sent
1245
:-(
conn_with_ack
1246
1247 {:error, changeset} ->
1248
:-(
Logger.error("[ReqPay] Failed to store ReqPay: #{inspect(changeset)}")
1249
:-(
send_error_response(conn, "96", "System malfunction")
1250 end
1251
1252 else
1253 {:error, :invalid_request} ->
1254
:-(
Logger.error("[ReqPay] Invalid request - malformed XML or missing required fields")
1255
:-(
send_error_response(conn, "91", "Invalid request parameters")
1256
1257 {:error, parse_error} ->
1258
:-(
Logger.error("[ReqPay] Failed to parse request XML: #{inspect(parse_error)}")
1259
:-(
send_error_response(conn, "96", "System malfunction")
1260 end
1261 end
1262
1263 # Async payment processing function
1264 defp process_credit_transaction_async(req_pay, parsed_data, original_xml) do
1265
:-(
msg_id = parsed_data.msg_id || "unknown"
1266
:-(
org_txn_id = parsed_data.org_txn_id || "unknown"
1267
1268
:-(
Logger.info("[ASYNC Processing] Starting validation for orgTxnId=#{org_txn_id}")
1269
1270 # Update ReqPay status to indicate processing has started
1271
:-(
ReqPayService.append_event(req_pay, "processing_started", %{
1272 msg_id: msg_id,
1273 org_txn_id: org_txn_id,
1274 processing_stage: "validation"
1275 })
1276
1277
:-(
case validate_credit_request(parsed_data) do
1278 :ok ->
1279 # Log validation success
1280
:-(
ReqPayService.append_event(req_pay, "validation_success", %{
1281 validation_stage: "merchant_validation",
1282 msg_id: msg_id,
1283 org_txn_id: org_txn_id
1284 })
1285
1286 # Merchant validation successful - process the payment
1287
:-(
case process_credit_transaction(parsed_data) do
1288 {:ok, processed_data} ->
1289
:-(
Logger.info("[ASYNC Processing] Payment validation successful for orgTxnId=#{org_txn_id}")
1290
1291 # Log payment processing success
1292
:-(
ReqPayService.append_event(req_pay, "payment_processed", %{
1293 processing_stage: "payment_completion",
1294 partner_response: processed_data,
1295 msg_id: msg_id,
1296 org_txn_id: org_txn_id
1297 })
1298
1299 # Mark as processed
1300
:-(
ReqPayService.mark_as_processed(req_pay, processed_data)
1301
1302
:-(
generate_success_resppay(parsed_data, processed_data, msg_id, org_txn_id)
1303
1304 {:error, process_error} ->
1305
:-(
Logger.error("[ASYNC Processing] Payment processing failed for orgTxnId=#{org_txn_id}: #{inspect(process_error)}")
1306
1307 # Log payment processing failure
1308
:-(
ReqPayService.append_event(req_pay, "payment_failed", %{
1309 processing_stage: "payment_completion",
1310 error: process_error,
1311 msg_id: msg_id,
1312 org_txn_id: org_txn_id
1313 })
1314
1315
:-(
generate_failure_resppay(parsed_data, process_error, msg_id, org_txn_id)
1316 end
1317
1318 {:error, validation_error} ->
1319 # Merchant validation failed - generate FAILURE response
1320
:-(
Logger.warning("[ASYNC Processing] Merchant validation failed for orgTxnId=#{org_txn_id}: #{inspect(validation_error)}")
1321
1322 # Log validation failure
1323
:-(
ReqPayService.append_event(req_pay, "validation_failed", %{
1324 validation_stage: "merchant_validation",
1325 error: validation_error,
1326 msg_id: msg_id,
1327 org_txn_id: org_txn_id
1328 })
1329
1330
:-(
generate_failure_resppay(parsed_data, validation_error, msg_id, org_txn_id)
1331 end
1332 end
1333
1334 # Generate SUCCESS RespPay response
1335 defp generate_success_resppay(parsed_data, processed_data, msg_id, org_txn_id) do
1336 # Build successful response data
1337
:-(
response_data = %{
1338 msg_id: "MSW" <> generate_message_id(),
1339 org_id: "MER101",
1340
:-(
ts: parsed_data.timestamp || DateTime.utc_now() |> DateTime.to_iso8601(),
1341 ver: "2.0",
1342
1343 # Transaction details
1344
:-(
txn_id: parsed_data.txn_id,
1345
:-(
cust_ref: parsed_data.cust_ref,
1346
:-(
initiation_mode: parsed_data.initiation_mode || "01",
1347
:-(
note: parsed_data.note,
1348 org_txn_id: org_txn_id,
1349
:-(
purpose: parsed_data.purpose || "11",
1350
:-(
ref_category: parsed_data.ref_category || "00",
1351
:-(
ref_id: parsed_data.ref_id,
1352 ref_url: "https://mercurypay.ariticapp.com",
1353
:-(
sub_type: parsed_data.sub_type || "PAY",
1354 type: "CREDIT",
1355
1356 # Risk scores
1357
:-(
risk_scores: parsed_data.risk_scores,
1358
1359 # QR details
1360
:-(
qr_data: parsed_data.qr_data,
1361
1362 # Response details
1363 req_msg_id: msg_id,
1364
:-(
result: processed_data.result,
1365
:-(
err_code: processed_data.err_code,
1366
1367 # Payee reference
1368 payee_ref: %{
1369
:-(
addr: parsed_data.payee_addr,
1370
:-(
code: parsed_data.payee_code || "8999",
1371 seq_num: "1",
1372 type: "PAYEE",
1373
:-(
ifsc: parsed_data.payee_ifsc || "MSWI0000000",
1374
:-(
ac_num: parsed_data.payee_ac_num,
1375
:-(
acc_type: parsed_data.payee_ac_type || "CURRENT",
1376
:-(
reg_name: parsed_data.payee_name || "Unknown Merchant",
1377
:-(
org_amount: parsed_data.amount,
1378
:-(
resp_code: processed_data.resp_code,
1379
:-(
sett_amount: processed_data.sett_amount || "0.00",
1380
:-(
sett_currency: parsed_data.payee_currency || "INR"
1381 }
1382 }
1383
1384 # Generate SUCCESS RespPay XML
1385
:-(
case UpiXmlSchema.generate_credit_response(response_data) do
1386 {:ok, response_xml} ->
1387
:-(
Logger.info("[ASYNC Processing] Generated successful RespPay XML for orgTxnId=#{org_txn_id}")
1388
:-(
Logger.debug("[ASYNC Processing] RespPay XML: #{response_xml}")
1389
1390 # Return success tuple for the main case statement
1391 {:ok, {response_xml, processed_data}}
1392
1393 {:error, xml_error} ->
1394
:-(
Logger.error("[ASYNC Processing] Failed to generate SUCCESS RespPay XML for orgTxnId=#{org_txn_id}: #{inspect(xml_error)}")
1395 {:error, {nil, xml_error}}
1396 end
1397 end
1398
1399 # Generate FAILURE RespPay response
1400 defp generate_failure_resppay(parsed_data, validation_error, msg_id, org_txn_id) do
1401 # Map validation errors to UPI error codes
1402
:-(
{err_code, error_message} = case validation_error do
1403
:-(
:invalid_amount -> {"26", "Invalid amount"}
1404
:-(
:invalid_merchant -> {"ZR", "Invalid merchant"}
1405
:-(
:merchant_not_found -> {"IV", "Invalid VPA or merchant"}
1406
:-(
:invalid_qr_data -> {"ZR", "Invalid QR data"}
1407
:-(
:validation_failed -> {"96", "System malfunction"}
1408
:-(
:database_error -> {"96", "System malfunction"}
1409
:-(
_ -> {"96", "System malfunction"}
1410 end
1411
1412 # Build failure response data
1413
:-(
error_response_data = %{
1414 msg_id: "MSW" <> generate_message_id(),
1415 org_id: "MSW102",
1416
:-(
ts: parsed_data.timestamp || DateTime.utc_now() |> DateTime.to_iso8601(),
1417 ver: "2.0",
1418
1419 # Transaction details
1420
:-(
txn_id: parsed_data.txn_id,
1421
:-(
cust_ref: parsed_data.cust_ref,
1422
:-(
initiation_mode: parsed_data.initiation_mode || "01",
1423
:-(
note: parsed_data.note,
1424 org_txn_id: org_txn_id,
1425
:-(
purpose: parsed_data.purpose || "11",
1426
:-(
ref_category: parsed_data.ref_category || "00",
1427
:-(
ref_id: parsed_data.ref_id,
1428 ref_url: "https://mercurypay.ariticapp.com",
1429
:-(
sub_type: parsed_data.sub_type || "PAY",
1430 type: "CREDIT",
1431
1432 # Risk scores
1433
:-(
risk_scores: parsed_data.risk_scores,
1434
1435 # QR details
1436
:-(
qr_data: parsed_data.qr_data,
1437
1438 # Response details (FAILURE)
1439 req_msg_id: msg_id,
1440 result: "FAILURE",
1441 err_code: err_code,
1442
1443 # Payee reference with failure codes
1444 payee_ref: %{
1445
:-(
addr: parsed_data.payee_addr,
1446
:-(
code: parsed_data.payee_code || "8999",
1447 seq_num: "1",
1448 type: "PAYEE",
1449
:-(
ifsc: parsed_data.payee_ifsc || "MSWI0000000",
1450
:-(
ac_num: parsed_data.payee_ac_num,
1451
:-(
acc_type: parsed_data.payee_ac_type || "CURRENT",
1452
:-(
reg_name: parsed_data.payee_name || "Unknown Merchant",
1453
:-(
org_amount: parsed_data.amount,
1454 resp_code: err_code, # Use error code as response code
1455 sett_amount: "0.00", # No settlement amount for failed transactions
1456
:-(
sett_currency: parsed_data.payee_currency || "INR"
1457 }
1458 }
1459
1460 # Generate FAILURE RespPay XML
1461
:-(
case UpiXmlSchema.generate_credit_response(error_response_data) do
1462 {:ok, response_xml} ->
1463
:-(
Logger.info("[ASYNC Processing] Generated FAILURE RespPay XML for orgTxnId=#{org_txn_id} - Error: #{error_message}")
1464
:-(
Logger.debug("[ASYNC Processing] FAILURE RespPay XML: #{response_xml}")
1465
1466 # Return error tuple for the main case statement
1467 {:error, {response_xml, err_code}}
1468
1469 {:error, xml_error} ->
1470
:-(
Logger.error("[ASYNC Processing] Failed to generate FAILURE RespPay XML for orgTxnId=#{org_txn_id}: #{inspect(xml_error)}")
1471 {:error, {nil, xml_error}}
1472 end
1473 end
1474
1475 # Send RespPay to NPCI endpoints asynchronously
1476 defp send_async_resppay_to_npci(parsed_data, response_xml, processed_data) do
1477
:-(
txn_id = parsed_data.txn_id
1478
:-(
org_txn_id = parsed_data.org_txn_id
1479
1480
:-(
Logger.info("[ASYNC ReqPay] Payment processed successfully for orgTxnId=#{org_txn_id}")
1481
:-(
Logger.info("[ASYNC ReqPay] Processing result: #{inspect(processed_data)}")
1482
1483 # Send RespPay using NPCI adapter
1484
:-(
DaProductApp.Adapters.NpciAdapter.send_async_resppay(response_xml, txn_id, org_txn_id, processed_data)
1485 end
1486
1487 defp build_error_response_data(parsed_data, err_code, error_message) do
1488
:-(
%{
1489 msg_id: "MSW" <> generate_message_id(),
1490 org_id: "MSW102",
1491 ts: DateTime.utc_now() |> DateTime.to_iso8601(),
1492 ver: "2.0",
1493
:-(
txn_id: parsed_data.txn_id,
1494
:-(
org_txn_id: parsed_data.org_txn_id || "unknown",
1495
:-(
req_msg_id: parsed_data.msg_id || "unknown",
1496 result: "FAILURE",
1497 err_code: err_code,
1498 type: "CREDIT",
1499 payee_ref: %{
1500
:-(
addr: parsed_data.payee_addr,
1501 resp_code: err_code,
1502 sett_amount: "0.00",
1503
:-(
sett_currency: parsed_data.payee_currency || "INR"
1504 }
1505 }
1506 end
1507
1508 defp generate_error_response_xml(error_response_data, org_txn_id) do
1509 case UpiXmlSchema.generate_credit_response(error_response_data) do
1510 {:ok, error_xml} ->
1511 Logger.info("[ASYNC Processing] Generated error RespPay XML for orgTxnId=#{org_txn_id}")
1512 {:error, {error_xml, error_response_data.err_code}}
1513 {:error, xml_error} ->
1514 Logger.error("[ASYNC Processing] Failed to generate error XML for orgTxnId=#{org_txn_id}: #{inspect(xml_error)}")
1515 {:error, xml_error}
1516 end
1517 end
1518
1519 # ---------------------
1520 # Helper functions
1521 # ---------------------
1522
1523 # Reads raw request body (XML)
1524 defp read_request_body(conn) do
1525
:-(
case Plug.Conn.read_body(conn) do
1526
:-(
{:ok, body, _conn} -> {:ok, body}
1527
:-(
{:more, _, _} -> {:error, :body_too_large}
1528
:-(
{:error, _} -> {:error, :invalid_request}
1529 end
1530 end
1531
1532 # Enhanced validation with merchant database lookup
1533 defp validate_credit_request(parsed_data) do
1534
:-(
amount =
1535
:-(
parsed_data[:amount] ||
1536
:-(
parsed_data[:payee_amount] ||
1537
:-(
parsed_data[:payer_amount]
1538
1539
:-(
Logger.info("[Validation] Starting merchant validation for payee_addr=#{parsed_data[:payee_addr]}")
1540
1541
:-(
cond do
1542
:-(
is_nil(amount) or amount == "" ->
1543
:-(
Logger.error("[Validation] Invalid amount: #{inspect(amount)}")
1544 {:error, :invalid_amount}
1545
1546
:-(
is_nil(parsed_data[:payee_addr]) or parsed_data[:payee_addr] == "" ->
1547
:-(
Logger.error("[Validation] Invalid payee_addr: #{inspect(parsed_data[:payee_addr])}")
1548 {:error, :invalid_merchant}
1549
1550
:-(
true ->
1551 # Extract msid and pa from QR payload and validate against merchant database
1552
:-(
validate_merchant_from_qr_payload(parsed_data)
1553 end
1554 end
1555
1556 # Extract msid and pa values from QR payload and validate against merchant database
1557 defp validate_merchant_from_qr_payload(parsed_data) do
1558
:-(
qr_payload = parsed_data[:qr_payload]
1559
:-(
payee_addr = parsed_data[:payee_addr] # This is the 'pa' value from QR
1560
1561
:-(
Logger.info("[Merchant Validation] Starting QR validation - payee_addr=#{payee_addr}")
1562
:-(
Logger.debug("[Merchant Validation] QR payload: #{inspect(qr_payload)}")
1563
1564
:-(
case extract_msid_from_qr_payload(qr_payload) do
1565 {:ok, msid} ->
1566
:-(
Logger.info("[Merchant Validation] Extracted msid=#{msid}, pa=#{payee_addr}")
1567
1568 # Check if merchant exists in database using sid column and merchant_vpa
1569
:-(
case find_merchant_by_sid_and_vpa(msid, payee_addr) do
1570 {:ok, merchant} ->
1571
:-(
Logger.info("[Merchant Validation] Found merchant: #{merchant.legal_name || merchant.brand_name} (ID: #{merchant.id})")
1572 :ok
1573
1574 {:error, :not_found} ->
1575
:-(
Logger.warning("[Merchant Validation] Merchant not found for sid=#{msid}, merchant_vpa=#{payee_addr}")
1576 {:error, :merchant_not_found}
1577
1578 {:error, reason} ->
1579
:-(
Logger.error("[Merchant Validation] Database error: #{inspect(reason)}")
1580 {:error, :validation_failed}
1581 end
1582
1583 {:error, :msid_not_found} ->
1584
:-(
Logger.warning("[Merchant Validation] msid not found in QR payload")
1585 {:error, :invalid_qr_data}
1586
1587 {:error, reason} ->
1588
:-(
Logger.error("[Merchant Validation] Failed to extract msid: #{inspect(reason)}")
1589 {:error, :invalid_qr_data}
1590 end
1591 end
1592
1593 # Find merchant in database by sid (store ID) and merchant VPA
1594 defp find_merchant_by_sid_and_vpa(sid, merchant_vpa) do
1595 require Logger
1596
1597
:-(
try do
1598 # Query merchants table for matching mid (merchant ID) and merchant_vpa
1599
:-(
query = from(m in DaProductApp.Merchants.Merchant,
1600 where: m.mid == ^sid and m.merchant_vpa == ^merchant_vpa,
1601 select: m
1602 )
1603
1604
:-(
case DaProductApp.Repo.one(query) do
1605 nil ->
1606
:-(
Logger.info("[DB Query] No merchant found for mid=#{sid}, merchant_vpa=#{merchant_vpa}")
1607 {:error, :not_found}
1608
1609 merchant ->
1610
:-(
Logger.info("[DB Query] Found merchant: #{merchant.legal_name || merchant.brand_name} (mid=#{sid}, merchant_vpa=#{merchant_vpa})")
1611 {:ok, merchant}
1612 end
1613 rescue
1614
:-(
error ->
1615
:-(
Logger.error("[DB Query] Database error: #{inspect(error)}")
1616 {:error, :database_error}
1617 end
1618 end
1619
1620
1621 # Extract msid from QR payload string
1622
:-(
defp extract_msid_from_qr_payload(nil), do: {:error, :no_qr_payload}
1623
:-(
defp extract_msid_from_qr_payload(""), do: {:error, :empty_qr_payload}
1624
1625 defp extract_msid_from_qr_payload(qr_payload) do
1626 # QR payload format: "upiGlobal://pay?ver=01&mode=16&...&msid=SGCOF001&..."
1627
:-(
case Regex.run(~r/[&?]msid=([^&]+)/, qr_payload) do
1628 [_full_match, msid] ->
1629
:-(
Logger.debug("[QR Extract] Found msid=#{msid}")
1630 {:ok, String.trim(msid)}
1631
1632 nil ->
1633
:-(
Logger.warning("[QR Extract] msid parameter not found in QR payload")
1634 {:error, :msid_not_found}
1635 end
1636 end
1637
1638 # Find merchant in database by msid (store ID) and payee address (merchant VPA)
1639 defp find_merchant_by_msid_and_pa(msid, payee_addr) do
1640 require Logger
1641
1642 try do
1643 # Query merchants table for matching sid and merchant_vpa
1644 query = from(m in DaProductApp.Merchants.Merchant,
1645 where: m.sid == ^msid and m.merchant_vpa == ^payee_addr,
1646 select: m
1647 )
1648
1649 case DaProductApp.Repo.one(query) do
1650 nil ->
1651 Logger.info("[DB Query] No merchant found for msid=#{msid}, merchant_vpa=#{payee_addr}")
1652 {:error, :not_found}
1653
1654 merchant ->
1655 Logger.info("[DB Query] Found merchant: #{merchant.legal_name || merchant.brand_name} (msid=#{msid}, merchant_vpa=#{payee_addr})")
1656 {:ok, merchant}
1657 end
1658 rescue
1659 error ->
1660 Logger.error("[DB Query] Database error: #{inspect(error)}")
1661 {:error, :database_error}
1662 end
1663 end
1664
1665 # Simulates transaction processing (replace with DB or external API calls)
1666 defp process_credit_transaction(parsed_data) do
1667
:-(
amount =
1668
:-(
parsed_data[:amount] ||
1669
:-(
parsed_data[:payee_amount] ||
1670
:-(
parsed_data[:payer_amount]
1671
1672 {:ok,
1673 %{
1674 result: "SUCCESS",
1675 err_code: "00",
1676 resp_code: "00",
1677 sett_amount: amount
1678 }}
1679 end
1680
1681 # Build ReqPay attributes from parsed data
1682 defp build_req_pay_attrs(parsed_data) do
1683 # Use Map.get to safely retrieve optional keys (avoids KeyError when using dot access)
1684 %{
1685 # Core UPI fields
1686 msg_id: Map.get(parsed_data, :msg_id),
1687 org_id: Map.get(parsed_data, :org_id),
1688 txn_id: Map.get(parsed_data, :txn_id),
1689 ref_id: Map.get(parsed_data, :ref_id),
1690 ref_url: Map.get(parsed_data, :ref_url),
1691
1692 # Payer/Payee information
1693 payer_addr: Map.get(parsed_data, :payer_addr),
1694 payee_addr: Map.get(parsed_data, :payee_addr),
1695 payer_name: Map.get(parsed_data, :payer_name),
1696 payee_name: Map.get(parsed_data, :payee_name),
1697
1698 # Transaction details
1699
:-(
amount: Map.get(parsed_data, :amount) || Map.get(parsed_data, :payee_amount) || Map.get(parsed_data, :payer_amount),
1700 currency: Map.get(parsed_data, :currency, "INR"),
1701 payment_type: determine_payment_type(parsed_data),
1702 payment_purpose: Map.get(parsed_data, :purpose),
1703
1704 # Status fields
1705 status: "PENDING",
1706 validation_type: determine_validation_type(parsed_data),
1707
1708 # QR specific fields if applicable
1709 qr_string: Map.get(parsed_data, :qr_string),
1710 qr_medium: Map.get(parsed_data, :qr_medium),
1711 merchant_category_code: Map.get(parsed_data, :merchant_category_code),
1712 merchant_vpa: Map.get(parsed_data, :merchant_vpa),
1713
1714 # International fields if applicable
1715 corridor: Map.get(parsed_data, :corridor),
1716 fx_rate: Map.get(parsed_data, :fx_rate),
1717 base_amount: Map.get(parsed_data, :base_amount),
1718 base_currency: Map.get(parsed_data, :base_currency),
1719
1720 # Network information
1721 network_inst_id: Map.get(parsed_data, :network_inst_id),
1722
1723 # Additional fields
1724 rrn: Map.get(parsed_data, :rrn),
1725 npci_txn_id: Map.get(parsed_data, :npci_txn_id)
1726 }
1727
:-(
|> Enum.filter(fn {_k, v} -> not is_nil(v) end)
1728
:-(
|> Enum.into(%{})
1729 end
1730
1731 # Determine payment type from parsed data
1732 defp determine_payment_type(parsed_data) do
1733
:-(
cond do
1734
:-(
Map.get(parsed_data, :qr_string) -> "QR"
1735
:-(
Map.get(parsed_data, :txn_type) == "COLLECT" -> "COLLECT"
1736
:-(
true -> "INTENT"
1737 end
1738 end
1739
1740 # Determine validation type (domestic vs international)
1741 defp determine_validation_type(parsed_data) do
1742
:-(
if Map.get(parsed_data, :corridor) || Map.get(parsed_data, :base_currency) do
1743 "INTERNATIONAL"
1744 else
1745 "DOMESTIC"
1746 end
1747 end
1748
1749 # Generate unique message IDs
1750 defp generate_message_id do
1751 # Generate exactly 35 characters as required by NPCI
1752
:-(
prefix = "MERMSG" # 6 chars
1753 # Need 29 more chars (35 - 6 = 29)
1754 # Use 14 bytes (28 hex chars) + 1 extra char = 29 chars
1755
:-(
suffix = (:crypto.strong_rand_bytes(14) |> Base.encode16()) <> "A"
1756
:-(
(prefix <> suffix) |> String.slice(0, 35)
1757 end
1758
1759 defp send_error_response(conn, err_code, error_message) do
1760
:-(
response_data = %{
1761 msg_id: "ERR" <> generate_message_id(),
1762 org_id: "MSW102",
1763 ts: DateTime.utc_now() |> DateTime.to_iso8601(),
1764 ver: "2.0",
1765 txn_id: "N/A",
1766 cust_ref: "N/A",
1767 initiation_mode: "01",
1768 note: error_message,
1769 org_txn_id: "N/A",
1770 purpose: "00",
1771 ref_category: "00",
1772 ref_id: "N/A",
1773 ref_url: "https://www.mswipedemo.com",
1774 sub_type: "PAY",
1775 type: "CREDIT",
1776 risk_scores: %{},
1777 qr_data: %{},
1778 req_msg_id: "N/A",
1779 result: "FAILURE",
1780 err_code: err_code,
1781 payee_ref: %{
1782 addr: "N/A",
1783 code: "8999",
1784 seq_num: "1",
1785 type: "PAYEE",
1786 ifsc: "MSWI0000000",
1787 ac_num: "N/A",
1788 acc_type: "CURRENT",
1789 reg_name: "N/A",
1790 org_amount: "0.00",
1791 resp_code: err_code,
1792 sett_amount: "0.00",
1793 sett_currency: "INR"
1794 }
1795 }
1796
1797
:-(
{:ok, xml} = UpiXmlSchema.generate_credit_response(response_data)
1798
1799 conn
1800 |> put_resp_content_type("application/xml")
1801
:-(
|> send_resp(500, xml)
1802 end
1803
1804 defp get_timestamp do
1805
:-(
case Application.get_env(:da_product_app, :use_timex_for_timestamps, true) do
1806 true ->
1807 DateTime.utc_now()
1808 |> Timex.to_datetime("Asia/Kolkata")
1809
:-(
|> Timex.format!("{YYYY}-{0M}-{0D}T{h24}:{m}:{s}+05:30")
1810 _ ->
1811 DateTime.utc_now()
1812 |> DateTime.to_iso8601()
1813
:-(
|> String.replace("Z", "+00:00")
1814 end
1815 end
1816
1817 defp generate_msg_id do
1818 # Generate exactly 35 characters as required by NPCI
1819
:-(
prefix = "MERMSG" # 6 chars
1820 # Need 29 more chars (35 - 6 = 29)
1821 # Use 14 bytes (28 hex chars) + 1 extra char = 29 chars
1822
:-(
suffix = (:crypto.strong_rand_bytes(14) |> Base.encode16()) <> "A"
1823
:-(
(prefix <> suffix) |> String.slice(0, 35)
1824 end
1825 end
Line Hits Source