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

1
:-(
defmodule DaProductAppWeb.Api.V1.NpciCallbackController do
2 @moduledoc """
3 Controller for handling NPCI-initiated callbacks as per UPI International specification.
4
5 NPCI makes these calls to our PSP:
6 1. ReqPay (CREDIT) - Already handled by InternationalTransactionController
7 2. ReqChkTxn - Check transaction status (timeout scenario)
8 3. ReqPay (REVERSAL) - Initiate reversal (timeout scenario)
9 4. ReqHbt - Heartbeat check
10 """
11
:-(
use DaProductAppWeb, :controller
12
13 alias DaProductApp.Transactions.UpiInternationalService
14 # Note: UpiXmlParser needs to be implemented for full XML parsing
15 # For now we'll use a simplified parser
16
17 action_fallback DaProductAppWeb.FallbackController
18
19 @doc """
20 Handle NPCI ReqChkTxn - Check transaction status
21 Called by NPCI after 30 seconds if we don't respond to ReqPay (CREDIT)
22 """
23 def check_transaction(conn, %{"xml" => xml_string}) do
24
:-(
case parse_simple_xml(xml_string) do
25 {:ok, parsed_xml} ->
26
:-(
case get_transaction_from_check_request(parsed_xml) do
27 {:ok, transaction} ->
28
:-(
case build_check_transaction_response(transaction, parsed_xml) do
29 {:ok, response_xml} ->
30 conn
31 |> put_resp_content_type("application/xml")
32
:-(
|> send_resp(200, response_xml)
33
34 {:error, reason} ->
35
:-(
send_error_response(conn, parsed_xml, "U30", "Processing error: #{inspect(reason)}")
36 end
37
38 {:error, :transaction_not_found} ->
39
:-(
send_error_response(conn, parsed_xml, "U16", "Transaction not found")
40 end
41
42 {:error, reason} ->
43
:-(
send_error_response(conn, nil, "U30", "XML parsing error: #{inspect(reason)}")
44 end
45 end
46
47 @doc """
48 Handle NPCI ReqPay (REVERSAL) - Process reversal request
49 Called by NPCI if check transaction times out or fails
50 """
51 def process_reversal(conn, %{"xml" => xml_string}) do
52
:-(
case parse_simple_xml(xml_string) do
53 {:ok, parsed_xml} ->
54
:-(
case get_transaction_from_reversal_request(parsed_xml) do
55 {:ok, transaction} ->
56
:-(
case process_transaction_reversal(transaction) do
57 {:ok, reversal_result} ->
58
:-(
case build_reversal_response(reversal_result, parsed_xml) do
59 {:ok, response_xml} ->
60 conn
61 |> put_resp_content_type("application/xml")
62
:-(
|> send_resp(200, response_xml)
63
64 {:error, reason} ->
65
:-(
send_error_response(conn, parsed_xml, "U30", "Response building error: #{inspect(reason)}")
66 end
67
68 {:error, reason} ->
69
:-(
send_error_response(conn, parsed_xml, "U30", "Reversal processing error: #{inspect(reason)}")
70 end
71
72 {:error, :transaction_not_found} ->
73
:-(
send_error_response(conn, parsed_xml, "U16", "Transaction not found for reversal")
74 end
75
76 {:error, reason} ->
77
:-(
send_error_response(conn, nil, "U30", "XML parsing error: #{inspect(reason)}")
78 end
79 end
80
81 @doc """
82 Handle NPCI ReqHbt - Heartbeat check
83 Confirms our system is operational
84 """
85 def heartbeat(conn, %{"xml" => xml_string}) do
86
:-(
case parse_simple_xml(xml_string) do
87 {:ok, parsed_xml} ->
88
:-(
case build_heartbeat_response(parsed_xml) do
89 {:ok, response_xml} ->
90 conn
91 |> put_resp_content_type("application/xml")
92
:-(
|> send_resp(200, response_xml)
93
94 {:error, _reason} ->
95 # Even on parsing error, we should respond to show we're alive
96
:-(
basic_heartbeat_response = """
97 <?xml version="1.0" encoding="UTF-8"?>
98 <upi:RespHbt xmlns:upi="http://npci.org/upi/schema/">
99
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="HBT#{:rand.uniform(999999)}"/>
100
:-(
<Resp reqMsgId="#{parsed_xml.header.msg_id}" result="SUCCESS" errCode=""/>
101 </upi:RespHbt>
102 """
103
104 conn
105 |> put_resp_content_type("application/xml")
106
:-(
|> send_resp(200, basic_heartbeat_response)
107 end
108
109 {:error, _reason} ->
110 # Even on parsing error, we should respond to show we're alive
111
:-(
basic_heartbeat_response = """
112 <?xml version="1.0" encoding="UTF-8"?>
113 <upi:RespHbt xmlns:upi="http://npci.org/upi/schema/">
114
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="HBT#{:rand.uniform(999999)}"/>
115 <Resp reqMsgId="UNKNOWN" result="SUCCESS" errCode=""/>
116 </upi:RespHbt>
117 """
118
119 conn
120 |> put_resp_content_type("application/xml")
121
:-(
|> send_resp(200, basic_heartbeat_response)
122 end
123 end
124
125 # Simple XML parser for NPCI requests (placeholder until full UpiXmlParser is implemented)
126 defp parse_simple_xml(xml_string) do
127
:-(
try do
128 # Extract orgTxnId using regex for now
129
:-(
org_txn_id = case Regex.run(~r/orgTxnId="([^"]+)"/, xml_string) do
130
:-(
[_, id] -> id
131
:-(
_ -> "UNKNOWN"
132 end
133
134 # Extract msgId
135
:-(
msg_id = case Regex.run(~r/msgId="([^"]+)"/, xml_string) do
136
:-(
[_, id] -> id
137
:-(
_ -> "UNKNOWN"
138 end
139
140 # Extract type
141
:-(
type = cond do
142
:-(
String.contains?(xml_string, "ReqChkTxn") -> "ChkTxn"
143
:-(
String.contains?(xml_string, "ReqPay") && String.contains?(xml_string, "REVERSAL") -> "REVERSAL"
144
:-(
String.contains?(xml_string, "ReqHbt") -> "Heartbeat"
145
:-(
true -> "UNKNOWN"
146 end
147
148
:-(
parsed = %{
149 header: %{msg_id: msg_id},
150 transaction: %{
151 org_txn_id: org_txn_id,
152 type: type,
153 ref_id: "REF123",
154 cust_ref: "CUST123"
155 }
156 }
157
158 {:ok, parsed}
159 rescue
160
:-(
_ -> {:error, "XML parsing failed"}
161 end
162 end
163
164 # Private helper functions
165
166 defp get_transaction_from_check_request(parsed_xml) do
167
:-(
org_txn_id = parsed_xml.transaction.org_txn_id
168
169
:-(
case UpiInternationalService.get_transaction_by_org_id(org_txn_id) do
170
:-(
nil -> {:error, :transaction_not_found}
171
:-(
transaction -> {:ok, transaction}
172 end
173 end
174
175 defp get_transaction_from_reversal_request(parsed_xml) do
176
:-(
org_txn_id = parsed_xml.transaction.org_txn_id
177
178
:-(
case UpiInternationalService.get_transaction_by_org_id(org_txn_id) do
179
:-(
nil -> {:error, :transaction_not_found}
180
:-(
transaction -> {:ok, transaction}
181 end
182 end
183
184 defp process_transaction_reversal(transaction) do
185
:-(
case transaction.current_state do
186 "credit_pending" ->
187 # Confirm reversal - we haven't credited partner yet
188
:-(
UpiInternationalService.handle_partner_reversal(transaction)
189 {:ok, %{status: "REVERSAL_CONFIRMED", resp_code: "00"}}
190
191
:-(
"success" ->
192 # Confirm credit success - transaction already completed
193 {:ok, %{status: "CREDIT_SUCCESS", resp_code: "CS"}}
194
195
:-(
state when state in ["failure", "reversed", "deemed"] ->
196 # Transaction already failed/reversed
197 {:ok, %{status: "ALREADY_REVERSED", resp_code: "00"}}
198
199 _ ->
200 # Unknown state - safe to reverse
201
:-(
UpiInternationalService.handle_partner_reversal(transaction)
202 {:ok, %{status: "REVERSAL_CONFIRMED", resp_code: "00"}}
203 end
204 end
205
206 defp build_check_transaction_response(transaction, parsed_xml) do
207 # Determine response based on transaction state
208
:-(
{result, resp_code, _status} = case transaction.current_state do
209
:-(
"success" -> {"SUCCESS", "CS", "SUCCESS"}
210
:-(
"failure" -> {"FAILURE", transaction.failure_code || "U30", "FAILURE"}
211
:-(
"deemed" -> {"DEEMED", "U30", "DEEMED"}
212
:-(
"reversed" -> {"FAILURE", "00", "FAILURE"}
213
:-(
_ -> {"PENDING", "", "PENDING"} # Still processing
214 end
215
216
:-(
xml = """
217 <?xml version="1.0" encoding="UTF-8"?>
218 <upi:RespChkTxn xmlns:upi="http://npci.org/upi/schema/">
219
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="CHK#{:rand.uniform(999999)}"/>
220
:-(
<Txn id="#{transaction.org_txn_id}" note="International payment" refId="#{parsed_xml.transaction.ref_id || ""}"
221
:-(
refUrl="" refCategory="" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" custRef="#{parsed_xml.transaction.cust_ref || ""}"
222
:-(
type="ChkTxn" orgMsgId="#{parsed_xml.header.msg_id}" orgTxnId="#{transaction.org_txn_id}"
223
:-(
orgTxnDate="#{transaction.inserted_at |> DateTime.to_iso8601()}" initiationMode="00" purpose="11" subType="CREDIT"/>
224
:-(
<Resp reqMsgId="#{parsed_xml.header.msg_id}" result="#{result}" errCode="">
225
:-(
<Ref type="PAYEE" seqNum="1" addr="#{transaction.payee_addr}" code="#{transaction.payee_mid}"
226
:-(
orgAmount="#{transaction.foreign_amount}" respCode="#{resp_code}" regName="#{transaction.payee_name}"
227
:-(
IFSC="" acNum="" accType="" approvalNum="#{transaction.partner_txn_id || ""}"
228
:-(
settAmount="#{transaction.foreign_amount}" settCurrency="#{transaction.foreign_currency}"/>
229 </Resp>
230 </upi:RespChkTxn>
231 """
232
233 {:ok, xml}
234 end
235
236 defp build_reversal_response(reversal_result, parsed_xml) do
237
:-(
xml = """
238 <?xml version="1.0" encoding="UTF-8"?>
239 <upi:RespPay xmlns:upi="http://npci.org/upi/schema/">
240
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="REV#{:rand.uniform(999999)}"/>
241
:-(
<Txn id="#{parsed_xml.transaction.id}" note="Reversal processed" refId="#{parsed_xml.transaction.ref_id || ""}"
242
:-(
refUrl="" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" type="REVERSAL"
243
:-(
orgTxnId="#{parsed_xml.transaction.org_txn_id}" initiationMode="00" purpose="11" custRef="#{parsed_xml.transaction.cust_ref || ""}"/>
244
:-(
<Resp reqMsgId="#{parsed_xml.header.msg_id}" result="SUCCESS" errCode="">
245
:-(
<Ref type="PAYEE" seqNum="1" addr="" code="" orgAmount="" respCode="#{reversal_result.resp_code}"
246 regName="" IFSC="" acNum="" accType="" approvalNum="" settAmount="" settCurrency=""/>
247 </Resp>
248 </upi:RespPay>
249 """
250
251 {:ok, xml}
252 end
253
254 defp build_heartbeat_response(parsed_xml) do
255
:-(
xml = """
256 <?xml version="1.0" encoding="UTF-8"?>
257 <upi:RespHbt xmlns:upi="http://npci.org/upi/schema/">
258
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="HBT#{:rand.uniform(999999)}"/>
259
:-(
<Resp reqMsgId="#{parsed_xml.header.msg_id}" result="SUCCESS" errCode=""/>
260 </upi:RespHbt>
261 """
262
263 {:ok, xml}
264 end
265
266 defp send_error_response(conn, parsed_xml, error_code, _error_message) do
267
:-(
msg_id = if parsed_xml && parsed_xml.header, do: parsed_xml.header.msg_id, else: "UNKNOWN"
268
269
:-(
xml = """
270 <?xml version="1.0" encoding="UTF-8"?>
271 <upi:RespChkTxn xmlns:upi="http://npci.org/upi/schema/">
272
:-(
<Head ver="2.0" ts="#{DateTime.utc_now() |> DateTime.to_iso8601()}" orgId="MERCURYPSP" msgId="ERR#{:rand.uniform(999999)}"/>
273
:-(
<Resp reqMsgId="#{msg_id}" result="FAILURE" errCode="#{error_code}"/>
274 </upi:RespChkTxn>
275 """
276
277 conn
278 |> put_resp_content_type("application/xml")
279
:-(
|> send_resp(400, xml)
280 end
281 end
Line Hits Source