cover/Elixir.DaProductAppWeb.ApiDocsLive.html

1
:-(
defmodule DaProductAppWeb.ApiDocsLive do
2 use DaProductAppWeb, :live_view
3
4 alias DaProductApp.Accounts
5
6
:-(
def mount(_params, session, socket) do
7
:-(
case get_current_user(session) do
8
:-(
nil ->
9 {:ok, redirect(socket, to: ~p"/login")}
10
11 user ->
12
:-(
if has_access?(user) do
13 # Initialize state
14
:-(
socket =
15 socket
16 |> assign(
17 current_user: user,
18 user: user, # Keep for backward compatibility
19 api_key: nil,
20 page_title: "API Documentation",
21 current_page: :api_docs,
22 api_groups: get_api_groups(),
23 selected_group: nil,
24 selected_endpoint: nil,
25 search_term: "",
26 filtered_endpoints: [],
27 show_endpoint_modal: false,
28 test_request_body: "",
29 test_response: nil,
30 api_key: nil,
31 testing_target_url: nil,
32 testing_endpoint: false,
33 request_validation: nil
34 )
35 |> filter_endpoints()
36
37 {:ok, socket}
38 else
39 {:ok,
40 socket
41 |> put_flash(:error, "Access denied")
42 |> redirect(to: ~p"/login")}
43 end
44 end
45 end
46
47
:-(
def handle_params(%{"group" => group, "endpoint" => endpoint_id}, _uri, socket) do
48
:-(
endpoint = get_endpoint_by_id(endpoint_id)
49
50
:-(
socket =
51 socket
52 |> assign(
53 selected_group: group,
54 selected_endpoint: endpoint,
55 show_endpoint_modal: true,
56 test_request_body: get_default_request_body(endpoint),
57 test_response: nil
58 )
59
60 {:noreply, socket}
61 end
62
63
:-(
def handle_params(%{"group" => group}, _uri, socket) do
64
:-(
socket =
65 socket
66 |> assign(
67 selected_group: group,
68 selected_endpoint: nil,
69 show_endpoint_modal: false
70 )
71 |> filter_endpoints()
72
73 {:noreply, socket}
74 end
75
76
:-(
def handle_params(_params, _uri, socket) do
77
:-(
socket =
78 socket
79 |> assign(
80 selected_group: nil,
81 selected_endpoint: nil,
82 show_endpoint_modal: false
83 )
84 |> filter_endpoints()
85
86 {:noreply, socket}
87 end
88
89
:-(
def handle_event("search", %{"search_term" => search_term}, socket) do
90
:-(
socket =
91 socket
92 |> assign(search_term: search_term)
93 |> filter_endpoints()
94
95 {:noreply, socket}
96 end
97
98
:-(
def handle_event("select_group", %{"group" => group}, socket) do
99 {:noreply, push_patch(socket, to: ~p"/api-docs/#{group}")}
100 end
101
102
:-(
def handle_event("test_endpoint", %{"request_body" => request_body} = params, socket) do
103
:-(
endpoint = socket.assigns.selected_endpoint
104
:-(
api_key = Map.get(params, "api_key", "")
105
106 # If the form submitted a testing_url (hidden input), prefer that over the
107 # current socket assign. This ensures path-only or full-URL overrides entered
108 # by the user are respected for the test request.
109
:-(
form_override = Map.get(params, "testing_url")
110
:-(
override = if form_override in ["", nil], do: socket.assigns.testing_target_url, else: String.trim(form_override)
111
112
:-(
socket =
113 socket
114 |> assign(testing_endpoint: true, test_request_body: request_body, api_key: api_key, testing_target_url: override)
115
116 # Try to perform a real HTTP request (non-blocking). If we can't determine
117 # a real target URL or the request fails, fall back to the simulated response.
118
:-(
parent = self()
119
:-(
Task.start(fn ->
120
:-(
response = perform_real_api_call(endpoint, request_body, api_key, override)
121
:-(
send(parent, {:test_result, response})
122 end)
123
124 {:noreply, socket}
125 end
126
127
:-(
def handle_event("test_with_sample", _params, socket) do
128
:-(
endpoint = socket.assigns.selected_endpoint
129
:-(
sample_body = get_default_request_body(endpoint)
130
131 # Load the sample into the request editor but do not auto-send.
132 # User can review and then click "Test Endpoint" to submit.
133
:-(
socket =
134 socket
135 |> assign(test_request_body: sample_body)
136
137 {:noreply, socket}
138 end
139
140
:-(
def handle_event("validate_request", %{"request_body" => request_body}, socket) do
141
:-(
endpoint = socket.assigns.selected_endpoint
142
143
:-(
validation_result = validate_request_body(endpoint, request_body)
144
145
:-(
socket = assign(socket, request_validation: validation_result)
146
147 {:noreply, socket}
148 end
149
150
:-(
def handle_event("load_template", %{"template" => template_type}, socket) do
151
:-(
endpoint = socket.assigns.selected_endpoint
152
:-(
template_body = get_template_for_endpoint(endpoint, template_type)
153
154
:-(
socket =
155 socket
156 |> assign(test_request_body: template_body)
157 |> assign(request_validation: nil)
158
159 {:noreply, socket}
160 end
161
162
:-(
def handle_event("update_testing_url", %{"testing_url" => url}, socket) do
163
:-(
{:noreply, assign(socket, testing_target_url: (if url in ["", nil], do: nil, else: String.trim(url)))}
164 end
165
166 def handle_event("close_endpoint", _params, socket) do
167
:-(
case socket.assigns.selected_group do
168
:-(
nil -> {:noreply, push_patch(socket, to: ~p"/api-docs")}
169
:-(
group -> {:noreply, push_patch(socket, to: ~p"/api-docs/#{group}")}
170 end
171 end
172
173
:-(
def handle_event("copy_curl", %{"curl" => curl_command}, socket) do
174 # In a real implementation, this would copy to clipboard via JavaScript
175 {:noreply, put_flash(socket, :info, "cURL command copied to clipboard")}
176 end
177
178 # Handle theme switching
179
:-(
def handle_event("toggle_theme", %{"theme" => _theme}, socket) do
180 {:noreply, socket}
181 end
182
183 # Handle the hide user menu event
184
:-(
def handle_event("hide_user_menu", _params, socket) do
185 {:noreply, socket}
186 end
187
188
:-(
def handle_info({:test_result, response}, socket) do
189
:-(
socket =
190 socket
191 |> assign(testing_endpoint: false, test_response: response)
192
193 {:noreply, socket}
194 end
195
196 defp filter_endpoints(socket) do
197
:-(
search_term = socket.assigns.search_term
198
:-(
selected_group = socket.assigns.selected_group
199
200
:-(
all_endpoints = get_all_endpoints()
201
202
:-(
filtered =
203 all_endpoints
204 |> Enum.filter(fn endpoint ->
205 # Filter by group if selected
206
:-(
group_match = is_nil(selected_group) or endpoint.group == selected_group
207
208 # Filter by search term
209
:-(
search_match = search_term == "" or
210
:-(
String.contains?(String.downcase(endpoint.name), String.downcase(search_term)) or
211
:-(
String.contains?(String.downcase(endpoint.description), String.downcase(search_term)) or
212
:-(
String.contains?(String.downcase(endpoint.path), String.downcase(search_term))
213
214
:-(
group_match and search_match
215 end)
216
217
:-(
assign(socket, filtered_endpoints: filtered)
218 end
219
220 # API Documentation Data
221
:-(
defp get_api_groups do
222 [
223 %{
224 id: "npci_interface",
225 name: "๐Ÿ›๏ธ NPCI โ†’ PSP Interface",
226 description: "Official NPCI UPI protocol APIs - Communication from NPCI to PSP",
227 icon: "๐Ÿ›๏ธ",
228 endpoint_count: 13
229 },
230 %{
231 id: "partner_interface",
232 name: "๐Ÿค Partner โ†’ PSP Interface",
233 description: "Partner-facing APIs for QR generation, merchant management and related operations",
234 icon: "๐Ÿค",
235 endpoint_count: 17
236 },
237 %{
238 id: "international_queries",
239 name: "๐Ÿ” International UPI Queries",
240 description: "PSP internal APIs for international transaction support",
241 icon: "๐Ÿ”",
242 endpoint_count: 2
243 },
244 %{
245 id: "legacy_transactions",
246 name: "๐Ÿ“Š Legacy Transaction APIs",
247 description: "Legacy transaction management APIs for backward compatibility",
248 icon: "๏ฟฝ",
249 endpoint_count: 2
250 },
251 %{
252 id: "test_development",
253 name: "๐Ÿงช Test & Development",
254 description: "Testing and development utilities",
255 icon: "๐Ÿงช",
256 endpoint_count: 1
257 }
258 ]
259 end
260
261
:-(
defp get_all_endpoints do
262 [
263 # # Transaction endpoints
264 # %{
265 # id: "create_transaction",
266 # group: "npci_interface",
267 # name: "Create Transaction",
268 # method: "POST",
269 # path: "/api/v1/transactions",
270 # description: "Initiate a new UPI transaction",
271 # status: "active",
272 # auth_required: true,
273 # rate_limit: "100/min",
274 # version: "1.0",
275 # parameters: [
276 # %{name: "payerVPA", type: "string", required: true, description: "Payer UPI ID"},
277 # %{name: "payeeVPA", type: "string", required: true, description: "Payee UPI ID"},
278 # %{name: "amount", type: "decimal", required: true, description: "Transaction amount"},
279 # %{name: "note", type: "string", required: false, description: "Transaction note"}
280 # ],
281 # example_request: %{
282 # payerVPA: "user@paytm",
283 # payeeVPA: "merchant@upi",
284 # amount: "100.00",
285 # note: "Payment for order #12345"
286 # },
287 # example_response: %{
288 # txnId: "TXN123456789",
289 # status: "PENDING",
290 # message: "Transaction initiated successfully"
291 # },
292 # example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"payerVPA\":\"user@paytm\",\"payeeVPA\":\"merchant@upi\",\"amount\":\"100.00\",\"note\":\"Payment for order #12345\"}' \"http://demo.ctrmv.com:4041/api/v1/transactions\""
293 # },
294 %{
295 id: "req_hbt",
296 group: "npci_interface",
297 name: "Heartbeat (ReqHbt)",
298 method: "POST",
299 path: "https://precert.nfinite.in/iupi/RespHbt/2.0/urn:txnid:",
300 description: "NPCI initiates a heartbeat (ReqHbt) to verify PSP liveness; PSP responds with immediate Ack and asynchronous RespHbt sequence.",
301 status: "active",
302 auth_required: false,
303 rate_limit: "300/min",
304 version: "1.0",
305 parameters: [
306 %{name: "Txn.id", type: "string", required: true, description: "Unique transaction / heartbeat identifier (txn id)"},
307 %{name: "Txn.ts", type: "datetime", required: true, description: "ISO8601 timestamp of heartbeat"},
308 %{name: "Txn.refId", type: "string", required: false, description: "Reference ID provided by NPCI"},
309 %{name: "Txn.type", type: "string", required: true, description: "Must be 'Hbt'"},
310 %{name: "HbtMsg.type", type: "string", required: true, description: "Heartbeat message type e.g. ALIVE"},
311 %{name: "HbtMsg.value", type: "string", required: true, description: "Additional heartbeat value (often 'NA')"}
312 ],
313 example_request: """
314 <?xml version="1.0" encoding="UTF-8"?>
315 <ns2:ReqHbt xmlns:ns2="http://npci.org/upi/schema/">
316 <Head msgId="MER1468871dacd343b58008118a959346ec" orgId="NPCI" ts="2025-09-09T15:29:00+05:30" ver="2.0"/>
317 <Txn custRef="039580821991" id="MER83175e51b6a44328a98fc64b63d06a94" note="ReqHbt" refId="039580821991" refUrl="www.test.co.in" ts="2025-09-09T15:29:00+05:30" type="Hbt"/>
318 <HbtMsg type="ALIVE" value="NA"/>
319 <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
320 <!-- Signature removed for brevity in docs -->
321 <SignatureValue>SIGNATURE_BASE64_PLACEHOLDER</SignatureValue>
322 </Signature>
323 </ns2:ReqHbt>
324 """ |> String.trim(),
325 example_response: %{
326 xml_ack: """
327 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
328 <ns2:Ack api="ReqHbt" reqMsgId="\"MSG123456789\"" ts="2025-09-19T09:15:06.050530Z" xmlns:ns2="http://npci.org/upi/schema/"/>
329 """ |> String.trim(),
330 explanation: "Immediate ACK response (RespHbt) indicates PSP received the heartbeat. A subsequent internal async flow finalizes processing."
331 },
332 example_curl: "curl -X POST -H \"Content-Type: application/xml\" --data-binary @reqhbt.xml \"http://demo.ctrmv.com:4041/ReqHbt\""
333 },
334 %{
335 id: "req_valqr",
336 group: "npci_interface",
337 name: "QR Validation (ReqValQr)",
338 method: "POST",
339 path: "https://precert.nfinite.in/iupi/ReqValQr/2.0/urn:txnid:",
340 description: "NPCI sends a ReqValQr XML to validate a QR code; PSP must respond with an immediate ACK and optionally an asynchronous response.",
341 status: "active",
342 auth_required: false,
343 rate_limit: "300/min",
344 version: "1.0",
345 parameters: [
346 %{name: "Txn.id", type: "string", required: true, description: "Unique transaction identifier"},
347 %{name: "Txn.ts", type: "datetime", required: true, description: "Timestamp of the request"},
348 %{name: "Txn.type", type: "string", required: true, description: "Should be 'Val' for validation"},
349 %{name: "ValQr.qrString", type: "string", required: true, description: "The QR string being validated"}
350 ],
351 example_request: """
352 <?xml version=\"1.0\" encoding=\"UTF-8\"?>
353 <ns2:ReqValQr xmlns:ns2=\"http://npci.org/upi/schema/\">
354 <Head msgId=\"MSG123456789\" orgId=\"NPCI\" ts=\"2025-09-19T09:15:06+05:30\" ver=\"2.0\"/>
355 <Txn id=\"VAL123456789\" ts=\"2025-09-19T09:15:06+05:30\" type=\"Val\"/>
356 <ValQr>
357 <qrString>upi://pay?pa=merchant@upi&amp;pn=Demo%20Merchant&amp;am=500.00</qrString>
358 </ValQr>
359 </ns2:ReqValQr>
360 """ |> String.trim(),
361 example_response: %{
362 xml_ack: """
363 <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
364 <ns2:Ack api=\"ReqValQr\" reqMsgId=\"MSG123456789\" ts=\"2025-09-19T09:15:06.050530Z\" xmlns:ns2=\"http://npci.org/upi/schema/\"/>
365 """ |> String.trim(),
366 explanation: "Immediate ACK indicates request received. PSP may asynchronously send a validation result to NPCI."
367 },
368 example_curl: "curl -X POST -H \"Content-Type: application/xml\" --data-binary @reqvalqr.xml \"http://demo.ctrmv.com:4041/ReqValQr\""
369 },
370 %{
371 id: "req_pay",
372 group: "npci_interface",
373 name: "Payment Request (ReqPay)",
374 method: "POST",
375 path: "https://precert.nfinite.in/iupi/ReqPay/2.0/urn:txnid:",
376 description: "NPCI sends a ReqPay XML for payment requests; PSP must return an immediate ACK and then process the payment (RespPay).",
377 status: "active",
378 auth_required: false,
379 rate_limit: "300/min",
380 version: "1.0",
381 parameters: [
382 %{name: "Txn.id", type: "string", required: true, description: "Unique transaction identifier"},
383 %{name: "Txn.ts", type: "datetime", required: true, description: "Timestamp of the request"},
384 %{name: "Txn.type", type: "string", required: true, description: "Should be 'Pay' for payment request"},
385 %{name: "Pay.amount", type: "decimal", required: true, description: "Payment amount"},
386 %{name: "Pay.payerVPA", type: "string", required: true, description: "Payer VPA"},
387 %{name: "Pay.payeeVPA", type: "string", required: true, description: "Payee VPA"}
388 ],
389 example_request: """
390 <?xml version=\"1.0\" encoding=\"UTF-8\"?>
391 <ns2:ReqPay xmlns:ns2=\"http://npci.org/upi/schema/\">
392 <Head msgId=\"MSG987654321\" orgId=\"NPCI\" ts=\"2025-09-19T09:20:00+05:30\" ver=\"2.0\"/>
393 <Txn id=\"PAY123456789\" ts=\"2025-09-19T09:20:00+05:30\" type=\"Pay\"/>
394 <Pay>
395 <amount>100.00</amount>
396 <payerVPA>user@paytm</payerVPA>
397 <payeeVPA>merchant@upi</payeeVPA>
398 <note>Payment for order 12345</note>
399 </Pay>
400 </ns2:ReqPay>
401 """ |> String.trim(),
402 example_response: %{
403 xml_ack: """
404 <?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
405 <ns2:Ack api=\"ReqPay\" reqMsgId=\"MSG987654321\" ts=\"2025-09-19T09:20:00.050530Z\" xmlns:ns2=\"http://npci.org/upi/schema/\"/>
406 """ |> String.trim(),
407 explanation: "Immediate ACK indicates PSP received the payment request. Processing and final status will be part of RespPay flows."
408 },
409 example_curl: "curl -X POST -H \"Content-Type: application/xml\" --data-binary @reqpay.xml \"http://demo.ctrmv.com:4041/ReqPay\""
410 },
411 # %{
412 # id: "get_transaction",
413 # group: "npci_interface",
414 # name: "Get Transaction",
415 # method: "GET",
416 # path: "/api/v1/transactions/{txnId}",
417 # description: "Retrieve transaction details by ID",
418 # status: "active",
419 # auth_required: true,
420 # rate_limit: "200/min",
421 # version: "1.0",
422 # parameters: [
423 # %{name: "txnId", type: "string", required: true, description: "Transaction ID"}
424 # ],
425 # example_request: %{},
426 # example_response: %{
427 # txnId: "TXN123456789",
428 # status: "SUCCESS",
429 # amount: "100.00",
430 # payerVPA: "user@paytm",
431 # payeeVPA: "merchant@upi",
432 # timestamp: "2024-01-15T10:30:00Z"
433 # },
434 # example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"http://demo.ctrmv.com:4041/api/v1/transactions/{txnId}\""
435 # },
436 # %{
437 # id: "transaction_status",
438 # group: "npci_interface",
439 # name: "Check Transaction Status",
440 # method: "POST",
441 # path: "/api/v1/transactions/status",
442 # description: "Check status of multiple transactions",
443 # status: "active",
444 # auth_required: true,
445 # rate_limit: "50/min",
446 # version: "1.0",
447 # parameters: [
448 # %{name: "txnIds", type: "array", required: true, description: "Array of transaction IDs"}
449 # ],
450 # example_request: %{
451 # txnIds: ["TXN123456789", "TXN987654321"]
452 # },
453 # example_response: %{
454 # results: [
455 # %{txnId: "TXN123456789", status: "SUCCESS"},
456 # %{txnId: "TXN987654321", status: "PENDING"}
457 # ]
458 # },
459 # example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"txnIds\":[\"TXN123456789\",\"TXN987654321\"]}' \"http://demo.ctrmv.com:4041/api/v1/transactions/status\""
460 # },
461
462 # QR Code endpoints
463 %{
464 id: "generate_qr",
465 group: "partner_interface",
466 name: "Generate QR Code",
467 method: "POST",
468 path: "/api/v1/qr-generate",
469 description: "Generate a new UPI QR code",
470 status: "active",
471 auth_required: true,
472 rate_limit: "100/min",
473 version: "1.0",
474 parameters: [
475 %{name: "payeeVPA", type: "string", required: true, description: "Payee UPI ID"},
476 %{name: "amount", type: "decimal", required: false, description: "Fixed amount (optional)"},
477 %{name: "merchantName", type: "string", required: true, description: "Merchant name"},
478 %{name: "note", type: "string", required: false, description: "Transaction note"},
479 # Partner-style parameters
480 %{name: "partner_id", type: "string", required: false, description: "Partner identifier"},
481 %{name: "merchant_id", type: "string", required: true, description: "Merchant identifier"},
482 %{name: "currency", type: "string", required: false, description: "Currency code"},
483 %{name: "corridor", type: "string", required: true, description: "Payment corridor"},
484 %{name: "merchant_category", type: "string", required: false, description: "Merchant category code"},
485 %{name: "purpose_code", type: "string", required: false, description: "Purpose code"},
486 %{name: "validity_minutes", type: "integer", required: false, description: "QR validity in minutes"},
487 %{name: "max_usage_count", type: "integer", required: false, description: "Max usage count"},
488 %{name: "metadata", type: "object", required: false, description: "Additional metadata object"}
489 ],
490 example_request: %{
491 # Keep legacy simple example
492 payeeVPA: "merchant@upi",
493 amount: "500.00",
494 merchantName: "ABC Store",
495 note: "Payment for products",
496
497 # Partner-style international sample (preferred for partner interface testing)
498 partner_id: "SGCOF001",
499 partner_merchant_payload: %{
500 partner_id: "SGCOF001",
501 merchant_id: "SGBK0001234",
502 amount: "5000.00",
503 currency: "SGD",
504 corridor: "singapore",
505 merchant_name: "Singapore Coffee House",
506 merchant_category: "5814",
507 purpose_code: "P0101",
508 validity_minutes: 300,
509 max_usage_count: 1,
510 metadata: %{
511 invoice_id: "SG_COFFEE_001",
512 customer_ref: "coffeehouse@mercury"
513 }
514 }
515 },
516 example_response: %{
517 success: true,
518 message: "QR code generated successfully",
519 data: %{
520 currency: "SGD",
521 inr_amount: "426810.00",
522 fx_rate: 83.28,
523 expires_at: "2025-09-19T10:38:53.030794Z",
524 qr_string: "upiGlobal://pay?ver=01&mode=16&...",
525 merchant_id: "SGBK0001234",
526 amount: "5000.00",
527 qr_id: "QR_SI_1758260333_3449",
528 qr_url: "https://mercurypay.ariticapp.com/qr/..."
529 }
530 },
531 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"payeeVPA\":\"merchant@upi\",\"amount\":\"500.00\",\"merchantName\":\"ABC Store\",\"note\":\"Payment for products\"}' \"http://demo.ctrmv.com:4041/api/v1/qr-generate\""
532 },
533
534 # Partner -> Merchant management endpoints
535 %{
536 id: "create_partner_merchant",
537 group: "partner_interface",
538 name: "Create Partner Merchant",
539 method: "POST",
540 path: "/api/v1/partners/:partner_id/merchants",
541 description: "Enroll a new merchant under the given partner",
542 status: "active",
543 auth_required: true,
544 rate_limit: "50/min",
545 version: "1.0",
546 parameters: [
547 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
548 %{name: "merchant_code", type: "string", required: true, description: "Unique merchant code"},
549 %{name: "brand_name", type: "string", required: true, description: "Merchant display name"},
550 %{name: "merchant_vpa", type: "string", required: true, description: "Merchant VPA/UPI ID"}
551 ],
552 example_request: %{
553 merchant_code: "MERC123",
554 brand_name: "ABC Store",
555 merchant_vpa: "abc@upi",
556 business_type: "RETAIL"
557 },
558 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"merchant_code\":\"MERC123\",\"brand_name\":\"ABC Store\",\"merchant_vpa\":\"abc@upi\",\"business_type\":\"RETAIL\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants\"",
559 example_response: %{
560 success: true,
561 data: %{id: "MERCHANT123", merchant_code: "MERC123", brand_name: "ABC Store"},
562 message: "Merchant enrolled successfully"
563 }
564 },
565 %{
566 id: "list_partner_merchants",
567 group: "partner_interface",
568 name: "List Partner Merchants",
569 method: "GET",
570 path: "/api/v1/partners/:partner_id/merchants",
571 description: "List all merchants for a partner with optional filters",
572 status: "active",
573 auth_required: true,
574 rate_limit: "100/min",
575 version: "1.0",
576 parameters: [
577 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
578 %{name: "status", type: "string", required: false, description: "Filter by merchant status"},
579 %{name: "corridor", type: "string", required: false, description: "Filter by corridor"},
580 %{name: "page", type: "integer", required: false, description: "Page number"},
581 %{name: "per_page", type: "integer", required: false, description: "Results per page"}
582 ],
583 example_request: %{},
584 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants?status=ACTIVE&page=1&per_page=20\"",
585 example_response: %{
586 data: [
587 %{id: "MERCHANT123", merchant_code: "MERC123", brand_name: "ABC Store"}
588 ],
589 meta: %{total: 1}
590 }
591 },
592 %{
593 id: "get_partner_merchant",
594 group: "partner_interface",
595 name: "Get Partner Merchant",
596 method: "GET",
597 path: "/api/v1/partners/:partner_id/merchants/:id",
598 description: "Retrieve details for a specific merchant",
599 status: "active",
600 auth_required: true,
601 rate_limit: "200/min",
602 version: "1.0",
603 parameters: [
604 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
605 %{name: "id", type: "string", required: true, description: "Merchant id"}
606 ],
607 example_request: %{},
608 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/:id\"",
609 example_response: %{
610 id: "MERCHANT123",
611 merchant_code: "MERC123",
612 brand_name: "ABC Store",
613 merchant_vpa: "abc@upi",
614 status: "ACTIVE"
615 }
616 },
617 %{
618 id: "update_partner_merchant",
619 group: "partner_interface",
620 name: "Update Partner Merchant",
621 method: "PUT",
622 path: "/api/v1/partners/:partner_id/merchants/:id",
623 description: "Update merchant information",
624 status: "active",
625 auth_required: true,
626 rate_limit: "50/min",
627 version: "1.0",
628 parameters: [
629 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
630 %{name: "id", type: "string", required: true, description: "Merchant id"}
631 ],
632 example_request: %{
633 brand_name: "New Brand",
634 contact_email: "owner@example.com"
635 },
636 example_curl: "curl -X PUT -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"brand_name\":\"New Brand\",\"contact_email\":\"owner@example.com\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/:id\"",
637 example_response: %{
638 success: true,
639 data: %{id: "MERCHANT123", brand_name: "New Brand"},
640 message: "Merchant updated successfully"
641 }
642 },
643 %{
644 id: "update_partner_merchant_status",
645 group: "partner_interface",
646 name: "Update Merchant Status",
647 method: "PATCH",
648 path: "/api/v1/partners/:partner_id/merchants/:id/status",
649 description: "Update merchant status (ACTIVE, SUSPENDED, INACTIVE)",
650 status: "active",
651 auth_required: true,
652 rate_limit: "50/min",
653 version: "1.0",
654 parameters: [
655 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
656 %{name: "id", type: "string", required: true, description: "Merchant id"},
657 %{name: "status", type: "string", required: true, description: "New status"}
658 ],
659 example_request: %{
660 status: "SUSPENDED"
661 },
662 example_curl: "curl -X PATCH -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"status\":\"SUSPENDED\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/:id/status\"",
663 example_response: %{
664 success: true,
665 data: %{id: "MERCHANT123", status: "SUSPENDED"},
666 message: "Merchant status updated successfully"
667 }
668 },
669 %{
670 id: "validate_partner_merchant",
671 group: "partner_interface",
672 name: "Validate Merchant",
673 method: "GET",
674 path: "/api/v1/partners/:partner_id/merchants/:id/validate",
675 description: "Validate merchant for transaction processing",
676 status: "active",
677 auth_required: true,
678 rate_limit: "200/min",
679 version: "1.0",
680 parameters: [
681 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
682 %{name: "id", type: "string", required: true, description: "Merchant id"}
683 ],
684 example_request: %{},
685 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/:id/validate\"",
686 example_response: %{
687 valid: true,
688 message: "Merchant is valid for transactions"
689 }
690 },
691 %{
692 id: "check_partner_merchant_limits",
693 group: "partner_interface",
694 name: "Check Merchant Limits",
695 method: "POST",
696 path: "/api/v1/partners/:partner_id/merchants/:id/check-limits",
697 description: "Check transaction limits for a merchant",
698 status: "active",
699 auth_required: true,
700 rate_limit: "200/min",
701 version: "1.0",
702 parameters: [
703 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
704 %{name: "id", type: "string", required: true, description: "Merchant id"},
705 %{name: "amount", type: "decimal", required: true, description: "Transaction amount"}
706 ],
707 example_request: %{
708 amount: "100.00"
709 },
710 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"amount\":\"100.00\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/:id/check-limits\"",
711 example_response: %{
712 allowed: true,
713 message: "Transaction within limits"
714 }
715 },
716 %{
717 id: "partner_merchants_search_hyphen",
718 group: "partner_interface",
719 name: "Search Partner Merchants (router)",
720 method: "GET",
721 path: "/api/v1/partners/:partner_id/merchants-search",
722 description: "Search merchants using filters and query (router path with hyphen)",
723 status: "active",
724 auth_required: true,
725 rate_limit: "100/min",
726 version: "1.0",
727 parameters: [
728 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
729 %{name: "q", type: "string", required: false, description: "Search query"},
730 %{name: "status", type: "string", required: false, description: "Status filter"}
731 ],
732 example_request: %{},
733 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants-search?q=ABC&status=ACTIVE\"",
734 example_response: %{
735 data: [],
736 meta: %{total: 0}
737 }
738 },
739 %{
740 id: "partner_merchant_stats",
741 group: "partner_interface",
742 name: "Partner Merchant Stats",
743 method: "GET",
744 path: "/api/v1/partners/:partner_id/merchants/stats",
745 description: "Get merchant statistics for a partner",
746 status: "active",
747 auth_required: true,
748 rate_limit: "20/min",
749 version: "1.0",
750 parameters: [
751 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"}
752 ],
753 example_request: %{},
754 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/stats\"",
755 example_response: %{
756 total_merchants: 10,
757 active_merchants: 8,
758 by_corridor: %{"DOMESTIC" => 7},
759 by_business_type: %{"RETAIL" => 6}
760 }
761 },
762 # Router uses a hyphenated /merchants-stats path in router.ex; add alias so docs match routes
763 %{
764 id: "partner_merchant_stats_hyphen",
765 group: "partner_interface",
766 name: "Partner Merchant Stats (router)",
767 method: "GET",
768 path: "/api/v1/partners/:partner_id/merchants-stats",
769 description: "Get merchant statistics for a partner (router path)",
770 status: "active",
771 auth_required: true,
772 rate_limit: "20/min",
773 version: "1.0",
774 parameters: [
775 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"}
776 ],
777 example_request: %{},
778 example_response: %{
779 total_merchants: 10,
780 active_merchants: 8,
781 by_corridor: %{"DOMESTIC" => 7},
782 by_business_type: %{"RETAIL" => 6}
783 },
784 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants-stats\""
785 },
786 %{
787 id: "partner_merchants_validate_collection",
788 group: "partner_interface",
789 name: "Validate Merchants (collection)",
790 method: "GET",
791 path: "/api/v1/partners/:partner_id/merchants/validate",
792 description: "Validate merchant(s) for transaction processing (collection route as defined in router)",
793 status: "active",
794 auth_required: true,
795 rate_limit: "200/min",
796 version: "1.0",
797 parameters: [
798 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"}
799 ],
800 example_request: %{},
801 example_response: %{
802 valid: true,
803 message: "Merchant(s) valid for transactions"
804 },
805 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/validate\""
806 },
807 %{
808 id: "partner_merchants_status_collection",
809 group: "partner_interface",
810 name: "Update Merchant Status (collection)",
811 method: "PATCH",
812 path: "/api/v1/partners/:partner_id/merchants/status",
813 description: "Update merchant status (collection route)",
814 status: "active",
815 auth_required: true,
816 rate_limit: "50/min",
817 version: "1.0",
818 parameters: [
819 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
820 %{name: "status", type: "string", required: true, description: "New status"}
821 ],
822 example_request: %{
823 status: "SUSPENDED"
824 },
825 example_response: %{
826 success: true,
827 message: "Merchant status updated"
828 },
829 example_curl: "curl -X PATCH -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"status\":\"SUSPENDED\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/status\""
830 },
831 %{
832 id: "partner_merchants_check_limits_collection",
833 group: "partner_interface",
834 name: "Check Merchant Limits (collection)",
835 method: "POST",
836 path: "/api/v1/partners/:partner_id/merchants/check-limits",
837 description: "Check transaction limits for merchants (collection route)",
838 status: "active",
839 auth_required: true,
840 rate_limit: "200/min",
841 version: "1.0",
842 parameters: [
843 %{name: "partner_id", type: "string", required: true, description: "Partner identifier"},
844 %{name: "amount", type: "decimal", required: true, description: "Transaction amount"}
845 ],
846 example_request: %{
847 amount: "100.00"
848 },
849 example_response: %{
850 allowed: true,
851 message: "Transactions within limits"
852 },
853 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"amount\":\"100.00\"}' \"https://api.mercurypay.com/api/v1/partners/:partner_id/merchants/check-limits\""
854 },
855 %{
856 id: "validate_qr",
857 group: "partner_interface",
858 name: "Validate QR Code",
859 method: "POST",
860 path: "/api/v1/qr/validate",
861 description: "Validate QR code format and compliance",
862 status: "active",
863 auth_required: true,
864 rate_limit: "200/min",
865 version: "1.0",
866 parameters: [
867 %{name: "qrString", type: "string", required: true, description: "QR code string to validate"}
868 ],
869 example_request: %{
870 qrString: "upi://pay?pa=merchant@upi&pn=ABC%20Store&am=500.00"
871 },
872 example_response: %{
873 valid: true,
874 format: "UPI_QR",
875 compliance: "NPCI_COMPLIANT",
876 extractedData: %{
877 payeeVPA: "merchant@upi",
878 payeeName: "ABC Store",
879 amount: "500.00"
880 }
881 },
882 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"qrString\":\"upi://pay?pa=merchant@upi&pn=ABC%20Store&am=500.00\"}' \"http://demo.ctrmv.com:4041/api/v1/qr/validate\""
883 },
884
885 # International Payment endpoints
886 %{
887 id: "create_international_payment",
888 group: "international_queries",
889 name: "Create International Payment",
890 method: "POST",
891 path: "/api/v1/international/payments",
892 description: "Initiate cross-border payment",
893 status: "active",
894 auth_required: true,
895 rate_limit: "50/min",
896 version: "1.0",
897 parameters: [
898 %{name: "corridor", type: "string", required: true, description: "Payment corridor (SGD-INR, USD-INR, etc.)"},
899 %{name: "senderDetails", type: "object", required: true, description: "Sender information"},
900 %{name: "receiverDetails", type: "object", required: true, description: "Receiver information"},
901 %{name: "amount", type: "decimal", required: true, description: "Amount in source currency"},
902 %{name: "purpose", type: "string", required: true, description: "Payment purpose"}
903 ],
904 example_request: %{
905 corridor: "SGD-INR",
906 senderDetails: %{
907 name: "John Doe",
908 country: "Singapore",
909 currency: "SGD"
910 },
911 receiverDetails: %{
912 name: "Ravi Kumar",
913 upiId: "ravi@paytm",
914 country: "India"
915 },
916 amount: "100.00",
917 purpose: "Family Support"
918 },
919 example_response: %{
920 paymentId: "INTL123456789",
921 status: "PENDING",
922 fxRate: "61.50",
923 inrAmount: "6150.00",
924 estimatedDelivery: "2024-01-15T15:30:00Z"
925 },
926 example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"corridor\":\"SGD-INR\",\"senderDetails\":{\"name\":\"John Doe\",\"country\":\"Singapore\",\"currency\":\"SGD\"},\"receiverDetails\":{\"name\":\"Ravi Kumar\",\"upiId\":\"ravi@paytm\",\"country\":\"India\"},\"amount\":\"100.00\",\"purpose\":\"Family Support\"}' \"http://demo.ctrmv.com:4041/api/v1/international/payments\""
927 },
928
929 # Settlement endpoints
930 %{
931 id: "get_settlements",
932 group: "international_queries",
933 name: "Get Settlements",
934 method: "GET",
935 path: "/api/v1/settlements",
936 description: "Retrieve settlement information",
937 status: "active",
938 auth_required: true,
939 rate_limit: "100/min",
940 version: "1.0",
941 parameters: [
942 %{name: "status", type: "string", required: false, description: "Filter by status"},
943 %{name: "type", type: "string", required: false, description: "Settlement type"},
944 %{name: "date_from", type: "date", required: false, description: "Start date"},
945 %{name: "date_to", type: "date", required: false, description: "End date"}
946 ],
947 example_request: %{},
948 example_response: %{
949 settlements: [
950 %{
951 settlementId: "SET123456789",
952 type: "net",
953 status: "completed",
954 amount: "50000.00",
955 completedAt: "2024-01-15T18:00:00Z"
956 }
957 ],
958 pagination: %{
959 page: 1,
960 perPage: 20,
961 total: 45
962 }
963 },
964 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"http://demo.ctrmv.com:4041/api/v1/settlements?status=completed&type=net\""
965 },
966
967 # # Validation endpoints
968 # %{
969 # id: "validate_vpa",
970 # group: "npci_interface",
971 # name: "Validate VPA",
972 # method: "POST",
973 # path: "/api/v1/validation/vpa",
974 # description: "Validate UPI Virtual Payment Address",
975 # status: "active",
976 # auth_required: true,
977 # rate_limit: "500/min",
978 # version: "1.0",
979 # parameters: [
980 # %{name: "vpa", type: "string", required: true, description: "VPA to validate"}
981 # ],
982 # example_request: %{
983 # vpa: "user@paytm"
984 # },
985 # example_response: %{
986 # valid: true,
987 # accountName: "John Doe",
988 # bankName: "Paytm Payments Bank",
989 # status: "ACTIVE"
990 # },
991 # example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"vpa\":\"user@paytm\"}' \"http://demo.ctrmv.com:4041/api/v1/validation/vpa\""
992 # },
993
994 # # Webhook endpoints
995 # %{
996 # id: "register_webhook",
997 # group: "npci_interface",
998 # name: "Register Webhook",
999 # method: "POST",
1000 # path: "/api/v1/webhooks",
1001 # description: "Register a new webhook endpoint",
1002 # status: "active",
1003 # auth_required: true,
1004 # rate_limit: "10/min",
1005 # version: "1.0",
1006 # parameters: [
1007 # %{name: "url", type: "string", required: true, description: "Webhook URL"},
1008 # %{name: "events", type: "array", required: true, description: "Events to subscribe to"},
1009 # %{name: "secret", type: "string", required: false, description: "Webhook secret for verification"}
1010 # ],
1011 # example_request: %{
1012 # url: "https://your-server.com/webhooks/upi",
1013 # events: ["transaction.success", "transaction.failed"],
1014 # secret: "your-webhook-secret"
1015 # },
1016 # example_response: %{
1017 # webhookId: "WEBHOOK123456789",
1018 # status: "active",
1019 # verificationToken: "verify_token_abc123"
1020 # },
1021 # example_curl: "curl -X POST -H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\" -d '{\"url\":\"https://your-server.com/webhooks/upi\",\"events\":[\"transaction.success\",\"transaction.failed\"],\"secret\":\"your-webhook-secret\"}' \"http://demo.ctrmv.com:4041/api/v1/webhooks\""
1022 # },
1023
1024 # Reports endpoints
1025 %{
1026 id: "transaction_report",
1027 group: "legacy_transactions",
1028 name: "Transaction Report",
1029 method: "GET",
1030 path: "/api/v1/reports/transactions",
1031 description: "Generate transaction analytics report",
1032 status: "active",
1033 auth_required: true,
1034 rate_limit: "20/min",
1035 version: "1.0",
1036 parameters: [
1037 %{name: "date_from", type: "date", required: true, description: "Report start date"},
1038 %{name: "date_to", type: "date", required: true, description: "Report end date"},
1039 %{name: "format", type: "string", required: false, description: "Report format (json/csv)"}
1040 ],
1041 example_request: %{},
1042 example_response: %{
1043 summary: %{
1044 totalTransactions: 1250,
1045 successfulTransactions: 1200,
1046 totalVolume: "2500000.00",
1047 averageAmount: "2000.00"
1048 },
1049 breakdown: %{
1050 byStatus: %{
1051 success: 1200,
1052 failed: 50
1053 },
1054 byHour: [
1055 %{hour: "09:00", count: 150, volume: "300000.00"},
1056 %{hour: "10:00", count: 180, volume: "360000.00"}
1057 ]
1058 }
1059 },
1060 example_curl: "curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \"http://demo.ctrmv.com:4041/api/v1/reports/transactions?date_from=2024-01-01&date_to=2024-01-31&format=json\""
1061 },
1062 # Add ReqChkTxn endpoint metadata for NPCI "check transaction" flow
1063 # This will appear alongside other endpoints returned by get_all_endpoints/0
1064
1065 # Batch transaction check endpoint (BatchReq) - accepts a batch XML of ReqChkTxn elements
1066 %{
1067 id: "batch_req_chk_txn",
1068 name: "BatchReqChkTxn",
1069 version: "1.0",
1070 group: "npci_interface",
1071 method: "POST",
1072 path: "/ReqChkTxn/Batch",
1073 description: "Batch Check Transaction Request (NPCI โ†’ PSP). PSP should respond with a batch RespChkTxn XML.",
1074 status: "active",
1075 auth_required: false,
1076 rate_limit: nil,
1077 parameters: [
1078 %{name: "Batch.head.MsgId", description: "Batch message id", type: "string"},
1079 %{name: "Batch.Txn[*].id", description: "Individual Txn id attribute", type: "string"}
1080 ],
1081 example_request: """
1082 <?xml version="1.0" encoding="UTF-8"?>
1083 <ns2:BatchReqChkTxn xmlns:ns2="urn:iso:std:iso:20022:tech:xsd:Request">
1084 <Head>
1085 <MsgId>BATCHCHK0001</MsgId>
1086 <CreDtTm>2025-09-19T12:00:00Z</CreDtTm>
1087 </Head>
1088 <Batch>
1089 <ReqChkTxn>
1090 <Txn id="CHK123456789" ts="2025-09-19T12:00:00Z" type="ChkTxn" />
1091 <ChkTxn>
1092 <txnRef>CHKREF-20250919-0001</txnRef>
1093 </ChkTxn>
1094 </ReqChkTxn>
1095 <ReqChkTxn>
1096 <Txn id="CHK123456790" ts="2025-09-19T12:01:00Z" type="ChkTxn" />
1097 <ChkTxn>
1098 <txnRef>CHKREF-20250919-0002</txnRef>
1099 </ChkTxn>
1100 </ReqChkTxn>
1101 </Batch>
1102 </ns2:BatchReqChkTxn>
1103 """,
1104 example_response: """
1105 <?xml version="1.0" encoding="UTF-8"?>
1106 <ns2:BatchRespChkTxn xmlns:ns2="urn:iso:std:iso:20022:tech:xsd:Response">
1107 <Head>
1108 <MsgId>BATCHRESPCHK0001</MsgId>
1109 <CreDtTm>2025-09-19T12:00:02Z</CreDtTm>
1110 </Head>
1111 <Batch>
1112 <RespChkTxn>
1113 <Txn id="CHK123456789" ts="2025-09-19T12:00:01Z" type="RespChkTxn" />
1114 <ChkResp>
1115 <txnRef>CHKREF-20250919-0001</txnRef>
1116 <status>FOUND</status>
1117 <message>Transaction found</message>
1118 </ChkResp>
1119 </RespChkTxn>
1120 <RespChkTxn>
1121 <Txn id="CHK123456790" ts="2025-09-19T12:01:01Z" type="RespChkTxn" />
1122 <ChkResp>
1123 <txnRef>CHKREF-20250919-0002</txnRef>
1124 <status>NOT_FOUND</status>
1125 <message>Transaction not found</message>
1126 </ChkResp>
1127 </RespChkTxn>
1128 </Batch>
1129 </ns2:BatchRespChkTxn>
1130 """,
1131 example_curl: "curl -X POST 'https://sandbox.partner/npci/req_chk_txn_batch' -H 'Content-Type: application/xml' -d '<ns2:BatchReqChkTxn>...</ns2:BatchReqChkTxn>'"
1132 },
1133 %{
1134 id: "reconciliation",
1135 name: "ReconciliationReq",
1136 version: "1.0",
1137 group: "npci_interface",
1138 method: "POST",
1139 path: "/api/v1/upi/reconciliation",
1140 description: "Reconciliation Request (NPCI โ†’ PSP). PSP responds with RespReconciliation XML.",
1141 status: "active",
1142 auth_required: false,
1143 rate_limit: nil,
1144 parameters: [
1145 %{name: "ReconciliationInfo.fromDate", description: "From date (YYYY-MM-DD)", type: "string"},
1146 %{name: "ReconciliationInfo.toDate", description: "To date (YYYY-MM-DD)", type: "string"},
1147 %{name: "ReconciliationInfo.partnerCode", description: "Partner code filter", type: "string"}
1148 ],
1149 example_request: """
1150 <?xml version="1.0" encoding="UTF-8"?>
1151 <ReconciliationReq>
1152 <Head ver="2.0" ts="2025-01-11T10:30:00+05:30" orgId="NPCI" msgId="REC123456789012345678901234567890123"/>
1153 <ReconciliationInfo>
1154 <fromDate>2025-01-10</fromDate>
1155 <toDate>2025-01-11</toDate>
1156 <partnerCode>SINGAPORE_PARTNER</partnerCode>
1157 </ReconciliationInfo>
1158 </ReconciliationReq>
1159 """,
1160 example_response: """
1161 <?xml version="1.0" encoding="UTF-8"?>
1162 <RespReconciliation>
1163 <Head ver="2.0" ts="2025-01-11T10:30:05+05:30" orgId="PSPXYZ" msgId="RESPREC1234567890"/>
1164 <ReconciliationResult>
1165 <fromDate>2025-01-10</fromDate>
1166 <toDate>2025-01-11</toDate>
1167 <transactionsCount>2</transactionsCount>
1168 <totalInrAmount>6150.00</totalInrAmount>
1169 </ReconciliationResult>
1170 </RespReconciliation>
1171 """,
1172 example_curl: "curl -X POST 'http://demo.ctrmv.com:4041/api/v1/upi/reconciliation' -H 'Content-Type: application/xml' -d '<ReconciliationReq>...</ReconciliationReq>'"
1173 },
1174 %{
1175 id: "mandate_request",
1176 name: "MandateReq",
1177 version: "1.0",
1178 group: "npci_interface",
1179 method: "POST",
1180 path: "/api/v1/upi/mandate-request",
1181 description: "Mandate creation request (NPCI โ†’ PSP). PSP responds with RespMandate XML acknowledging mandate creation.",
1182 status: "active",
1183 auth_required: false,
1184 rate_limit: nil,
1185 parameters: [
1186 %{name: "Mandate.mandateId", description: "Mandate identifier", type: "string"},
1187 %{name: "Mandate.payerVPA", description: "Payer VPA", type: "string"},
1188 %{name: "Mandate.amount", description: "Mandate amount or limit", type: "string"}
1189 ],
1190 example_request: """
1191 <?xml version="1.0" encoding="UTF-8"?>
1192 <MandateReq>
1193 <Head ver="2.0" ts="2025-09-19T12:00:00+05:30" orgId="NPCI" msgId="MDT1234567890"/>
1194 <Mandate>
1195 <mandateId>MDT-0001</mandateId>
1196 <payerVPA>user@bank</payerVPA>
1197 <amount>500.00</amount>
1198 <frequency>MONTHLY</frequency>
1199 <startDate>2025-10-01</startDate>
1200 </Mandate>
1201 </MandateReq>
1202 """,
1203 example_response: """
1204 <?xml version="1.0" encoding="UTF-8"?>
1205 <RespMandate>
1206 <Head ver="2.0" ts="2025-09-19T12:00:05+05:30" orgId="PSPXYZ" msgId="RESPMDT123456"/>
1207 <MandateResp>
1208 <mandateId>MDT-0001</mandateId>
1209 <result>SUCCESS</result>
1210 <errCode>00</errCode>
1211 </MandateResp>
1212 </RespMandate>
1213 """,
1214 example_curl: "curl -X POST 'http://demo.ctrmv.com:4041/api/v1/upi/mandate-request' -H 'Content-Type: application/xml' -d '<MandateReq>...</MandateReq>'"
1215 },
1216 %{
1217 id: "req_chk_txn",
1218 name: "ReqChkTxn",
1219 version: "1.0",
1220 group: "npci_interface",
1221 method: "POST",
1222 path: "/api/v1/upi/check-transaction",
1223 description: "Transaction status check request (NPCI โ†’ PSP). PSP responds with RespChkTxn XML containing transaction status and details.",
1224 status: "active",
1225 auth_required: false,
1226 rate_limit: nil,
1227 parameters: [
1228 %{name: "ChkTxn.txnId", description: "Original transaction ID to check", type: "string"},
1229 %{name: "ChkTxn.payerAddr", description: "Payer UPI address", type: "string"},
1230 %{name: "ChkTxn.payeeAddr", description: "Payee UPI address", type: "string"}
1231 ],
1232 example_request: """
1233 <?xml version="1.0" encoding="UTF-8"?>
1234 <ReqChkTxn>
1235 <Head ver="2.0" ts="2025-09-19T12:00:00+05:30" orgId="NPCI" msgId="CHK1234567890"/>
1236 <ChkTxn>
1237 <txnId>TXN202509191200001</txnId>
1238 <payerAddr>customer@bank</payerAddr>
1239 <payeeAddr>merchant@upi</payeeAddr>
1240 <checkType>STATUS</checkType>
1241 <reason>Customer inquiry</reason>
1242 </ChkTxn>
1243 </ReqChkTxn>
1244 """,
1245 example_response: """
1246 <?xml version="1.0" encoding="UTF-8"?>
1247 <RespChkTxn>
1248 <Head ver="2.0" ts="2025-09-19T12:00:05+05:30" orgId="PSPXYZ" msgId="RESPCHK123456"/>
1249 <ChkTxnResp>
1250 <txnId>TXN202509191200001</txnId>
1251 <status>SUCCESS</status>
1252 <txnStatus>SUCCESS</txnStatus>
1253 <amount>1500.00</amount>
1254 <currency>INR</currency>
1255 <settlementStatus>SETTLED</settementStatus>
1256 <errCode>00</errCode>
1257 <respCode>00</respCode>
1258 </ChkTxnResp>
1259 </RespChkTxn>
1260 """,
1261 example_curl: "curl -X POST 'http://demo.ctrmv.com:4041/api/v1/upi/check-transaction' -H 'Content-Type: application/xml' -d '<ReqChkTxn>...</ReqChkTxn>'"
1262 }
1263 ]
1264 end
1265
1266 defp get_endpoint_by_id(endpoint_id) do
1267 get_all_endpoints()
1268
:-(
|> Enum.find(&(&1.id == endpoint_id))
1269 end
1270
1271 defp get_default_request_body(endpoint) do
1272
:-(
case endpoint.example_request do
1273
:-(
nil -> "{}"
1274
:-(
request when is_binary(request) -> request
1275
:-(
request -> Jason.encode!(request, pretty: true)
1276 end
1277 end
1278
1279 defp simulate_api_call(endpoint, request_body) do
1280 # Try JSON first
1281
:-(
case Jason.decode(request_body || "") do
1282 {:ok, parsed} ->
1283
:-(
response = generate_realistic_response(endpoint, parsed)
1284
:-(
%{
1285 status: 200,
1286 headers: %{
1287 "Content-Type" => "application/json",
1288
:-(
"X-Request-ID" => "req_#{:rand.uniform(999999)}",
1289 "X-Rate-Limit-Remaining" => "99"
1290 },
1291 body: Jason.encode!(response, pretty: true),
1292 timing: :rand.uniform(500) + 100
1293 }
1294
1295 {:error, _} ->
1296 # Not JSON: try raw XML or JSON with an "xml" key
1297
:-(
xml_string =
1298 cond do
1299
:-(
String.trim(request_body || "") |> String.starts_with?("<") -> String.trim(request_body)
1300
:-(
true ->
1301
:-(
case Jason.decode(request_body || "{}") do
1302
:-(
{:ok, %{"xml" => xml}} when is_binary(xml) -> String.trim(xml)
1303
:-(
_ -> nil
1304 end
1305 end
1306
1307
:-(
if is_binary(xml_string) do
1308 # Extract a few fields we validate in the docs
1309
:-(
txn_id = Regex.run(~r/\bTxn[^>]*\bid="([^"]+)"/, xml_string) |> case do
1310
:-(
[_, v] -> v
1311
:-(
_ -> nil
1312 end
1313
:-(
txn_ts = Regex.run(~r/\bTxn[^>]*\bts="([^"]+)"/, xml_string) |> case do
1314
:-(
[_, v] -> v
1315
:-(
_ -> nil
1316 end
1317
:-(
txn_type = Regex.run(~r/\bTxn[^>]*\btype="([^"]+)"/, xml_string) |> case do
1318
:-(
[_, v] -> v
1319
:-(
_ -> nil
1320 end
1321
:-(
hbt_type = Regex.run(~r/<HbtMsg[^>]*\btype="([^"]+)"/, xml_string) |> case do
1322
:-(
[_, v] -> v
1323
:-(
_ -> nil
1324 end
1325
:-(
hbt_value = Regex.run(~r/<HbtMsg[^>]*\bvalue="([^"]+)"/, xml_string) |> case do
1326
:-(
[_, v] -> v
1327
:-(
_ -> nil
1328 end
1329
1330
:-(
parsed = %{
1331 "Txn.id" => txn_id,
1332 "Txn.ts" => txn_ts,
1333 "Txn.type" => txn_type,
1334 "HbtMsg.type" => hbt_type,
1335 "HbtMsg.value" => hbt_value
1336 }
1337
1338
:-(
response = generate_realistic_response(endpoint, parsed)
1339
1340
:-(
case response do
1341 %{:ack => %{xml: xml_ack}} when is_binary(xml_ack) ->
1342
:-(
%{
1343 status: 200,
1344 headers: %{
1345 "Content-Type" => "application/xml",
1346
:-(
"X-Request-ID" => "req_#{:rand.uniform(999999)}"
1347 },
1348 body: xml_ack,
1349 timing: :rand.uniform(500) + 100
1350 }
1351
1352 _ ->
1353
:-(
%{
1354 status: 200,
1355 headers: %{
1356 "Content-Type" => "application/json",
1357
:-(
"X-Request-ID" => "req_#{:rand.uniform(999999)}"
1358 },
1359 body: Jason.encode!(response, pretty: true),
1360 timing: :rand.uniform(500) + 100
1361 }
1362 end
1363
1364 else
1365
:-(
%{
1366 status: 400,
1367 headers: %{"Content-Type" => "application/json"},
1368 body: Jason.encode!(%{error: "Invalid request format. Expecting JSON or raw XML."}, pretty: true),
1369 timing: 50
1370 }
1371 end
1372 end
1373
1374 end
1375
1376 defp generate_realistic_response(endpoint, _parsed_request) do
1377
:-(
case endpoint.id do
1378 "create_partner_merchant" ->
1379
:-(
%{
1380 success: true,
1381 data: %{
1382
:-(
id: "MERCHANT_#{:rand.uniform(999999)}",
1383
:-(
merchant_code: "MERC#{:rand.uniform(999)}",
1384 brand_name: "Test Merchant Store",
1385 merchant_vpa: "testmerchant@upi",
1386 status: "ACTIVE",
1387 created_at: DateTime.utc_now() |> DateTime.to_iso8601(),
1388 corridors: ["DOMESTIC", "SGD-INR"]
1389 },
1390 message: "Merchant enrolled successfully"
1391 }
1392
1393 "list_partner_merchants" ->
1394
:-(
%{
1395 data: [
1396 %{
1397 id: "MERCHANT_123",
1398 merchant_code: "MERC123",
1399 brand_name: "ABC Store",
1400 status: "ACTIVE",
1401 corridor: "DOMESTIC"
1402 },
1403 %{
1404 id: "MERCHANT_456",
1405 merchant_code: "MERC456",
1406 brand_name: "XYZ Restaurant",
1407 status: "ACTIVE",
1408 corridor: "SGD-INR"
1409 }
1410 ],
1411 meta: %{
1412 page: 1,
1413 per_page: 20,
1414 total: 2,
1415 total_pages: 1
1416 }
1417 }
1418
1419 "get_partner_merchant" ->
1420
:-(
%{
1421 id: "MERCHANT_123",
1422 merchant_code: "MERC123",
1423 brand_name: "ABC Store",
1424 merchant_vpa: "abc@upi",
1425 status: "ACTIVE",
1426 business_type: "RETAIL",
1427 corridors: ["DOMESTIC", "SGD-INR"],
1428 created_at: "2024-01-15T10:00:00Z",
1429 updated_at: "2024-01-15T10:00:00Z",
1430 daily_limit: "100000.00",
1431 monthly_limit: "3000000.00"
1432 }
1433
1434 "validate_partner_merchant" ->
1435
:-(
%{
1436 valid: true,
1437 merchant_id: "MERCHANT_123",
1438 status: "ACTIVE",
1439 checks: %{
1440 kyc_verified: true,
1441 limits_configured: true,
1442 corridors_active: true
1443 },
1444 message: "Merchant is valid for transactions"
1445 }
1446
1447 "check_partner_merchant_limits" ->
1448
:-(
%{
1449 allowed: true,
1450 merchant_id: "MERCHANT_123",
1451 requested_amount: "100.00",
1452 daily_used: "2500.00",
1453 daily_limit: "100000.00",
1454 monthly_used: "75000.00",
1455 monthly_limit: "3000000.00",
1456 message: "Transaction within limits"
1457 }
1458
1459 "generate_qr_partner" ->
1460
:-(
%{
1461
:-(
qr_id: "QR_#{:rand.uniform(999999999)}",
1462
:-(
qr_string: "upi://pay?pa=merchant@upi&pn=Test%20Merchant&am=500.00&tr=#{:rand.uniform(999999)}",
1463 qr_image_base64: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA...",
1464 expiry_time: DateTime.utc_now() |> DateTime.add(15, :minute) |> DateTime.to_iso8601(),
1465 merchant_name: "Test Merchant Store",
1466 status: "ACTIVE"
1467 }
1468
1469 "generate_qr" ->
1470
:-(
%{
1471
:-(
qrId: "QR_#{:rand.uniform(999999999)}",
1472
:-(
qrString: "upi://pay?pa=merchant@upi&pn=Demo%20Merchant&am=500.00&tr=#{:rand.uniform(999999)}",
1473 qrImage: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA...",
1474 expiryTime: DateTime.utc_now() |> DateTime.add(15, :minute) |> DateTime.to_iso8601(),
1475 merchantName: "Demo Merchant Store",
1476 status: "ACTIVE"
1477 }
1478
1479 "req_hbt" ->
1480
:-(
now = DateTime.utc_now() |> DateTime.to_iso8601()
1481
:-(
%{
1482 ack: %{
1483 xml: """
1484 <ns2:RespHbt xmlns:ns2=\"http://npci.org/upi/schema/\">
1485
:-(
<Head ver=\"2.0\" ts=\"#{now}\" orgId=\"PSPXYZ\" msgId=\"ACK#{:rand.uniform(999_999)}\"/>
1486
:-(
<Txn id=\"PAM#{:rand.uniform(9_999_999)}\" note=\"RespHbt\" type=\"Hbt\" />
1487 <Resp code=\"000\" desc=\"ACK\"/>
1488 </ns2:RespHbt>
1489 """ |> String.trim()
1490 },
1491 message: "Simulated heartbeat acknowledgement (RespHbt). Async processing will generate final handling logs.",
1492 processing: %{
1493 async_response_scheduled: true,
1494 expected_followup: "NPCI will receive RespHbt ACK, internal task completes Steps 3 & 4"
1495 }
1496 }
1497
1498 "req_valqr" ->
1499
:-(
now = DateTime.utc_now() |> DateTime.to_iso8601()
1500
:-(
%{
1501 ack: %{
1502 xml: """
1503 <ns2:RespValQr xmlns:ns2=\"http://npci.org/upi/schema/\">
1504
:-(
<Head ver=\"2.0\" ts=\"#{now}\" orgId=\"PSPXYZ\" msgId=\"ACK#{:rand.uniform(999_999)}\"/>
1505
:-(
<Txn id=\"VALPAM#{:rand.uniform(9_999_999)}\" note=\"RespValQr\" type=\"Val\" />
1506 <Resp code=\"000\" desc=\"ACK\"/>
1507 </ns2:RespValQr>
1508 """ |> String.trim()
1509 },
1510 message: "Simulated ReqValQr acknowledgement. PSP would validate QR and respond asynchronously if required.",
1511 processing: %{
1512 validation_result: "VALID",
1513 notes: "QR string parsed and accepted"
1514 }
1515 }
1516
1517 "req_pay" ->
1518
:-(
now = DateTime.utc_now() |> DateTime.to_iso8601()
1519
:-(
%{
1520 ack: %{
1521 xml: """
1522 <ns2:RespPay xmlns:ns2=\"http://npci.org/upi/schema/\">
1523
:-(
<Head ver=\"2.0\" ts=\"#{now}\" orgId=\"PSPXYZ\" msgId=\"ACK#{:rand.uniform(999_999)}\"/>
1524
:-(
<Txn id=\"PAYPAM#{:rand.uniform(9_999_999)}\" note=\"RespPay\" type=\"Pay\" />
1525 <Resp code=\"000\" desc=\"ACK\"/>
1526 </ns2:RespPay>
1527 """ |> String.trim()
1528 },
1529 message: "Simulated ReqPay acknowledgement. Payment processing simulated asynchronously.",
1530 processing: %{
1531 scheduled_settlement: true,
1532 expected_status: "PENDING"
1533 }
1534 }
1535
1536 # Add RespChkTxn simulation
1537 "req_chk_txn" ->
1538
:-(
txn_id = "CHK#{:rand.uniform(999999)}"
1539
:-(
txn_ts = DateTime.utc_now() |> DateTime.to_iso8601()
1540
:-(
txn_ref = "CHKREF-#{:rand.uniform(999999)}"
1541
1542
:-(
ack_xml = """
1543 <?xml version="1.0" encoding="UTF-8"?>
1544 <ns2:RespChkTxn xmlns:ns2=\"urn:iso:std:iso:20022:tech:xsd:Response\">
1545 <Head>
1546
:-(
<MsgId>RESPCHK-#{txn_ref}</MsgId>
1547
:-(
<CreDtTm>#{txn_ts}</CreDtTm>
1548 </Head>
1549
:-(
<Txn id=\"#{txn_id}\" ts=\"#{txn_ts}\" type=\"RespChkTxn\" />
1550 <ChkResp>
1551
:-(
<txnRef>#{txn_ref}</txnRef>
1552 <status>FOUND</status>
1553 <message>Simulated check response - transaction located</message>
1554 </ChkResp>
1555 </ns2:RespChkTxn>
1556 """
1557
1558
:-(
%{
1559 ack: %{xml: ack_xml},
1560 processing: %{found: true, note: "Simulated: found matching transaction"}
1561 }
1562
1563 _ ->
1564
:-(
endpoint.example_response || %{message: "Test response generated"}
1565 end
1566 end
1567
1568 defp validate_request_body(endpoint, request_body) do
1569 # For ReqHbt, ReqValQr or ReqPay endpoints, prioritize raw XML handling
1570
:-(
if endpoint.id in ["req_hbt", "req_valqr", "req_pay", "req_chk_txn"] do
1571
:-(
xml_string = String.trim(request_body || "")
1572
:-(
if xml_string |> String.starts_with?("<") do
1573 # Raw XML for ReqHbt - extract parameters directly
1574
:-(
txn_id = Regex.run(~r/\bTxn[^>]*\bid="([^"]+)"/, xml_string) |> case do
1575
:-(
[_, v] -> v
1576
:-(
_ -> nil
1577 end
1578
:-(
txn_ts = Regex.run(~r/\bTxn[^>]*\bts="([^"]+)"/, xml_string) |> case do
1579
:-(
[_, v] -> v
1580
:-(
_ -> nil
1581 end
1582
:-(
txn_type = Regex.run(~r/\bTxn[^>]*\btype="([^"]+)"/, xml_string) |> case do
1583
:-(
[_, v] -> v
1584
:-(
_ -> nil
1585 end
1586
:-(
hbt_type = Regex.run(~r/<HbtMsg[^>]*\btype="([^"]+)"/, xml_string) |> case do
1587
:-(
[_, v] -> v
1588
:-(
_ -> nil
1589 end
1590
:-(
hbt_value = Regex.run(~r/<HbtMsg[^>]*\bvalue="([^"]+)"/, xml_string) |> case do
1591
:-(
[_, v] -> v
1592
:-(
_ -> nil
1593 end
1594
1595 # For ReqValQr extract the qrString element
1596
:-(
qr_string = Regex.run(~r/<qrString>([^<]+)<\/qrString>/, xml_string) |> case do
1597
:-(
[_, v] -> v
1598
:-(
_ -> nil
1599 end
1600
1601 # For ReqPay extract pay fields
1602
:-(
pay_amount = Regex.run(~r/<Pay>.*?<amount>([^<]+)<\/amount>.*?<\/Pay>/s, xml_string) |> case do
1603
:-(
[_, v] -> v
1604
:-(
_ -> nil
1605 end
1606
:-(
pay_payer = Regex.run(~r/<payerVPA>([^<]+)<\/payerVPA>/, xml_string) |> case do
1607
:-(
[_, v] -> v
1608
:-(
_ -> nil
1609 end
1610
:-(
pay_payee = Regex.run(~r/<payeeVPA>([^<]+)<\/payeeVPA>/, xml_string) |> case do
1611
:-(
[_, v] -> v
1612
:-(
_ -> nil
1613 end
1614
1615
:-(
parsed = %{
1616 "Txn.id" => txn_id,
1617 "Txn.ts" => txn_ts,
1618 "Txn.type" => txn_type,
1619 "HbtMsg.type" => hbt_type,
1620 "HbtMsg.value" => hbt_value,
1621 "ValQr.qrString" => qr_string,
1622 "Pay.amount" => pay_amount,
1623 "Pay.payerVPA" => pay_payer,
1624 "Pay.payeeVPA" => pay_payee
1625 }
1626
1627
:-(
errors = validate_endpoint_parameters(endpoint, parsed)
1628
:-(
warnings = generate_warnings(endpoint, parsed)
1629
1630
:-(
%{
1631 valid: Enum.empty?(errors),
1632 errors: errors,
1633 warnings: warnings
1634 }
1635 else
1636
:-(
%{
1637 valid: false,
1638 errors: ["ReqHbt endpoint expects raw XML in request body"],
1639 warnings: []
1640 }
1641 end
1642 else
1643 # For non-ReqHbt endpoints, use standard JSON handling
1644 # Accept JSON, or a JSON object with an "xml" key, or raw XML string.
1645
:-(
case Jason.decode(request_body || "") do
1646 {:ok, parsed} ->
1647
:-(
errors = validate_endpoint_parameters(endpoint, parsed)
1648
:-(
%{
1649 valid: Enum.empty?(errors),
1650 errors: errors,
1651 warnings: generate_warnings(endpoint, parsed)
1652 }
1653
1654 _ ->
1655 # Try JSON-with-xml key
1656
:-(
case Jason.decode(request_body || "{}") do
1657 {:ok, %{"xml" => xml} = _m} when is_binary(xml) ->
1658 # Extract XML parameters from the XML string
1659
:-(
txn_id = Regex.run(~r/\bTxn[^>]*\bid="([^"]+)"/, xml) |> case do
1660
:-(
[_, v] -> v
1661
:-(
_ -> nil
1662 end
1663
:-(
txn_ts = Regex.run(~r/\bTxn[^>]*\bts="([^"]+)"/, xml) |> case do
1664
:-(
[_, v] -> v
1665
:-(
_ -> nil
1666 end
1667
:-(
txn_type = Regex.run(~r/\bTxn[^>]*\btype="([^"]+)"/, xml) |> case do
1668
:-(
[_, v] -> v
1669
:-(
_ -> nil
1670 end
1671
:-(
hbt_type = Regex.run(~r/<HbtMsg[^>]*\btype="([^"]+)"/, xml) |> case do
1672
:-(
[_, v] -> v
1673
:-(
_ -> nil
1674 end
1675
:-(
hbt_value = Regex.run(~r/<HbtMsg[^>]*\bvalue="([^"]+)"/, xml) |> case do
1676
:-(
[_, v] -> v
1677
:-(
_ -> nil
1678 end
1679
1680
:-(
parsed = %{
1681 "Txn.id" => txn_id,
1682 "Txn.ts" => txn_ts,
1683 "Txn.type" => txn_type,
1684 "HbtMsg.type" => hbt_type,
1685 "HbtMsg.value" => hbt_value,
1686 "xml" => xml # Keep the original xml for warnings
1687 }
1688
1689
:-(
errors = validate_endpoint_parameters(endpoint, parsed)
1690
:-(
%{
1691 valid: Enum.empty?(errors),
1692 errors: errors,
1693 warnings: generate_warnings(endpoint, parsed)
1694 }
1695
1696 _ ->
1697 # Raw XML attempt: try to extract known txn/hbt fields
1698
:-(
xml_string = String.trim(request_body || "")
1699
:-(
if xml_string |> String.starts_with?("<") do
1700 # Build a parsed map for validation checks
1701
:-(
txn_id = Regex.run(~r/\bTxn[^>]*\bid="([^"]+)"/, xml_string) |> case do
1702
:-(
[_, v] -> v
1703
:-(
_ -> nil
1704 end
1705
:-(
txn_ts = Regex.run(~r/\bTxn[^>]*\bts="([^"]+)"/, xml_string) |> case do
1706
:-(
[_, v] -> v
1707
:-(
_ -> nil
1708 end
1709
:-(
txn_type = Regex.run(~r/\bTxn[^>]*\btype="([^"]+)"/, xml_string) |> case do
1710
:-(
[_, v] -> v
1711
:-(
_ -> nil
1712 end
1713
:-(
hbt_type = Regex.run(~r/<HbtMsg[^>]*\btype="([^"]+)"/, xml_string) |> case do
1714
:-(
[_, v] -> v
1715
:-(
_ -> nil
1716 end
1717
:-(
hbt_value = Regex.run(~r/<HbtMsg[^>]*\bvalue="([^"]+)"/, xml_string) |> case do
1718
:-(
[_, v] -> v
1719
:-(
_ -> nil
1720 end
1721
1722
:-(
parsed = %{
1723 "Txn.id" => txn_id,
1724 "Txn.ts" => txn_ts,
1725 "Txn.type" => txn_type,
1726 "HbtMsg.type" => hbt_type,
1727 "HbtMsg.value" => hbt_value
1728 }
1729
1730
:-(
errors = validate_endpoint_parameters(endpoint, parsed)
1731
:-(
warnings = generate_warnings(endpoint, parsed)
1732
1733
:-(
%{
1734 valid: Enum.empty?(errors),
1735 errors: errors,
1736 warnings: warnings
1737 }
1738 else
1739
:-(
%{
1740 valid: false,
1741 errors: ["Invalid JSON and not recognized XML payload"],
1742 warnings: []
1743 }
1744 end
1745 end
1746 end
1747 end
1748 end
1749
1750 defp validate_endpoint_parameters(endpoint, parsed_body) do
1751
:-(
parameters = Map.get(endpoint, :parameters, [])
1752
:-(
required_params = Enum.filter(parameters, &(Map.get(&1, :required, false)))
1753
1754
:-(
Enum.reduce(required_params, [], fn param, errors ->
1755
:-(
param_name = Map.get(param, :name, "")
1756
:-(
if param_present?(parsed_body, param_name) do
1757
:-(
errors
1758 else
1759
:-(
["Missing required parameter: #{param_name}" | errors]
1760 end
1761 end)
1762 end
1763
1764 # Accepts top-level keys, camelCase/snake_case variants and nested
1765 # partner_merchant_payload or metadata maps used by partner-style requests.
1766 defp param_present?(parsed, param_name) when is_map(parsed) and is_binary(param_name) do
1767 # direct match
1768
:-(
cond do
1769
:-(
Map.has_key?(parsed, param_name) -> true
1770
:-(
Map.has_key?(parsed, snake(param_name)) -> true
1771
:-(
true ->
1772 # check common alternative keys
1773
:-(
case param_name do
1774 "payeeVPA" ->
1775
:-(
any_present?(parsed, ["payeeVPA", "payee_vpa", "merchant_vpa", "merchantVPA"], ["partner_merchant_payload", "metadata"])
1776 "merchantName" ->
1777
:-(
any_present?(parsed, ["merchantName", "merchant_name", "brand_name", "merchantName"], ["partner_merchant_payload"])
1778 "merchant_id" ->
1779
:-(
any_present?(parsed, ["merchant_id", "merchantId"], ["partner_merchant_payload"])
1780 "currency" ->
1781
:-(
any_present?(parsed, ["currency", "curr"], ["partner_merchant_payload"])
1782 other ->
1783 # generic check inside partner_merchant_payload
1784
:-(
any_present?(parsed, [other, snake(other)], ["partner_merchant_payload"])
1785 end
1786 end
1787 end
1788
1789
:-(
defp param_present?(_, _), do: false
1790
1791 defp any_present?(parsed, keys, nested_containers) do
1792
:-(
Enum.any?(keys, fn k -> Map.has_key?(parsed, k) end) or
1793
:-(
Enum.any?(nested_containers, fn container ->
1794
:-(
case Map.get(parsed, container) do
1795
:-(
%{} = nested -> Enum.any?(keys, fn k -> Map.has_key?(nested, k) or Map.has_key?(nested, snake(k)) end)
1796
:-(
_ -> false
1797 end
1798 end)
1799 end
1800
1801 defp snake(str) when is_binary(str) do
1802 str
1803 |> String.replace(~r/([A-Z])/, "_\\1")
1804 |> String.downcase()
1805
:-(
|> String.trim_leading("_")
1806 end
1807
1808 defp generate_warnings(endpoint, parsed_body) do
1809
:-(
warnings = []
1810
1811 # Check for unknown parameters
1812
:-(
known_params = Enum.map(endpoint.parameters || [], &(&1.name))
1813
1814 # If body contains nested maps (like metadata), treat top-level keys as known
1815
:-(
top_level_keys = Map.keys(parsed_body)
1816
1817 # Consider metadata and nested partner payload keys as acceptable
1818
:-(
allowed_extra_keys = ["metadata", "partner_merchant_payload", "partner_id", "merchant_id", "merchant_name", "currency", "corridor", "purpose_code", "merchant_category", "validity_minutes", "max_usage_count", "xml", "raw_xml"]
1819
1820
:-(
unknown_params = top_level_keys -- (known_params ++ allowed_extra_keys)
1821
1822
:-(
if length(unknown_params) > 0 do
1823 ["Unknown parameters will be ignored: #{Enum.join(unknown_params, ", ")}" | warnings]
1824 else
1825
:-(
warnings
1826 end
1827 end
1828
1829
1830 # Attempt a real HTTP request using Finch/HTTP client if available.
1831 # Falls back to simulated response if network call fails or URL cannot be determined.
1832 defp perform_real_api_call(endpoint, request_body, api_key, override_url \\ nil) do
1833 # Determine base URL from example_curl if present, otherwise use demo host
1834
:-(
target_url =
1835 cond do
1836
:-(
is_binary(override_url) and override_url != "" ->
1837 # If override is a path (e.g. "/api/v1/generate-static-qr" or "api/v1/generate-static-qr"),
1838 # convert it to a full demo URL. Otherwise, assume it's a full URL.
1839
:-(
case override_url do
1840
:-(
<<"/"::binary, _rest::binary>> -> "http://demo.ctrmv.com:4041" <> override_url
1841
:-(
<<first, _::binary>> when first != 47 -> "http://demo.ctrmv.com:4041/" <> String.trim(override_url)
1842
:-(
_ -> override_url
1843 end
1844
:-(
is_binary(endpoint.example_curl) ->
1845
:-(
case Regex.run(~r/"(https?:\/\/[^\"]+)"/, endpoint.example_curl) do
1846
:-(
[_, url] -> url
1847
:-(
_ -> "http://demo.ctrmv.com:4041" <> endpoint.path
1848 end
1849
:-(
true -> "http://demo.ctrmv.com:4041" <> endpoint.path
1850 end
1851
1852 # Build headers using provided api_key when available
1853
:-(
auth_header = if api_key in [nil, "", "YOUR_API_TOKEN"], do: {"Authorization", "Bearer YOUR_API_TOKEN"}, else: {"Authorization", "Bearer " <> api_key}
1854
:-(
headers = [{"Content-Type", "application/json"}, auth_header]
1855
1856 # Attempt a simple HTTP POST using :httpc (Erlang inets). If anything fails,
1857 # fall back to the simulated response to keep the LiveView responsive.
1858
:-(
try do
1859
:-(
:inets.start()
1860
:-(
:ssl.start()
1861
1862 # :httpc expects charlists for the URL but headers/body can be binaries if using body_format :binary
1863
:-(
http_headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
1864
:-(
http_body = to_string(request_body)
1865
1866
:-(
request = {to_charlist(target_url), http_headers, to_charlist("application/json"), http_body}
1867
1868
:-(
case :httpc.request(:post, request, [], [{:body_format, :binary}]) do
1869 {:ok, {{_http_version, status_code, _reason_phrase}, resp_headers, resp_body}} ->
1870
:-(
%{
1871 status: status_code,
1872
:-(
headers: Map.new(Enum.map(resp_headers, fn {k, v} -> {to_string(k), to_string(v)} end)),
1873
:-(
body: format_json(to_string(resp_body)),
1874 timing: :rand.uniform(500) + 50
1875 }
1876
1877 {:error, _} ->
1878
:-(
simulate_api_call(endpoint, request_body)
1879 end
1880 rescue
1881
:-(
_ -> simulate_api_call(endpoint, request_body)
1882 end
1883 end
1884
1885 defp get_template_for_endpoint(endpoint, template_type) do
1886
:-(
templates = %{
1887 "retail_merchant" => %{
1888
:-(
merchant_code: "RETAIL#{:rand.uniform(999)}",
1889 brand_name: "Retail Store ABC",
1890 merchant_vpa: "retailstore@upi",
1891 business_type: "RETAIL",
1892 corridors: ["DOMESTIC"],
1893 contact_email: "owner@retailstore.com",
1894 contact_phone: "+91-9876543210"
1895 },
1896 "restaurant_merchant" => %{
1897
:-(
merchant_code: "REST#{:rand.uniform(999)}",
1898 brand_name: "Delicious Restaurant",
1899 merchant_vpa: "restaurant@upi",
1900 business_type: "FOOD_AND_BEVERAGE",
1901 corridors: ["DOMESTIC"],
1902 contact_email: "manager@restaurant.com",
1903 contact_phone: "+91-9876543211"
1904 },
1905 "ecommerce_merchant" => %{
1906
:-(
merchant_code: "ECOM#{:rand.uniform(999)}",
1907 brand_name: "E-Shop Online",
1908 merchant_vpa: "eshop@upi",
1909 business_type: "ECOMMERCE",
1910 corridors: ["DOMESTIC", "SGD-INR"],
1911 contact_email: "support@eshop.com",
1912 contact_phone: "+91-9876543212",
1913 website: "https://eshop.com"
1914 },
1915 "international_merchant" => %{
1916
:-(
merchant_code: "INTL#{:rand.uniform(999)}",
1917 brand_name: "Global Trade Corp",
1918 merchant_vpa: "globaltrade@upi",
1919 business_type: "IMPORT_EXPORT",
1920 corridors: ["SGD-INR", "USD-INR", "EUR-INR"],
1921 contact_email: "finance@globaltrade.com",
1922 contact_phone: "+91-9876543213",
1923 fbar_number: "FBAR123456789"
1924 },
1925 "small_amount" => %{
1926 amount: "50.00"
1927 },
1928 "large_amount" => %{
1929 amount: "5000.00"
1930 }
1931 }
1932
1933
:-(
template = templates[template_type] || endpoint.example_request || %{}
1934
:-(
Jason.encode!(template, pretty: true)
1935 end
1936
1937 # Helper functions for UI
1938 def method_badge_class(method) do
1939
:-(
case String.upcase(method) do
1940
:-(
"GET" -> "badge-info"
1941
:-(
"POST" -> "badge-success"
1942
:-(
"PUT" -> "badge-warning"
1943
:-(
"DELETE" -> "badge-error"
1944
:-(
"PATCH" -> "badge-accent"
1945
:-(
_ -> "badge-neutral"
1946 end
1947 end
1948
1949 def status_badge_class(status) do
1950
:-(
case status do
1951
:-(
"active" -> "badge-success"
1952
:-(
"deprecated" -> "badge-warning"
1953
:-(
"beta" -> "badge-info"
1954
:-(
"experimental" -> "badge-accent"
1955
:-(
_ -> "badge-neutral"
1956 end
1957 end
1958
1959 def format_json(json_string) when is_binary(json_string) do
1960
:-(
case Jason.decode(json_string) do
1961
:-(
{:ok, parsed} -> Jason.encode!(parsed, pretty: true)
1962
:-(
{:error, _} -> json_string
1963 end
1964 end
1965
1966
:-(
def format_json(data), do: Jason.encode!(data, pretty: true)
1967
1968
:-(
def generate_curl_command(endpoint, request_body \\ "") do
1969
:-(
method_flag = case String.upcase(endpoint.method) do
1970
:-(
"GET" -> ""
1971
:-(
other -> "-X #{other}"
1972 end
1973
1974
:-(
headers = "-H \"Content-Type: application/json\" -H \"Authorization: Bearer YOUR_API_TOKEN\""
1975
1976
:-(
data_flag = case endpoint.method do
1977
:-(
"GET" -> ""
1978
:-(
_ -> if request_body != "", do: " -d '#{request_body}'", else: ""
1979 end
1980
1981
:-(
"curl #{method_flag} #{headers}#{data_flag} \"https://api.mercurypay.com#{endpoint.path}\""
1982 end
1983
1984 def group_endpoints_by_group(endpoints) do
1985 endpoints
1986
:-(
|> Enum.group_by(& &1.group)
1987
:-(
|> Map.to_list()
1988 end
1989
1990 # Helper functions for authentication and access control
1991
:-(
defp get_current_user(session) do
1992
:-(
case session do
1993 %{"user_id" => user_id} when is_binary(user_id) or is_integer(user_id) ->
1994
:-(
Accounts.get_user!(user_id)
1995
:-(
_ ->
1996 nil
1997 end
1998 rescue
1999
:-(
_ -> nil
2000 end
2001
2002
:-(
defp has_access?(_user), do: true
2003 end
Line Hits Source