cover/Elixir.DaProductAppWeb.UpiTransactionManager.html

1 defmodule DaProductAppWeb.UpiTransactionManager do
2 @moduledoc """
3 Complete UPI Transaction State Manager
4 Handles the full transaction lifecycle as per UPI specification
5 """
6 require Logger
7 import SweetXml
8 import Ecto.Query
9
10 alias DaProductApp.Repo
11
12 alias DaProductApp.Transactions.UpiInternationalService
13 alias DaProductAppWeb.UpiXmlSchema
14 alias DaProductApp.QRValidation.Service, as: QRValidationService
15 alias DaProductApp.Transactions.{ReqChkTxn, ReqChkTxnService}
16 alias DaProductApp.Transactions.ReqPayService
17
18 # Transaction states as per UPI specification
19 @transaction_states [
20 :initiated, # Transaction initiated by NPCI
21 :validated, # QR/Merchant validated
22 :processing, # Payment processing with partner
23 :success, # Transaction completed successfully
24 :failed, # Transaction failed
25 :timeout, # Transaction timed out
26 :reversed, # Transaction reversed
27 :deemed_success # Deemed success after timeout
28 ]
29
30 # Transaction types
31 @transaction_types [
32 "COLLECT", # Collect request
33 "PAY", # Payment request
34 "REVERSAL", # Reversal request
35 "CHECK" # Status check
36 ]
37
38 @doc """
39 Process QR validation request with full merchant validation
40 Handles NPCI ReqValQr specification with namespace support
41 """
42 def process_qr_validation(qr_data) do
43
:-(
case UpiXmlSchema.parse_req_val_qr(qr_data) do
44 {:ok, parsed_data_raw} ->
45 # normalize parsed_data so callers can use map.field (atom keys) safely
46
:-(
parsed_data = normalize_parsed_data(parsed_data_raw)
47
:-(
case validate_merchant_qr(parsed_data.qr_payload, parsed_data.initiation_mode) do
48 {:ok, merchant_info} ->
49 # Create QR validation record for monitoring
50
:-(
qr_validation_attrs = %{
51
:-(
txn_id: parsed_data.txn_id,
52
:-(
msg_id: parsed_data.msg_id,
53
:-(
org_id: parsed_data.org_id,
54
:-(
payer_addr: parsed_data.payer_addr,
55
:-(
payer_name: parsed_data.payer_name,
56
:-(
payee_addr: merchant_info.upi_id,
57
:-(
payee_name: merchant_info.merchant_name,
58
:-(
network_inst_id: parsed_data.net_inst_id,
59
:-(
con_code: parsed_data.con_code,
60 qr_version: "2.0",
61 qr_medium: "04",
62
:-(
merchant_type: extract_merchant_type_from_qr(parsed_data.qr_payload) || merchant_info.merchant_type || "SMALL",
63
:-(
base_amount: extract_amount_from_qr(parsed_data.qr_payload) || "0.00",
64
:-(
base_currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
65
:-(
foreign_amount: extract_amount_from_qr(parsed_data.qr_payload) || "0.00",
66
:-(
foreign_currency: extract_currency_from_qr(parsed_data.qr_payload) || "INR",
67 fx_rate: "1.00",
68
:-(
markup_pct: merchant_info.markup_percentage || "0.00",
69 ver_token: generate_ver_token(),
70 status: "validated",
71 validation_type: determine_validation_type(parsed_data),
72
:-(
corridor: determine_corridor_from_context(parsed_data, extract_currency_from_qr(parsed_data.qr_payload) || "INR"),
73
:-(
merchant_category: merchant_info.merchant_code || "5411",
74
:-(
initiation_mode: parsed_data.initiation_mode, # Add initiation mode for static/dynamic QR validation
75 raw_xml: qr_data,
76 fx_timestamp: DateTime.utc_now(),
77 npci_request_received_at: DateTime.utc_now() # When NPCI request was received by PSP
78 }
79
80 # Store in QR validations table
81
:-(
case QRValidationService.create_validation(qr_validation_attrs) do
82 {:ok, qr_validation} ->
83
:-(
Logger.info("QR validation stored successfully with ID: #{qr_validation.id}")
84
:-(
case create_validation_transaction(parsed_data, merchant_info, qr_validation.id) do
85 {:ok, transaction} ->
86
:-(
response_data = %{
87 org_id: get_psp_org_id(),
88 msg_id: generate_message_id(),
89
:-(
req_msg_id: parsed_data.msg_id,
90 result: "SUCCESS",
91 err_code: "00",
92
:-(
txn_id: parsed_data.txn_id, # Use NPCI transaction ID from request
93
:-(
note: parsed_data.note || "MT_VALQRDEP",
94
:-(
ref_id: parsed_data.ref_id || generate_reference_id(),
95
:-(
ref_url: parsed_data.ref_url || merchant_info.website_url || "",
96
:-(
purpose: parsed_data.purpose || "11",
97
:-(
cust_ref: parsed_data.cust_ref || generate_customer_reference(),
98
:-(
initiation_mode: parsed_data.initiation_mode,
99
:-(
txn_timestamp: parsed_data.timestamp || get_timestamp(),
100
:-(
qr_payload: parsed_data.qr_payload,
101
102 # Payee information
103
:-(
payee_addr: merchant_info.upi_id,
104
:-(
payee_name: merchant_info.merchant_name,
105
:-(
payee_type: merchant_info.merchant_type || "ENTITY",
106
:-(
merchant_code: merchant_info.merchant_code || "5411",
107
108 # Institution information
109
:-(
country_code: merchant_info.country_code || "IN",
110 net_inst_id: "MER1010001",
111
112 # Merchant identification
113
:-(
sub_code: extract_merchant_category_from_qr(parsed_data.qr_payload) || merchant_info.merchant_code || "5411",
114
:-(
merchant_id: merchant_info.original_mid || merchant_info.merchant_id, # Use original QR mid if available
115
:-(
store_id: merchant_info.store_id || "STORE001",
116
:-(
terminal_id: merchant_info.terminal_id || "TERM001",
117
:-(
merchant_genre: extract_merchant_genre_from_qr(parsed_data.qr_payload) || merchant_info.genre || "RETAIL",
118
:-(
onboarding_type: merchant_info.onboarding_type || "AGGREGATOR",
119
:-(
reg_id: merchant_info.reg_id || "REG001",
120
:-(
pin_code: merchant_info.pin_code || "560001",
121
:-(
tier: merchant_info.tier || "M2",
122
:-(
merchant_location: merchant_info.location || "Bangalore",
123
:-(
merchant_inst_code: merchant_info.inst_code || "MERC",
124
125 # Merchant names
126
:-(
merchant_brand: merchant_info.brand || merchant_info.merchant_name,
127
:-(
legal_name: merchant_info.legal_name || merchant_info.merchant_name,
128
:-(
franchise_name: merchant_info.franchise_name || merchant_info.brand || merchant_info.merchant_name,
129
:-(
merchant_type: extract_merchant_type_from_qr(parsed_data.qr_payload) || merchant_info.merchant_type || "SMALL",
130 # Ownership and invoice
131
:-(
ownership_type: merchant_info.ownership_type || "PRIVATE",
132
:-(
invoice_name: extract_invoice_name_from_qr(parsed_data.qr_payload) || merchant_info.invoice_name || merchant_info.merchant_name,
133
:-(
invoice_number: extract_invoice_number_from_qr(parsed_data.qr_payload),
134
:-(
invoice_date: extract_invoice_date_from_qr(parsed_data.qr_payload) || get_timestamp(),
135 # Account details
136
:-(
ifsc_code: merchant_info.ifsc_code || "MERC0000001",
137
:-(
account_type: merchant_info.account_type || "CURRENT",
138
:-(
account_number: merchant_info.account_number || "1234567890",
139 # Amount and currency
140
:-(
currency: extract_currency_from_qr(parsed_data.qr_payload) || Map.get(parsed_data, :base_curr, "INR"),
141
:-(
amount: extract_amount_from_qr(parsed_data.qr_payload) || "0.00",
142 split_name: "MAIN",
143 # FX information
144
:-(
base_amount: extract_amount_from_qr(parsed_data.qr_payload) || "0.00",
145
:-(
base_currency: extract_currency_from_qr(parsed_data.qr_payload) || Map.get(parsed_data, :base_curr, "INR"),
146 fx_active: "Y",
147
:-(
fx_rate: merchant_info.fx_rate || "1.00",
148
:-(
markup_percentage: merchant_info.markup_percentage || "0.00",
149
150 # QR information
151 qr_version: "02",
152 qr_medium: "03",
153 expire_ts: get_expiry_timestamp(),
154
:-(
ver_token: qr_validation.ver_token,
155 stan: generate_stan()
156 }
157
158
:-(
case UpiXmlSchema.generate_resp_val_qr(response_data) do
159
:-(
{:ok, respvalqr_xml} -> {:ok, {respvalqr_xml, qr_validation}}
160
:-(
{:error, reason} -> {:error, reason}
161 end
162
163 {:error, reason} ->
164
:-(
error_message = case reason do
165 %Ecto.Changeset{} = changeset ->
166 # Extract validation errors from changeset
167
:-(
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
168
:-(
Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
169
:-(
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
170 end)
171 end)
172
:-(
"Validation failed: #{inspect(errors)}"
173 _ ->
174
:-(
"Transaction creation failed: #{inspect(reason)}"
175 end
176
:-(
generate_error_response(parsed_data, "02", error_message)
177 end
178
179 {:error, qr_validation_error} ->
180
:-(
generate_error_response(parsed_data, "02", "QR validation storage failed: #{inspect(qr_validation_error)}")
181 end
182
183 {:error, "QR_NOT_FOUND"} ->
184
:-(
generate_error_response(parsed_data, "ZQ", "QR code not found")
185
186 {:error, "MERCHANT_NOT_FOUND"} ->
187
:-(
generate_error_response(parsed_data, "ZR", "Merchant not found")
188
189 {:error, "TID_MISMATCH"} ->
190 # Terminal ID in QR did not match merchant record - return specific XW error
191
:-(
generate_error_response(parsed_data, "XW", "Terminal ID mismatch")
192
193 {:error, reason} ->
194
:-(
generate_error_response(parsed_data, "05", "Invalid merchant QR: #{inspect(reason)}")
195 end
196
197 {:error, reason} ->
198 # When parsing fails, we need a default parsed_data structure
199
:-(
default_parsed_data = %{msg_id: generate_message_id()}
200
:-(
generate_error_response(default_parsed_data, "ZH", "Invalid XML: #{reason}")
201 end
202 end
203
204 # Helper functions for QR validation
205 defp generate_ver_token do
206
:-(
"VER" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
207 end
208
209 defp determine_corridor(currency) do
210 case currency do
211 "SGD" -> "SINGAPORE"
212 "AED" -> "UAE"
213 "USD" -> "USA"
214 "INR" -> nil # Domestic transactions have nil corridor
215 _ -> nil # Default to domestic for unknown currencies
216 end
217 end
218
219 defp determine_corridor_from_context(parsed_data, currency) do
220 # For international QR validation, determine corridor based on:
221 # 1. Country code from the transaction
222 # 2. Network institution ID patterns
223 # 3. Currency as fallback
224
225
:-(
cond do
226 # Check country code first (most reliable for international transactions)
227
:-(
parsed_data.con_code == "SG" -> "SINGAPORE"
228
:-(
parsed_data.con_code == "AE" -> "UAE"
229
:-(
parsed_data.con_code == "US" -> "USA"
230
231 # Check network institution ID patterns
232
:-(
String.starts_with?(parsed_data.net_inst_id || "", "SG") -> "SINGAPORE"
233
:-(
String.starts_with?(parsed_data.net_inst_id || "", "AE") -> "UAE"
234
:-(
String.starts_with?(parsed_data.net_inst_id || "", "US") -> "USA"
235
236 # Fallback to currency-based determination
237
:-(
currency == "SGD" -> "SINGAPORE"
238
:-(
currency == "AED" -> "UAE"
239
:-(
currency == "USD" -> "USA"
240
241 # For domestic INR transactions
242
:-(
currency == "INR" && (parsed_data.con_code == "IN" || is_nil(parsed_data.con_code)) -> nil
243
244 # Default to Singapore for unknown international patterns (since most QRs seem to be SG)
245
:-(
currency == "INR" && parsed_data.con_code not in ["IN", nil] -> "SINGAPORE"
246
247 # True domestic
248
:-(
true -> nil
249 end
250 end
251
252 defp determine_validation_type(parsed_data) do
253 # Determine if this is an international or domestic validation
254 # based on country code and institution patterns
255
:-(
cond do
256 # Clear international indicators
257
:-(
parsed_data.con_code not in ["IN", nil] -> "INTERNATIONAL"
258
:-(
String.contains?(parsed_data.net_inst_id || "", "SG") -> "INTERNATIONAL"
259
:-(
String.contains?(parsed_data.net_inst_id || "", "AE") -> "INTERNATIONAL"
260
:-(
String.contains?(parsed_data.net_inst_id || "", "US") -> "INTERNATIONAL"
261
262 # Domestic by default
263
:-(
true -> "DOMESTIC"
264 end
265 end
266
267 @doc """
268 Process payment request with complete flow
269 """
270 def process_payment_request(payment_data) do
271
:-(
case UpiXmlSchema.parse_req_pay(payment_data) do
272 {:ok, parsed_data_raw} ->
273 # normalize parsed_data so downstream code that uses dot access won't KeyError
274
:-(
parsed_data = normalize_parsed_data(parsed_data_raw)
275
:-(
case create_payment_transaction(parsed_data) do
276 {:ok, transaction} ->
277
:-(
case process_with_partner(transaction) do
278 {:ok, partner_response} ->
279 # Update transaction status
280
:-(
update_transaction_status(transaction.id, :success, partner_response)
281
282
:-(
response_data = %{
283 org_id: get_psp_org_id(),
284 msg_id: generate_message_id(),
285
:-(
req_msg_id: parsed_data.msg_id,
286 result: "SUCCESS",
287 err_code: "00",
288
:-(
txn_id: parsed_data.txn_id,
289
:-(
cust_ref: parsed_data.cust_ref,
290
:-(
txn_type: parsed_data.txn_type || "CREDIT", # CRITICAL: Use original txn_type from ReqPay for T07 compliance
291 add_info: build_additional_info(partner_response),
292 request_data: parsed_data
293 }
294
295
:-(
UpiXmlSchema.generate_resp_pay(response_data)
296
297 {:error, :insufficient_funds} ->
298
:-(
generate_payment_error_response(parsed_data, "10", "Debit has been failed")
299
300 {:error, :partner_timeout} ->
301 # Start timeout escalation process
302
:-(
start_timeout_escalation(parsed_data)
303
:-(
generate_payment_error_response(parsed_data, "01", "Transaction is pending")
304
305 {:error, :partner_error} ->
306
:-(
generate_payment_error_response(parsed_data, "14", "External Error")
307
308 {:error, reason} ->
309
:-(
generate_payment_error_response(parsed_data, "02", "Transaction failed: #{reason}")
310 end
311
312 {:error, reason} ->
313
:-(
generate_payment_error_response(parsed_data, "02", "Transaction failed: #{reason}")
314 end
315
316 {:error, reason} ->
317 # When parsing fails, we need a default parsed_data structure
318
:-(
default_parsed_data = %{msg_id: generate_message_id(), cust_ref: ""}
319
:-(
generate_payment_error_response(default_parsed_data, "ZH", "Invalid XML: #{reason}")
320 end
321 end
322
323 @doc """
324 Process QR payment request with complete flow
325 Handles ReqPay with QR payload - combines QR validation logic with payment processing
326 Sends immediate ACK and processes payment asynchronously
327 """
328 def process_qr_payment_request(payment_data) do
329
:-(
Logger.info(" Processing ReqPay with QR payload from NPCI")
330
331 # Try to parse with flexible QR parser first, fallback to international parser
332 # Ensure :inr_amount is present for domestic payments
333
:-(
payment_data =
334 case payment_data do
335 %{"transaction_type" => "DOMESTIC", "amount" => amount} ->
336
:-(
Map.put(payment_data, :inr_amount, amount)
337 _ ->
338
:-(
payment_data
339 end
340
341
:-(
case parse_qr_payment_xml(payment_data) do
342 {:ok, parsed_data} ->
343
:-(
Logger.info(" Step 1: Successfully parsed ReqPay XML, msg_id: #{parsed_data.msg_id}, txn_id: #{parsed_data.txn_id}")
344
345 # ALWAYS send ACK first, regardless of QR payload validation
346
:-(
ack_xml = UpiXmlSchema.generate_ack_reqpay_response(parsed_data.msg_id)
347
:-(
Logger.info(" Step 2: Generated ACK, sending to NPCI...")
348
:-(
send_ack_to_npci(ack_xml, parsed_data.txn_id)
349
350 # Check if QR payload exists and process accordingly
351
:-(
qr_payload = Map.get(parsed_data, :qr_payload, "")
352
353
:-(
if qr_payload != "" do
354
:-(
Logger.info(" Found QR payload, checking QR expiry...")
355
356 # Check QR expiry after ACK but before merchant validation
357
:-(
case maybe_handle_qr_expiry(parsed_data) do
358 :ok ->
359
:-(
Logger.info(" QR expiry check passed, proceeding with merchant validation")
360
:-(
case validate_merchant_qr(qr_payload) do
361 {:ok, merchant_info} ->
362
:-(
Logger.info(" Merchant validation successful: #{merchant_info.merchant_name}")
363
:-(
process_qr_payment_and_send_response(parsed_data, merchant_info)
364 {:error, reason} ->
365
:-(
Logger.warning(" Merchant validation failed: #{reason}, sending error response")
366
:-(
error_response = generate_payment_error_response(parsed_data, "ZR", "Merchant validation failed: #{reason}")
367
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
368 end
369
370 {:error, :expired} ->
371
:-(
Logger.warning(" QR code expired for msg_id: #{parsed_data.msg_id}, txn_id: #{parsed_data.txn_id}")
372
:-(
handle_expired_qr_response(parsed_data)
373
374 {:error, reason} ->
375
:-(
Logger.warning(" QR expiry check failed: #{inspect(reason)}, proceeding with merchant validation")
376
:-(
case validate_merchant_qr(qr_payload) do
377 {:ok, merchant_info} ->
378
:-(
Logger.info(" Merchant validation successful: #{merchant_info.merchant_name}")
379
:-(
process_qr_payment_and_send_response(parsed_data, merchant_info)
380 {:error, reason} ->
381
:-(
Logger.warning(" Merchant validation failed: #{reason}, sending error response")
382
:-(
error_response = generate_payment_error_response(parsed_data, "ZR", "Merchant validation failed: #{reason}")
383
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
384 end
385 end
386 else
387
:-(
Logger.info(" No QR payload found, processing as regular payment")
388
:-(
case create_payment_transaction(parsed_data) do
389 {:ok, transaction} ->
390
:-(
case process_with_partner(transaction) do
391 {:ok, partner_response} ->
392
:-(
response_data = %{
393 org_id: get_psp_org_id(),
394 msg_id: generate_message_id(),
395
:-(
req_msg_id: parsed_data.msg_id,
396 result: "SUCCESS",
397 err_code: "00",
398
:-(
txn_id: parsed_data.txn_id,
399
:-(
cust_ref: parsed_data.cust_ref,
400
:-(
txn_type: parsed_data.txn_type || "CREDIT", # CRITICAL: Use original txn_type from ReqPay for T07 compliance
401 add_info: build_additional_info(partner_response),
402
:-(
expire_ts: parsed_data.expire_ts, # Explicitly include expire_ts from original request
403
:-(
org_txn_id: parsed_data.org_txn_id, # CRITICAL: Use original orgTxnId from ReqPay for NPCI OR2 compliance
404
:-(
note: parsed_data.note, # CRITICAL: Include original note for T03 compliance
405
:-(
ref_url: parsed_data.ref_url, # CRITICAL: Include original ref_url from ReqPay
406
407 # CRITICAL: Include all Payee fields from ReqPay for Ref element in RespPay
408
:-(
seq_num: parsed_data.payee_seq_num || "1",
409
:-(
payee_addr: parsed_data.payee_addr,
410
:-(
payee_code: validate_payee_code(parsed_data.payee_code), # E17 fix: exactly 4 digits
411
:-(
org_amount: parsed_data.payee_amount || parsed_data.amount,
412 resp_code: "00", # Success response code
413
:-(
reg_name: parsed_data.payee_name || parsed_data.brand || parsed_data.legal,
414
:-(
ifsc: validate_ifsc_code(parsed_data.payee_ifsc), # E18 fix: exactly 11 chars
415
:-(
ac_num: validate_account_number(parsed_data.payee_ac_num), # E16 fix: 1-16 chars
416
:-(
acc_type: validate_account_type(parsed_data.payee_ac_type), # E19 fix: must be present
417
:-(
approval_num: partner_response.approval_num || generate_approval_number(),
418
:-(
sett_amount: partner_response.settlement_amount || parsed_data.payee_amount || parsed_data.amount,
419
:-(
sett_currency: parsed_data.payee_currency || parsed_data.currency || "INR",
420
421 request_data: parsed_data
422 }
423
:-(
case UpiXmlSchema.generate_resp_pay(response_data) do
424 {:ok, resp_pay_xml} ->
425
:-(
send_resp_pay_to_npci(resp_pay_xml, parsed_data.txn_id)
426 error_xml when is_binary(error_xml) ->
427
:-(
send_resp_pay_to_npci(error_xml, parsed_data.txn_id)
428 {:error, reason} ->
429
:-(
Logger.error("Failed to generate RespPay: #{reason}")
430 end
431 {:error, reason} ->
432
:-(
error_response = generate_payment_error_response(parsed_data, "02", "Payment failed: #{reason}")
433
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
434 end
435 {:error, reason} ->
436
:-(
error_response = generate_payment_error_response(parsed_data, "02", "Transaction creation failed: #{reason}")
437
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
438 end
439 end
440
441
:-(
Logger.info(" Step 3: ACK sent, RespPay sent synchronously")
442 # Return ACK immediately (this is what Postman will see)
443
:-(
ack_xml
444
445 {:error, reason} ->
446
:-(
Logger.error(" Failed to parse ReqPay XML: #{reason}")
447
:-(
Logger.info(" Parsing failed, but attempting to send ACK anyway...")
448
449 # Even if parsing fails completely, try to extract basic info and send ACK
450
:-(
basic_parsed_data = extract_basic_xml_info(payment_data)
451
452
:-(
ack_xml = UpiXmlSchema.generate_ack_reqpay_response(basic_parsed_data.msg_id)
453
:-(
Logger.info(" Generated ACK despite parsing failure, sending to NPCI...")
454
:-(
send_ack_to_npci(ack_xml, basic_parsed_data.txn_id)
455
:-(
error_response = generate_payment_error_response(basic_parsed_data, "ZH", "Invalid XML: #{reason}")
456
:-(
send_error_resp_pay_to_npci(error_response, basic_parsed_data.txn_id)
457 # Return ACK immediately
458
:-(
ack_xml
459 end
460 end
461
462 # Process QR payment transaction and send RespPay to NPCI
463 defp process_qr_payment_and_send_response(parsed_data, merchant_info) do
464
:-(
Logger.info(" Starting QR payment processing with merchant: #{merchant_info.merchant_name}")
465
466 # COMPREHENSIVE MERCHANT VALIDATION BEFORE ANY CREDIT ATTEMPT
467
:-(
case validate_merchant_creditworthiness(parsed_data, merchant_info) do
468 {:error, {error_code, error_message}} ->
469
:-(
Logger.warning("Merchant validation failed: #{error_message} - sending #{error_code} RespPay")
470 # Build an error RespPay and send to NPCI
471
:-(
error_resp = generate_payment_error_response(parsed_data, error_code, error_message)
472
:-(
send_error_resp_pay_to_npci(error_resp, parsed_data.txn_id)
473
474 # Mark as declined by IMA for audit trail
475
:-(
case DaProductApp.Transactions.ReqPayService.get_by_txn_id(parsed_data.txn_id) do
476 {:ok, req_pay} ->
477
:-(
DaProductApp.Transactions.ReqPayService.mark_as_declined_by_ima(req_pay, error_code, error_message)
478 _ ->
479
:-(
Logger.warn("Could not find ReqPay record to mark as declined")
480 end
481
482 :ok
483
484 {:ok, enhanced_merchant_info} ->
485
:-(
case create_validation_transaction(parsed_data, enhanced_merchant_info, nil) do
486 {:ok, transaction} ->
487
:-(
Logger.info("QR payment transaction created with ID: #{transaction.id}, org_txn_id: #{transaction.org_txn_id}")
488
489
:-(
case process_qr_payment_with_partner(transaction, enhanced_merchant_info) do
490 {:ok, partner_response} ->
491
:-(
Logger.info(" Partner processing successful")
492
493 # Update transaction status
494
:-(
update_transaction_status(transaction.id, :success, partner_response)
495
496
:-(
response_data = %{
497 org_id: get_psp_org_id(),
498 msg_id: generate_message_id(),
499
:-(
req_msg_id: parsed_data.msg_id,
500 result: "SUCCESS",
501 err_code: "00",
502
:-(
txn_id: parsed_data.txn_id,
503
:-(
note: parsed_data.note || "QR Payment processed",
504
:-(
ref_id: parsed_data.ref_id || transaction.id,
505
:-(
cust_ref: parsed_data.cust_ref,
506
:-(
ref_url: parsed_data.ref_url || merchant_info.website_url || "",
507 txn_type: "CREDIT",
508 sub_type: Map.get(parsed_data, :sub_type, ""),
509 initiation_mode: Map.get(parsed_data, :initiation_mode, "QR"),
510
:-(
org_txn_id: Map.get(parsed_data, :org_txn_id, transaction.id),
511 org_rrn: Map.get(parsed_data, :org_rrn, generate_rrn()),
512 prod_type: "UPI", # QR payments use "UPI" not "UPI_INTL"
513
514 # Merchant/Payee details
515
:-(
payee_addr: merchant_info.upi_id,
516
:-(
payee_code: merchant_info.merchant_code || "MERC001",
517
:-(
payee_name: merchant_info.merchant_name,
518
:-(
reg_name: merchant_info.legal_name || merchant_info.merchant_name,
519
:-(
ifsc: merchant_info.ifsc_code || "MERC0000001",
520
:-(
ac_num: merchant_info.account_number || "1234567890",
521
:-(
acc_type: merchant_info.account_type || "CURRENT",
522
523 # Amount details - extract from parsed_data or QR payload
524
:-(
org_amount: extract_amount_from_qr(Map.get(parsed_data, :qr_payload, "")) ||
525
:-(
Map.get(parsed_data, :payee_amount, "0.00"),
526
:-(
sett_amount: extract_amount_from_qr(Map.get(parsed_data, :qr_payload, "")) ||
527
:-(
Map.get(parsed_data, :payee_amount, "0.00"),
528
:-(
sett_currency: extract_currency_from_qr(Map.get(parsed_data, :qr_payload, "")) ||
529
:-(
Map.get(parsed_data, :payee_currency, "INR"),
530
531 # QR details
532 qr_ver: "2.0",
533 qr_medium: "04",
534 ver_token: generate_ver_token(),
535 stan: generate_stan(),
536
537 # Partner response details
538
:-(
approval_num: partner_response.approval_number || generate_approval_number(),
539
:-(
resp_code: partner_response.resp_code || "00",
540 request_data: parsed_data
541 }
542
543
:-(
case UpiXmlSchema.generate_resp_pay(response_data) do
544 {:ok, resp_pay_xml} ->
545
:-(
Logger.info(" Generated RespPay XML, sending to NPCI")
546
:-(
send_resp_pay_to_npci(resp_pay_xml, parsed_data.txn_id)
547
548 error_xml when is_binary(error_xml) ->
549
:-(
Logger.info(" Generated error RespPay XML, sending to NPCI")
550
:-(
send_resp_pay_to_npci(error_xml, parsed_data.txn_id)
551
552 {:error, reason} ->
553
:-(
Logger.error(" Failed to generate RespPay XML: #{inspect(reason)}")
554 end
555
556 {:error, :insufficient_funds} ->
557
:-(
Logger.error(" Insufficient funds")
558
:-(
error_response = generate_payment_error_response(parsed_data, "10", "Debit has been failed")
559
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
560
561 {:error, :partner_timeout} ->
562
:-(
Logger.error(" Partner timeout")
563
:-(
start_timeout_escalation(parsed_data)
564
:-(
error_response = generate_payment_error_response(parsed_data, "01", "Transaction is pending")
565
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
566
567 {:error, :partner_error} ->
568
:-(
Logger.error(" Partner error")
569
:-(
error_response = generate_payment_error_response(parsed_data, "14", "External Error")
570
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
571
572 {:error, reason} ->
573
:-(
Logger.error(" Payment processing failed: #{inspect(reason)}")
574
:-(
error_response = generate_payment_error_response(parsed_data, "02", "Transaction failed: #{reason}")
575
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
576 end
577
578 {:error, reason} ->
579
:-(
Logger.error("Transaction creation failed: #{inspect(reason)}")
580
:-(
error_response = generate_payment_error_response(parsed_data, "02", "Transaction failed: #{reason}")
581
:-(
send_error_resp_pay_to_npci(error_response, parsed_data.txn_id)
582 end
583 end
584 end
585
586 @doc """
587 Process transaction status check
588 """
589 def process_status_check(check_data) do
590
:-(
case UpiXmlSchema.parse_req_chk_txn(check_data) do
591 {:ok, parsed_data} ->
592 # Extract orgMsgId from parsed data for transaction lookup. If parser didn't populate it,
593 # Extract orgMsgId from ReqChkTxn - try parsed_data fields first, then raw XML extraction
594
:-(
org_msg_id = Map.get(parsed_data, :org_msg_id) || Map.get(parsed_data, "org_msg_id") ||
595
:-(
Map.get(parsed_data, :orgMsgId) || Map.get(parsed_data, "orgMsgId") ||
596 # Some parsers embed nested maps (header/txn)
597
:-(
(case Map.get(parsed_data, :txn) || Map.get(parsed_data, "txn") do
598
:-(
%{} = txn -> Map.get(txn, :org_msg_id) || Map.get(txn, "org_msg_id") || Map.get(txn, :orgMsgId) || Map.get(txn, "orgMsgId")
599
:-(
_ -> nil
600
:-(
end) ||
601 # Extract from raw XML - try Txn element first (most common location)
602
:-(
(case Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") || check_data do
603 xml when is_binary(xml) ->
604 # Try Txn element orgMsgId attribute first
605
:-(
case Regex.run(~r/<Txn[^>]*orgMsgId\s*=\s*"([^"]+)"/i, xml) do
606
:-(
[_, id] -> id
607 _ ->
608 # Fallback to any orgMsgId attribute
609
:-(
case Regex.run(~r/orgMsgId\s*=\s*"([^"]+)"/i, xml) do
610
:-(
[_, id2] -> id2
611 _ ->
612 # Try single quotes as last resort
613
:-(
case Regex.run(~r/orgMsgId\s*=\s*'([^']+)'/i, xml) do
614
:-(
[_, id3] -> id3
615
:-(
_ -> nil
616 end
617 end
618 end
619
:-(
_ -> nil
620 end)
621
622
:-(
Logger.info("=== ReqChkTxn Transaction Lookup ===")
623
:-(
Logger.info("Org Msg ID from ReqChkTxn: #{inspect(org_msg_id)}")
624 # Ensure we also have a robust org_txn_id value to log / lookup
625
:-(
org_txn_id = Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") || Map.get(parsed_data, :orgTxnId) || Map.get(parsed_data, "orgTxnId") ||
626
:-(
(case Map.get(parsed_data, :txn) || Map.get(parsed_data, "txn") do
627
:-(
%{} = txn -> Map.get(txn, :id) || Map.get(txn, "id") || Map.get(txn, :org_txn_id) || Map.get(txn, "org_txn_id")
628
:-(
_ -> nil
629
:-(
end) ||
630
:-(
(case Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") || check_data do
631 xml when is_binary(xml) ->
632
:-(
case Regex.run(~r/orgTxnId\s*=\s*"([^"]+)"/i, xml) do
633
:-(
[_, id] -> id
634 _ ->
635
:-(
case Regex.run(~r/orgTxnId\s*=\s*'([^']+)'/i, xml) do
636
:-(
[_, id2] -> id2
637
:-(
_ -> nil
638 end
639 end
640
:-(
_ -> nil
641 end)
642
643
:-(
Logger.info("Org Txn ID from ReqChkTxn: #{inspect(org_txn_id)}")
644
645 # Also extract the Txn 'id' attribute (the ReqChkTxn's own id) since NPCI sometimes
646 # populates the transaction identifier into the Txn/@id field. Try parsed values,
647 # nested :txn maps, and finally fall back to regex over the raw XML.
648
:-(
txn_id = Map.get(parsed_data, :id) || Map.get(parsed_data, "id") ||
649
:-(
(case Map.get(parsed_data, :txn) || Map.get(parsed_data, "txn") do
650
:-(
%{} = t -> Map.get(t, :id) || Map.get(t, "id")
651
:-(
_ -> nil
652
:-(
end) ||
653
:-(
(case Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") || check_data do
654 xml when is_binary(xml) ->
655
:-(
case Regex.run(~r/<Txn[^>]*\bid\s*=\s*"([^"]+)"/i, xml) do
656
:-(
[_, id] -> id
657 _ ->
658
:-(
case Regex.run(~r/\bid\s*=\s*'([^']+)'/i, xml) do
659
:-(
[_, id2] -> id2
660
:-(
_ -> nil
661 end
662 end
663
:-(
_ -> nil
664 end)
665
666
:-(
Logger.info("Txn id from ReqChkTxn: #{inspect(txn_id)}")
667
668 # First try to look up by orgMsgId (recommended), then fallback to orgTxnId
669
:-(
transaction = cond do
670
:-(
org_msg_id && org_msg_id != "" ->
671 # Try to find the transaction by the original request message id first (req_msg_id)
672
:-(
Logger.info("Looking up transaction by req_msg_id = #{org_msg_id}")
673
:-(
txn_query_reqmsg = from t in "transactions",
674 where: field(t, :req_msg_id) == ^org_msg_id,
675 select: %{
676 id: field(t, :id),
677 status: field(t, :status),
678 currency: field(t, :currency),
679 inr_amount: field(t, :inr_amount),
680 org_txn_id: field(t, :org_txn_id)
681 }
682
683
:-(
case Repo.one(txn_query_reqmsg) do
684 nil ->
685 # If not found by req_msg_id, try org_txn_id == org_msg_id as a secondary attempt
686
:-(
Logger.info("Not found by req_msg_id; trying org_txn_id = #{org_msg_id}")
687
:-(
txn_query_org = from t in "transactions",
688 where: field(t, :org_txn_id) == ^org_msg_id,
689 select: %{
690 id: field(t, :id),
691 status: field(t, :status),
692 currency: field(t, :currency),
693 inr_amount: field(t, :inr_amount),
694 org_txn_id: field(t, :org_txn_id)
695 }
696
:-(
Repo.one(txn_query_org)
697
:-(
rec -> rec
698 end
699
700 # If the ReqChkTxn provides a Txn/@id attribute, try matching that too. NPCI may
701 # use the Txn id as either a request message id or the original txn id.
702
:-(
txn_id && txn_id != "" ->
703
:-(
Logger.info("Looking up transaction by txn_id = #{txn_id}")
704 # First try matching as req_msg_id, then as org_txn_id
705
:-(
case Repo.one(from t in "transactions",
706 where: field(t, :req_msg_id) == ^txn_id,
707 select: %{
708 id: field(t, :id),
709 status: field(t, :status),
710 currency: field(t, :currency),
711 inr_amount: field(t, :inr_amount),
712 org_txn_id: field(t, :org_txn_id)
713 }) do
714 nil ->
715
:-(
Logger.info("Not found by req_msg_id for txn_id; trying org_txn_id = #{txn_id}")
716
:-(
Repo.one(from t in "transactions",
717 where: field(t, :org_txn_id) == ^txn_id,
718 select: %{
719 id: field(t, :id),
720 status: field(t, :status),
721 currency: field(t, :currency),
722 inr_amount: field(t, :inr_amount),
723 org_txn_id: field(t, :org_txn_id)
724 })
725
:-(
rec -> rec
726 end
727
728
:-(
parsed_data.org_txn_id && parsed_data.org_txn_id != "" ->
729
:-(
Logger.info("Fallback: Looking up transaction by org_txn_id = #{parsed_data.org_txn_id}")
730
:-(
txn_query = from t in "transactions",
731
:-(
where: field(t, :org_txn_id) == ^parsed_data.org_txn_id,
732 select: %{
733 id: field(t, :id),
734 status: field(t, :status),
735 currency: field(t, :currency),
736 inr_amount: field(t, :inr_amount),
737 org_txn_id: field(t, :org_txn_id)
738 }
739
:-(
Repo.one(txn_query)
740
741
:-(
true ->
742
:-(
Logger.warn("No valid transaction ID found in ReqChkTxn - attempting req_pays fallback lookup")
743
744 # Try to find a matching ReqPay record using common identifier columns.
745 # Search order: txn_id, msg_id, npci_txn_id, partner_txn_id (best-effort).
746
:-(
reqpay = cond do
747
:-(
txn_id && txn_id != "" ->
748
:-(
Logger.info("ReqChkTxn fallback: searching req_pays by txn_id=#{txn_id}")
749
:-(
Repo.one(from r in "req_pays", where: field(r, :txn_id) == ^txn_id or field(r, :msg_id) == ^txn_id or field(r, :npci_txn_id) == ^txn_id or field(r, :partner_txn_id) == ^txn_id, select: %{id: field(r, :id), transaction_id: field(r, :transaction_id), txn_id: field(r, :txn_id), msg_id: field(r, :msg_id)})
750
751
:-(
org_msg_id && org_msg_id != "" ->
752
:-(
Logger.info("ReqChkTxn fallback: searching req_pays by org_msg_id/msg_id=#{org_msg_id}")
753
:-(
Repo.one(from r in "req_pays", where: field(r, :msg_id) == ^org_msg_id or field(r, :txn_id) == ^org_msg_id or field(r, :npci_txn_id) == ^org_msg_id, select: %{id: field(r, :id), transaction_id: field(r, :transaction_id), txn_id: field(r, :txn_id), msg_id: field(r, :msg_id)})
754
755
:-(
parsed_data.org_txn_id && parsed_data.org_txn_id != "" ->
756
:-(
Logger.info("ReqChkTxn fallback: searching req_pays by parsed org_txn_id=#{parsed_data.org_txn_id}")
757
:-(
Repo.one(from r in "req_pays", where: field(r, :txn_id) == ^parsed_data.org_txn_id or field(r, :npci_txn_id) == ^parsed_data.org_txn_id or field(r, :partner_txn_id) == ^parsed_data.org_txn_id, select: %{id: field(r, :id), transaction_id: field(r, :transaction_id), txn_id: field(r, :txn_id), msg_id: field(r, :msg_id)})
758
759
:-(
true ->
760 nil
761 end
762
763
:-(
if reqpay && Map.get(reqpay, :transaction_id) do
764
:-(
Logger.info("ReqPay matched (id=#{reqpay.id}) and links to transaction_id=#{inspect(reqpay.transaction_id)}; loading Transaction")
765
:-(
Repo.one(from t in "transactions", where: field(t, :id) == ^reqpay.transaction_id, select: %{id: field(t, :id), status: field(t, :status), currency: field(t, :currency), inr_amount: field(t, :inr_amount), org_txn_id: field(t, :org_txn_id)})
766 else
767
:-(
Logger.warn("ReqPays fallback did not find a linked transaction")
768 nil
769 end
770 end
771
772
:-(
case transaction do
773 nil ->
774
:-(
Logger.warn("=== Transaction Not Found ===")
775
:-(
Logger.warn("Org Msg ID: #{inspect(org_msg_id)}")
776
:-(
Logger.warn("Org Txn ID: #{inspect(parsed_data.org_txn_id)}")
777
778 # extra fallback: try matching incoming head msgId or txn id against transactions.org_txn_id
779
:-(
incoming_msg_id = Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id") || org_msg_id
780
:-(
incoming_txn_id = Map.get(parsed_data, :txn_id) || Map.get(parsed_data, "txn_id") || parsed_data.org_txn_id || org_txn_id
781
782
:-(
Logger.info("Attempting extended fallback lookups: incoming_msg_id=#{inspect(incoming_msg_id)}, incoming_txn_id=#{inspect(incoming_txn_id)}")
783
784
:-(
fallback_txn = cond do
785
:-(
incoming_msg_id && incoming_msg_id != "" ->
786
:-(
Logger.info("Fallback: looking up by transactions.org_txn_id == incoming_msg_id")
787
:-(
Repo.one(from t in "transactions", where: field(t, :org_txn_id) == ^incoming_msg_id, select: %{id: field(t, :id), status: field(t, :status), currency: field(t, :currency), inr_amount: field(t, :inr_amount), org_txn_id: field(t, :org_txn_id)})
788
789
:-(
incoming_txn_id && incoming_txn_id != "" ->
790
:-(
Logger.info("Fallback: looking up by transactions.org_txn_id == incoming_txn_id")
791
:-(
Repo.one(from t in "transactions", where: field(t, :org_txn_id) == ^incoming_txn_id, select: %{id: field(t, :id), status: field(t, :status), currency: field(t, :currency), inr_amount: field(t, :inr_amount), org_txn_id: field(t, :org_txn_id)})
792
793
:-(
true -> nil
794 end
795
796
:-(
if is_nil(fallback_txn) do
797
:-(
Logger.warn("Extended fallback did not find a transaction either. Trying req_pays -> transaction mapping as last resort")
798
799 # Last resort: try to find a ReqPay that matches the incoming identifiers
800 # and use its linked transaction_id (if any) to resolve the Transaction.
801
:-(
reqpay_match = cond do
802
:-(
txn_id && txn_id != "" ->
803
:-(
Logger.info("Trying req_pays lookup by txn_id = #{txn_id}")
804
:-(
Repo.one(from r in "req_pays", where: field(r, :txn_id) == ^txn_id, select: %{id: field(r, :id), transaction_id: field(r, :transaction_id), msg_id: field(r, :msg_id)})
805
806
:-(
org_msg_id && org_msg_id != "" ->
807
:-(
Logger.info("Trying req_pays lookup by msg_id = #{org_msg_id}")
808
:-(
Repo.one(from r in "req_pays", where: field(r, :msg_id) == ^org_msg_id, select: %{id: field(r, :id), transaction_id: field(r, :transaction_id), msg_id: field(r, :msg_id)})
809
810
:-(
true -> nil
811 end
812
813
:-(
fallback_txn =
814
:-(
if reqpay_match && Map.get(reqpay_match, :transaction_id) do
815
:-(
Logger.info("Found ReqPay mapping to transaction_id=#{inspect(reqpay_match.transaction_id)}; fetching Transaction")
816
:-(
Repo.one(from t in "transactions", where: field(t, :id) == ^reqpay_match.transaction_id, select: %{id: field(t, :id), status: field(t, :status), currency: field(t, :currency), inr_amount: field(t, :inr_amount), org_txn_id: field(t, :org_txn_id)})
817 else
818
:-(
Logger.warn("ReqPays lookup failed to find a linked transaction")
819 nil
820 end
821
822
:-(
if is_nil(fallback_txn) do
823
:-(
Logger.warn("All fallback lookups failed. Transaction lookup failed, sending error RespChkTxn")
824
825 # Generate error response and send to NPCI, then return immediately to avoid nil deref
826
:-(
{:ok, error_xml} = generate_status_error_response(parsed_data, "05", "Invalid transaction")
827
:-(
send_resp_chk_txn_to_npci(error_xml, parsed_data)
828 {:ok, error_xml}
829 else
830
:-(
Logger.info("Found transaction via req_pays fallback: #{inspect(fallback_txn)}")
831 # Rebind transaction and fall through to normal handling below
832
:-(
transaction = fallback_txn
833 end
834 else
835
:-(
Logger.info("Found transaction via extended fallback: #{inspect(fallback_txn)}")
836 # Rebind transaction and fall through to normal handling below
837
:-(
transaction = fallback_txn
838
839 # Now create ReqChkTxn record with the proper transaction_id
840 # Store the full parsed payload and request XML hash for auditability
841
:-(
req_xml = Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") || check_data
842
:-(
req_xml_hash = if is_binary(req_xml), do: :crypto.hash(:sha256, req_xml), else: nil
843
844
:-(
req_attrs = %{
845
:-(
msg_id: Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id") || generate_message_id(),
846
:-(
org_id: Map.get(parsed_data, :org_id) || Map.get(parsed_data, "org_id") || "NPCI",
847
:-(
original_txn_id: Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") || Map.get(parsed_data, :orgTxnId) || Map.get(parsed_data, "orgTxnId"),
848
:-(
transaction_id: (if transaction, do: Map.get(transaction, :id) || transaction.id, else: nil), # Set the required transaction_id defensively
849 status: "PENDING",
850 validation_type: "DOMESTIC",
851 checked_at: DateTime.utc_now(),
852 payload: parsed_data,
853 req_xml_hash: req_xml_hash
854 }
855
856
:-(
inserted_req_chk_txn =
857 case ReqChkTxn.changeset(%ReqChkTxn{}, req_attrs) |> Repo.insert() do
858
:-(
{:ok, rec} -> rec
859 {:error, changeset} ->
860
:-(
Logger.warn("Failed to persist ReqChkTxn pre-store: #{inspect(changeset.errors)}")
861 nil
862 end
863
864
:-(
now = DateTime.utc_now() |> DateTime.truncate(:second)
865
866 # Robustly extract commonly used ReqChkTxn attributes from parsed_data
867 # Support flat keys, nested :txn or :header maps, or falling back to raw_xml attribute regex.
868
:-(
ref_id =
869 case parsed_data do
870 %{} = pd ->
871
:-(
Map.get(pd, :ref_id) || Map.get(pd, "ref_id") || Map.get(pd, :refId) || Map.get(pd, "refId") ||
872
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
873
:-(
%{} = txn -> Map.get(txn, :refId) || Map.get(txn, "refId") || Map.get(txn, :ref_id) || Map.get(txn, "ref_id")
874
:-(
_ -> nil
875
:-(
end) ||
876
:-(
(case Map.get(pd, :header) || Map.get(pd, "header") do
877
:-(
%{} = h -> Map.get(h, :refId) || Map.get(h, "refId") || Map.get(h, :ref_id) || Map.get(h, "ref_id")
878
:-(
_ -> nil
879
:-(
end) ||
880
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
881 xml when is_binary(xml) ->
882
:-(
case Regex.run(~r/refId=\"([^\"]+)\"/, xml) do
883
:-(
[_, id] -> id
884
:-(
_ -> nil
885 end
886
:-(
_ -> nil
887 end)
888
:-(
_ -> nil
889 end
890
891 # Reuse the same robust extraction pattern for other optional attributes
892
:-(
ref_url =
893 case parsed_data do
894 %{} = pd ->
895
:-(
Map.get(pd, :ref_url) || Map.get(pd, "ref_url") || Map.get(pd, :refUrl) || Map.get(pd, "refUrl") ||
896
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
897
:-(
%{} = txn -> Map.get(txn, :refUrl) || Map.get(txn, "refUrl") || Map.get(txn, :ref_url) || Map.get(txn, "ref_url")
898
:-(
_ -> nil
899
:-(
end) ||
900
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
901 xml when is_binary(xml) ->
902
:-(
case Regex.run(~r/refUrl=\"([^\"]+)\"/, xml) do
903
:-(
[_, v] -> v
904
:-(
_ -> nil
905 end
906
:-(
_ -> nil
907 end)
908
:-(
_ -> nil
909 end
910
911
:-(
note =
912 case parsed_data do
913 %{} = pd ->
914
:-(
Map.get(pd, :note) || Map.get(pd, "note") ||
915
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
916
:-(
%{} = txn -> Map.get(txn, :note) || Map.get(txn, "note")
917
:-(
_ -> nil
918
:-(
end) ||
919
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
920 xml when is_binary(xml) ->
921
:-(
case Regex.run(~r/note=\"([^\"]+)\"/, xml) do
922
:-(
[_, v] -> v
923
:-(
_ -> nil
924 end
925
:-(
_ -> nil
926 end)
927
:-(
_ -> nil
928 end
929
930
:-(
cust_ref =
931 case parsed_data do
932 %{} = pd ->
933
:-(
Map.get(pd, :cust_ref) || Map.get(pd, "cust_ref") || Map.get(pd, :custRef) || Map.get(pd, "custRef") ||
934
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
935
:-(
%{} = txn -> Map.get(txn, :custRef) || Map.get(txn, "custRef") || Map.get(txn, :cust_ref) || Map.get(txn, "cust_ref")
936
:-(
_ -> nil
937 end)
938
:-(
_ -> nil
939 end
940
941
:-(
initiation_mode =
942 case parsed_data do
943 %{} = pd ->
944
:-(
Map.get(pd, :initiation_mode) || Map.get(pd, "initiation_mode") || Map.get(pd, :initiationMode) || Map.get(pd, "initiationMode") ||
945
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
946
:-(
%{} = txn -> Map.get(txn, :initiationMode) || Map.get(txn, "initiationMode") || Map.get(txn, :initiation_mode) || Map.get(txn, "initiation_mode")
947
:-(
_ -> nil
948 end)
949
:-(
_ -> nil
950 end
951
952
:-(
purpose =
953 case parsed_data do
954 %{} = pd ->
955
:-(
Map.get(pd, :purpose) || Map.get(pd, "purpose") ||
956
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
957
:-(
%{} = txn -> Map.get(txn, :purpose) || Map.get(txn, "purpose")
958
:-(
_ -> nil
959
:-(
end) ||
960
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
961 xml when is_binary(xml) ->
962
:-(
case Regex.run(~r/purpose=\"([^\"]+)\"/, xml) do
963
:-(
[_, v] -> v
964
:-(
_ -> nil
965 end
966
:-(
_ -> nil
967 end)
968
:-(
_ -> nil
969 end
970
971
:-(
seq_num =
972 case parsed_data do
973 %{} = pd ->
974
:-(
Map.get(pd, :seq_num) || Map.get(pd, "seq_num") || Map.get(pd, :seqNum) || Map.get(pd, "seqNum") ||
975
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
976
:-(
%{} = txn -> Map.get(txn, :seqNum) || Map.get(txn, "seqNum") || Map.get(txn, :seq_num) || Map.get(txn, "seq_num")
977
:-(
_ -> nil
978
:-(
end) || UpiXmlSchema.generate_numeric_seq_num()
979
:-(
_ -> UpiXmlSchema.generate_numeric_seq_num()
980 end
981
982 # Update chktxn_requested_at and chk_ref_id (if ref_id is present)
983 # Use update_all to avoid loading the full schema struct.
984
:-(
if transaction do
985
:-(
try do
986
:-(
update_fields = [chktxn_requested_at: now]
987
988 # Add chk_ref_id to update if ref_id was extracted from the request
989
:-(
update_fields = if ref_id do
990 [{:chk_ref_id, ref_id} | update_fields]
991 else
992
:-(
update_fields
993 end
994
995
:-(
{count, _} =
996 Repo.update_all(
997
:-(
from(t in "transactions", where: field(t, :id) == ^(Map.get(transaction, :id) || transaction.id)),
998 set: update_fields
999 )
1000
1001
:-(
if count > 0 do
1002 # Defensive header access: parsed_data may be either a flat map with msg_id/txn_id keys
1003 # or a nested shape like %{header: %{...}} depending on the parser. Normalize to `header`.
1004
:-(
header = Map.get(parsed_data, :header) || Map.get(parsed_data, "header") || parsed_data || %{}
1005
1006
:-(
req_msg_id =
1007
:-(
Map.get(header, :msg_id) || Map.get(header, "msg_id") || Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id")
1008
1009
:-(
_ = DaProductApp.Transactions.Service.append_event((Map.get(transaction, :id) || transaction.id), :chktxn_received, %{
1010 ref_id: ref_id,
1011 req_msg_id: req_msg_id,
1012 requested_at: now
1013 })
1014
1015 # If we created a ReqChkTxn record, append a req_chk_txn event
1016 # (this will also dual-log into transaction_events).
1017
:-(
if inserted_req_chk_txn do
1018
:-(
try do
1019 # Append event for req_chk_txn; this will call TransactionEventChainService
1020 # and DaProductApp.Transactions.Service to dual-log into transaction_events.
1021 # Include full parsed payload in the event for rich audit trail
1022
:-(
case ReqChkTxnService.append_event(inserted_req_chk_txn, "received", Map.merge(%{ref_id: ref_id, req_msg_id: req_msg_id, requested_at: now}, parsed_data || %{})) do
1023
:-(
{:ok, _evt} -> :ok
1024
:-(
{:error, reason} -> Logger.warn("Failed to append req_chk_txn event: #{inspect(reason)}")
1025 end
1026 rescue
1027
:-(
e -> Logger.warn("Error appending ReqChkTxn event for txn=#{if transaction, do: (Map.get(transaction, :id) || transaction.id), else: "UNKNOWN"}: #{inspect(e)}")
1028 end
1029 end
1030 else
1031
:-(
Logger.warn("Failed to mark chktxn_requested_at for txn #{if transaction, do: (Map.get(transaction, :id) || transaction.id), else: "UNKNOWN"} (update returned 0 rows)")
1032 end
1033 rescue
1034
:-(
e -> Logger.warn("Error while updating transaction with ReqChkTxn metadata for org_txn_id=#{parsed_data.org_txn_id}, error: #{inspect(e)}")
1035 end
1036 else
1037
:-(
Logger.warn("Skipping DB update for chktxn_requested_at because transaction is nil")
1038 end
1039
1040
:-(
status =
1041 try do
1042
:-(
internal_status = if is_binary(Map.get(transaction, :status) || transaction.status), do: String.to_atom(Map.get(transaction, :status) || transaction.status), else: Map.get(transaction, :status) || transaction.status
1043
:-(
map_internal_status_to_upi(internal_status)
1044 rescue
1045
:-(
_ -> map_internal_status_to_upi(nil)
1046 end
1047
1048
:-(
response_data = %{
1049 org_id: get_psp_org_id(),
1050 msg_id: generate_message_id(),
1051
:-(
org_msg_id: Map.get(parsed_data, :org_msg_id) || Map.get(parsed_data, "org_msg_id") || Map.get(parsed_data, :orgMsgId) || Map.get(parsed_data, "orgMsgId") || "",
1052
:-(
req_msg_id: Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id" ) || "",
1053 result: "SUCCESS",
1054 err_code: "00",
1055
:-(
txn_id: Map.get(parsed_data, :txn_id) || Map.get(parsed_data, "txn_id") || "",
1056
:-(
org_txn_id: Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") || Map.get(parsed_data, :orgTxnId) || Map.get(parsed_data, "orgTxnId") || "",
1057 org_txn_date: (case parsed_data do
1058
:-(
%{} = pd -> Map.get(pd, :org_txn_date) || Map.get(pd, "org_txn_date") || Map.get(pd, :orgTxnDate) || Map.get(pd, "orgTxnDate") || (case Map.get(pd, :txn) || Map.get(pd, "txn") do
1059
:-(
%{} = txn -> Map.get(txn, :orgTxnDate) || Map.get(txn, "orgTxnDate") || Map.get(txn, :org_txn_date) || Map.get(txn, "org_txn_date")
1060
:-(
_ -> nil
1061 end)
1062
:-(
_ -> nil
1063
:-(
end) || Map.get(parsed_data, :org_txn_date) || Map.get(parsed_data, "org_txn_date") || "",
1064
:-(
ref_id: ref_id || "",
1065
:-(
ref_url: ref_url || "",
1066
:-(
note: note || "",
1067
:-(
cust_ref: cust_ref || "",
1068
:-(
initiation_mode: initiation_mode || "",
1069
:-(
seq_num: seq_num || UpiXmlSchema.generate_numeric_seq_num(),
1070
:-(
purpose: purpose || "",
1071 status: status,
1072
:-(
currency: Map.get(transaction, :currency) || "INR",
1073
:-(
amount: Map.get(transaction, :inr_amount) || "0.00",
1074
:-(
sub_type: Map.get(parsed_data, :sub_type) || Map.get(parsed_data, "sub_type") || Map.get(parsed_data, :subType) || Map.get(parsed_data, "subType") || ""
1075 }
1076
1077 # Log the response data being prepared for NPCI
1078
:-(
Logger.info("=== Preparing RespChkTxn for NPCI ===")
1079
:-(
Logger.info("Transaction ID: #{Map.get(transaction, :id) || transaction.id}")
1080
:-(
Logger.info("Org Txn ID: #{parsed_data.org_txn_id}")
1081
:-(
Logger.info("Request Msg ID: #{parsed_data.msg_id}")
1082
:-(
Logger.info("Response Msg ID: #{response_data.msg_id}")
1083
:-(
Logger.info("Transaction Status: #{status}")
1084
:-(
Logger.info("Currency: #{Map.get(transaction, :currency) || "INR"}")
1085
:-(
Logger.info("Amount: #{Map.get(transaction, :inr_amount) || "0.00"}")
1086
:-(
Logger.info("Response Data: #{inspect(response_data)}")
1087
1088
:-(
case UpiXmlSchema.generate_resp_chk_txn(response_data) do
1089 {:ok, xml_response} ->
1090
:-(
Logger.info("RespChkTxn XML generated successfully for org_txn_id: #{parsed_data.org_txn_id}")
1091
1092 # Send the response to NPCI endpoint
1093
:-(
send_resp_chk_txn_to_npci(xml_response, parsed_data)
1094
1095 {:ok, xml_response}
1096
1097 {:error, reason} = error ->
1098
:-(
Logger.error("Failed to generate RespChkTxn XML for org_txn_id: #{parsed_data.org_txn_id}, reason: #{reason}")
1099
:-(
error
1100 end
1101 end
1102
1103 tx ->
1104 # transaction was found earlier; proceed with the same handling as above
1105
:-(
transaction = tx
1106
1107 # Now create ReqChkTxn record with the proper transaction_id
1108
:-(
req_xml = Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") || check_data
1109
:-(
req_xml_hash = if is_binary(req_xml), do: :crypto.hash(:sha256, req_xml), else: nil
1110
1111
:-(
req_attrs = %{
1112
:-(
msg_id: Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id") || generate_message_id(),
1113
:-(
org_id: Map.get(parsed_data, :org_id) || Map.get(parsed_data, "org_id") || "NPCI",
1114
:-(
original_txn_id: Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") || Map.get(parsed_data, :orgTxnId) || Map.get(parsed_data, "orgTxnId"),
1115
:-(
transaction_id: (if transaction, do: Map.get(transaction, :id) || transaction.id, else: nil), # Set the required transaction_id defensively
1116 status: "PENDING",
1117 validation_type: "DOMESTIC",
1118 checked_at: DateTime.utc_now(),
1119 payload: parsed_data,
1120 req_xml_hash: req_xml_hash
1121 }
1122
1123
:-(
inserted_req_chk_txn =
1124 case ReqChkTxn.changeset(%ReqChkTxn{}, req_attrs) |> Repo.insert() do
1125
:-(
{:ok, rec} -> rec
1126 {:error, changeset} ->
1127
:-(
Logger.warn("Failed to persist ReqChkTxn pre-store: #{inspect(changeset.errors)}")
1128 nil
1129 end
1130
1131
:-(
now = DateTime.utc_now() |> DateTime.truncate(:second)
1132
1133 # Extract ref_id robustly from parsed_data (support multiple key formats and nested txn/header or raw XML)
1134
:-(
ref_id =
1135 case parsed_data do
1136 %{} = pd ->
1137
:-(
Map.get(pd, :ref_id) ||
1138
:-(
Map.get(pd, "ref_id") ||
1139
:-(
Map.get(pd, :refId) ||
1140
:-(
Map.get(pd, "refId") ||
1141
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1142 %{} = txn ->
1143
:-(
Map.get(txn, :refId) ||
1144
:-(
Map.get(txn, "refId") ||
1145
:-(
Map.get(txn, :ref_id) ||
1146
:-(
Map.get(txn, "ref_id")
1147
:-(
_ -> nil
1148
:-(
end) ||
1149
:-(
(case Map.get(pd, :header) || Map.get(pd, "header") do
1150 %{} = h ->
1151
:-(
Map.get(h, :refId) ||
1152
:-(
Map.get(h, "refId") ||
1153
:-(
Map.get(h, :ref_id) ||
1154
:-(
Map.get(h, "ref_id")
1155
:-(
_ -> nil
1156
:-(
end) ||
1157
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
1158 xml when is_binary(xml) ->
1159
:-(
case Regex.run(~r/refId="([^"]+)"/, xml) do
1160
:-(
[_, id] -> id
1161
:-(
_ -> nil
1162 end
1163
:-(
_ -> nil
1164 end)
1165
:-(
_ -> nil
1166 end
1167
1168 # Update chktxn_requested_at and chk_ref_id (if ref_id is present)
1169 # Use update_all to avoid loading the full schema struct.
1170
:-(
if transaction do
1171
:-(
try do
1172
:-(
update_fields = [chktxn_requested_at: now]
1173
1174 # Add chk_ref_id to update if ref_id was extracted from the request
1175
:-(
update_fields = if ref_id do
1176 [{:chk_ref_id, ref_id} | update_fields]
1177 else
1178
:-(
update_fields
1179 end
1180
1181
:-(
{count, _} =
1182 Repo.update_all(
1183
:-(
from(t in "transactions", where: field(t, :id) == ^(Map.get(transaction, :id) || transaction.id)),
1184 set: update_fields
1185 )
1186
1187
:-(
if count > 0 do
1188 # Defensive header access: parsed_data may be either a flat map with msg_id/txn_id keys
1189 # or a nested shape like %{header: %{...}} depending on the parser. Normalize to `header`.
1190
:-(
header = Map.get(parsed_data, :header) || Map.get(parsed_data, "header") || parsed_data || %{}
1191
1192
:-(
req_msg_id =
1193
:-(
Map.get(header, :msg_id) || Map.get(header, "msg_id") || Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id")
1194
1195
:-(
_ = DaProductApp.Transactions.Service.append_event((Map.get(transaction, :id) || transaction.id), :chktxn_received, %{
1196 ref_id: ref_id,
1197 req_msg_id: req_msg_id,
1198 requested_at: now
1199 })
1200
1201 # If we created a ReqChkTxn record, append a req_chk_txn event
1202 # (this will also dual-log into transaction_events).
1203
:-(
if inserted_req_chk_txn do
1204
:-(
try do
1205 # Append event for req_chk_txn; this will call TransactionEventChainService
1206 # and DaProductApp.Transactions.Service to dual-log into transaction_events.
1207
:-(
case ReqChkTxnService.append_event(inserted_req_chk_txn, "received", Map.merge(%{ref_id: ref_id, req_msg_id: req_msg_id, requested_at: now}, parsed_data || %{})) do
1208
:-(
{:ok, _evt} -> :ok
1209
:-(
{:error, reason} -> Logger.warn("Failed to append req_chk_txn event: #{inspect(reason)}")
1210 end
1211 rescue
1212
:-(
e -> Logger.warn("Error appending ReqChkTxn event for txn=#{if transaction, do: (Map.get(transaction, :id) || transaction.id), else: "UNKNOWN"}: #{inspect(e)}")
1213 end
1214 end
1215 else
1216
:-(
Logger.warn("Failed to mark chktxn_requested_at for txn #{if transaction, do: (Map.get(transaction, :id) || transaction.id), else: "UNKNOWN"} (update returned 0 rows)")
1217 end
1218 rescue
1219
:-(
e -> Logger.warn("Error while updating transaction with ReqChkTxn metadata for org_txn_id=#{parsed_data.org_txn_id}, error: #{inspect(e)}")
1220 end
1221 else
1222
:-(
Logger.warn("Skipping DB update for chktxn_requested_at because transaction is nil")
1223 end
1224
1225
:-(
status =
1226 try do
1227
:-(
internal_status = if is_binary(Map.get(transaction, :status) || transaction.status), do: String.to_atom(Map.get(transaction, :status) || transaction.status), else: Map.get(transaction, :status) || transaction.status
1228
:-(
map_internal_status_to_upi(internal_status)
1229 rescue
1230
:-(
_ -> map_internal_status_to_upi(nil)
1231 end
1232
1233 # Extract commonly used fields robustly (flat keys, nested txn/header, or raw_xml)
1234
:-(
ref_id =
1235 case parsed_data do
1236 %{} = pd ->
1237
:-(
Map.get(pd, :ref_id) || Map.get(pd, "ref_id") || Map.get(pd, :refId) || Map.get(pd, "refId") ||
1238
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1239
:-(
%{} = txn -> Map.get(txn, :refId) || Map.get(txn, "refId") || Map.get(txn, :ref_id) || Map.get(txn, "ref_id")
1240
:-(
_ -> nil
1241
:-(
end) ||
1242
:-(
(case Map.get(pd, :header) || Map.get(pd, "header") do
1243
:-(
%{} = h -> Map.get(h, :refId) || Map.get(h, "refId") || Map.get(h, :ref_id) || Map.get(h, "ref_id")
1244
:-(
_ -> nil
1245
:-(
end) ||
1246
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
1247 xml when is_binary(xml) ->
1248
:-(
case Regex.run(~r/refId=\"([^\"]+)\"/, xml) do
1249
:-(
[_, id] -> id
1250
:-(
_ -> nil
1251 end
1252
:-(
_ -> nil
1253 end)
1254
:-(
_ -> nil
1255 end
1256
1257
:-(
ref_url =
1258 case parsed_data do
1259 %{} = pd ->
1260
:-(
Map.get(pd, :ref_url) || Map.get(pd, "ref_url") || Map.get(pd, :refUrl) || Map.get(pd, "refUrl") ||
1261
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1262
:-(
%{} = txn -> Map.get(txn, :refUrl) || Map.get(txn, "refUrl") || Map.get(txn, :ref_url) || Map.get(txn, "ref_url")
1263
:-(
_ -> nil
1264
:-(
end) ||
1265
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
1266 xml when is_binary(xml) ->
1267
:-(
case Regex.run(~r/refUrl=\"([^\"]+)\"/, xml) do
1268
:-(
[_, v] -> v
1269
:-(
_ -> nil
1270 end
1271
:-(
_ -> nil
1272 end)
1273
:-(
_ -> nil
1274 end
1275
1276
:-(
note =
1277 case parsed_data do
1278 %{} = pd ->
1279
:-(
Map.get(pd, :note) || Map.get(pd, "note") ||
1280
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1281
:-(
%{} = txn -> Map.get(txn, :note) || Map.get(txn, "note")
1282
:-(
_ -> nil
1283
:-(
end) ||
1284
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
1285 xml when is_binary(xml) ->
1286
:-(
case Regex.run(~r/note=\"([^\"]+)\"/, xml) do
1287
:-(
[_, v] -> v
1288
:-(
_ -> nil
1289 end
1290
:-(
_ -> nil
1291 end)
1292
:-(
_ -> nil
1293 end
1294
1295
:-(
cust_ref =
1296 case parsed_data do
1297 %{} = pd ->
1298
:-(
Map.get(pd, :cust_ref) || Map.get(pd, "cust_ref") || Map.get(pd, :custRef) || Map.get(pd, "custRef") ||
1299
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1300
:-(
%{} = txn -> Map.get(txn, :custRef) || Map.get(txn, "custRef") || Map.get(txn, :cust_ref) || Map.get(txn, "cust_ref")
1301
:-(
_ -> nil
1302 end)
1303
:-(
_ -> nil
1304 end
1305
1306
:-(
initiation_mode =
1307 case parsed_data do
1308 %{} = pd ->
1309
:-(
Map.get(pd, :initiation_mode) || Map.get(pd, "initiation_mode") || Map.get(pd, :initiationMode) || Map.get(pd, "initiationMode") ||
1310
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1311
:-(
%{} = txn -> Map.get(txn, :initiationMode) || Map.get(txn, "initiationMode") || Map.get(txn, :initiation_mode) || Map.get(txn, "initiation_mode")
1312
:-(
_ -> nil
1313 end)
1314
:-(
_ -> nil
1315 end
1316
1317
:-(
purpose =
1318 case parsed_data do
1319 %{} = pd ->
1320
:-(
Map.get(pd, :purpose) || Map.get(pd, "purpose") ||
1321
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1322
:-(
%{} = txn -> Map.get(txn, :purpose) || Map.get(txn, "purpose")
1323
:-(
_ -> nil
1324
:-(
end) ||
1325
:-(
(case Map.get(pd, :raw_xml) || Map.get(pd, "raw_xml") do
1326 xml when is_binary(xml) ->
1327
:-(
case Regex.run(~r/purpose=\"([^\"]+)\"/, xml) do
1328
:-(
[_, v] -> v
1329
:-(
_ -> nil
1330 end
1331
:-(
_ -> nil
1332 end)
1333
:-(
_ -> nil
1334 end
1335
1336
:-(
seq_num =
1337 case parsed_data do
1338 %{} = pd ->
1339
:-(
Map.get(pd, :seq_num) || Map.get(pd, "seq_num") || Map.get(pd, :seqNum) || Map.get(pd, "seqNum") ||
1340
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1341
:-(
%{} = txn -> Map.get(txn, :seqNum) || Map.get(txn, "seqNum") || Map.get(txn, :seq_num) || Map.get(txn, "seq_num")
1342
:-(
_ -> nil
1343
:-(
end) || UpiXmlSchema.generate_numeric_seq_num()
1344
:-(
_ -> UpiXmlSchema.generate_numeric_seq_num()
1345 end
1346
1347
:-(
org_txn_date =
1348 case parsed_data do
1349 %{} = pd ->
1350
:-(
Map.get(pd, :org_txn_date) || Map.get(pd, "org_txn_date") || Map.get(pd, :orgTxnDate) || Map.get(pd, "orgTxnDate") ||
1351
:-(
(case Map.get(pd, :txn) || Map.get(pd, "txn") do
1352
:-(
%{} = txn -> Map.get(txn, :orgTxnDate) || Map.get(txn, "orgTxnDate") || Map.get(txn, :org_txn_date) || Map.get(txn, "org_txn_date")
1353
:-(
_ -> nil
1354 end)
1355
:-(
_ -> nil
1356 end
1357
1358
:-(
response_data = %{
1359 org_id: get_psp_org_id(),
1360 msg_id: generate_message_id(),
1361
:-(
org_msg_id: Map.get(parsed_data, :org_msg_id) || Map.get(parsed_data, "org_msg_id") || Map.get(parsed_data, :orgMsgId) || Map.get(parsed_data, "orgMsgId") || "",
1362
:-(
req_msg_id: Map.get(parsed_data, :msg_id) || Map.get(parsed_data, "msg_id") || "",
1363 result: "SUCCESS",
1364 err_code: "00",
1365
:-(
txn_id: Map.get(parsed_data, :txn_id) || Map.get(parsed_data, "txn_id") || "",
1366
:-(
org_txn_id: Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") || Map.get(parsed_data, :orgTxnId) || Map.get(parsed_data, "orgTxnId") || "",
1367
:-(
org_txn_date: org_txn_date || "",
1368
:-(
ref_id: ref_id || "",
1369
:-(
ref_url: ref_url || "",
1370
:-(
note: note || "",
1371
:-(
cust_ref: cust_ref || "",
1372
:-(
initiation_mode: initiation_mode || "",
1373
:-(
seq_num: seq_num || UpiXmlSchema.generate_numeric_seq_num(),
1374
:-(
purpose: purpose || "",
1375 status: status,
1376
:-(
currency: Map.get(transaction, :currency) || "INR",
1377
:-(
amount: Map.get(transaction, :inr_amount) || "0.00"
1378 }
1379
1380 # Log the response data being prepared for NPCI
1381
:-(
Logger.info("=== Preparing RespChkTxn for NPCI ===")
1382
:-(
Logger.info("Transaction ID: #{Map.get(transaction, :id) || transaction.id}")
1383
:-(
Logger.info("Org Txn ID: #{parsed_data.org_txn_id}")
1384
:-(
Logger.info("Request Msg ID: #{parsed_data.msg_id}")
1385
:-(
Logger.info("Response Msg ID: #{response_data.msg_id}")
1386
:-(
Logger.info("Transaction Status: #{status}")
1387
:-(
Logger.info("Currency: #{Map.get(transaction, :currency) || "INR"}")
1388
:-(
Logger.info("Amount: #{Map.get(transaction, :inr_amount) || "0.00"}")
1389
:-(
Logger.info("Response Data: #{inspect(response_data)}")
1390
1391
:-(
case UpiXmlSchema.generate_resp_chk_txn(response_data) do
1392 {:ok, xml_response} ->
1393
:-(
Logger.info("RespChkTxn XML generated successfully for org_txn_id: #{parsed_data.org_txn_id}")
1394
1395 # Send the response to NPCI endpoint
1396
:-(
send_resp_chk_txn_to_npci(xml_response, parsed_data)
1397
1398 {:ok, xml_response}
1399
1400 {:error, reason} = error ->
1401
:-(
Logger.error("Failed to generate RespChkTxn XML for org_txn_id: #{parsed_data.org_txn_id}, reason: #{reason}")
1402
:-(
error
1403 end
1404 end
1405
1406 {:error, reason} ->
1407 # When parsing fails, we need a default parsed_data structure
1408
:-(
Logger.error("=== ReqChkTxn Parsing Failed ===")
1409
:-(
Logger.error("Parse Error: #{reason}")
1410
:-(
Logger.error("Generating error RespChkTxn response")
1411
1412
:-(
default_parsed_data = %{msg_id: generate_message_id(), org_txn_id: "UNKNOWN"}
1413
1414
:-(
case generate_status_error_response(default_parsed_data, "96", "System malfunction: #{reason}") do
1415 {:ok, error_xml} ->
1416
:-(
Logger.info("=== Error RespChkTxn Response ===")
1417
:-(
Logger.info("Error XML Response: #{error_xml}")
1418
1419 # Send error response to NPCI
1420
:-(
send_resp_chk_txn_to_npci(error_xml, default_parsed_data)
1421
1422 {:ok, error_xml}
1423
1424 {:error, err} = error_result ->
1425
:-(
Logger.error("Failed to generate error response: #{err}")
1426
:-(
error_result
1427 end
1428 end
1429 end
1430
1431 @doc """
1432 Process heartbeat request (Complete 4-step flow)
1433 Step 1: Receive ReqHbt from NPCI
1434 Step 2: Send Ack to NPCI
1435 Step 3: Send RespHbt to NPCI
1436 Step 4: Receive Ack from NPCI
1437 """
1438 def process_heartbeat(heartbeat_data) do
1439 require Logger
1440
:-(
Logger.info("Processing heartbeat request")
1441
1442
:-(
with {:ok, parsed_data} <- UpiXmlSchema.parse_req_hbt(heartbeat_data) do
1443 # Extract reqMsgId and txnId
1444
:-(
req_msg_id = parsed_data.msg_id || "UNKNOWN"
1445
:-(
req_txn_id = parsed_data.txn_id || "UNKNOWN"
1446
:-(
Logger.info("Processing heartbeat with request message ID: #{req_msg_id} and txn_id: #{req_txn_id}")
1447
1448 # Build the full NPCI endpoint URL with the txn_id
1449
:-(
base_endpoint = Application.get_env(:da_product_app, :npci_heartbeat_endpoint)
1450
:-(
npci_endpoint = base_endpoint <> req_txn_id
1451
:-(
Logger.info("NPCI endpoint being used: #{npci_endpoint}")
1452
1453 # Log extracted txnid for clarity
1454
:-(
path_info = extract_npci_txnid(npci_endpoint)
1455
:-(
if path_info.txnid == "" do
1456
:-(
Logger.warn("No txnid found in NPCI endpoint path: #{npci_endpoint}")
1457 else
1458
:-(
Logger.info("RespHbt txnid being sent: #{path_info.txnid}")
1459 end
1460
1461 # Step 2: Generate immediate Ack response
1462
:-(
ack_response_data = %{
1463 org_id: get_psp_org_id(),
1464 msg_id: generate_message_id(),
1465 req_msg_id: req_msg_id
1466 }
1467
1468 # Start async process for steps 3 and 4
1469
:-(
spawn(fn ->
1470
:-(
process_heartbeat_response_async(parsed_data)
1471 end)
1472
1473 # Return immediate Ack (Step 2)
1474
:-(
UpiXmlSchema.generate_resp_hbt(ack_response_data)
1475 else
1476
:-(
{:error, reason} ->
1477
:-(
{:error, "Heartbeat processing failed: #{reason}"}
1478 end
1479 end
1480
1481 @doc """
1482 Send RespHbt to NPCI and handle Ack response (Steps 3 & 4)
1483 This runs asynchronously after sending the initial Ack
1484 """
1485 def process_heartbeat_response_async(parsed_req_data) do
1486 require Logger
1487
:-(
Logger.info("Starting async heartbeat response process (Steps 3 & 4)")
1488
1489 # Step 3: Prepare RespHbt request to send to NPCI
1490
:-(
resp_hbt_data = %{
1491 msg_id: generate_message_id(),
1492 org_id: get_psp_org_id(),
1493
:-(
req_msg_id: parsed_req_data.msg_id,
1494
:-(
txn_id: parsed_req_data.txn_id,
1495
:-(
cust_ref: parsed_req_data.cust_ref || generate_customer_reference(),
1496
:-(
ref_id: parsed_req_data.ref_id || generate_reference_id(),
1497
:-(
ref_url: parsed_req_data.ref_url || get_psp_ref_url()
1498 }
1499
1500
:-(
case UpiXmlSchema.generate_resp_hbt_request(resp_hbt_data) do
1501 {:ok, resp_hbt_xml} ->
1502
:-(
Logger.info("Generated RespHbt XML for NPCI")
1503
:-(
Logger.debug("RespHbt XML: #{resp_hbt_xml}")
1504
1505 # Step 3: Send RespHbt to NPCI
1506
:-(
case send_resp_hbt_to_npci(resp_hbt_xml, resp_hbt_data.msg_id,resp_hbt_data.txn_id) do
1507 {:ok, ack_response} ->
1508
:-(
Logger.info("Successfully completed heartbeat flow - received Ack from NPCI")
1509
:-(
Logger.debug("NPCI Ack response: #{ack_response}")
1510
1511 # Step 4: Process the Ack response from NPCI
1512
:-(
case UpiXmlSchema.parse_ack_response(ack_response) do
1513 {:ok, ack_data} ->
1514
:-(
Logger.info("Heartbeat flow completed successfully - Steps 1-4 done")
1515
:-(
record_heartbeat_success(resp_hbt_data.msg_id, ack_data)
1516
1517 {:error, reason} ->
1518
:-(
Logger.error("Failed to parse NPCI Ack response: #{reason}")
1519
:-(
record_heartbeat_error(resp_hbt_data.msg_id, "Ack parsing failed: #{reason}")
1520 end
1521
1522 {:error, reason} ->
1523
:-(
Logger.error("Failed to send RespHbt to NPCI: #{reason}")
1524
:-(
record_heartbeat_error(resp_hbt_data.msg_id, "RespHbt send failed: #{reason}")
1525 end
1526
1527 {:error, reason} ->
1528
:-(
Logger.error("Failed to generate RespHbt XML: #{reason}")
1529 end
1530 end
1531
1532 @doc """
1533 Send RespHbt XML to NPCI endpoint
1534 """
1535 defp send_resp_hbt_to_npci(xml_content, msg_id,txn_id) do
1536 require Logger
1537
1538 # Get NPCI endpoint from configuration
1539
:-(
npci_endpoint = get_npci_heartbeat_endpoint()
1540
1541
:-(
headers = [
1542 {"content-type", "application/xml"},
1543 {"accept", "application/xml"},
1544 {"x-message-id", msg_id}
1545 ]
1546
1547
1548
:-(
path_info = extract_npci_txnid(npci_endpoint)
1549
:-(
Logger.info("Sending RespHbt to NPCI endpoint: #{path_info.full_path}")
1550 # https://precert.nfinite.in/iupi/RespHbt/2.0/urn:txnid:MER739c95e2edd2431f9c43db5cd5780d6d
1551 # add txn_id into my npci_endpoint where current https://precert.nfinite.in/iupi/RespHbt/2.0/urn:txnid:
1552 # Ensure the endpoint includes the txn_id after "urn:txnid:"
1553
:-(
npci_endpoint =
1554 if String.ends_with?(npci_endpoint, "urn:txnid:") do
1555
:-(
npci_endpoint <> (txn_id || "")
1556 else
1557
:-(
npci_endpoint
1558 end
1559
:-(
Logger.info("RespHbt txnid being sent: #{npci_endpoint}")
1560
1561
:-(
case Req.post(npci_endpoint, body: xml_content, headers: headers, receive_timeout: 30_000) do
1562 {:ok, %Req.Response{status: 200, body: body}} ->
1563
:-(
Logger.info("Successfully sent RespHbt to NPCI, received response")
1564 {:ok, body}
1565
1566 {:ok, %Req.Response{status: status_code, body: body}} ->
1567
:-(
Logger.error("NPCI returned non-200 status: #{status_code}, body: #{body}")
1568
:-(
{:error, "NPCI returned status #{status_code}: #{body}"}
1569
1570 {:error, reason} ->
1571
:-(
Logger.error("HTTP error sending RespHbt to NPCI: #{inspect(reason)}")
1572 {:error, "HTTP error: #{inspect(reason)}"}
1573 end
1574 end
1575
1576 # Send RespChkTxn to NPCI endpoint
1577 defp send_resp_chk_txn_to_npci(xml_content, parsed_data) do
1578 require Logger
1579
1580 # Prefer the dedicated RespChkTxn endpoint (not the ReqChkTxn endpoint)
1581
:-(
npci_base_endpoint = Application.get_env(:da_product_app, :npci_respchktxn_endpoint, "https://precert.nfinite.in/iupi/RespChkTxn/2.0/urn:txnid:")
1582
1583 # Extract org_txn_id or appropriate txn ID for the endpoint URL
1584
:-(
txn_id = Map.get(parsed_data, :org_txn_id) || Map.get(parsed_data, "org_txn_id") ||
1585
:-(
Map.get(parsed_data, :txn_id) || Map.get(parsed_data, "txn_id") || "UNKNOWN"
1586
1587 # Build the complete NPCI endpoint URL (append txn id when configured as urn:txnid:)
1588
:-(
npci_endpoint = if String.ends_with?(npci_base_endpoint, "urn:txnid:") do
1589
:-(
npci_base_endpoint <> txn_id
1590 else
1591
:-(
npci_base_endpoint
1592 end
1593
1594
:-(
headers = [
1595 {"Content-Type", "application/xml; charset=UTF-8"},
1596 {"Accept", "application/xml"},
1597 {"User-Agent", "Mercury-UPI-PSP/1.0"}
1598 ]
1599
1600
:-(
Logger.info("=== Sending RespChkTxn to NPCI ===")
1601
:-(
Logger.info("NPCI Endpoint: #{npci_endpoint}")
1602
:-(
Logger.info("Transaction ID: #{txn_id}")
1603
:-(
Logger.info("Request Headers: #{inspect(headers)}")
1604
:-(
Logger.info("RespChkTxn XML Content:")
1605
:-(
Logger.info(xml_content)
1606
1607 # Send the request to NPCI
1608
:-(
case Req.post(npci_endpoint, body: xml_content, headers: headers, receive_timeout: 30_000) do
1609 {:ok, %Req.Response{status: 200, body: body}} ->
1610
:-(
Logger.info("✅ Successfully sent RespChkTxn to NPCI")
1611
:-(
Logger.info("NPCI Response Body: #{inspect(body)}")
1612 {:ok, body}
1613
1614 {:ok, %Req.Response{status: status_code, body: body}} ->
1615
:-(
Logger.error("❌ NPCI returned non-200 status: #{status_code}")
1616
:-(
Logger.error("NPCI Error Response Body: #{inspect(body)}")
1617
:-(
{:error, "NPCI returned status #{status_code}: #{body}"}
1618
1619 {:error, reason} ->
1620
:-(
Logger.error("❌ HTTP error sending RespChkTxn to NPCI: #{inspect(reason)}")
1621 {:error, "HTTP error: #{inspect(reason)}"}
1622 end
1623 end
1624
1625 defp extract_npci_txnid(npci_endpoint) when is_binary(npci_endpoint) do
1626 # Extracts txnid from the last segment if present as urn:txnid:...
1627
:-(
uri = URI.parse(npci_endpoint)
1628
:-(
path_segments = String.split(uri.path || "", "/", trim: true)
1629
:-(
txnid =
1630 path_segments
1631
:-(
|> Enum.find(fn seg -> String.starts_with?(seg, "urn:txnid:") end)
1632 |> case do
1633
:-(
nil -> ""
1634
:-(
"urn:txnid:" <> id -> id
1635
:-(
_ -> ""
1636 end
1637
1638
:-(
%{
1639
:-(
full_path: "#{uri.scheme}://#{uri.host}#{uri.path}",
1640 txnid: txnid
1641 }
1642 end
1643 # Add this helper function to format error response maps
1644 defp format_error_response(error_map) when is_map(error_map) do
1645 case error_map do
1646 %{"detail" => detail} when is_binary(detail) -> detail
1647 %{"message" => message} when is_binary(message) -> message
1648 _ -> Jason.encode!(error_map)
1649 end
1650 rescue
1651 _ -> "Unknown error format: #{inspect(error_map)}"
1652 end
1653
1654
1655
1656 # Helper functions for heartbeat flow
1657
1658 defp get_npci_heartbeat_endpoint do
1659 # In production, this should come from configuration
1660 # For now, we'll use a configurable value
1661
:-(
Application.get_env(:da_product_app, :npci_heartbeat_endpoint, "https://npci-uat.in/upi/heartbeat")
1662 end
1663
1664 defp get_npci_reqchktxn_endpoint do
1665 # Get the configured NPCI ReqChkTxn endpoint
1666 Application.get_env(:da_product_app, :npci_reqchktxn_endpoint, "https://precert.nfinite.in/iupi/ReqChkTxn/2.0/urn:txnid:")
1667 end
1668
1669 defp get_psp_ref_url do
1670
:-(
Application.get_env(:da_product_app, :psp_ref_url, "https://mercury.upi.in")
1671 end
1672
1673 defp generate_customer_reference do
1674
:-(
"MERC" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
1675 end
1676
1677 defp generate_reference_id do
1678
:-(
timestamp = System.system_time(:millisecond)
1679
:-(
"REF#{timestamp}"
1680 end
1681
1682 defp generate_transaction_id do
1683
:-(
"MER" <> (:crypto.strong_rand_bytes(16) |> Base.encode16())
1684 end
1685
1686 defp record_heartbeat_success(msg_id, ack_data) do
1687
:-(
Logger.info("Recording heartbeat success: #{msg_id}")
1688 # Here you could store the successful heartbeat in database for monitoring
1689 # For example: HeartbeatLog.create(%{msg_id: msg_id, status: "SUCCESS", ack_data: ack_data})
1690 end
1691
1692 defp record_heartbeat_error(msg_id, error_reason) do
1693
:-(
Logger.error("Recording heartbeat error: #{msg_id} - #{error_reason}")
1694 # Here you could store the failed heartbeat in database for monitoring
1695 # For example: HeartbeatLog.create(%{msg_id: msg_id, status: "ERROR", error: error_reason})
1696 end
1697
1698 @doc """
1699 Process reversal request (for timeout scenarios)
1700 """
1701 def process_reversal_request(reversal_data) do
1702
:-(
case UpiXmlSchema.parse_req_pay(reversal_data) do
1703 {:ok, parsed_data} ->
1704
:-(
case get_transaction_by_org_id(parsed_data.org_txn_id) do
1705 nil ->
1706
:-(
generate_payment_error_response(parsed_data, "05", "Original transaction not found")
1707
1708 original_txn ->
1709
:-(
case reverse_with_partner(original_txn) do
1710 {:ok, reversal_response} ->
1711 # Update transaction status
1712
:-(
update_transaction_status(original_txn.id, :reversed, reversal_response)
1713
1714
:-(
response_data = %{
1715 org_id: get_psp_org_id(),
1716 msg_id: generate_message_id(),
1717
:-(
req_msg_id: parsed_data.msg_id,
1718 result: "SUCCESS",
1719 err_code: "00",
1720 txn_id: generate_transaction_id(),
1721
:-(
cust_ref: parsed_data.cust_ref,
1722 txn_type: "REVERSAL",
1723 add_info: "Transaction reversed successfully"
1724 }
1725
1726
:-(
UpiXmlSchema.generate_resp_pay(response_data)
1727
1728 {:error, :reversal_failed} ->
1729
:-(
generate_payment_error_response(parsed_data, "20", "Credit reversal timeout")
1730
1731 {:error, reason} ->
1732
:-(
generate_payment_error_response(parsed_data, "02", "Reversal failed: #{reason}")
1733 end
1734 end
1735
1736 {:error, reason} ->
1737 # When parsing fails, we need a default parsed_data structure
1738
:-(
default_parsed_data = %{msg_id: generate_message_id(), cust_ref: "", org_txn_id: "UNKNOWN"}
1739
:-(
generate_payment_error_response(default_parsed_data, "ZH", "Invalid XML: #{reason}")
1740 end
1741 end
1742
1743 # Private helper functions
1744
1745
:-(
defp validate_merchant_qr(qr_string, initiation_mode \\ nil) do
1746 # Decode QR string and validate merchant
1747
:-(
case decode_upi_qr(qr_string) do
1748 {:ok, qr_data} ->
1749 # Use initiation_mode from XML if provided, otherwise from QR payload
1750
:-(
effective_mode = initiation_mode || qr_data.mode
1751
1752 # If a terminal id (mtid) is present in the QR, prefer a TID-first lookup
1753 # and return a specific TID_MISMATCH error if TID is present but not found.
1754
:-(
case Map.get(qr_data, :terminal_id) || Map.get(qr_data, :mtid) do
1755 tid when is_binary(tid) and tid != "" ->
1756
:-(
Logger.info("Terminal ID present in QR, attempting TID-first lookup: #{tid}")
1757
:-(
case DaProductApp.Merchants.get_merchant_by_tid(tid) do
1758 %DaProductApp.Merchants.Merchant{} = merchant ->
1759 # Found merchant by TID - build merchant info preserving QR
1760
:-(
get_merchant_info_with_qr_data(merchant.mid || merchant.merchant_vpa, qr_data)
1761
:-(
nil ->
1762 # Terminal id provided in QR but no matching merchant found
1763 {:error, "TID_MISMATCH"}
1764 end
1765
1766 _ ->
1767 # For dynamic QRs (mode 16), use MSID for lookup
1768 # For static QRs (mode 01), use MID for lookup
1769
:-(
case effective_mode do
1770 "16" when not is_nil(qr_data.merchant_store_id) ->
1771 # Dynamic QR - lookup by MSID (Store ID)
1772
:-(
Logger.info("Dynamic QR detected (mode 16), looking up merchant by MSID: #{qr_data.merchant_store_id}")
1773
:-(
case get_merchant_info_by_msid_with_qr_data(qr_data.merchant_store_id, qr_data) do
1774
:-(
{:ok, merchant_info} -> {:ok, merchant_info}
1775
:-(
{:error, reason} -> {:error, reason}
1776 end
1777
1778 _ ->
1779 # Static QR or fallback - lookup by MID
1780
:-(
Logger.info("Static QR or fallback (mode #{effective_mode}), looking up merchant by MID: #{qr_data.merchant_id}")
1781
:-(
case get_merchant_info_with_qr_data(qr_data.merchant_id, qr_data) do
1782
:-(
{:ok, merchant_info} -> {:ok, merchant_info}
1783
:-(
{:error, reason} -> {:error, reason}
1784 end
1785 end
1786 end
1787
1788
:-(
{:error, reason} ->
1789 {:error, reason}
1790 end
1791 end
1792
1793 defp decode_upi_qr(qr_string) do
1794
:-(
try do
1795 # Extract UPI payment address (VPA)
1796
:-(
upi_id =
1797 case Regex.run(~r/(?:[?&]|^)pa=([^&]+)/, qr_string) do
1798
:-(
[_, pa] -> URI.decode(pa) # Decode URL encoding
1799
:-(
_ -> nil
1800 end
1801
1802 # Extract merchant ID from mid parameter (for upiGlobal format)
1803
:-(
merchant_id =
1804 case Regex.run(~r/(?:[?&]|^)mid=([^&]+)/, qr_string) do
1805
:-(
[_, mid] -> mid
1806 _ ->
1807 # Fallback to using VPA for lookup if mid not found
1808
:-(
upi_id
1809 end
1810
1811 # Extract merchant store ID (MSID) for dynamic QR lookup
1812
:-(
merchant_store_id =
1813 case Regex.run(~r/(?:[?&]|^)msid=([^&]+)/, qr_string) do
1814
:-(
[_, msid] -> msid
1815
:-(
_ -> nil
1816 end
1817
1818 # Extract terminal id (mtid) if present - used for TID validation
1819
:-(
terminal_id =
1820 case Regex.run(~r/(?:[?&]|^)mtid=([^&]+)/, qr_string) do
1821
:-(
[_, mtid] -> URI.decode(mtid)
1822
:-(
_ -> nil
1823 end
1824
1825 # Extract mode/initiation mode to determine QR type
1826
:-(
qr_mode =
1827 case Regex.run(~r/(?:[?&]|^)mode=([^&]+)/, qr_string) do
1828
:-(
[_, mode] -> mode
1829
:-(
_ -> "01" # Default to static QR
1830 end
1831
1832
:-(
IO.inspect(merchant_id, label: "Extracted MID for merchant lookup")
1833
:-(
IO.inspect(merchant_store_id, label: "Extracted MSID for dynamic QR lookup")
1834
:-(
IO.inspect(terminal_id, label: "Extracted MTID (terminal id) from QR")
1835
:-(
IO.inspect(qr_mode, label: "Extracted QR mode")
1836
:-(
IO.inspect(upi_id, label: "Extracted VPA")
1837
1838
:-(
amount =
1839 case Regex.run(~r/(?:[?&]|^)am=([0-9.]+)/, qr_string) do
1840
:-(
[_, am] -> am
1841
:-(
_ -> nil
1842 end
1843
1844
:-(
merchant_category =
1845 case Regex.run(~r/(?:[?&]|^)mc=([^&]+)/, qr_string) do
1846
:-(
[_, mc] -> mc
1847
:-(
_ -> "5411" # Default merchant category
1848 end
1849
1850
:-(
if merchant_id do
1851 {:ok, %{
1852 merchant_id: merchant_id,
1853 merchant_store_id: merchant_store_id,
1854 terminal_id: terminal_id,
1855 mode: qr_mode, # Use 'mode' instead of 'qr_mode' for consistency
1856 pa: upi_id, # Use 'pa' for payment address
1857 msid: merchant_store_id, # Include msid directly
1858 qr_mode: qr_mode,
1859 upi_id: upi_id,
1860 amount: amount,
1861 merchant_category: merchant_category
1862 }}
1863 else
1864 {:error, "MID not found in QR"}
1865 end
1866 rescue
1867
:-(
_ -> {:error, "Invalid QR format"}
1868 end
1869 end
1870
1871 @doc """
1872 Comprehensive merchant validation for creditworthiness before attempting any credit operation.
1873 This implements proper UPI PSP flow:
1874 - Step 1: Validate merchant details, account status, currency
1875 - Step 2: Check limits, FX rules, AML/compliance
1876
1877 Returns {:ok, enhanced_merchant_info} or {:error, {error_code, error_message}}
1878 """
1879 defp validate_merchant_creditworthiness(parsed_data, merchant_info) do
1880
:-(
Logger.info("[Creditworthiness] Starting comprehensive merchant validation for #{merchant_info.merchant_name}")
1881
1882
:-(
with {:ok, verified_merchant} <- verify_merchant_exists_and_active(parsed_data, merchant_info),
1883
:-(
{:ok, _} <- validate_settlement_account(verified_merchant, parsed_data),
1884
:-(
{:ok, _} <- check_merchant_limits(verified_merchant, parsed_data),
1885
:-(
{:ok, _} <- validate_fx_and_currency(verified_merchant, parsed_data),
1886
:-(
{:ok, _} <- check_aml_compliance(verified_merchant, parsed_data) do
1887
1888
:-(
Logger.info("[Creditworthiness] All validations passed for merchant #{verified_merchant.mid}")
1889 {:ok, verified_merchant}
1890 else
1891 {:error, {code, msg}} ->
1892
:-(
Logger.error("[Creditworthiness] Validation failed: #{msg}")
1893 {:error, {code, msg}}
1894 end
1895 end
1896
1897 # Step 1a: Verify merchant exists in database and is active
1898 defp verify_merchant_exists_and_active(parsed_data, merchant_info) do
1899 # First try to get detailed merchant record from database
1900
:-(
merchant_id = merchant_info[:merchant_id] || parsed_data[:mid] || parsed_data[:payee_addr]
1901
1902
:-(
case DaProductApp.Merchants.get_merchant_by_mid(merchant_id) do
1903 %DaProductApp.Merchants.Merchant{} = merchant ->
1904
:-(
if merchant.status == "ACTIVE" do
1905
:-(
enhanced_info = Map.merge(merchant_info, %{
1906
:-(
merchant_id: merchant.mid,
1907
:-(
merchant_name: merchant.brand_name || merchant.legal_name,
1908
:-(
ifsc_code: merchant.settlement_account_ifsc,
1909
:-(
account_number: merchant.settlement_account_number,
1910
:-(
status: merchant.status,
1911
:-(
merchant_type: merchant.merchant_type || "ENTITY",
1912 db_merchant: merchant # Store full merchant record
1913 })
1914 {:ok, enhanced_info}
1915 else
1916 {:error, {"X7", "Merchant account inactive"}}
1917 end
1918
1919 nil ->
1920 # Try VPA lookup as fallback
1921
:-(
case DaProductApp.Merchants.get_merchant_by_vpa(parsed_data[:payee_addr]) do
1922 %DaProductApp.Merchants.Merchant{} = merchant ->
1923
:-(
if merchant.status == "ACTIVE" do
1924
:-(
enhanced_info = Map.merge(merchant_info, %{
1925
:-(
merchant_id: merchant.mid,
1926
:-(
merchant_name: merchant.brand_name || merchant.legal_name,
1927
:-(
ifsc_code: merchant.settlement_account_ifsc,
1928
:-(
account_number: merchant.settlement_account_number,
1929
:-(
status: merchant.status,
1930 db_merchant: merchant
1931 })
1932 {:ok, enhanced_info}
1933 else
1934 {:error, {"X7", "Merchant account inactive"}}
1935 end
1936
1937
:-(
nil ->
1938 {:error, {"X7", "Merchant not found in database"}}
1939 end
1940 end
1941 end
1942
1943 # Step 1b: Validate settlement account details
1944 defp validate_settlement_account(merchant_info, parsed_data) do
1945
:-(
merchant = merchant_info[:db_merchant]
1946
:-(
req_ifsc = parsed_data[:payee_ifsc] || parsed_data[:ifsc]
1947
1948
:-(
cond do
1949
:-(
is_nil(merchant) ->
1950 {:error, {"X7", "Merchant record not found"}}
1951
1952
:-(
is_nil(merchant.settlement_account_ifsc) or merchant.settlement_account_ifsc == "" ->
1953 {:error, {"X7", "Settlement IFSC not configured"}}
1954
1955
:-(
is_nil(merchant.settlement_account_number) or merchant.settlement_account_number == "" ->
1956 {:error, {"X7", "Settlement account not configured"}}
1957
1958 # Validate IFSC format (should be 11 characters: 4 letters + 7 alphanumeric)
1959
:-(
not Regex.match?(~r/^[A-Z]{4}[A-Z0-9]{7}$/, merchant.settlement_account_ifsc) ->
1960 {:error, {"X7", "Invalid settlement IFSC format"}}
1961
1962 # If ReqPay specified an IFSC, ensure it matches merchant's settlement IFSC
1963
:-(
req_ifsc != nil and req_ifsc != "" and req_ifsc != merchant.settlement_account_ifsc ->
1964 {:error, {"X7", "IFSC mismatch: ReqPay IFSC does not match merchant settlement IFSC"}}
1965
1966
:-(
true ->
1967 {:ok, :validated}
1968 end
1969 end
1970
1971 # Step 2a: Check transaction limits
1972 defp check_merchant_limits(merchant_info, parsed_data) do
1973
:-(
merchant = merchant_info[:db_merchant]
1974
:-(
amount = parse_amount(parsed_data[:amount] || parsed_data[:payee_amount])
1975
1976
:-(
cond do
1977
:-(
amount <= 0 ->
1978 {:error, {"X7", "Invalid transaction amount"}}
1979
1980
:-(
merchant.daily_limit && amount > merchant.daily_limit ->
1981 {:error, {"X7", "Transaction exceeds daily limit"}}
1982
1983
:-(
merchant.per_transaction_limit && amount > merchant.per_transaction_limit ->
1984 {:error, {"X7", "Transaction exceeds per-transaction limit"}}
1985
1986 # TODO: Check daily/monthly transaction count and volume limits
1987
1988
:-(
true ->
1989 {:ok, :validated}
1990 end
1991 end
1992
1993 # Step 2b: Validate FX and currency rules
1994 defp validate_fx_and_currency(merchant_info, parsed_data) do
1995
:-(
merchant = merchant_info[:db_merchant]
1996
:-(
currency = parsed_data[:currency] || parsed_data[:payee_currency] || "INR"
1997
1998
:-(
cond do
1999 # For international merchants, validate FX corridor
2000
:-(
currency != "INR" and not merchant.supports_international ->
2001 {:error, {"X7", "Merchant not authorized for international payments"}}
2002
2003
:-(
currency != "INR" and is_nil(parsed_data[:fx_rate]) ->
2004 {:error, {"X7", "FX rate required for international transaction"}}
2005
2006
:-(
currency != "INR" and not valid_fx_corridor?(currency) ->
2007 {:error, {"X7", "Currency corridor not supported"}}
2008
2009
:-(
true ->
2010 {:ok, :validated}
2011 end
2012 end
2013
2014 # Step 2c: AML/Compliance checks
2015 defp check_aml_compliance(merchant_info, parsed_data) do
2016
:-(
merchant = merchant_info[:db_merchant]
2017
2018
:-(
cond do
2019
:-(
merchant.kyc_status != "VERIFIED" ->
2020 {:error, {"X7", "Merchant KYC not verified"}}
2021
2022
:-(
merchant.risk_category == "HIGH" ->
2023 {:error, {"X7", "High-risk merchant blocked"}}
2024
2025 # TODO: Add more AML checks based on transaction patterns, velocity, etc.
2026
2027
:-(
true ->
2028 {:ok, :validated}
2029 end
2030 end
2031
2032 # Helper functions
2033
:-(
defp parse_amount(nil), do: 0
2034
:-(
defp parse_amount(""), do: 0
2035 defp parse_amount(amount) when is_binary(amount) do
2036
:-(
case Float.parse(amount) do
2037
:-(
{val, _} -> val
2038
:-(
:error -> 0
2039 end
2040 end
2041
:-(
defp parse_amount(amount) when is_number(amount), do: amount
2042
2043 defp valid_fx_corridor?(currency) do
2044 # Check if currency is in supported corridors
2045
:-(
supported_currencies = Application.get_env(:da_product_app, :supported_currencies, ["SGD", "AED", "USD", "EUR"])
2046
:-(
currency in supported_currencies
2047 end
2048
2049 # Extract merchant info from parsed ReqPay data
2050 defp extract_merchant_info_from_parsed_data(parsed_data) do
2051
:-(
%{
2052
:-(
merchant_id: parsed_data[:mid] || parsed_data[:payee_addr],
2053
:-(
merchant_name: parsed_data[:legal] || parsed_data[:brand] || parsed_data[:payee_name] || "Unknown Merchant",
2054 payee_addr: parsed_data[:payee_addr]
2055 }
2056 end
2057
2058 defp get_merchant_info(merchant_identifier) do
2059 # Try to lookup merchant by MID first, then by VPA
2060
:-(
merchant =
2061 case DaProductApp.Merchants.get_merchant_by_mid(merchant_identifier) do
2062
:-(
%DaProductApp.Merchants.Merchant{} = merchant -> merchant
2063 nil ->
2064 # If not found by MID, try by VPA
2065
:-(
DaProductApp.Merchants.get_merchant_by_vpa(merchant_identifier)
2066 end
2067
2068
:-(
case merchant do
2069 %DaProductApp.Merchants.Merchant{} = merchant ->
2070 # Build merchant data from database for NPCI response
2071
:-(
merchant_data = %{
2072
:-(
merchant_id: merchant.mid,
2073
:-(
merchant_code: merchant.merchant_code,
2074
:-(
merchant_name: merchant.brand_name,
2075
:-(
upi_id: merchant.merchant_vpa,
2076 merchant_type: "ENTITY",
2077
:-(
merchant_code: merchant.business_category || "5411",
2078
:-(
genre: merchant.merchant_genre || "RETAIL",
2079
:-(
type: merchant.merchant_type || "SMALL",
2080
:-(
brand: merchant.brand_name,
2081 logo_url: "", # Could be added to schema later
2082 website_url: "", # Could be added to schema later
2083
2084 # Additional NPCI required fields
2085
:-(
country_code: merchant.country_code || "IN",
2086
:-(
corridor: merchant.corridor || "DOMESTIC",
2087
:-(
net_inst_id: merchant.network_inst_id || merchant.partner.partner_code,
2088
:-(
sub_code: merchant.sub_code || "01",
2089
:-(
store_id: merchant.sid || "STORE_001",
2090
:-(
terminal_id: merchant.tid,
2091
:-(
onboarding_type: merchant.onboarding_type || "AGGREGATOR",
2092
:-(
reg_id: "REG_#{merchant.merchant_code}",
2093
:-(
pin_code: merchant.pincode || "560001",
2094 tier: determine_merchant_tier(merchant),
2095
:-(
location: "#{merchant.city}, #{merchant.state}" |> String.trim(", "),
2096
:-(
inst_code: merchant.inst_code || merchant.partner.partner_code,
2097
2098 # Legal and branding
2099
:-(
legal_name: merchant.legal_name || merchant.brand_name,
2100
:-(
franchise_name: merchant.franchise_name || merchant.brand_name,
2101
:-(
ownership_type: merchant.ownership_type || "PRIVATE",
2102
:-(
invoice_name: merchant.brand_name,
2103 invoice_number: generate_invoice_number(),
2104
2105 # Account details
2106
:-(
ifsc_code: merchant.settlement_account_ifsc || "MERC0000001",
2107
:-(
account_type: merchant.settlement_account_type || "CURRENT",
2108
:-(
account_number: merchant.settlement_account_number || "1234567890",
2109
2110 # FX details for international merchants
2111 fx_rate: get_current_fx_rate(merchant),
2112
:-(
markup_percentage: format_markup_rate(merchant.fx_markup_rate)
2113 }
2114
2115 {:ok, merchant_data}
2116
2117 nil ->
2118 # Log the missing merchant for monitoring
2119 require Logger
2120
:-(
Logger.warning("Merchant not found in database: #{merchant_identifier}")
2121
2122 # Return error instead of demo data for production use
2123 {:error, "MERCHANT_NOT_FOUND"}
2124 end
2125 end
2126
2127 defp get_merchant_info_by_msid(merchant_store_id) do
2128 # For dynamic QRs, try multiple lookup strategies since MSID might map to different database fields
2129
:-(
Logger.info("Looking up merchant by MSID: #{merchant_store_id}")
2130
2131 # Strategy 1: Try lookup by SID (Store ID) first
2132
:-(
merchant = DaProductApp.Merchants.get_merchant_by_msid(merchant_store_id)
2133
2134 # Strategy 2: If not found by SID, try lookup by MID (some QRs use msid parameter for merchant ID)
2135
:-(
merchant = if is_nil(merchant) do
2136
:-(
Logger.info("Merchant not found by SID, trying lookup by MID: #{merchant_store_id}")
2137
:-(
DaProductApp.Merchants.get_merchant_by_mid(merchant_store_id)
2138 else
2139
:-(
merchant
2140 end
2141
2142 # Strategy 3: If still not found, try lookup by VPA
2143
:-(
merchant = if is_nil(merchant) do
2144
:-(
Logger.info("Merchant not found by MID, trying lookup by VPA: #{merchant_store_id}")
2145
:-(
DaProductApp.Merchants.get_merchant_by_vpa(merchant_store_id)
2146 else
2147
:-(
merchant
2148 end
2149
2150
:-(
case merchant do
2151 %DaProductApp.Merchants.Merchant{} = merchant ->
2152
:-(
Logger.info("Merchant found: #{merchant.mid} - #{merchant.brand_name}")
2153 # Build merchant data from database for NPCI response
2154
:-(
merchant_data = %{
2155
:-(
merchant_id: merchant.mid,
2156
:-(
merchant_code: merchant.merchant_code,
2157
:-(
merchant_name: merchant.brand_name,
2158
:-(
upi_id: merchant.merchant_vpa,
2159 merchant_type: "ENTITY",
2160
:-(
merchant_code: merchant.business_category || "5411",
2161
:-(
genre: merchant.merchant_genre || "RETAIL",
2162
:-(
type: merchant.merchant_type || "SMALL",
2163
:-(
brand: merchant.brand_name,
2164 logo_url: "", # Could be added to schema later
2165 website_url: "", # Could be added to schema later
2166
2167 # Additional NPCI required fields
2168
:-(
country_code: merchant.country_code || "IN",
2169
:-(
corridor: merchant.corridor || "DOMESTIC",
2170
:-(
net_inst_id: merchant.network_inst_id || merchant.partner.partner_code,
2171
:-(
sub_code: merchant.sub_code || "01",
2172
:-(
store_id: merchant.sid || "STORE_001",
2173
:-(
terminal_id: merchant.tid,
2174
:-(
onboarding_type: merchant.onboarding_type || "AGGREGATOR",
2175
:-(
reg_id: "REG_#{merchant.merchant_code}",
2176
:-(
pin_code: merchant.pincode || "560001",
2177 tier: determine_merchant_tier(merchant),
2178
:-(
location: "#{merchant.city}, #{merchant.state}" |> String.trim(", "),
2179
:-(
inst_code: merchant.inst_code || merchant.partner.partner_code,
2180
2181 # Legal and branding
2182
:-(
legal_name: merchant.legal_name || merchant.brand_name,
2183
:-(
franchise_name: merchant.franchise_name || merchant.brand_name,
2184
:-(
ownership_type: merchant.ownership_type || "PRIVATE",
2185
:-(
invoice_name: merchant.brand_name,
2186 invoice_number: generate_invoice_number(),
2187
2188 # Account details
2189
:-(
ifsc_code: merchant.settlement_account_ifsc || "MERC0000001",
2190
:-(
account_type: merchant.settlement_account_type || "CURRENT",
2191
:-(
account_number: merchant.settlement_account_number || "1234567890",
2192
2193 # FX details for international merchants
2194 fx_rate: get_current_fx_rate(merchant),
2195
:-(
markup_percentage: format_markup_rate(merchant.fx_markup_rate)
2196 }
2197
2198 {:ok, merchant_data}
2199
2200 nil ->
2201 # Log the missing merchant for monitoring
2202 require Logger
2203
:-(
Logger.warning("Merchant not found in database by MSID: #{merchant_store_id} (tried SID, MID, and VPA lookups)")
2204
2205 # Return error instead of demo data for production use
2206 {:error, "MERCHANT_NOT_FOUND"}
2207 end
2208 end
2209
2210 # Function to get merchant info while preserving original QR data
2211 defp get_merchant_info_with_qr_data(merchant_id, qr_data) do
2212
:-(
case get_merchant_info(merchant_id) do
2213 {:ok, merchant_info} ->
2214 # Preserve original QR data in merchant info and validate terminal id if present
2215
:-(
enhanced_merchant_info = Map.merge(merchant_info, %{
2216
:-(
original_mid: qr_data.pa,
2217
:-(
original_msid: qr_data.msid,
2218
:-(
terminal_id: Map.get(qr_data, :terminal_id) || Map.get(qr_data, :mtid),
2219
:-(
qr_mode: qr_data.mode
2220 })
2221
2222 # If QR provided a terminal id and merchant has a tid configured, ensure they match
2223
:-(
case {Map.get(enhanced_merchant_info, :terminal_id), merchant_info[:terminal_id] || merchant_info[:tid]} do
2224
:-(
{nil, _} -> {:ok, enhanced_merchant_info}
2225
:-(
{_, nil} -> {:ok, enhanced_merchant_info}
2226
:-(
{qr_tid, merchant_tid} when qr_tid == merchant_tid -> {:ok, enhanced_merchant_info}
2227
:-(
{_qr_tid, _merchant_tid} -> {:error, "TID_MISMATCH"}
2228 end
2229
2230
:-(
error -> error
2231 end
2232 end
2233
2234 # Function to get merchant by MSID while preserving original QR data
2235 defp get_merchant_info_by_msid_with_qr_data(merchant_store_id, qr_data) do
2236
:-(
case get_merchant_info_by_msid(merchant_store_id) do
2237 {:ok, merchant_info} ->
2238 # Preserve original QR data in merchant info and validate terminal id if present
2239
:-(
enhanced_merchant_info = Map.merge(merchant_info, %{
2240
:-(
original_mid: qr_data.pa,
2241
:-(
original_msid: qr_data.msid,
2242
:-(
terminal_id: Map.get(qr_data, :terminal_id) || Map.get(qr_data, :mtid),
2243
:-(
qr_mode: qr_data.mode
2244 })
2245
2246
:-(
case {Map.get(enhanced_merchant_info, :terminal_id), merchant_info[:terminal_id] || merchant_info[:tid]} do
2247
:-(
{nil, _} -> {:ok, enhanced_merchant_info}
2248
:-(
{_, nil} -> {:ok, enhanced_merchant_info}
2249
:-(
{qr_tid, merchant_tid} when qr_tid == merchant_tid -> {:ok, enhanced_merchant_info}
2250
:-(
{_qr_tid, _merchant_tid} -> {:error, "TID_MISMATCH"}
2251 end
2252
2253
:-(
error -> error
2254 end
2255 end
2256
2257 # Helper function to determine merchant tier based on transaction limits
2258 defp determine_merchant_tier(merchant) do
2259
:-(
case merchant.max_transaction_limit do
2260
:-(
nil -> "M2"
2261 limit ->
2262
:-(
cond do
2263
:-(
Decimal.compare(limit, Decimal.new("50000")) == :lt -> "M1"
2264
:-(
Decimal.compare(limit, Decimal.new("200000")) == :lt -> "M2"
2265
:-(
true -> "M3"
2266 end
2267 end
2268 end
2269
2270 # Helper function to get current FX rate for international merchants
2271 defp get_current_fx_rate(merchant) do
2272
:-(
if merchant.corridor != "DOMESTIC" do
2273 # In production, this would call FX service
2274
:-(
case merchant.local_currency do
2275
:-(
"SGD" -> "83.25"
2276
:-(
"AED" -> "22.50"
2277
:-(
"USD" -> "84.00"
2278
:-(
_ -> "1.00"
2279 end
2280 else
2281 "1.00"
2282 end
2283 end
2284
2285 # Helper function to format markup rate
2286
:-(
defp format_markup_rate(nil), do: "0.00"
2287
:-(
defp format_markup_rate(rate), do: Decimal.to_string(rate)
2288
2289 defp create_validation_transaction(parsed_data, merchant_info, qr_validation_id) do
2290 # Extract amount from QR payload if not directly provided
2291
:-(
amount = case Map.get(parsed_data, :amount) do
2292
:-(
nil -> extract_amount_from_qr(parsed_data.qr_payload)
2293
:-(
amount -> amount
2294 end
2295
2296 # Convert amount to Decimal for database storage
2297
:-(
decimal_amount = case amount do
2298
:-(
nil -> Decimal.new("0.00")
2299
:-(
amount when is_binary(amount) -> Decimal.new(amount)
2300
:-(
amount -> Decimal.new(amount)
2301 end
2302
2303 # Check if transaction already exists (duplicate request)
2304
:-(
case UpiInternationalService.get_transaction_by_org_id(parsed_data.msg_id) do
2305 nil ->
2306 # Create new transaction with QR validation link
2307
:-(
transaction_data = %{
2308
:-(
org_txn_id: parsed_data.txn_id, # FIXED: Use txn_id instead of msg_id for ReqChkTxn lookup compatibility
2309 parent_qr_validation_id: qr_validation_id, # Link to QR validation
2310 transaction_type: "DOMESTIC", # Use string as per schema field definition
2311 status: "pending", # Use string as per schema field definition
2312
:-(
payee_mid: merchant_info.merchant_id,
2313
:-(
payee_addr: merchant_info.upi_id,
2314 inr_amount: decimal_amount,
2315 currency: "INR",
2316 failure_reason: "QR Validation",
2317
:-(
req_msg_id: parsed_data.msg_id # Store original msg_id in req_msg_id field
2318 }
2319
2320
:-(
UpiInternationalService.create_transaction(transaction_data)
2321
2322
:-(
existing_transaction ->
2323 # Return existing transaction for duplicate request
2324 {:ok, existing_transaction}
2325 end
2326 end
2327
2328 # Normalize parsed_data maps coming from XML parser which may have string keys.
2329 # We only convert a small whitelist of expected keys to avoid creating atoms from
2330 # arbitrary input (which can exhaust the atom table). If a key is missing, keep nil.
2331 defp normalize_parsed_data(parsed) when is_map(parsed) do
2332
:-(
keys = [
2333 :msg_id, :txn_id, :org_id, :payer_addr, :payer_name, :net_inst_id, :con_code,
2334 :note, :ref_id, :ref_url, :purpose, :cust_ref, :initiation_mode, :timestamp,
2335 :qr_payload, :amount, :currency, :base_curr, :tx_type, :txn_type
2336 ]
2337
2338
:-(
Enum.reduce(keys, %{}, fn key, acc ->
2339
:-(
value = Map.get(parsed, key) || Map.get(parsed, Atom.to_string(key)) || Map.get(parsed, to_string(key))
2340
:-(
Map.put(acc, key, value)
2341 end)
2342 end
2343
2344
:-(
defp normalize_parsed_data(other), do: other
2345
2346 defp create_payment_transaction(parsed_data) do
2347 # helper to read either atom or string keys from parsed_data maps
2348
:-(
get = fn map, key -> Map.get(map, key) || Map.get(map, Atom.to_string(key)) end
2349
2350
:-(
transaction_data = %{
2351 org_txn_id: get.(parsed_data, :org_txn_id),
2352 type: "PAY",
2353 status: "processing",
2354 payer_addr: get.(parsed_data, :payer_addr),
2355 payee_addr: get.(parsed_data, :payee_addr),
2356 amount: get.(parsed_data, :amount),
2357
:-(
currency: get.(parsed_data, :currency) || "INR",
2358 note: get.(parsed_data, :note),
2359 ref_id: get.(parsed_data, :ref_id),
2360 cust_ref: get.(parsed_data, :cust_ref),
2361 transaction_type: "DOMESTIC" # Ensure this key is present for changeset
2362 }
2363
2364
:-(
UpiInternationalService.create_transaction(transaction_data)
2365 end
2366
2367 defp process_with_partner(transaction) do
2368
:-(
try do
2369
:-(
case UpiInternationalService.process_with_partner(transaction) do
2370
:-(
{:ok, response} -> {:ok, response}
2371
:-(
{:error, :timeout} -> {:error, :partner_timeout}
2372
:-(
{:error, _reason} -> {:error, :partner_error}
2373 end
2374 rescue
2375
:-(
_error -> {:error, :partner_error}
2376 end
2377 end
2378
2379 defp reverse_with_partner(transaction) do
2380
:-(
case UpiInternationalService.reverse_transaction(transaction) do
2381
:-(
{:ok, response} -> {:ok, response}
2382
:-(
{:error, _reason} -> {:error, :reversal_failed}
2383 end
2384 end
2385
2386 defp start_timeout_escalation(parsed_data) do
2387 # Start the timeout escalation process
2388 # This will trigger ChkTxn -> Reversal -> Deemed flow
2389
:-(
spawn(fn ->
2390
:-(
:timer.sleep(30_000) # 30 second timeout
2391
:-(
escalate_timeout(parsed_data.org_txn_id)
2392 end)
2393 end
2394
2395 defp escalate_timeout(org_txn_id) do
2396
:-(
case get_transaction_by_org_id(org_txn_id) do
2397 {:ok, transaction} when transaction.status == :processing ->
2398
:-(
update_transaction_status(transaction.id, :timeout, %{reason: "Partner timeout"})
2399
2400 # Wait for potential reversal, then deem success
2401
:-(
:timer.sleep(60_000) # 60 second reversal window
2402
2403
:-(
case get_transaction_by_org_id(org_txn_id) do
2404 {:ok, txn} when txn.status == :timeout ->
2405
:-(
update_transaction_status(txn.id, :deemed_success, %{reason: "Auto-deemed after timeout"})
2406
:-(
_ ->
2407 :ok # Transaction state changed, no action needed
2408 end
2409
2410
:-(
_ ->
2411 :ok # Transaction already completed or failed
2412 end
2413 end
2414
2415 defp get_transaction_by_org_id(org_txn_id) do
2416
:-(
case UpiInternationalService.get_transaction_by_org_id(org_txn_id) do
2417
:-(
nil -> nil
2418
:-(
transaction -> transaction
2419 end
2420 end
2421
2422 defp update_transaction_status(txn_id, status, response_data) do
2423
:-(
UpiInternationalService.update_transaction_status(txn_id, status, response_data)
2424 end
2425
2426 defp map_internal_status_to_upi(status) do
2427
:-(
case status do
2428
:-(
:success -> "SUCCESS"
2429
:-(
:failed -> "FAILED"
2430
:-(
:processing -> "PENDING"
2431
:-(
:timeout -> "PENDING"
2432
:-(
:reversed -> "FAILED"
2433
:-(
:deemed_success -> "SUCCESS"
2434
:-(
_ -> "PENDING"
2435 end
2436 end
2437
2438 defp build_additional_info(partner_response) do
2439
:-(
"Partner Ref: #{partner_response.partner_txn_id}, Rate: #{partner_response.fx_rate}"
2440 end
2441
2442 defp generate_error_response(parsed_data, error_code, error_msg) do
2443
:-(
response_data = %{
2444 org_id: get_psp_org_id(),
2445 msg_id: generate_message_id(),
2446
:-(
req_msg_id: parsed_data.msg_id || generate_message_id(),
2447 result: "FAILURE",
2448 err_code: error_code,
2449
:-(
txn_id: parsed_data.txn_id || generate_transaction_id(), # Use NPCI txn_id if available
2450 # Use the incoming ReqValQr txn note when available (T03 compliance) otherwise fall back to the error message
2451
:-(
note: Map.get(parsed_data, :note) || error_msg,
2452 ref_id: generate_reference_id(),
2453 ref_url: "https://mercurypay.ariticapp.com",
2454
:-(
purpose: Map.get(parsed_data, :purpose) || extract_purpose(parsed_data),
2455
:-(
cust_ref: Map.get(parsed_data, :cust_ref) || generate_customer_reference(),
2456
:-(
qr_payload: parsed_data.qr_payload || "",
2457
:-(
initiation_mode: parsed_data.initiation_mode || "01",
2458
2459 # Extract payee information from request data
2460
:-(
payee_addr: parsed_data.payer_addr || "merchant@mercury",
2461
:-(
payee_name: extract_payee_name_from_qr(parsed_data.qr_payload) || "Mercury PSP",
2462 payee_type: "ENTITY",
2463
:-(
merchant_code: extract_merchant_code_from_qr(parsed_data.qr_payload) || "0000",
2464
2465 # Default institution information
2466 country_code: "IN",
2467 net_inst_id: "MER1010001",
2468
2469 # Default merchant identification
2470 sub_code: "00",
2471 merchant_id: "MERCURY001",
2472 store_id: "STORE001",
2473 terminal_id: "TERM001",
2474 merchant_type: "SMALL",
2475 merchant_genre: "RETAIL",
2476 onboarding_type: "AGGREGATOR",
2477 reg_id: "REG001",
2478 pin_code: "000000",
2479 tier: "M2",
2480 merchant_location: "IN",
2481 merchant_inst_code: "MERC",
2482
2483 # Default merchant names
2484 merchant_brand: "Mercury PSP",
2485 legal_name: "Mercury Payment Services",
2486 franchise_name: "Mercury",
2487
2488 # Default ownership and invoice
2489 ownership_type: "PRIVATE",
2490 invoice_name: "Mercury PSP",
2491 invoice_number: "INV000000",
2492
2493 # Default account details
2494 ifsc_code: "MERC0000001",
2495 account_type: "CURRENT",
2496 account_number: "0000000000",
2497
2498 # Default amount and currency
2499 currency: "INR",
2500 amount: "0.00",
2501 split_name: "MAIN",
2502
2503 # Default FX information
2504 base_amount: "0.00",
2505 base_currency: "INR",
2506 fx_active: "N",
2507 fx_rate: "1.00",
2508 markup_percentage: "0.00"
2509 }
2510
2511
:-(
case UpiXmlSchema.generate_resp_val_qr(response_data) do
2512
:-(
{:ok, error_xml} -> {:ok, error_xml}
2513
:-(
error_xml when is_binary(error_xml) -> {:ok, error_xml}
2514
:-(
{:error, reason} -> {:error, reason}
2515 end
2516 end
2517
2518 defp generate_payment_error_response(parsed_data, error_code, error_msg) do
2519 # Ensure PAY subtype always uses CREDIT transaction type, even in error responses
2520
:-(
final_txn_type = case Map.get(parsed_data, :sub_type) do
2521
:-(
"PAY" -> "CREDIT"
2522
:-(
_ -> Map.get(parsed_data, :txn_type, "CREDIT")
2523 end
2524
2525
:-(
response_data = %{
2526 org_id: get_psp_org_id(),
2527 msg_id: generate_message_id(),
2528
:-(
req_msg_id: parsed_data.msg_id,
2529 result: "FAILURE",
2530 err_code: error_code,
2531 txn_id: generate_transaction_id(),
2532
:-(
cust_ref: parsed_data.cust_ref || "",
2533 txn_type: final_txn_type,
2534 sub_type: Map.get(parsed_data, :sub_type, ""),
2535 add_info: error_msg,
2536
2537 # Include payee details extracted from the request for proper RespPay <Ref> element
2538 seq_num: Map.get(parsed_data, :payee_seq_num, "1"),
2539 payee_addr: Map.get(parsed_data, :payee_addr, ""),
2540 payee_code: validate_payee_code(Map.get(parsed_data, :payee_code)), # E17 fix: exactly 4 digits
2541 org_amount: "0.00", # CRITICAL: For failure cases, must be 0.00 (E14 error fix)
2542 reg_name: Map.get(parsed_data, :merchant_legal_name, Map.get(parsed_data, :merchant_brand_name, Map.get(parsed_data, :payee_name, ""))),
2543 ifsc: validate_ifsc_code(Map.get(parsed_data, :payee_ifsc)), # E18 fix: exactly 11 chars
2544 ac_num: validate_account_number(Map.get(parsed_data, :payee_ac_num)), # E16 fix: 1-16 chars
2545 acc_type: validate_account_type(Map.get(parsed_data, :payee_ac_type)), # E19 fix: must be present
2546 sett_amount: "0.00", # CRITICAL: For failure cases, settlement amount must be 0.00 (E14 error fix)
2547 sett_currency: Map.get(parsed_data, :currency, Map.get(parsed_data, :payee_currency, "INR")),
2548
2549 # Include note and orgTxnId from request data
2550 note: Map.get(parsed_data, :note, ""),
2551 org_txn_id: Map.get(parsed_data, :org_txn_id, ""), # CRITICAL: Use original orgTxnId from ReqPay for NPCI OR2 compliance
2552
:-(
ref_id: Map.get(parsed_data, :ref_id) || Map.get(parsed_data, :cust_ref, ""), # Use ref_id or fallback to cust_ref
2553 ref_url: Map.get(parsed_data, :ref_url, ""), # CRITICAL: Use exact ref_url from ReqPay, never override with PSP default
2554
2555 # CRITICAL: Include expire_ts from original request to avoid NPCI rejection 'CZ:TXN QR ExpireTs Not Match'
2556 expire_ts: Map.get(parsed_data, :expire_ts),
2557 qr_ver: Map.get(parsed_data, :qr_ver),
2558 qr_medium: Map.get(parsed_data, :qr_medium),
2559 qr_query: Map.get(parsed_data, :qr_query),
2560 ver_token: Map.get(parsed_data, :ver_token),
2561
2562 resp_code: error_code,
2563 request_data: parsed_data # Include original request data for XML schema access
2564 }
2565
2566
:-(
UpiXmlSchema.generate_resp_pay(response_data)
2567 end
2568
2569 defp generate_status_error_response(parsed_data, error_code, error_msg) do
2570
:-(
Logger.info("=== Generating Error RespChkTxn ===")
2571
:-(
Logger.info("Error Code: #{error_code}")
2572
:-(
Logger.info("Error Message: #{error_msg}")
2573
:-(
Logger.info("Request Msg ID: #{parsed_data.msg_id}")
2574
:-(
Logger.info("Org Txn ID: #{parsed_data.org_txn_id}")
2575
2576
:-(
response_data = %{
2577 org_id: get_psp_org_id(),
2578 msg_id: generate_message_id(),
2579
:-(
req_msg_id: parsed_data.msg_id,
2580 result: "FAILURE",
2581 err_code: error_code,
2582 txn_id: generate_transaction_id(),
2583
:-(
org_txn_id: parsed_data.org_txn_id,
2584
:-(
org_msg_id: Map.get(parsed_data, :org_msg_id) || Map.get(parsed_data, "org_msg_id") ||
2585
:-(
Map.get(parsed_data, :orgMsgId) || Map.get(parsed_data, "orgMsgId") ||
2586
:-(
(case Map.get(parsed_data, :txn) || Map.get(parsed_data, "txn") do
2587
:-(
%{} = txn -> Map.get(txn, :org_msg_id) || Map.get(txn, "org_msg_id") || Map.get(txn, :orgMsgId) || Map.get(txn, "orgMsgId")
2588
:-(
_ -> nil
2589
:-(
end) ||
2590
:-(
(case Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, "raw_xml") do
2591 xml when is_binary(xml) ->
2592
:-(
case Regex.run(~r/orgMsgId\s*=\s*"([^"]+)"/i, xml) do
2593
:-(
[_, id] -> id
2594 _ ->
2595
:-(
case Regex.run(~r/orgMsgId\s*=\s*'([^']+)'/i, xml) do
2596
:-(
[_, id2] -> id2
2597
:-(
_ -> ""
2598 end
2599 end
2600
:-(
_ -> ""
2601 end),
2602
:-(
note: Map.get(parsed_data, :note) || Map.get(parsed_data, "note") || "",
2603
:-(
ref_id: Map.get(parsed_data, :ref_id) || Map.get(parsed_data, "ref_id") ||
2604
:-(
Map.get(parsed_data, :refId) || Map.get(parsed_data, "refId") || "",
2605
:-(
ref_url: Map.get(parsed_data, :ref_url) || Map.get(parsed_data, "ref_url") ||
2606
:-(
Map.get(parsed_data, :refUrl) || Map.get(parsed_data, "refUrl") || "",
2607
:-(
cust_ref: Map.get(parsed_data, :cust_ref) || Map.get(parsed_data, "cust_ref") ||
2608
:-(
Map.get(parsed_data, :custRef) || Map.get(parsed_data, "custRef") || "",
2609
:-(
txn_type: Map.get(parsed_data, :txn_type) || Map.get(parsed_data, "txn_type") ||
2610
:-(
Map.get(parsed_data, :txnType) || Map.get(parsed_data, "txnType") || "CREDIT",
2611
:-(
sub_type: Map.get(parsed_data, :sub_type) || Map.get(parsed_data, "sub_type") ||
2612
:-(
Map.get(parsed_data, :subType) || Map.get(parsed_data, "subType") || "",
2613 add_info: error_msg,
2614 status: "FAILED",
2615 currency: "INR",
2616 amount: "0.00",
2617
2618 # Extract payee/ref attributes from request data
2619
:-(
addr: Map.get(parsed_data, :payee_addr) || Map.get(parsed_data, "payee_addr") ||
2620
:-(
Map.get(parsed_data, :addr) || Map.get(parsed_data, "addr") || "",
2621
:-(
code: validate_payee_code(Map.get(parsed_data, :payee_code) || Map.get(parsed_data, "payee_code") ||
2622
:-(
Map.get(parsed_data, :code) || Map.get(parsed_data, "code")),
2623
:-(
org_amount: Map.get(parsed_data, :payee_amount) || Map.get(parsed_data, "payee_amount") ||
2624
:-(
Map.get(parsed_data, :org_amount) || Map.get(parsed_data, "org_amount") ||
2625
:-(
Map.get(parsed_data, :amount) || Map.get(parsed_data, "amount") || "0.00",
2626 resp_code: error_code,
2627
:-(
reg_name: Map.get(parsed_data, :payee_name) || Map.get(parsed_data, "payee_name") ||
2628
:-(
Map.get(parsed_data, :reg_name) || Map.get(parsed_data, "reg_name") ||
2629
:-(
Map.get(parsed_data, :merchant_legal_name) || Map.get(parsed_data, "merchant_legal_name") ||
2630
:-(
Map.get(parsed_data, :merchant_brand_name) || Map.get(parsed_data, "merchant_brand_name") || "",
2631
:-(
ifsc: validate_ifsc_code(Map.get(parsed_data, :payee_ifsc) || Map.get(parsed_data, "payee_ifsc") ||
2632
:-(
Map.get(parsed_data, :ifsc) || Map.get(parsed_data, "ifsc")),
2633
:-(
ac_num: validate_account_number(Map.get(parsed_data, :payee_ac_num) || Map.get(parsed_data, "payee_ac_num") ||
2634
:-(
Map.get(parsed_data, :ac_num) || Map.get(parsed_data, "ac_num")),
2635
:-(
acc_type: validate_account_type(Map.get(parsed_data, :payee_ac_type) || Map.get(parsed_data, "payee_ac_type") ||
2636
:-(
Map.get(parsed_data, :acc_type) || Map.get(parsed_data, "acc_type")),
2637
:-(
approval_num: Map.get(parsed_data, :approval_num) || Map.get(parsed_data, "approval_num") ||
2638
:-(
generate_approval_number(),
2639
:-(
sett_amount: Map.get(parsed_data, :sett_amount) || Map.get(parsed_data, "sett_amount") ||
2640
:-(
Map.get(parsed_data, :settlement_amount) || Map.get(parsed_data, "settlement_amount") || "0.00",
2641
:-(
sett_currency: Map.get(parsed_data, :sett_currency) || Map.get(parsed_data, "sett_currency") ||
2642
:-(
Map.get(parsed_data, :settlement_currency) || Map.get(parsed_data, "settlement_currency") ||
2643
:-(
Map.get(parsed_data, :payee_currency) || Map.get(parsed_data, "payee_currency") ||
2644
:-(
Map.get(parsed_data, :currency) || Map.get(parsed_data, "currency") || "INR"
2645 }
2646
2647
:-(
Logger.info("Error Response Data: #{inspect(response_data)}")
2648
2649
:-(
case UpiXmlSchema.generate_resp_chk_txn(response_data) do
2650 {:ok, xml_response} ->
2651
:-(
Logger.info("Error RespChkTxn XML generated successfully")
2652
:-(
Logger.info("Error XML: #{xml_response}")
2653 {:ok, xml_response}
2654
2655 {:error, reason} = error ->
2656
:-(
Logger.error("Failed to generate error RespChkTxn XML: #{reason}")
2657
:-(
error
2658 end
2659 end
2660
2661
:-(
defp get_psp_org_id, do: Application.get_env(:da_product_app, :psp_org_id, "MERCURY")
2662
2663 defp generate_message_id do
2664 # Generate exactly 35 characters as required by NPCI
2665
:-(
prefix = "MERMSG" # 6 chars
2666 # Need 29 more chars (35 - 6 = 29)
2667 # Use 14 bytes (28 hex chars) + 1 extra char = 29 chars
2668
:-(
suffix = (:crypto.strong_rand_bytes(14) |> Base.encode16()) <> "A"
2669
:-(
(prefix <> suffix) |> String.slice(0, 35)
2670 end
2671
2672 defp generate_transaction_id do
2673
:-(
"TXN" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
2674 end
2675
2676 defp generate_reference_id do
2677
:-(
"REF" <> (:crypto.strong_rand_bytes(6) |> Base.encode16())
2678 end
2679
2680 defp generate_invoice_number do
2681
:-(
"INV" <> (:crypto.strong_rand_bytes(4) |> Base.encode16())
2682 end
2683
2684 defp generate_customer_reference do
2685 # Generate a 12-character customer reference as required by T12
2686
:-(
timestamp = System.system_time(:millisecond) |> Integer.to_string() |> String.slice(-6, 6)
2687
:-(
random = :crypto.strong_rand_bytes(3) |> Base.encode16() |> String.slice(0, 6)
2688 (timestamp <> random)
2689 |> String.pad_trailing(12, "0")
2690
:-(
|> String.slice(0, 12)
2691 end
2692
2693 defp extract_amount_from_qr(qr_payload) do
2694 # Extract amount from UPI QR string - handle different formats
2695
:-(
cond do
2696 # upiGlobal format: upiGlobal://pay?...&bAm=15000.00
2697
:-(
String.contains?(qr_payload || "", "bAm=") ->
2698
:-(
case Regex.run(~r/bAm=([0-9.]+)/, qr_payload) do
2699
:-(
[_, amount] -> amount
2700
:-(
_ -> "100.00" # Default amount for testing
2701 end
2702
2703 # Standard UPI URL format: upi://pay?pa=...&am=100.00
2704
:-(
String.contains?(qr_payload || "", "am=") ->
2705
:-(
case Regex.run(~r/am=([0-9.]+)/, qr_payload) do
2706
:-(
[_, amount] -> amount
2707
:-(
_ -> "100.00" # Default amount for testing
2708 end
2709
2710 # EMV QR Code format (structured format with tags)
2711
:-(
String.length(qr_payload || "") > 50 ->
2712 # For EMV format, amount is usually in tag 54 (Transaction Amount)
2713 # For now, return a default amount since QR validation doesn't require the exact amount
2714 "100.00"
2715
2716 # Fallback
2717
:-(
true ->
2718 "100.00" # Default amount for QR validation
2719 end
2720 end
2721
2722 # Try to extract purpose code from parsed_data (field), QR payload (upiGlobal/upi URL) or raw ReqValQr XML
2723 defp extract_purpose(parsed_data) do
2724 # 1) parsed_data may already have purpose
2725
:-(
parsed_purpose = Map.get(parsed_data, :purpose)
2726
:-(
if parsed_purpose && parsed_purpose != "" do
2727
:-(
parsed_purpose
2728 else
2729 # 2) try QR payload (upiGlobal url or upi://pay query param)
2730
:-(
qr = Map.get(parsed_data, :qr_payload) || ""
2731
:-(
case Regex.run(~r/[?&]purpose=([0-9]{1,3})/, qr) do
2732
:-(
[_, p] -> p
2733 _ ->
2734 # 3) try raw XML (ReqValQr <Txn ... purpose="11"/>)
2735
:-(
raw = Map.get(parsed_data, :raw_xml) || Map.get(parsed_data, :raw_request) || ""
2736
:-(
case Regex.run(~r/purpose\s*=\s*"(\d{1,3})"/, raw) do
2737
:-(
[_, p2] -> p2
2738
:-(
_ -> "00"
2739 end
2740 end
2741 end
2742 end
2743
2744 defp extract_currency_from_qr(qr_payload) do
2745 # Extract currency from UPI QR string - handle different formats
2746
:-(
cond do
2747 # upiGlobal format: upiGlobal://pay?...&cu=AED&bCurr=AED
2748
:-(
String.contains?(qr_payload || "", "bCurr=") ->
2749
:-(
case Regex.run(~r/bCurr=([A-Z]{3})/, qr_payload) do
2750
:-(
[_, currency] -> currency
2751
:-(
_ -> "INR" # Default currency
2752 end
2753
2754 # upiGlobal format: upiGlobal://pay?...&cu=AED
2755
:-(
String.contains?(qr_payload || "", "cu=") ->
2756
:-(
case Regex.run(~r/cu=([A-Z]{3})/, qr_payload) do
2757
:-(
[_, currency] -> currency
2758
:-(
_ -> "INR" # Default currency
2759 end
2760
2761 # Standard UPI URL format: upi://pay?pa=...&cu=INR
2762
:-(
String.contains?(qr_payload || "", "cu=") ->
2763
:-(
case Regex.run(~r/cu=([A-Z]{3})/, qr_payload) do
2764
:-(
[_, currency] -> currency
2765
:-(
_ -> "INR" # Default currency
2766 end
2767
2768 # Fallback
2769
:-(
true ->
2770 "INR" # Default currency for India
2771 end
2772 end
2773
2774 defp extract_merchant_type_from_qr(qr_payload) do
2775 # Extract merchant type from UPI QR string - handle different formats
2776 require Logger
2777
:-(
Logger.debug("Extracting merchant type from QR payload: #{inspect(qr_payload)}")
2778
2779
:-(
result = cond do
2780 # Look for merchant type parameter in QR (case insensitive)
2781
:-(
String.contains?(qr_payload || "", "mType=") or String.contains?(qr_payload || "", "mtype=") ->
2782
:-(
case Regex.run(~r/mtype=([^&\s]+)/i, qr_payload) do
2783 [_, merchant_type] ->
2784
:-(
mtype = String.upcase(String.trim(merchant_type))
2785
:-(
Logger.debug("Found mType via query param: #{mtype}")
2786
:-(
mtype
2787
:-(
_ -> nil
2788 end
2789
2790 # Look for business type parameter
2791
:-(
String.contains?(qr_payload || "", "bt=") ->
2792
:-(
case Regex.run(~r/bt=([^&]+)/, qr_payload) do
2793 [_, business_type] ->
2794
:-(
Logger.debug("Found business type: #{business_type}")
2795
:-(
business_type
2796
:-(
_ -> nil
2797 end
2798
2799 # Try XML-like format: <mType>VALUE</mType>
2800
:-(
String.contains?(qr_payload || "", "<mType>") ->
2801
:-(
case Regex.run(~r/<mType>([^<]+)<\/mType>/i, qr_payload) do
2802 [_, merchant_type] ->
2803
:-(
mtype = String.upcase(String.trim(merchant_type))
2804
:-(
Logger.debug("Found mType via XML: #{mtype}")
2805
:-(
mtype
2806
:-(
_ -> nil
2807 end
2808
2809 # For EMV QR codes, merchant type might be in structured data
2810
:-(
String.length(qr_payload || "") > 50 ->
2811 # EMV QR codes have merchant category in tag 52
2812 # For now, return nil and let it fall back to merchant_info
2813
:-(
Logger.debug("EMV QR detected, no merchant type extraction")
2814 nil
2815
2816 # Fallback
2817
:-(
true ->
2818
:-(
Logger.debug("No merchant type found in QR payload")
2819 nil
2820 end
2821
2822
:-(
Logger.debug("Final extracted merchant type: #{inspect(result)}")
2823
:-(
result
2824 end
2825
2826 defp extract_merchant_genre_from_qr(qr_payload) do
2827 # Extract merchant genre from UPI QR string - handle different formats
2828
:-(
cond do
2829 # Look for merchant genre parameter in QR (mGr= parameter)
2830
:-(
String.contains?(qr_payload || "", "mGr=") ->
2831
:-(
case Regex.run(~r/mGr=([^&]+)/, qr_payload) do
2832
:-(
[_, merchant_genre] -> merchant_genre
2833
:-(
_ -> nil
2834 end
2835
2836 # Look for merchant genre parameter in QR (mg= parameter)
2837
:-(
String.contains?(qr_payload || "", "mg=") ->
2838
:-(
case Regex.run(~r/mg=([^&]+)/, qr_payload) do
2839
:-(
[_, merchant_genre] -> merchant_genre
2840
:-(
_ -> nil
2841 end
2842
2843 # Look for business genre parameter
2844
:-(
String.contains?(qr_payload || "", "bg=") ->
2845
:-(
case Regex.run(~r/bg=([^&]+)/, qr_payload) do
2846
:-(
[_, business_genre] -> business_genre
2847
:-(
_ -> nil
2848 end
2849
2850 # Look for category genre parameter
2851
:-(
String.contains?(qr_payload || "", "cg=") ->
2852
:-(
case Regex.run(~r/cg=([^&]+)/, qr_payload) do
2853
:-(
[_, category_genre] -> category_genre
2854
:-(
_ -> nil
2855 end
2856
2857 # For EMV QR codes, merchant genre might be in structured data
2858
:-(
String.length(qr_payload || "") > 50 ->
2859 # EMV QR codes might have merchant genre in additional data
2860 # For now, return nil and let it fall back to merchant_info
2861 nil
2862
2863 # Fallback
2864
:-(
true ->
2865 nil
2866 end
2867 end
2868
2869 defp extract_invoice_number_from_qr(qr_payload) do
2870 # Extract invoice number from UPI QR string - handle different formats
2871
:-(
cond do
2872 # Look for invoice number parameter in QR
2873
:-(
String.contains?(qr_payload || "", "invoiceNo=") ->
2874
:-(
case Regex.run(~r/invoiceNo=([^&]+)/, qr_payload) do
2875
:-(
[_, invoice_no] -> URI.decode(invoice_no)
2876
:-(
_ -> nil
2877 end
2878
2879 # Look for invoice ID parameter
2880
:-(
String.contains?(qr_payload || "", "invoice=") ->
2881
:-(
case Regex.run(~r/invoice=([^&]+)/, qr_payload) do
2882
:-(
[_, invoice] -> URI.decode(invoice)
2883
:-(
_ -> nil
2884 end
2885
2886 # Look for invoice reference parameter
2887
:-(
String.contains?(qr_payload || "", "invRef=") ->
2888
:-(
case Regex.run(~r/invRef=([^&]+)/, qr_payload) do
2889
:-(
[_, inv_ref] -> URI.decode(inv_ref)
2890
:-(
_ -> nil
2891 end
2892
2893 # For EMV QR codes, invoice might be in additional data
2894
:-(
String.length(qr_payload || "") > 50 ->
2895 # EMV QR codes might have invoice in tag 62 (Additional Data Field Template)
2896 # For now, return nil and let it fall back to merchant_info
2897 nil
2898
2899 # Fallback
2900
:-(
true ->
2901 nil
2902 end
2903 end
2904
2905 defp extract_invoice_date_from_qr(qr_payload) do
2906 # Extract invoice date from UPI QR string - handle different formats
2907
:-(
cond do
2908 # Look for invoice date parameter in QR
2909
:-(
String.contains?(qr_payload || "", "invoiceDate=") ->
2910
:-(
case Regex.run(~r/invoiceDate=([^&]+)/, qr_payload) do
2911
:-(
[_, invoice_date] -> URI.decode(invoice_date)
2912
:-(
_ -> nil
2913 end
2914
2915 # Look for date parameter
2916
:-(
String.contains?(qr_payload || "", "date=") ->
2917
:-(
case Regex.run(~r/date=([^&]+)/, qr_payload) do
2918
:-(
[_, date] -> URI.decode(date)
2919
:-(
_ -> nil
2920 end
2921
2922 # Look for invoice date reference parameter
2923
:-(
String.contains?(qr_payload || "", "invDate=") ->
2924
:-(
case Regex.run(~r/invDate=([^&]+)/, qr_payload) do
2925
:-(
[_, inv_date] -> URI.decode(inv_date)
2926
:-(
_ -> nil
2927 end
2928
2929 # For EMV QR codes, date might be in additional data
2930
:-(
String.length(qr_payload || "") > 50 ->
2931 # EMV QR codes might have date in tag 62 (Additional Data Field Template)
2932 # For now, return nil and let it fall back to default timestamp
2933 nil
2934
2935 # Fallback
2936
:-(
true ->
2937 nil
2938 end
2939 end
2940
2941 defp extract_invoice_name_from_qr(qr_payload) do
2942 # Extract invoice name from UPI QR string - handle different formats
2943
:-(
cond do
2944 # Look for invoice name parameter in QR
2945
:-(
String.contains?(qr_payload || "", "invoiceName=") ->
2946
:-(
case Regex.run(~r/invoiceName=([^&]+)/, qr_payload) do
2947
:-(
[_, invoice_name] -> URI.decode(invoice_name)
2948
:-(
_ -> nil
2949 end
2950
2951 # Look for invoice display name parameter
2952
:-(
String.contains?(qr_payload || "", "invName=") ->
2953
:-(
case Regex.run(~r/invName=([^&]+)/, qr_payload) do
2954
:-(
[_, inv_name] -> URI.decode(inv_name)
2955
:-(
_ -> nil
2956 end
2957
2958 # Look for bill name parameter
2959
:-(
String.contains?(qr_payload || "", "billName=") ->
2960
:-(
case Regex.run(~r/billName=([^&]+)/, qr_payload) do
2961
:-(
[_, bill_name] -> URI.decode(bill_name)
2962
:-(
_ -> nil
2963 end
2964
2965 # For EMV QR codes, invoice name might be in additional data
2966
:-(
String.length(qr_payload || "") > 50 ->
2967 # EMV QR codes might have invoice name in tag 62 (Additional Data Field Template)
2968 # For now, return nil and let it fall back to merchant_info
2969 nil
2970
2971 # Fallback
2972
:-(
true ->
2973 nil
2974 end
2975 end
2976
2977 defp extract_merchant_category_from_qr(qr_payload) do
2978 # Extract merchant category code from UPI QR string - handle different formats
2979
:-(
cond do
2980 # Look for merchant category parameter in QR (mc= parameter)
2981
:-(
String.contains?(qr_payload || "", "mc=") ->
2982
:-(
case Regex.run(~r/mc=([^&]+)/, qr_payload) do
2983
:-(
[_, merchant_category] -> merchant_category
2984
:-(
_ -> nil
2985 end
2986
2987 # Look for merchant code parameter
2988
:-(
String.contains?(qr_payload || "", "mcode=") ->
2989
:-(
case Regex.run(~r/mcode=([^&]+)/, qr_payload) do
2990
:-(
[_, merchant_code] -> merchant_code
2991
:-(
_ -> nil
2992 end
2993
2994 # Look for category code parameter
2995
:-(
String.contains?(qr_payload || "", "cc=") ->
2996
:-(
case Regex.run(~r/cc=([^&]+)/, qr_payload) do
2997
:-(
[_, category_code] -> category_code
2998
:-(
_ -> nil
2999 end
3000
3001 # For EMV QR codes, merchant category is in tag 52
3002
:-(
String.length(qr_payload || "") > 50 ->
3003 # EMV QR codes have merchant category in tag 52 (Merchant Category Code)
3004 # For now, return nil and let it fall back to merchant_info
3005 nil
3006
3007 # Fallback
3008
:-(
true ->
3009 nil
3010 end
3011 end
3012
3013 defp extract_payee_name_from_qr(qr_payload) do
3014 # Extract payee name from UPI QR string - handle different formats
3015
:-(
cond do
3016 # Look for payee name parameter in QR (pn= parameter)
3017
:-(
String.contains?(qr_payload || "", "pn=") ->
3018
:-(
case Regex.run(~r/pn=([^&]+)/, qr_payload) do
3019
:-(
[_, payee_name] -> URI.decode(payee_name)
3020
:-(
_ -> nil
3021 end
3022
3023 # Look for merchant name parameter
3024
:-(
String.contains?(qr_payload || "", "mn=") ->
3025
:-(
case Regex.run(~r/mn=([^&]+)/, qr_payload) do
3026
:-(
[_, merchant_name] -> URI.decode(merchant_name)
3027
:-(
_ -> nil
3028 end
3029
3030 # Look for name parameter
3031
:-(
String.contains?(qr_payload || "", "name=") ->
3032
:-(
case Regex.run(~r/name=([^&]+)/, qr_payload) do
3033
:-(
[_, name] -> URI.decode(name)
3034
:-(
_ -> nil
3035 end
3036
3037 # For EMV QR codes, merchant name might be in tag 59
3038
:-(
String.length(qr_payload || "") > 50 ->
3039 # EMV QR codes have merchant name in tag 59 (Merchant Name)
3040 # For now, return nil and let it fall back to merchant_info
3041 nil
3042
3043 # Fallback
3044
:-(
true ->
3045 nil
3046 end
3047 end
3048
3049 defp extract_merchant_code_from_qr(qr_payload) do
3050 # Extract merchant code from UPI QR string - handle different formats
3051
:-(
cond do
3052 # Look for merchant code parameter in QR (mc= parameter)
3053
:-(
String.contains?(qr_payload || "", "mc=") ->
3054
:-(
case Regex.run(~r/mc=([^&]+)/, qr_payload) do
3055
:-(
[_, merchant_code] -> merchant_code
3056
:-(
_ -> nil
3057 end
3058
3059 # Look for business category parameter
3060
:-(
String.contains?(qr_payload || "", "bc=") ->
3061
:-(
case Regex.run(~r/bc=([^&]+)/, qr_payload) do
3062
:-(
[_, business_category] -> business_category
3063
:-(
_ -> nil
3064 end
3065
3066 # Look for category parameter
3067
:-(
String.contains?(qr_payload || "", "cat=") ->
3068
:-(
case Regex.run(~r/cat=([^&]+)/, qr_payload) do
3069
:-(
[_, category] -> category
3070
:-(
_ -> nil
3071 end
3072
3073 # For EMV QR codes, merchant code might be in structured data
3074
:-(
String.length(qr_payload || "") > 50 ->
3075 # EMV QR codes might have merchant code in tag 52
3076 # For now, return nil and let it fall back to merchant_info
3077 nil
3078
3079 # Fallback
3080
:-(
true ->
3081 nil
3082 end
3083 end
3084
3085 # ================================
3086 # International Payment Processing Functions
3087 # ================================
3088
3089 @doc """
3090 Process international credit request (ReqPay) with enhanced structure
3091 Handles merchant details, risk scores, device info, and FX splits
3092 """
3093 def process_international_payment_request(payment_data) do
3094
:-(
case UpiXmlSchema.parse_req_pay(payment_data) do
3095 {:ok, parsed_data} ->
3096 # First validate basic international payment structure
3097
:-(
case validate_international_payment_data(parsed_data) do
3098 {:ok, basic_validated_data} ->
3099 # Then perform comprehensive merchant creditworthiness validation
3100
:-(
merchant_info = extract_merchant_info_from_parsed_data(basic_validated_data)
3101
:-(
case validate_merchant_creditworthiness(basic_validated_data, merchant_info) do
3102 {:error, {error_code, error_message}} ->
3103
:-(
Logger.warning("International payment merchant validation failed: #{error_message}")
3104
:-(
error_resp = generate_payment_error_response(basic_validated_data, error_code, error_message)
3105
:-(
send_error_resp_pay_to_npci(error_resp, basic_validated_data.txn_id)
3106
3107 # Mark as declined by IMA for audit trail
3108
:-(
case DaProductApp.Transactions.ReqPayService.get_by_txn_id(basic_validated_data.txn_id) do
3109 {:ok, req_pay} ->
3110
:-(
DaProductApp.Transactions.ReqPayService.mark_as_declined_by_ima(req_pay, error_code, error_message)
3111 _ ->
3112
:-(
Logger.warning("Could not find ReqPay record to mark as declined")
3113 end
3114
3115 {:error, error_message}
3116
3117 {:ok, enhanced_merchant_info} ->
3118 # Merge enhanced merchant info back into validated_data
3119
:-(
validated_data = Map.merge(basic_validated_data, enhanced_merchant_info)
3120
3121
:-(
case create_international_payment_transaction(validated_data) do
3122 {:ok, transaction} ->
3123
:-(
case process_international_payment_with_partner(transaction, validated_data) do
3124 {:ok, partner_response} ->
3125
:-(
update_transaction_status(transaction.id, :success, partner_response)
3126
:-(
final_txn_type = case Map.get(validated_data, :sub_type) do
3127
:-(
"PAY" -> "CREDIT"
3128
:-(
_ -> validated_data.txn_type || "CREDIT"
3129 end
3130
:-(
response_data = %{
3131 org_id: get_psp_org_id(),
3132 msg_id: generate_message_id(),
3133
:-(
req_msg_id: validated_data.msg_id,
3134 result: "SUCCESS",
3135 err_code: "00",
3136
:-(
txn_id: validated_data.txn_id,
3137
:-(
note: validated_data.note || "",
3138
:-(
ref_id: validated_data.ref_id || "",
3139
:-(
cust_ref: validated_data.cust_ref,
3140
:-(
ref_url: validated_data.ref_url || "",
3141 txn_type: final_txn_type,
3142
:-(
sub_type: validated_data.sub_type || "",
3143
:-(
initiation_mode: validated_data.initiation_mode || "QR",
3144
:-(
org_txn_id: validated_data.org_txn_id,
3145
:-(
org_rrn: validated_data.org_rrn || "",
3146
:-(
org_txn_date: validated_data.org_txn_date || get_date(),
3147
:-(
ref_category: validated_data.ref_category || "",
3148
:-(
prod_type: validated_data.prod_type || "UPI",
3149
:-(
sp_risk_score: validated_data.sp_risk_score || "0",
3150
:-(
npci_risk_score: validated_data.npci_risk_score || "0",
3151
:-(
expire_ts: validated_data.expire_ts,
3152
:-(
qr_ver: validated_data.qr_ver || "2.0",
3153
:-(
qr_medium: validated_data.qr_medium || "04",
3154
:-(
qr_query: validated_data.qr_query || "",
3155
:-(
ver_token: validated_data.ver_token,
3156
:-(
stan: validated_data.stan,
3157
:-(
seq_num: validated_data.payee_seq_num || "1",
3158
:-(
payee_addr: validated_data.payee_addr,
3159
:-(
payee_code: validate_payee_code(validated_data.payee_code), # E17 fix: exactly 4 digits
3160
:-(
org_amount: validated_data.base_amount || validated_data.payee_amount || validated_data.amount,
3161
:-(
resp_code: partner_response.resp_code || "00",
3162
:-(
reg_name: validated_data.merchant_legal_name || validated_data.payee_name,
3163
:-(
ifsc: validate_ifsc_code(validated_data.payee_ifsc), # E18 fix: exactly 11 chars
3164
:-(
ac_num: validate_account_number(validated_data.payee_ac_num), # E16 fix: 1-16 chars
3165
:-(
acc_type: validate_account_type(validated_data.payee_ac_type), # E19 fix: must be present
3166
:-(
approval_num: partner_response.approval_num || generate_approval_number(),
3167
:-(
sett_amount: partner_response.settlement_amount || validated_data.payee_amount,
3168 sett_currency: "INR",
3169 request_data: validated_data
3170 }
3171 # Generate RespPay XML
3172
:-(
{:ok, resp_pay_xml} = UpiXmlSchema.generate_resp_pay(response_data)
3173 # Display RespPay XML in terminal
3174
:-(
IO.puts("\n=== RespPay XML Sent to NPCI ===\n")
3175
:-(
IO.puts(resp_pay_xml)
3176
:-(
IO.puts("\n=== END RespPay XML ===\n")
3177 # Send RespPay to NPCI and parse/display ACK
3178
:-(
case send_resp_pay_to_npci(resp_pay_xml, transaction.id) do
3179 {:ok, %{status: status, body: ack_body}} when status in 200..299 ->
3180
:-(
IO.puts("\n=== NPCI ACK Response for RespPay ===\n")
3181
:-(
IO.puts(ack_body)
3182
:-(
IO.puts("\n=== END NPCI ACK ===\n")
3183
:-(
case UpiXmlSchema.parse_ack_response(ack_body) do
3184 {:ok, ack_data} ->
3185
:-(
IO.puts("Parsed NPCI ACK: ")
3186
:-(
IO.inspect(ack_data)
3187 {:error, reason} ->
3188
:-(
IO.puts("Failed to parse NPCI ACK: #{reason}")
3189 end
3190 {:ok, %{status: status, body: ack_body}} ->
3191
:-(
IO.puts("NPCI returned non-success status for RespPay: #{status}")
3192
:-(
IO.puts(ack_body)
3193 {:error, reason} ->
3194
:-(
IO.puts("Failed to send RespPay to NPCI: #{inspect(reason)}")
3195 end
3196 # Send ACK to NPCI (must be immediate)
3197
:-(
ack_xml = UpiXmlSchema.generate_ack_reqpay_response(validated_data.msg_id)
3198
:-(
case send_ack_to_npci(ack_xml, transaction.id) do
3199 {:ok, %{status: status, body: ack_body}} when status in 200..299 ->
3200
:-(
IO.puts("\n=== NPCI ACK Response for ReqPay ACK ===\n")
3201
:-(
IO.puts(ack_body)
3202
:-(
IO.puts("\n=== END NPCI ACK ===\n")
3203 {:ok, %{status: status, body: ack_body}} ->
3204
:-(
IO.puts("NPCI returned non-success status for ReqPay ACK: #{status}")
3205
:-(
IO.puts(ack_body)
3206 {:error, reason} ->
3207
:-(
IO.puts("Failed to send ACK to NPCI: #{inspect(reason)}")
3208 end
3209 :ok
3210 {:error, :insufficient_funds} ->
3211
:-(
generate_international_payment_error_response(validated_data, "10", "Debit has been failed")
3212 {:error, :risk_threshold_exceeded} ->
3213
:-(
generate_international_payment_error_response(validated_data, "16", "Risk threshold exceeded")
3214 {:error, :partner_timeout} ->
3215
:-(
start_international_timeout_escalation(validated_data)
3216
:-(
generate_international_payment_error_response(validated_data, "01", "Transaction is pending")
3217 {:error, :fx_conversion_failed} ->
3218
:-(
generate_international_payment_error_response(validated_data, "14", "FX conversion failed")
3219 {:error, :merchant_not_found} ->
3220
:-(
generate_international_payment_error_response(validated_data, "ZR", "Merchant not found")
3221 {:error, :partner_error} ->
3222
:-(
generate_international_payment_error_response(validated_data, "14", "External Error")
3223 {:error, reason} ->
3224
:-(
generate_international_payment_error_response(validated_data, "02", "Transaction failed: #{reason}")
3225 end
3226 {:error, reason} ->
3227
:-(
generate_international_payment_error_response(validated_data, "02", "Transaction creation failed: #{reason}")
3228 end
3229 end
3230 {:error, reason} ->
3231
:-(
generate_international_payment_error_response(parsed_data, "93", "Validation Error: #{reason}")
3232 end
3233 {:error, reason} ->
3234
:-(
default_parsed_data = %{
3235 msg_id: generate_message_id(),
3236 cust_ref: "",
3237 org_id: "UNKNOWN",
3238 txn_id: "UNKNOWN"
3239 }
3240
:-(
generate_international_payment_error_response(default_parsed_data, "ZH", "Invalid XML: #{reason}")
3241 end
3242 end
3243
3244 # ================================
3245 # International Payment Helper Functions
3246 # ================================
3247
3248 defp validate_international_payment_data(parsed_data) do
3249 # Enhanced validation for international payments
3250
:-(
cond do
3251
:-(
parsed_data[:purpose] != "11" ->
3252 {:error, "Invalid purpose code for international payment"}
3253
3254
:-(
parsed_data[:txn_type] not in ["CREDIT", "REVERSAL"] ->
3255 {:error, "Invalid transaction type for international payment"}
3256
3257
:-(
is_nil(parsed_data[:payee_addr]) or parsed_data[:payee_addr] == "" ->
3258 {:error, "Payee address is required"}
3259
3260
:-(
is_nil(parsed_data[:payer_addr]) or parsed_data[:payer_addr] == "" ->
3261 {:error, "Payer address is required"}
3262
3263
:-(
is_nil(parsed_data[:payee_amount]) or parsed_data[:payee_amount] == "" ->
3264 {:error, "Payment amount is required"}
3265
3266
:-(
parsed_data[:mid] && !validate_merchant_id(parsed_data[:mid]) ->
3267 {:error, "Invalid merchant ID format"}
3268
3269
:-(
parsed_data[:sp_risk_score] && !validate_risk_score(parsed_data[:sp_risk_score]) ->
3270 {:error, "Invalid SP risk score"}
3271
3272
:-(
parsed_data[:npci_risk_score] && !validate_risk_score(parsed_data[:npci_risk_score]) ->
3273 {:error, "Invalid NPCI risk score"}
3274
3275
:-(
true -> {:ok, parsed_data}
3276 end
3277 end
3278
3279 defp create_international_payment_transaction(validated_data) do
3280
:-(
transaction_data = %{
3281 id: generate_transaction_id(),
3282
:-(
external_reference: validated_data.org_txn_id,
3283
:-(
amount: validated_data.payee_amount,
3284
:-(
base_amount: validated_data.base_amount,
3285
:-(
base_currency: validated_data.base_curr,
3286
:-(
fx_rate: validated_data.fx_rate,
3287
:-(
markup: validated_data.markup,
3288
:-(
currency: validated_data.payee_currency || "INR",
3289
:-(
payer_vpa: validated_data.payer_addr,
3290
:-(
payee_vpa: validated_data.payee_addr,
3291
:-(
merchant_id: validated_data.mid,
3292
:-(
merchant_name: validated_data.legal || validated_data.payee_name,
3293
:-(
purpose: validated_data.purpose,
3294
:-(
transaction_type: validated_data.txn_type,
3295
:-(
risk_score_sp: validated_data.sp_risk_score,
3296
:-(
risk_score_npci: validated_data.npci_risk_score,
3297 device_info: extract_device_info(validated_data),
3298 status: :initiated,
3299 created_at: DateTime.utc_now(),
3300
:-(
expires_at: calculate_expiry_time(validated_data.expire_after)
3301 }
3302
3303 # Store transaction (in real implementation, this would go to database)
3304
:-(
case UpiInternationalService.create_transaction(transaction_data) do
3305
:-(
{:ok, transaction} -> {:ok, transaction}
3306
:-(
{:error, reason} -> {:error, reason}
3307 end
3308 end
3309
3310 defp process_international_payment_with_partner(transaction, validated_data) do
3311 # Enhanced processing for international payments
3312
:-(
case validate_risk_scores(validated_data) do
3313 :ok ->
3314
:-(
case perform_fx_conversion(validated_data) do
3315 {:ok, fx_details} ->
3316
:-(
case send_to_partner_with_merchant_details(transaction, validated_data, fx_details) do
3317
:-(
{:ok, partner_response} ->
3318 {:ok, Map.merge(partner_response, fx_details)}
3319
3320
:-(
{:error, reason} -> {:error, reason}
3321 end
3322
3323
:-(
{:error, _reason} -> {:error, :fx_conversion_failed}
3324 end
3325
3326
:-(
{:error, :risk_threshold_exceeded} -> {:error, :risk_threshold_exceeded}
3327 end
3328 end
3329
3330 defp validate_risk_scores(validated_data) do
3331
:-(
sp_score = String.to_integer(validated_data.sp_risk_score || "0")
3332
:-(
npci_score = String.to_integer(validated_data.npci_risk_score || "0")
3333
3334
:-(
cond do
3335
:-(
sp_score > 80 or npci_score > 80 -> {:error, :risk_threshold_exceeded}
3336
:-(
true -> :ok
3337 end
3338 end
3339
3340 defp perform_fx_conversion(validated_data) do
3341
:-(
case validated_data do
3342 %{base_amount: base_amount, fx_rate: fx_rate} when not is_nil(base_amount) and not is_nil(fx_rate) ->
3343
:-(
base = String.to_float(base_amount)
3344
:-(
rate = String.to_float(fx_rate)
3345
:-(
converted_amount = base * rate
3346
3347 {:ok, %{
3348 original_amount: base_amount,
3349 fx_rate: fx_rate,
3350 converted_amount: Float.to_string(converted_amount),
3351 conversion_timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
3352 }}
3353
3354
:-(
_ ->
3355 # No FX conversion needed or data missing
3356 {:ok, %{}}
3357 end
3358 end
3359
3360 defp send_to_partner_with_merchant_details(transaction, validated_data, fx_details) do
3361
:-(
partner_request = %{
3362
:-(
transaction_id: transaction.id,
3363
:-(
amount: validated_data.payee_amount,
3364
:-(
currency: validated_data.payee_currency || "INR",
3365
:-(
payer_vpa: validated_data.payer_addr,
3366
:-(
payee_vpa: validated_data.payee_addr,
3367 merchant_details: %{
3368
:-(
mid: validated_data.mid,
3369
:-(
legal_name: validated_data.legal,
3370
:-(
brand_name: validated_data.brand,
3371
:-(
franchise: validated_data.franchise,
3372
:-(
ownership_type: validated_data.ownership_type,
3373
:-(
merchant_type: validated_data.merchant_type,
3374
:-(
ifsc: validated_data.payee_ifsc,
3375
:-(
account_number: validated_data.payee_ac_num,
3376
:-(
account_type: validated_data.payee_ac_type
3377 },
3378 fx_details: fx_details,
3379 device_info: extract_device_info(validated_data),
3380 risk_scores: %{
3381
:-(
sp_score: validated_data.sp_risk_score,
3382
:-(
npci_score: validated_data.npci_risk_score
3383 }
3384 }
3385
3386 # Send to partner API (mock implementation)
3387
:-(
case UpiInternationalService.process_international_payment(partner_request) do
3388
:-(
{:ok, response} ->
3389 {:ok, %{
3390
:-(
resp_code: response.response_code,
3391
:-(
approval_num: response.approval_number,
3392
:-(
settlement_amount: response.settlement_amount,
3393
:-(
partner_txn_id: response.partner_transaction_id
3394 }}
3395
3396
:-(
{:error, reason} -> {:error, reason}
3397 end
3398 end
3399
3400 defp extract_device_info(validated_data) do
3401 %{
3402 mobile: Map.get(validated_data, :mobile),
3403
:-(
ip: validated_data.ip,
3404
:-(
geocode: validated_data.geocode,
3405
:-(
location: validated_data.location,
3406
:-(
device_type: validated_data.device_type,
3407 device_id: Map.get(validated_data, :device_id),
3408
:-(
os: validated_data.device_os,
3409
:-(
app: validated_data.device_app,
3410
:-(
capability: validated_data.device_capability
3411 }
3412
:-(
|> Enum.reject(fn {_k, v} -> is_nil(v) or v == "" end)
3413
:-(
|> Enum.into(%{})
3414 end
3415
3416 defp calculate_expiry_time(expire_after_minutes) do
3417
:-(
minutes = case expire_after_minutes do
3418
:-(
nil -> 30 # Default 30 minutes
3419
:-(
"" -> 30
3420 minutes_str when is_binary(minutes_str) ->
3421
:-(
case Integer.parse(minutes_str) do
3422
:-(
{minutes, _} when minutes >= 1 and minutes <= 64800 -> minutes
3423
:-(
_ -> 30
3424 end
3425
:-(
minutes when is_integer(minutes) -> minutes
3426 end
3427
3428
:-(
DateTime.utc_now() |> DateTime.add(minutes * 60, :second)
3429 end
3430
3431 defp start_international_timeout_escalation(validated_data) do
3432 # Start background process for international payment timeout handling
3433 # In real implementation, this would start a GenServer or use Oban for background jobs
3434
:-(
IO.puts("Starting international timeout escalation for txn: #{validated_data.org_txn_id}")
3435 :ok
3436 end
3437
3438 defp generate_international_payment_error_response(parsed_data, err_code, error_message) do
3439 # Ensure PAY subtype always uses CREDIT transaction type, even in error responses
3440
:-(
final_txn_type = case Map.get(parsed_data, :sub_type) do
3441
:-(
"PAY" -> "CREDIT"
3442
:-(
_ -> Map.get(parsed_data, :txn_type, "CREDIT")
3443 end
3444
3445
:-(
response_data = %{
3446 org_id: get_psp_org_id(),
3447 msg_id: generate_message_id(),
3448 req_msg_id: Map.get(parsed_data, :msg_id, "UNKNOWN"),
3449 result: "FAILURE",
3450 err_code: err_code,
3451 txn_id: Map.get(parsed_data, :txn_id, "UNKNOWN"),
3452 note: error_message,
3453 ref_id: Map.get(parsed_data, :ref_id, ""),
3454 cust_ref: Map.get(parsed_data, :cust_ref, ""),
3455 ref_url: Map.get(parsed_data, :ref_url, ""),
3456 txn_type: final_txn_type,
3457 sub_type: Map.get(parsed_data, :sub_type, ""),
3458 initiation_mode: Map.get(parsed_data, :initiation_mode, "QR"),
3459 org_txn_id: Map.get(parsed_data, :org_txn_id, "UNKNOWN"),
3460 org_rrn: Map.get(parsed_data, :org_rrn, ""),
3461 org_txn_date: Map.get(parsed_data, :org_txn_date, get_date()),
3462 ref_category: Map.get(parsed_data, :ref_category, ""),
3463 prod_type: Map.get(parsed_data, :prod_type, "UPI"),
3464 sp_risk_score: Map.get(parsed_data, :sp_risk_score, "0"),
3465 npci_risk_score: Map.get(parsed_data, :npci_risk_score, "0"),
3466 payee_addr: Map.get(parsed_data, :payee_addr, "UNKNOWN"),
3467 payee_code: Map.get(parsed_data, :payee_code, "0000"),
3468 org_amount: Map.get(parsed_data, :base_amount, Map.get(parsed_data, :payee_amount, Map.get(parsed_data, :amount, "0.00"))),
3469 resp_code: err_code,
3470 reg_name: Map.get(parsed_data, :merchant_legal_name, Map.get(parsed_data, :payee_name, "UNKNOWN")),
3471 ifsc: Map.get(parsed_data, :payee_ifsc, "UNKNOWN"),
3472 ac_num: Map.get(parsed_data, :payee_ac_num, "UNKNOWN"),
3473 acc_type: Map.get(parsed_data, :payee_ac_type, "CURRENT"),
3474
:-(
approval_num: "ERROR_#{System.unique_integer([:positive])}",
3475 sett_amount: "0.00",
3476 sett_currency: "INR",
3477 expire_ts: DateTime.utc_now() |> DateTime.add(300, :second) |> DateTime.to_iso8601()
3478 }
3479
3480
:-(
UpiXmlSchema.generate_resp_pay(response_data)
3481 end
3482
3483 defp validate_merchant_id(mid) do
3484 # Basic merchant ID validation - alphanumeric, 6-15 characters
3485
:-(
Regex.match?(~r/^[A-Za-z0-9]{6,15}$/, mid)
3486 end
3487
3488 defp validate_risk_score(score_str) do
3489
:-(
case Integer.parse(score_str) do
3490
:-(
{score, ""} when score >= 0 and score <= 100 -> true
3491
:-(
_ -> false
3492 end
3493 end
3494
3495 defp generate_approval_number do
3496
:-(
"APP" <> (:crypto.strong_rand_bytes(8) |> Base.encode16())
3497 end
3498
3499 # NPCI validation helper functions for Ref element attributes
3500
3501
:-(
defp validate_payee_code(nil), do: "0000"
3502
:-(
defp validate_payee_code(""), do: "0000"
3503 defp validate_payee_code(code) when is_binary(code) do
3504 # E17: REF.CODE MUST BE OF LENGTH 4 - pad or truncate to exactly 4 digits
3505 code
3506 |> String.replace(~r/[^0-9]/, "") # Keep only digits
3507 |> String.pad_leading(4, "0") # Pad with zeros if too short
3508
:-(
|> String.slice(0, 4) # Truncate if too long
3509 end
3510
:-(
defp validate_payee_code(_), do: "0000"
3511
3512
:-(
defp validate_ifsc_code(nil), do: ""
3513
:-(
defp validate_ifsc_code(""), do: ""
3514 defp validate_ifsc_code(ifsc) when is_binary(ifsc) do
3515 # E18: REF.IFSC MUST BE OF LENGTH 11 - pad or truncate to exactly 11 chars
3516
:-(
case String.length(ifsc) do
3517
:-(
11 -> ifsc
3518
:-(
len when len < 11 -> String.pad_trailing(ifsc, 11, "X") # Pad with X if too short
3519
:-(
_ -> String.slice(ifsc, 0, 11) # Truncate if too long
3520 end
3521 end
3522
:-(
defp validate_ifsc_code(_), do: ""
3523
3524
:-(
defp validate_account_number(nil), do: ""
3525
:-(
defp validate_account_number(""), do: ""
3526 defp validate_account_number(ac_num) when is_binary(ac_num) do
3527 # E16: REF.ACNUM MUST BE OF MINLENGTH 1 MAXLENGTH 16
3528
:-(
case String.length(ac_num) do
3529
:-(
0 -> ""
3530
:-(
len when len > 16 -> String.slice(ac_num, 0, 16) # Truncate if too long
3531
:-(
_ -> ac_num # Keep as is if within range
3532 end
3533 end
3534
:-(
defp validate_account_number(_), do: ""
3535
3536
:-(
defp validate_account_type(nil), do: "CURRENT"
3537
:-(
defp validate_account_type(""), do: "CURRENT"
3538 defp validate_account_type(acc_type) when is_binary(acc_type) do
3539 # E19: REF.ACCTYPE MUST BE PRESENT - ensure valid account type
3540
:-(
case String.upcase(acc_type) do
3541
:-(
type when type in ["CURRENT", "SAVINGS", "CREDIT", "NRE", "NRO"] -> type
3542
:-(
_ -> "CURRENT" # Default fallback
3543 end
3544 end
3545
:-(
defp validate_account_type(_), do: "CURRENT"
3546
3547 defp get_date do
3548
:-(
Date.utc_today() |> Date.to_iso8601()
3549 end
3550
3551 defp get_timestamp do
3552
:-(
case Application.get_env(:da_product_app, :use_timex_for_timestamps, true) do
3553 true ->
3554 DateTime.utc_now()
3555 |> Timex.to_datetime("Asia/Kolkata")
3556
:-(
|> Timex.format!("{YYYY}-{0M}-{0D}T{h24}:{m}:{s}+05:30")
3557 _ ->
3558
:-(
DateTime.utc_now() |> DateTime.to_iso8601()
3559 end
3560 end
3561
3562 defp get_expiry_timestamp do
3563
:-(
case Application.get_env(:da_product_app, :use_timex_for_timestamps, true) do
3564 true ->
3565 DateTime.utc_now()
3566 |> DateTime.add(30, :minute)
3567 |> Timex.to_datetime("Asia/Kolkata")
3568
:-(
|> Timex.format!("{YYYY}-{0M}-{0D}T{h24}:{m}:{s}+05:30")
3569 _ ->
3570 DateTime.utc_now()
3571 |> DateTime.add(30, :minute)
3572
:-(
|> DateTime.to_iso8601()
3573 end
3574 end
3575
3576 defp generate_stan do
3577
:-(
:rand.uniform(999999) |> Integer.to_string() |> String.pad_leading(6, "0")
3578 end
3579
3580 # Send ACK response to NPCI immediately
3581 defp send_ack_to_npci(ack_xml, txn_id) do
3582
:-(
npci_endpoint = get_npci_reqpay_endpoint() <> (txn_id || "")
3583
3584 # Clean up XML: strip BOM, leading/trailing whitespace
3585
:-(
ack_xml_clean = ack_xml
3586 |> :unicode.characters_to_binary(:utf8)
3587 |> String.trim()
3588
3589
:-(
Logger.info("=== SENDING ACK TO NPCI ===")
3590
:-(
Logger.info("NPCI Endpoint: #{npci_endpoint}")
3591
:-(
Logger.info("Transaction ID: #{txn_id}")
3592
:-(
Logger.info("ACK XML being sent:")
3593
:-(
Logger.info(ack_xml_clean)
3594
:-(
Logger.info("=== END ACK XML ===")
3595
3596
:-(
headers = [
3597 {"Content-Type", "application/xml"},
3598 {"Accept", "application/xml"},
3599 {"X-API-Version", "2.0"}
3600 ]
3601
3602
:-(
Logger.info("--- HTTP Request to NPCI ---")
3603
:-(
Logger.info("Endpoint: #{npci_endpoint}")
3604
:-(
Logger.info("Headers: #{inspect(headers)}")
3605
:-(
Logger.info("Body (UTF-8, trimmed): #{ack_xml_clean}")
3606
:-(
Logger.info("--- END HTTP Request ---")
3607
3608
:-(
case Req.post(npci_endpoint,
3609 body: ack_xml_clean,
3610 headers: headers,
3611 receive_timeout: 10_000
3612 ) do
3613 {:ok, %{status: status, body: body} = response} when status in 200..299 ->
3614
:-(
Logger.info("Successfully sent ACK to NPCI for txn_id: #{txn_id}, status: #{status}")
3615
:-(
Logger.info("NPCI Response Body: #{inspect(body)}")
3616 {:ok, body}
3617
3618 {:ok, %{status: status, body: body}} ->
3619
:-(
Logger.error("NPCI ACK returned non-success status #{status} for txn_id: #{txn_id}, body: #{inspect(body)}")
3620
:-(
{:error, "NPCI ACK error: #{status}"}
3621
3622 {:error, reason} ->
3623
:-(
Logger.error("Failed to send ACK to NPCI for txn_id: #{txn_id}, reason: #{inspect(reason)}")
3624 {:error, reason}
3625 end
3626 end
3627
3628 # Send RespPay response to NPCI
3629 defp send_resp_pay_to_npci(resp_pay_xml, txn_id) do
3630 # For RespPay, use ReqPay endpoint as per new requirement
3631
:-(
npci_endpoint = get_npci_resppay_endpoint() <> txn_id
3632
3633
:-(
Logger.info("=== SENDING RESPPAY TO NPCI ===")
3634
:-(
Logger.info("NPCI RespPay Endpoint URL: #{npci_endpoint}")
3635
:-(
Logger.info("Transaction ID: #{txn_id}")
3636
:-(
Logger.info("RespPay XML being sent:")
3637
3638 # Handle both {:ok, xml} and xml formats
3639
:-(
xml_to_send = case resp_pay_xml do
3640
:-(
{:ok, xml} -> xml
3641
:-(
xml when is_binary(xml) -> xml
3642
:-(
_ -> inspect(resp_pay_xml)
3643 end
3644
3645
:-(
Logger.info(xml_to_send)
3646
:-(
Logger.info("=== END RESPPAY XML ===")
3647
3648
:-(
headers = [
3649 {"Content-Type", "application/xml"},
3650 {"Accept", "application/xml"},
3651 {"X-API-Version", "2.0"}
3652 ]
3653
3654
:-(
case Req.post(npci_endpoint,
3655 body: xml_to_send,
3656 headers: headers,
3657 receive_timeout: 30_000
3658 ) do
3659 {:ok, %{status: status} = response} when status in 200..299 ->
3660
:-(
Logger.info(" Successfully sent RespPay to NPCI for txn_id: #{txn_id}, status: #{status}")
3661
:-(
Logger.info(" NPCI Response Body: #{inspect(response.body)}")
3662
:-(
Logger.debug(" NPCI Response Headers: #{inspect(response.headers)}")
3663
:-(
Logger.debug(" NPCI Response Status: #{inspect(response.status)}")
3664
:-(
Logger.debug(" NPCI Full Response: #{inspect(response)}")
3665 {:ok, response}
3666
3667 {:ok, %{status: status, body: body}} ->
3668
:-(
Logger.error("NPCI RespPay returned non-success status #{status} for txn_id: #{txn_id}, body: #{inspect(body)}")
3669
:-(
{:error, "NPCI RespPay error: #{status}"}
3670
3671 {:error, reason} ->
3672
:-(
Logger.error(" Failed to send RespPay to NPCI for txn_id: #{txn_id}, reason: #{inspect(reason)}")
3673 {:error, reason}
3674 end
3675 end
3676
3677 # Send error RespPay response to NPCI
3678 defp send_error_resp_pay_to_npci(error_xml, txn_id) do
3679
:-(
send_resp_pay_to_npci(error_xml, txn_id)
3680 end
3681
3682 # Get NPCI ReqPay endpoint
3683 defp get_npci_reqpay_endpoint do
3684
:-(
Application.get_env(:da_product_app, :npci_reqpay_endpoint, "https://precert.nfinite.in/iupi/ReqPay/2.0/urn:txnid:")
3685 end
3686
3687 defp get_npci_resppay_endpoint do
3688
:-(
Application.get_env(:da_product_app, :npci_resppay_endpoint, "https://precert.nfinite.in/iupi/RespPay/2.0/urn:txnid:")
3689 end
3690
3691 # Generate RRN (Retrieval Reference Number)
3692 defp generate_rrn do
3693
:-(
"RRN" <> (:crypto.strong_rand_bytes(6) |> Base.encode16())
3694 end
3695
3696 # Create QR payment transaction
3697 defp create_qr_payment_transaction(attrs) do
3698 # This would typically create a transaction record in the database
3699 # For now, return a mock transaction
3700 transaction = %{
3701 id: generate_transaction_id(),
3702 txn_id: attrs.txn_id,
3703 msg_id: attrs.msg_id,
3704 status: :initiated,
3705 inr_amount: attrs.amount,
3706 currency: attrs.currency,
3707 payee_addr: attrs.payee_addr,
3708 payee_name: attrs.payee_name,
3709 created_at: DateTime.utc_now()
3710 }
3711
3712 Logger.info("Created QR payment transaction: #{transaction.id}")
3713 {:ok, transaction}
3714 end
3715
3716 # Process QR payment with partner
3717 defp process_qr_payment_with_partner(transaction, merchant_info) do
3718
:-(
Logger.info(" Processing QR payment with partner for amount: #{transaction.inr_amount} #{transaction.currency}")
3719
3720 # Mock partner processing - in real implementation, this would call external APIs
3721 # For now, simulate successful processing
3722
:-(
partner_response = %{
3723 status: "SUCCESS",
3724 approval_number: generate_approval_number(),
3725 resp_code: "00",
3726
:-(
settlement_amount: transaction.inr_amount,
3727
:-(
settlement_currency: transaction.currency,
3728 settlement_id: "SETT" <> (:crypto.strong_rand_bytes(4) |> Base.encode16())
3729 }
3730
3731
:-(
Logger.info(" Partner processing successful: #{partner_response.approval_number}")
3732 {:ok, partner_response}
3733 end
3734
3735 # Flexible QR payment XML parser that handles different purpose codes
3736 defp parse_qr_payment_xml(xml_string) do
3737
:-(
try do
3738
:-(
Logger.debug("🔍 Attempting to parse QR payment XML with international parser first...")
3739
3740 # Try international parser first, but handle validation errors gracefully
3741
:-(
case UpiXmlSchema.parse_req_pay(xml_string) do
3742 {:ok, parsed_data} ->
3743
:-(
Logger.info(" International parser succeeded")
3744 {:ok, parsed_data}
3745 {:error, reason} when is_binary(reason) ->
3746
:-(
Logger.warning(" International parser failed: #{reason}")
3747
3748 # If international parser fails due to validation, try basic extraction
3749
:-(
if String.contains?(reason, "Purpose must be") or
3750
:-(
String.contains?(reason, "Product type must be") or
3751
:-(
String.contains?(reason, "Payer code must be") or
3752
:-(
String.contains?(reason, "purpose") or
3753
:-(
String.contains?(reason, "Purpose") or
3754
:-(
String.contains?(reason, "code must be") do
3755
:-(
Logger.info(" International validation failed, using flexible QR parser: #{reason}")
3756
:-(
extract_qr_payment_data_flexible(xml_string)
3757 else
3758
:-(
Logger.error(" Non-validation error from international parser: #{reason}")
3759 {:error, reason}
3760 end
3761 error ->
3762
:-(
Logger.error(" Unexpected error format from international parser: #{inspect(error)}")
3763
:-(
error
3764 end
3765 rescue
3766
:-(
e ->
3767
:-(
Logger.error(" Exception during XML parsing: #{inspect(e)}")
3768 {:error, "XML parsing failed: #{inspect(e)}"}
3769 end
3770 end
3771
3772 # Flexible extraction for QR payments that bypasses strict international validation
3773 defp extract_qr_payment_data_flexible(xml_string) do
3774
:-(
try do
3775 # First try to clean the XML and handle entity references
3776
:-(
cleaned_xml = xml_string
3777 |> String.replace("&amp;", "&") # Convert back to normal ampersand first
3778 |> String.replace("&", "&amp;") # Then properly escape all ampersands
3779 |> String.replace("&amp;amp;", "&amp;") # Fix double escaping
3780
3781 # Log the complete request payload for debugging
3782
:-(
Logger.info("=" |> String.duplicate(80))
3783
:-(
Logger.info(" COMPLETE REQUEST PAYLOAD - RAW XML")
3784
:-(
Logger.info("=" |> String.duplicate(80))
3785
:-(
Logger.info("#{xml_string}")
3786
:-(
Logger.info("=" |> String.duplicate(80))
3787
:-(
Logger.info(" COMPLETE REQUEST PAYLOAD - CLEANED XML")
3788
:-(
Logger.info("=" |> String.duplicate(80))
3789
:-(
Logger.info("#{cleaned_xml}")
3790
:-(
Logger.info("=" |> String.duplicate(80))
3791
3792 # Extract basic required fields without strict validation
3793
:-(
doc = SweetXml.parse(cleaned_xml)
3794
3795 # Extract basic header info - try both namespaced and local-name approaches
3796
:-(
msg_id = SweetXml.xpath(doc, ~x"//*[local-name()='Head']/@msgId"s) ||
3797
:-(
SweetXml.xpath(doc, ~x"//Head/@msgId"s) ||
3798
:-(
generate_message_id()
3799
3800
:-(
txn_id = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@id"s) ||
3801
:-(
SweetXml.xpath(doc, ~x"//Txn/@id"s) ||
3802
:-(
generate_transaction_id()
3803
3804
:-(
org_id = SweetXml.xpath(doc, ~x"//*[local-name()='Head']/@orgId"s) ||
3805
:-(
SweetXml.xpath(doc, ~x"//Head/@orgId"s) ||
3806 "UNKNOWN"
3807
3808 # Extract QR payload from QR section
3809
:-(
qr_payload = SweetXml.xpath(doc, ~x"//*[local-name()='QR']/@query"s) ||
3810
:-(
SweetXml.xpath(doc, ~x"//QR/@query"s) ||
3811 ""
3812
3813 # Extract other basic fields
3814
:-(
note = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@note"s) ||
3815
:-(
SweetXml.xpath(doc, ~x"//Txn/@note"s) ||
3816 "QR Payment"
3817
3818
:-(
cust_ref = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@custRef"s) ||
3819
:-(
SweetXml.xpath(doc, ~x"//Txn/@custRef"s) ||
3820 ""
3821
3822 # Extract org_txn_id (this is what create_payment_transaction expects)
3823
:-(
org_txn_id = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@orgTxnId"s) ||
3824
:-(
SweetXml.xpath(doc, ~x"//Txn/@orgTxnId"s) ||
3825
:-(
txn_id # Fallback to txn_id if orgTxnId not found
3826
3827 # Extract purpose
3828
:-(
purpose = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@purpose"s) ||
3829
:-(
SweetXml.xpath(doc, ~x"//Txn/@purpose"s) ||
3830 "11" # Default to international purpose
3831
3832 # Extract transaction type
3833
:-(
txn_type = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@type"s) ||
3834
:-(
SweetXml.xpath(doc, ~x"//Txn/@type"s) ||
3835 "CREDIT"
3836
3837 # Extract sub type
3838
:-(
sub_type = SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@subType"s) ||
3839
:-(
SweetXml.xpath(doc, ~x"//Txn/@subType"s) ||
3840 "PAY"
3841
3842 # Extract payee information
3843
:-(
payee_addr = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/@addr"s) ||
3844
:-(
SweetXml.xpath(doc, ~x"//Payee/@addr"s) ||
3845 ""
3846
3847
:-(
payee_name = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/@name"s) ||
3848
:-(
SweetXml.xpath(doc, ~x"//Payee/@name"s) ||
3849 ""
3850
3851 # Extract payee code
3852
:-(
payee_code = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/@code"s) ||
3853
:-(
SweetXml.xpath(doc, ~x"//Payee/@code"s) ||
3854 "0000"
3855
3856 # Extract payee account details
3857
:-(
payee_ifsc = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Ac']/*[local-name()='Detail'][@name='IFSC']/@value"s) ||
3858
:-(
SweetXml.xpath(doc, ~x"//Payee/Ac/Detail[@name='IFSC']/@value"s) ||
3859 ""
3860
3861
:-(
payee_ac_num = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Ac']/*[local-name()='Detail'][@name='ACNUM']/@value"s) ||
3862
:-(
SweetXml.xpath(doc, ~x"//Payee/Ac/Detail[@name='ACNUM']/@value"s) ||
3863 ""
3864
3865
:-(
payee_ac_type = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Ac']/*[local-name()='Detail'][@name='ACTYPE']/@value"s) ||
3866
:-(
SweetXml.xpath(doc, ~x"//Payee/Ac/Detail[@name='ACTYPE']/@value"s) ||
3867 "CURRENT"
3868
3869 # Extract merchant details
3870
:-(
merchant_legal_name = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Merchant']/*[local-name()='Name']/@legal"s) ||
3871
:-(
SweetXml.xpath(doc, ~x"//Payee/Merchant/Name/@legal"s) ||
3872
:-(
payee_name
3873
3874
:-(
merchant_brand_name = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Merchant']/*[local-name()='Name']/@brand"s) ||
3875
:-(
SweetXml.xpath(doc, ~x"//Payee/Merchant/Name/@brand"s) ||
3876
:-(
payee_name
3877
3878 # Extract payer information (needed for create_payment_transaction)
3879
:-(
payer_addr = SweetXml.xpath(doc, ~x"//*[local-name()='Payer']/@addr"s) ||
3880
:-(
SweetXml.xpath(doc, ~x"//Payer/@addr"s) ||
3881 ""
3882
3883
:-(
payer_name = SweetXml.xpath(doc, ~x"//*[local-name()='Payer']/@name"s) ||
3884
:-(
SweetXml.xpath(doc, ~x"//Payer/@name"s) ||
3885 ""
3886
3887 # Extract amount information (try multiple possible locations)
3888
:-(
amount = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Amount']/@value"s) ||
3889
:-(
SweetXml.xpath(doc, ~x"//Payee/Amount/@value"s) ||
3890
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Payer']/*[local-name()='Amount']/@value"s) ||
3891
:-(
SweetXml.xpath(doc, ~x"//Payer/Amount/@value"s) ||
3892
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Amount']/@value"s) ||
3893
:-(
SweetXml.xpath(doc, ~x"//Amount/@value"s) ||
3894 "0.00"
3895
3896
:-(
currency = SweetXml.xpath(doc, ~x"//*[local-name()='Payee']/*[local-name()='Amount']/@curr"s) ||
3897
:-(
SweetXml.xpath(doc, ~x"//Payee/Amount/@curr"s) ||
3898
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Payer']/*[local-name()='Amount']/@curr"s) ||
3899
:-(
SweetXml.xpath(doc, ~x"//Payer/Amount/@curr"s) ||
3900
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Amount']/@curr"s) ||
3901
:-(
SweetXml.xpath(doc, ~x"//Amount/@curr"s) ||
3902 "INR"
3903
3904
:-(
sp_risk_score =
3905
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='RiskScores']/*[local-name()='Score'][@provider='sp']/@value"s) ||
3906
:-(
SweetXml.xpath(doc, ~x"//RiskScores/Score[@provider='sp']/@value"s) ||
3907
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Score'][@provider='sp']/@value"s) ||
3908 ""
3909
3910
:-(
npci_risk_score =
3911
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='RiskScores']/*[local-name()='Score'][@provider='npci']/@value"s) ||
3912
:-(
SweetXml.xpath(doc, ~x"//RiskScores/Score[@provider='npci']/@value"s) ||
3913
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Score'][@provider='npci']/@value"s) ||
3914 ""
3915 # Add debug logging to see what we're actually extracting
3916
:-(
Logger.debug(" XML extraction results:")
3917
:-(
Logger.debug(" msg_id: #{inspect(msg_id)}")
3918
:-(
Logger.debug(" txn_id: #{inspect(txn_id)}")
3919
:-(
Logger.debug(" org_txn_id: #{inspect(org_txn_id)}")
3920
:-(
Logger.debug(" amount: #{inspect(amount)}")
3921
:-(
Logger.debug(" currency: #{inspect(currency)}")
3922
3923
:-(
parsed_data = %{
3924 msg_id: msg_id,
3925 txn_id: txn_id,
3926 org_id: org_id,
3927 org_txn_id: org_txn_id, # Add missing org_txn_id field
3928 note: note,
3929 cust_ref: cust_ref,
3930 ref_id: cust_ref, # Use cust_ref as ref_id if ref_id not present
3931 purpose: purpose, # Use extracted purpose instead of hardcoded
3932 txn_type: txn_type, # Use extracted type instead of hardcoded
3933 sub_type: sub_type, # Use extracted sub_type
3934 qr_payload: qr_payload,
3935 sp_risk_score: sp_risk_score,
3936 npci_risk_score: npci_risk_score,
3937 # Fields expected by create_payment_transaction
3938 payer_addr: payer_addr,
3939 payer_name: payer_name,
3940 payee_addr: payee_addr,
3941 payee_name: payee_name,
3942 amount: amount, # Field name expected by create_payment_transaction
3943 currency: currency, # Field name expected by create_payment_transaction
3944
3945 # Payee account and merchant details for RespPay
3946 payee_code: payee_code,
3947 payee_ifsc: payee_ifsc,
3948 payee_ac_num: payee_ac_num,
3949 payee_ac_type: payee_ac_type,
3950 merchant_legal_name: merchant_legal_name,
3951 merchant_brand_name: merchant_brand_name,
3952
3953 # Keep the original field names for backward compatibility
3954 payee_amount: amount,
3955 payee_currency: currency
3956 }
3957
3958
:-(
Logger.info("Flexible QR parsing successful, extracted: msg_id=#{msg_id}, txn_id=#{txn_id}, org_txn_id=#{org_txn_id}, amount=#{amount} #{currency}")
3959 {:ok, parsed_data}
3960
3961 rescue
3962
:-(
e -> {:error, "Flexible QR parsing failed: #{inspect(e)}"}
3963 end
3964 end
3965
3966 # Extract basic XML info for emergency ACK generation
3967 defp extract_basic_xml_info(xml_string) do
3968
:-(
try do
3969 # First try to clean the XML and handle entity references
3970
:-(
cleaned_xml = xml_string
3971 |> String.replace("&amp;", "&") # Convert back to normal ampersand first
3972 |> String.replace("&", "&amp;") # Then properly escape all ampersands
3973 |> String.replace("&amp;amp;", "&amp;") # Fix double escaping
3974
3975
:-(
doc = SweetXml.parse(cleaned_xml)
3976
3977
:-(
msg_id = SweetXml.xpath(doc, ~x"//upi:Head/@msgId"s) ||
3978
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Head']/@msgId"s) ||
3979
:-(
generate_message_id()
3980
3981
:-(
txn_id = SweetXml.xpath(doc, ~x"//upi:Txn/@id"s) ||
3982
:-(
SweetXml.xpath(doc, ~x"//*[local-name()='Txn']/@id"s) ||
3983
:-(
generate_transaction_id()
3984
3985
:-(
%{
3986 msg_id: msg_id,
3987 txn_id: txn_id,
3988 cust_ref: ""
3989 }
3990 rescue
3991
:-(
error ->
3992
:-(
Logger.warning("XML parsing failed: #{inspect(error)}, trying regex fallback")
3993
3994 # Fallback: try to extract using regex
3995
:-(
msg_id = case Regex.run(~r/msgId="([^"]+)"/, xml_string) do
3996
:-(
[_, id] -> id
3997
:-(
_ -> generate_message_id()
3998 end
3999
4000
:-(
txn_id = case Regex.run(~r/\bid="([^"]+)"/, xml_string) do
4001
:-(
[_, id] -> id
4002
:-(
_ -> generate_transaction_id()
4003 end
4004
4005
:-(
%{
4006 msg_id: msg_id,
4007 txn_id: txn_id,
4008 cust_ref: ""
4009 }
4010 end
4011 end
4012
4013 @doc """
4014 Check if QR code has expired based on QRts timestamp and validity window.
4015 Returns :ok if valid, {:error, :expired} if expired, or {:error, reason} if parsing fails.
4016 """
4017 defp maybe_handle_qr_expiry(parsed_data) do
4018 require Logger
4019
4020 # Get QR validity minutes from config
4021
:-(
qr_validity_minutes = Application.get_env(:da_product_app, :qr_validity_minutes, 30)
4022
4023 # Extract QRts from payload using existing helper
4024
:-(
qr_ts_string = UpiXmlSchema.extract_qr_ts_from_payload(parsed_data)
4025
4026
:-(
case qr_ts_string do
4027 nil ->
4028
:-(
Logger.debug("No QRts found in QR payload, proceeding without expiry check")
4029 :ok
4030
4031 qr_ts_string when is_binary(qr_ts_string) ->
4032
:-(
case DateTime.from_iso8601(qr_ts_string) do
4033 {:ok, qr_ts_dt, _offset} ->
4034
:-(
now = DateTime.utc_now()
4035
:-(
diff_seconds = DateTime.diff(now, qr_ts_dt)
4036
:-(
expired_by_window = diff_seconds > qr_validity_minutes * 60
4037
4038 # Optional: Check against transaction timestamp if available
4039
:-(
expired_by_txn_ts = case Map.get(parsed_data, :ts) do
4040
:-(
nil -> false
4041 txn_ts_string ->
4042
:-(
case DateTime.from_iso8601(txn_ts_string) do
4043 {:ok, txn_ts_dt, _offset} ->
4044
:-(
DateTime.compare(qr_ts_dt, txn_ts_dt) == :lt
4045
:-(
_ -> false
4046 end
4047 end
4048
4049
:-(
Logger.info("QRts parsed #{qr_ts_string}; expired_by_window=#{expired_by_window}; expired_by_txn_ts=#{expired_by_txn_ts}")
4050
4051
:-(
if expired_by_window do
4052 {:error, :expired}
4053 else
4054 :ok
4055 end
4056
4057 {:error, reason} ->
4058
:-(
Logger.debug("Failed to parse QRts '#{qr_ts_string}': #{inspect(reason)}, proceeding without expiry check")
4059 :ok
4060 end
4061 end
4062 end
4063
4064 @doc """
4065 Generate and send PE (Payment Expired) error RespPay for expired QR codes.
4066 """
4067 defp handle_expired_qr_response(parsed_data) do
4068 require Logger
4069
4070
:-(
response_data = %{
4071 org_id: get_psp_org_id(),
4072 msg_id: generate_message_id(),
4073
:-(
req_msg_id: parsed_data.msg_id,
4074 result: "FAILURE",
4075 err_code: "PE",
4076
:-(
txn_id: parsed_data.txn_id,
4077
:-(
cust_ref: Map.get(parsed_data, :cust_ref) || generate_customer_reference(),
4078
:-(
ref_id: Map.get(parsed_data, :ref_id) || generate_reference_id(),
4079 ref_url: Map.get(parsed_data, :ref_url, ""), # CRITICAL: Include original ref_url from ReqPay
4080 note: Map.get(parsed_data, :note, "QR Code Expired"), # CRITICAL: Use original note for T03 compliance
4081
:-(
txn_type: parsed_data.txn_type || "CREDIT", # CRITICAL: Use original txn_type from ReqPay for T07 compliance
4082 expire_ts: Map.get(parsed_data, :expire_ts), # CRITICAL: Include original expire_ts
4083 org_txn_id: Map.get(parsed_data, :org_txn_id, ""), # CRITICAL: Include original org_txn_id
4084
4085 # CRITICAL: Include all Payee/Ref attributes from ReqPay to fix NPCI validation errors
4086 seq_num: Map.get(parsed_data, :payee_seq_num, "1"),
4087 payee_addr: Map.get(parsed_data, :payee_addr, ""),
4088 payee_code: validate_payee_code(Map.get(parsed_data, :payee_code)), # E17 fix: exactly 4 digits
4089 org_amount: "0.00", # CRITICAL: For failure cases, must be 0.00 (E14 error fix)
4090 resp_code: "PE", # CRITICAL: Response code must be present (E04, E13 error fix)
4091 reg_name: Map.get(parsed_data, :payee_name, ""),
4092 ifsc: validate_ifsc_code(Map.get(parsed_data, :payee_ifsc)), # E18 fix: exactly 11 chars
4093 ac_num: validate_account_number(Map.get(parsed_data, :payee_ac_num)), # E16 fix: 1-16 chars
4094 acc_type: validate_account_type(Map.get(parsed_data, :payee_ac_type)), # E19 fix: must be present
4095 approval_num: generate_approval_number(),
4096 sett_amount: "0.00", # CRITICAL: For failure cases, settlement amount must be 0.00 (E14 error fix)
4097 sett_currency: Map.get(parsed_data, :payee_currency, "INR"),
4098
4099 request_data: parsed_data # Include original request data for proper field matching
4100 }
4101
4102
:-(
case UpiXmlSchema.generate_resp_pay(response_data) do
4103 {:ok, resp_pay_xml} ->
4104
:-(
Logger.info("Sending PE RespPay for expired QR - msg_id: #{parsed_data.msg_id}, txn_id: #{parsed_data.txn_id}")
4105
4106 # Send to NPCI
4107
:-(
send_result = send_resp_pay_to_npci(resp_pay_xml, parsed_data.txn_id)
4108
:-(
Logger.info("PE RespPay send result: #{inspect(send_result)}")
4109
4110 # Store response XML hash if QR validation exists (best effort)
4111
:-(
try do
4112
:-(
if qr_validation_id = get_qr_validation_id_by_msg_id(parsed_data.msg_id) do
4113
:-(
QRValidationService.store_response_xml_hash(qr_validation_id, resp_pay_xml)
4114 end
4115 rescue
4116
:-(
error ->
4117
:-(
Logger.warning("Failed to store response XML hash: #{inspect(error)}")
4118 end
4119
4120 {:ok, :expired_handled}
4121
4122 error ->
4123
:-(
Logger.error("Failed to generate PE RespPay: #{inspect(error)}")
4124 {:error, :response_generation_failed}
4125 end
4126 end
4127
4128 # Helper to find QR validation ID by message ID (best effort)
4129 defp get_qr_validation_id_by_msg_id(msg_id) do
4130
:-(
try do
4131
:-(
query = from qv in DaProductApp.QRValidation.QRValidation,
4132 where: qv.msg_id == ^msg_id,
4133 select: qv.id,
4134 limit: 1
4135
4136
:-(
Repo.one(query)
4137 rescue
4138
:-(
_ -> nil
4139 end
4140 end
4141 end
Line Hits Source