| 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&pn=Demo%20Merchant&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 |