cover/Elixir.DaProductApp.Monitoring.html

1 defmodule DaProductApp.Monitoring do
2 @moduledoc """
3 Monitoring context for read-only access to transactions, QR validations, analytics,
4 international payments, and settlements.
5
6 This context provides monitoring and analytics capabilities without performing
7 any operational activities. It's designed for administrative oversight and insights.
8 """
9
10 import Ecto.Query, warn: false
11 alias DaProductApp.Repo
12 alias DaProductApp.Transactions.{Transaction, ReqPay}
13 alias DaProductApp.QRValidation.QRValidation
14 alias DaProductApp.Accounts
15
16 # ================================
17 # TRANSACTION MONITORING
18 # ================================
19
20 @doc """
21 Returns a paginated list of transactions with optional filters.
22 """
23
:-(
def list_transactions(opts \\ []) do
24
:-(
search_term = Keyword.get(opts, :search_term, "")
25
:-(
status_filter = Keyword.get(opts, :status, :all)
26
:-(
state_filter = Keyword.get(opts, :state, :all)
27
:-(
transaction_type_filter = Keyword.get(opts, :transaction_type, :all)
28
:-(
date_from = Keyword.get(opts, :date_from)
29
:-(
date_to = Keyword.get(opts, :date_to)
30 # Pagination: support page & page_size OR offset & limit
31
:-(
page = Keyword.get(opts, :page, nil)
32
:-(
page_size = Keyword.get(opts, :page_size, nil)
33
:-(
offset = Keyword.get(opts, :offset, 0)
34
:-(
limit = Keyword.get(opts, :limit, nil) || page_size || 50
35
:-(
offset = case {page, page_size} do
36
:-(
{p, ps} when is_integer(p) and is_integer(ps) and p > 0 -> (p - 1) * ps
37
:-(
_ -> offset || 0
38 end
39
40
:-(
query = from t in Transaction,
41 left_join: p in assoc(t, :partner),
42 left_join: m in assoc(t, :merchant),
43 preload: [partner: p, merchant: m],
44 order_by: [desc: t.inserted_at]
45
46
:-(
query = apply_transaction_filters(query, search_term, status_filter, state_filter, transaction_type_filter, date_from, date_to)
47
48 query
49 |> limit(^limit)
50
:-(
|> offset(^offset)
51
:-(
|> Repo.all()
52 end
53
54 @doc """
55 Gets a single transaction with all associations.
56 """
57 def get_transaction!(id) do
58 Transaction
59
:-(
|> preload([:partner, :merchant, :events])
60
:-(
|> Repo.get!(id)
61 end
62
63 @doc """
64 Returns transaction statistics for analytics.
65 """
66
:-(
def get_transaction_stats(date_from \\ nil, date_to \\ nil) do
67
:-(
query = from t in Transaction
68
69
:-(
query = if date_from && date_to do
70 # Convert dates to datetime if they are Date structs
71
:-(
{start_datetime, end_datetime} = normalize_date_range(date_from, date_to)
72
:-(
from t in query,
73 where: t.inserted_at >= ^start_datetime and t.inserted_at <= ^end_datetime
74 else
75
:-(
query
76 end
77
78
:-(
total_count = Repo.aggregate(query, :count, :id)
79
80
:-(
status_counts = query
81 |> group_by([t], t.status)
82
:-(
|> select([t], {t.status, count(t.id)})
83 |> Repo.all()
84 |> Enum.into(%{})
85
86
:-(
state_counts = query
87 |> group_by([t], t.current_state)
88
:-(
|> select([t], {t.current_state, count(t.id)})
89 |> Repo.all()
90 |> Enum.into(%{})
91
92
:-(
currency_stats = query
93 |> where([t], not is_nil(t.foreign_currency))
94 |> group_by([t], t.foreign_currency)
95
:-(
|> select([t], {t.foreign_currency, count(t.id), sum(t.foreign_amount)})
96 |> Repo.all()
97
98
:-(
success_rate = if total_count > 0 do
99
:-(
case Map.get(status_counts, "success", 0) do
100
:-(
0 -> 0.0
101
:-(
success_count -> Float.round(success_count / total_count * 100, 2)
102 end
103 else
104 0.0
105 end
106
107
:-(
%{
108 total_count: total_count,
109 status_counts: status_counts,
110 state_counts: state_counts,
111 currency_stats: currency_stats,
112 success_rate: success_rate
113 }
114 end
115
116 # ================================
117 # QR VALIDATION MONITORING
118 # ================================
119
120 @doc """
121 Returns a paginated list of QR validations with optional filters.
122 """
123
:-(
def list_qr_validations(opts \\ []) do
124
:-(
search_term = Keyword.get(opts, :search_term, "")
125
:-(
status_filter = Keyword.get(opts, :status, :all)
126
:-(
validation_type_filter = Keyword.get(opts, :validation_type, :all)
127
:-(
corridor_filter = Keyword.get(opts, :corridor, :all)
128
:-(
date_from = Keyword.get(opts, :date_from)
129
:-(
date_to = Keyword.get(opts, :date_to)
130 # Pagination: support page & page_size OR offset & limit
131
:-(
page = Keyword.get(opts, :page, nil)
132
:-(
page_size = Keyword.get(opts, :page_size, nil)
133
:-(
offset = Keyword.get(opts, :offset, 0)
134
:-(
limit = Keyword.get(opts, :limit, nil) || page_size || 50
135
:-(
offset = case {page, page_size} do
136
:-(
{p, ps} when is_integer(p) and is_integer(ps) and p > 0 -> (p - 1) * ps
137
:-(
_ -> offset || 0
138 end
139
140
:-(
query = from qr in QRValidation,
141 left_join: p in assoc(qr, :partner),
142 left_join: m in assoc(qr, :merchant),
143 preload: [partner: p, merchant: m],
144 order_by: [desc: qr.inserted_at]
145
146
:-(
query = apply_qr_validation_filters(query, search_term, status_filter, validation_type_filter, corridor_filter, date_from, date_to)
147
148 query
149 |> limit(^limit)
150
:-(
|> offset(^offset)
151
:-(
|> Repo.all()
152 end
153
154 @doc """
155 Gets a single QR validation with all associations.
156 """
157 def get_qr_validation!(id) do
158 QRValidation
159
:-(
|> preload([:partner, :merchant, :events])
160
:-(
|> Repo.get!(id)
161 end
162
163 @doc """
164 Returns QR validation statistics for analytics.
165 """
166
:-(
def get_qr_validation_stats(date_from \\ nil, date_to \\ nil) do
167
:-(
query = from qr in QRValidation
168
169
:-(
query = if date_from && date_to do
170 # Convert dates to datetime if they are Date structs
171
:-(
{start_datetime, end_datetime} = normalize_date_range(date_from, date_to)
172
:-(
from qr in query,
173 where: qr.inserted_at >= ^start_datetime and qr.inserted_at <= ^end_datetime
174 else
175
:-(
query
176 end
177
178
:-(
total_count = Repo.aggregate(query, :count, :id)
179
180
:-(
status_counts = query
181 |> group_by([qr], qr.status)
182
:-(
|> select([qr], {qr.status, count(qr.id)})
183 |> Repo.all()
184 |> Enum.into(%{})
185
186
:-(
type_counts = query
187 |> group_by([qr], qr.validation_type)
188
:-(
|> select([qr], {qr.validation_type, count(qr.id)})
189 |> Repo.all()
190 |> Enum.into(%{})
191
192
:-(
corridor_stats = query
193 |> where([qr], not is_nil(qr.corridor))
194 |> group_by([qr], qr.corridor)
195
:-(
|> select([qr], {qr.corridor, count(qr.id), sum(qr.base_amount)})
196 |> Repo.all()
197
198
:-(
validation_rate = if total_count > 0 do
199
:-(
case Map.get(status_counts, "validated", 0) do
200
:-(
0 -> 0.0
201
:-(
validated_count -> Float.round(validated_count / total_count * 100, 2)
202 end
203 else
204 0.0
205 end
206
207
:-(
%{
208 total_count: total_count,
209 status_counts: status_counts,
210 type_counts: type_counts,
211 corridor_stats: corridor_stats,
212 validation_rate: validation_rate
213 }
214 end
215
216 # ================================
217 # REQPAY MONITORING
218 # ================================
219
220 @doc """
221 Returns a paginated list of ReqPay payment requests with optional filters.
222 """
223
:-(
def list_req_pays(opts \\ []) do
224
:-(
search_term = Keyword.get(opts, :search_term, "")
225
:-(
status_filter = Keyword.get(opts, :status, :all)
226
:-(
payment_status_filter = Keyword.get(opts, :payment_status, :all)
227
:-(
validation_type_filter = Keyword.get(opts, :validation_type, :all)
228
:-(
corridor_filter = Keyword.get(opts, :corridor, :all)
229
:-(
date_from = Keyword.get(opts, :date_from)
230
:-(
date_to = Keyword.get(opts, :date_to)
231
:-(
page = Keyword.get(opts, :page, nil)
232
:-(
page_size = Keyword.get(opts, :page_size, nil)
233
:-(
offset = Keyword.get(opts, :offset, 0)
234
:-(
limit = Keyword.get(opts, :limit, nil) || page_size || 50
235
:-(
offset = case {page, page_size} do
236
:-(
{p, ps} when is_integer(p) and is_integer(ps) and p > 0 -> (p - 1) * ps
237
:-(
_ -> offset || 0
238 end
239
240
:-(
query = from rp in ReqPay,
241 left_join: p in assoc(rp, :partner),
242 left_join: m in assoc(rp, :merchant),
243 left_join: t in assoc(rp, :transaction),
244 preload: [partner: p, merchant: m, transaction: t],
245 order_by: [desc: rp.inserted_at]
246
247
:-(
query = apply_req_pay_filters(query, search_term, status_filter, payment_status_filter, validation_type_filter, corridor_filter, date_from, date_to)
248
249 query
250 |> limit(^limit)
251
:-(
|> offset(^offset)
252
:-(
|> Repo.all()
253 end
254
255 @doc """
256 Gets a single ReqPay with all associations.
257 """
258 def get_req_pay!(id) do
259 ReqPay
260
:-(
|> preload([:partner, :merchant, :transaction])
261
:-(
|> Repo.get!(id)
262 end
263
264 @doc """
265 Returns ReqPay statistics for analytics.
266 """
267
:-(
def get_req_pay_stats(date_from \\ nil, date_to \\ nil) do
268
:-(
query = from rp in ReqPay
269
270
:-(
query = if date_from && date_to do
271 # Convert dates to datetime if they are Date structs
272
:-(
{start_datetime, end_datetime} = normalize_date_range(date_from, date_to)
273
:-(
from rp in query,
274 where: rp.inserted_at >= ^start_datetime and rp.inserted_at <= ^end_datetime
275 else
276
:-(
query
277 end
278
279
:-(
total_count = Repo.aggregate(query, :count, :id)
280
281
:-(
status_counts = query
282 |> group_by([rp], rp.status)
283
:-(
|> select([rp], {rp.status, count(rp.id)})
284 |> Repo.all()
285 |> Enum.into(%{})
286
287
:-(
payment_status_counts = query
288 |> group_by([rp], rp.payment_status)
289
:-(
|> select([rp], {rp.payment_status, count(rp.id)})
290 |> Repo.all()
291 |> Enum.into(%{})
292
293
:-(
type_counts = query
294 |> group_by([rp], rp.validation_type)
295
:-(
|> select([rp], {rp.validation_type, count(rp.id)})
296 |> Repo.all()
297 |> Enum.into(%{})
298
299
:-(
corridor_stats = query
300 |> where([rp], not is_nil(rp.corridor))
301 |> group_by([rp], rp.corridor)
302
:-(
|> select([rp], {rp.corridor, count(rp.id), sum(rp.amount)})
303 |> Repo.all()
304
305
:-(
success_rate = if total_count > 0 do
306
:-(
case Map.get(payment_status_counts, "SUCCESS", 0) do
307
:-(
0 -> 0.0
308
:-(
success_count -> Float.round(success_count / total_count * 100, 2)
309 end
310 else
311 0.0
312 end
313
314
:-(
%{
315 total_count: total_count,
316 status_counts: status_counts,
317 payment_status_counts: payment_status_counts,
318 type_counts: type_counts,
319 corridor_stats: corridor_stats,
320 success_rate: success_rate
321 }
322 end
323
324 # ================================
325 # ANALYTICS DASHBOARD DATA
326 # ================================
327
328 @doc """
329 Returns comprehensive analytics data for the dashboard.
330 """
331
:-(
def get_dashboard_analytics(date_from \\ nil, date_to \\ nil) do
332
:-(
%{
333 transactions: get_transaction_stats(date_from, date_to),
334 qr_validations: get_qr_validation_stats(date_from, date_to),
335 req_pays: get_req_pay_stats(date_from, date_to),
336 daily_trends: get_daily_trends(date_from, date_to),
337 corridor_performance: get_corridor_performance(date_from, date_to)
338 }
339 end
340
341 @doc """
342 Returns daily trends for charts.
343 """
344
:-(
def get_daily_trends(date_from \\ nil, date_to \\ nil) do
345 # Default to last 30 days if no dates provided
346
:-(
{start_date, end_date} = case {date_from, date_to} do
347 {nil, nil} ->
348
:-(
end_date = DateTime.utc_now() |> DateTime.to_date()
349
:-(
start_date = Date.add(end_date, -30)
350 {start_date, end_date}
351
:-(
{from, to} -> {from, to}
352 end
353
354 # Transaction trends
355
:-(
transaction_trends = from t in Transaction,
356 where: fragment("DATE(?)", t.inserted_at) >= ^start_date and fragment("DATE(?)", t.inserted_at) <= ^end_date,
357 group_by: fragment("DATE(?)", t.inserted_at),
358 select: {fragment("DATE(?)", t.inserted_at), count(t.id)},
359 order_by: [asc: fragment("DATE(?)", t.inserted_at)]
360
361 # QR validation trends
362
:-(
qr_trends = from qr in QRValidation,
363 where: fragment("DATE(?)", qr.inserted_at) >= ^start_date and fragment("DATE(?)", qr.inserted_at) <= ^end_date,
364 group_by: fragment("DATE(?)", qr.inserted_at),
365 select: {fragment("DATE(?)", qr.inserted_at), count(qr.id)},
366 order_by: [asc: fragment("DATE(?)", qr.inserted_at)]
367
368
:-(
%{
369 transactions: Repo.all(transaction_trends),
370 qr_validations: Repo.all(qr_trends)
371 }
372 end
373
374 @doc """
375 Returns corridor performance metrics.
376 """
377
:-(
def get_corridor_performance(date_from \\ nil, date_to \\ nil) do
378
:-(
query = from t in Transaction,
379 where: not is_nil(t.corridor)
380
381
:-(
query = if date_from && date_to do
382 # Convert dates to datetime if they are Date structs
383
:-(
{start_datetime, end_datetime} = normalize_date_range(date_from, date_to)
384
:-(
from t in query,
385 where: t.inserted_at >= ^start_datetime and t.inserted_at <= ^end_datetime
386 else
387
:-(
query
388 end
389
390 query
391 |> group_by([t], [t.corridor, t.status])
392
:-(
|> select([t], {t.corridor, t.status, count(t.id), sum(t.foreign_amount)})
393 |> Repo.all()
394
:-(
|> Enum.group_by(fn {corridor, _, _, _} -> corridor end)
395
:-(
|> Enum.into(%{}, fn {corridor, stats} ->
396
:-(
corridor_data = Enum.reduce(stats, %{total: 0, success: 0, failure: 0, volume: Decimal.new(0)}, fn
397 {_, "success", count, amount}, acc ->
398
:-(
%{acc | success: count, total: acc.total + count, volume: Decimal.add(acc.volume, amount || 0)}
399 {_, "failure", count, amount}, acc ->
400
:-(
%{acc | failure: count, total: acc.total + count, volume: Decimal.add(acc.volume, amount || 0)}
401 {_, _, count, amount}, acc ->
402
:-(
%{acc | total: acc.total + count, volume: Decimal.add(acc.volume, amount || 0)}
403 end)
404
405
:-(
success_rate = if corridor_data.total > 0 do
406
:-(
Float.round(corridor_data.success / corridor_data.total * 100, 2)
407 else
408 0.0
409 end
410
411 {corridor, Map.put(corridor_data, :success_rate, success_rate)}
412 end)
413 end
414
415 # ================================
416 # PRIVATE HELPER FUNCTIONS
417 # ================================
418
419 defp apply_transaction_filters(query, search_term, status_filter, state_filter, transaction_type_filter, date_from, date_to) do
420
:-(
query = if search_term != "" do
421
:-(
search_pattern = "%#{search_term}%"
422
:-(
from t in query,
423 where: ilike(t.org_txn_id, ^search_pattern) or
424 ilike(t.payer_addr, ^search_pattern) or
425 ilike(t.payee_addr, ^search_pattern) or
426 ilike(t.payer_name, ^search_pattern) or
427 ilike(t.payee_name, ^search_pattern)
428 else
429
:-(
query
430 end
431
432
:-(
query = if status_filter != :all do
433
:-(
from t in query, where: t.status == ^to_string(status_filter)
434 else
435
:-(
query
436 end
437
438
:-(
query = if state_filter != :all do
439
:-(
from t in query, where: t.current_state == ^to_string(state_filter)
440 else
441
:-(
query
442 end
443
444
:-(
query = if transaction_type_filter != :all do
445
:-(
from t in query, where: t.transaction_type == ^to_string(transaction_type_filter)
446 else
447
:-(
query
448 end
449
450
:-(
query = if date_from do
451 # Convert date to datetime if it's a Date struct
452
:-(
start_datetime = normalize_date_to_datetime(date_from, :start_of_day)
453
:-(
from t in query, where: t.inserted_at >= ^start_datetime
454 else
455
:-(
query
456 end
457
458
:-(
if date_to do
459 # Convert date to datetime if it's a Date struct
460
:-(
end_datetime = normalize_date_to_datetime(date_to, :end_of_day)
461
:-(
from t in query, where: t.inserted_at <= ^end_datetime
462 else
463
:-(
query
464 end
465 end
466
467 defp apply_qr_validation_filters(query, search_term, status_filter, validation_type_filter, corridor_filter, date_from, date_to) do
468
:-(
query = if search_term != "" do
469
:-(
search_pattern = "%#{search_term}%"
470
:-(
from qr in query,
471 where: ilike(qr.txn_id, ^search_pattern) or
472 ilike(qr.payer_addr, ^search_pattern) or
473 ilike(qr.payee_addr, ^search_pattern) or
474 ilike(qr.payer_name, ^search_pattern) or
475 ilike(qr.payee_name, ^search_pattern)
476 else
477
:-(
query
478 end
479
480
:-(
query = if status_filter != :all do
481
:-(
from qr in query, where: qr.status == ^to_string(status_filter)
482 else
483
:-(
query
484 end
485
486
:-(
query = if validation_type_filter != :all do
487
:-(
from qr in query, where: qr.validation_type == ^to_string(validation_type_filter)
488 else
489
:-(
query
490 end
491
492
:-(
query = if corridor_filter != :all do
493
:-(
from qr in query, where: qr.corridor == ^to_string(corridor_filter)
494 else
495
:-(
query
496 end
497
498
:-(
query = if date_from do
499 # Convert date to datetime if it's a Date struct
500
:-(
start_datetime = normalize_date_to_datetime(date_from, :start_of_day)
501
:-(
from qr in query, where: qr.inserted_at >= ^start_datetime
502 else
503
:-(
query
504 end
505
506
:-(
if date_to do
507 # Convert date to datetime if it's a Date struct
508
:-(
end_datetime = normalize_date_to_datetime(date_to, :end_of_day)
509
:-(
from qr in query, where: qr.inserted_at <= ^end_datetime
510 else
511
:-(
query
512 end
513 end
514
515 defp apply_req_pay_filters(query, search_term, status_filter, payment_status_filter, validation_type_filter, corridor_filter, date_from, date_to) do
516
:-(
query = if search_term != "" do
517
:-(
search_pattern = "%#{search_term}%"
518
:-(
from rp in query,
519 where: ilike(rp.msg_id, ^search_pattern) or
520 ilike(rp.txn_id, ^search_pattern) or
521 ilike(rp.payer_addr, ^search_pattern) or
522 ilike(rp.payee_addr, ^search_pattern) or
523 ilike(rp.payer_name, ^search_pattern) or
524 ilike(rp.payee_name, ^search_pattern)
525 else
526
:-(
query
527 end
528
529
:-(
query = if status_filter != :all do
530
:-(
from rp in query, where: rp.status == ^to_string(status_filter)
531 else
532
:-(
query
533 end
534
535
:-(
query = if payment_status_filter != :all do
536
:-(
from rp in query, where: rp.payment_status == ^to_string(payment_status_filter)
537 else
538
:-(
query
539 end
540
541
:-(
query = if validation_type_filter != :all do
542
:-(
from rp in query, where: rp.validation_type == ^to_string(validation_type_filter)
543 else
544
:-(
query
545 end
546
547
:-(
query = if corridor_filter != :all do
548
:-(
from rp in query, where: rp.corridor == ^to_string(corridor_filter)
549 else
550
:-(
query
551 end
552
553
:-(
query = if date_from do
554 # Convert date to datetime if it's a Date struct
555
:-(
start_datetime = normalize_date_to_datetime(date_from, :start_of_day)
556
:-(
from rp in query, where: rp.inserted_at >= ^start_datetime
557 else
558
:-(
query
559 end
560
561
:-(
if date_to do
562 # Convert date to datetime if it's a Date struct
563
:-(
end_datetime = normalize_date_to_datetime(date_to, :end_of_day)
564
:-(
from rp in query, where: rp.inserted_at <= ^end_datetime
565 else
566
:-(
query
567 end
568 end
569
570 # Helper function to normalize date range to datetime range
571 defp normalize_date_range(date_from, date_to) do
572
:-(
start_datetime = normalize_date_to_datetime(date_from, :start_of_day)
573
:-(
end_datetime = normalize_date_to_datetime(date_to, :end_of_day)
574 {start_datetime, end_datetime}
575 end
576
577 # Helper function to convert Date to DateTime
578 defp normalize_date_to_datetime(%Date{} = date, :start_of_day) do
579
:-(
DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
580 end
581
582 defp normalize_date_to_datetime(%Date{} = date, :end_of_day) do
583
:-(
DateTime.new!(date, ~T[23:59:59.999999], "Etc/UTC")
584 end
585
586 # If already a DateTime, return as-is
587
:-(
defp normalize_date_to_datetime(%DateTime{} = datetime, _), do: datetime
588
589 # If it's a naive datetime, assume UTC
590 defp normalize_date_to_datetime(%NaiveDateTime{} = naive_datetime, _) do
591
:-(
DateTime.from_naive!(naive_datetime, "Etc/UTC")
592 end
593
594 # ================================
595 # INTERNATIONAL PAYMENTS MONITORING
596 # ================================
597
598 @doc """
599 Returns international payments with filters for monitoring.
600 """
601
:-(
def list_international_payments_with_filters(filters \\ %{}) do
602
:-(
search_term = Map.get(filters, :search, "")
603
:-(
corridor_filter = Map.get(filters, :corridor, :all)
604
:-(
status_filter = Map.get(filters, :status, :all)
605
:-(
partner_filter = Map.get(filters, :partner_id, :all)
606
:-(
date_from = Map.get(filters, :date_from)
607
:-(
date_to = Map.get(filters, :date_to)
608
:-(
page = Map.get(filters, :page, 1)
609
:-(
per_page = Map.get(filters, :per_page, 20)
610
611 # Simulated international payments data
612
:-(
international_payments = [
613 %{
614 id: 1,
615 org_txn_id: "INTL20240115001",
616 partner_txn_id: "SING12345",
617 status: "success",
618 corridor: "SGD-INR",
619 payer_name: "John Doe",
620 payer_addr: "john@sg-bank",
621 payee_name: "Ravi Kumar",
622 payee_addr: "ravi@paytm",
623 payee_mid: nil,
624 foreign_amount: 100.0,
625 foreign_currency: "SGD",
626 inr_amount: 6150.0,
627 fx_rate: 61.50,
628 markup_rate: 0.5,
629 fx_provider: "reuters",
630 fx_locked_at: ~N[2024-01-15 10:30:00],
631 partner: %{id: 1, name: "Singapore Bank"},
632 inserted_at: ~N[2024-01-15 10:30:00],
633 npci_received_at: ~N[2024-01-15 10:31:00],
634 credit_requested_at: ~N[2024-01-15 10:31:30],
635 partner_credited_at: ~N[2024-01-15 10:32:00],
636 completed_at: ~N[2024-01-15 10:32:30],
637 failure_code: nil,
638 failure_reason: nil
639 },
640 %{
641 id: 2,
642 org_txn_id: "INTL20240115002",
643 partner_txn_id: "USA67890",
644 status: "pending",
645 corridor: "USD-INR",
646 payer_name: "Jane Smith",
647 payer_addr: "jane@us-bank",
648 payee_name: "Priya Sharma",
649 payee_addr: "priya@phonepe",
650 payee_mid: nil,
651 foreign_amount: 50.0,
652 foreign_currency: "USD",
653 inr_amount: 4160.0,
654 fx_rate: 83.20,
655 markup_rate: 0.5,
656 fx_provider: "reuters",
657 fx_locked_at: ~N[2024-01-15 11:00:00],
658 partner: %{id: 2, name: "US Bank Corp"},
659 inserted_at: ~N[2024-01-15 11:00:00],
660 npci_received_at: nil,
661 credit_requested_at: nil,
662 partner_credited_at: nil,
663 completed_at: nil,
664 failure_code: nil,
665 failure_reason: nil
666 }
667 ]
668
669 # Apply filters
670
:-(
filtered_payments = apply_international_payment_filters(international_payments, filters)
671
672 # Simulate pagination
673
:-(
offset = (page - 1) * per_page
674
:-(
Enum.slice(filtered_payments, offset, per_page)
675 end
676
677 @doc """
678 Gets international payment statistics.
679 """
680
:-(
def get_international_payment_stats(filters \\ %{}) do
681
:-(
payments = list_international_payments_with_filters(Map.put(filters, :per_page, 1000))
682
683
:-(
total_count = length(payments)
684
:-(
status_counts = Enum.group_by(payments, & &1.status) |> Enum.map(fn {k, v} -> {k, length(v)} end) |> Map.new()
685
686
:-(
corridor_performance = payments
687
:-(
|> Enum.group_by(& &1.corridor)
688 |> Enum.map(fn {corridor, corridor_payments} ->
689
:-(
total = length(corridor_payments)
690
:-(
success = Enum.count(corridor_payments, &(&1.status == "success"))
691
:-(
failure = total - success
692
:-(
volume = corridor_payments |> Enum.map(& &1.inr_amount) |> Enum.sum()
693
:-(
success_rate = if total > 0, do: Float.round(success / total * 100, 1), else: 0.0
694
695 {corridor, %{
696 total: total,
697 success: success,
698 failure: failure,
699 volume: volume,
700 success_rate: success_rate
701 }}
702 end)
703
:-(
|> Map.new()
704
705
:-(
fx_volume = payments |> Enum.map(& &1.inr_amount) |> Enum.sum()
706
707
:-(
%{
708 total_count: total_count,
709 status_counts: status_counts,
710 corridor_performance: corridor_performance,
711 fx_volume: fx_volume
712 }
713 end
714
715 @doc """
716 Gets current FX rates for display.
717 """
718 def get_current_fx_rates do
719
:-(
%{
720 "USD" => %{rate: 83.20, last_updated: NaiveDateTime.utc_now()},
721 "SGD" => %{rate: 61.50, last_updated: NaiveDateTime.utc_now()},
722 "AED" => %{rate: 22.65, last_updated: NaiveDateTime.utc_now()}
723 }
724 end
725
726 @doc """
727 Gets international payment by ID.
728 """
729 def get_international_payment_by_id(id) do
730
:-(
case Integer.parse(id) do
731 {int_id, ""} ->
732 list_international_payments_with_filters()
733
:-(
|> Enum.find(&(&1.id == int_id))
734
:-(
_ ->
735 nil
736 end
737 end
738
739 defp apply_international_payment_filters(payments, filters) do
740
:-(
search_term = Map.get(filters, :search, "")
741
:-(
corridor_filter = Map.get(filters, :corridor, :all)
742
:-(
status_filter = Map.get(filters, :status, :all)
743
744 payments
745
:-(
|> Enum.filter(fn payment ->
746
:-(
search_match = search_term == "" or
747
:-(
String.contains?(String.downcase(payment.org_txn_id), String.downcase(search_term)) or
748
:-(
String.contains?(String.downcase(payment.payer_name || ""), String.downcase(search_term)) or
749
:-(
String.contains?(String.downcase(payment.payee_name || ""), String.downcase(search_term))
750
751
:-(
corridor_match = corridor_filter == :all or payment.corridor == corridor_filter
752
:-(
status_match = status_filter == :all or payment.status == status_filter
753
754
:-(
search_match and corridor_match and status_match
755 end)
756 end
757
758 # ================================
759 # SETTLEMENTS MONITORING
760 # ================================
761
762 @doc """
763 Returns settlements with filters for monitoring.
764 """
765
:-(
def list_settlements_with_filters(filters \\ %{}) do
766
:-(
search_term = Map.get(filters, :search, "")
767
:-(
status_filter = Map.get(filters, :status, :all)
768
:-(
type_filter = Map.get(filters, :type, :all)
769
:-(
partner_filter = Map.get(filters, :partner_id, :all)
770
:-(
date_from = Map.get(filters, :date_from)
771
:-(
date_to = Map.get(filters, :date_to)
772
:-(
page = Map.get(filters, :page, 1)
773
:-(
per_page = Map.get(filters, :per_page, 20)
774
775 # Simulated settlements data
776
:-(
settlements = [
777 %{
778 id: "SET001",
779 reference_id: "REF2024001",
780 batch_id: "BATCH001",
781 type: "net",
782 status: "completed",
783 frequency: "daily",
784 priority: "normal",
785 settlement_amount: 50000.0,
786 fee_amount: 250.0,
787 tax_amount: 45.0,
788 net_amount: 49705.0,
789 transaction_count: 125,
790 currency: "INR",
791 partner: %{id: 1, name: "Partner Bank A"},
792 partner_account_number: "1234567890",
793 partner_ifsc: "BANK0123456",
794 partner_bank_name: "Partner Bank Ltd",
795 inserted_at: ~N[2024-01-15 09:00:00],
796 scheduled_at: ~N[2024-01-15 18:00:00],
797 started_at: ~N[2024-01-15 18:00:00],
798 approved_at: ~N[2024-01-15 18:05:00],
799 bank_processed_at: ~N[2024-01-15 18:10:00],
800 completed_at: ~N[2024-01-15 18:15:00],
801 settlement_window_start: ~N[2024-01-15 18:00:00],
802 settlement_window_end: ~N[2024-01-15 22:00:00],
803 failure_code: nil,
804 failure_reason: nil,
805 retry_count: 0,
806 reconciliation_status: "reconciled",
807 reconciled_at: ~N[2024-01-15 18:20:00],
808 reconciliation_reference: "REC001"
809 },
810 %{
811 id: "SET002",
812 reference_id: "REF2024002",
813 batch_id: "BATCH002",
814 type: "merchant",
815 status: "processing",
816 frequency: "daily",
817 priority: "high",
818 settlement_amount: 25000.0,
819 fee_amount: 125.0,
820 tax_amount: 22.5,
821 net_amount: 24852.5,
822 transaction_count: 62,
823 currency: "INR",
824 partner: %{id: 2, name: "Merchant Services Inc"},
825 partner_account_number: "9876543210",
826 partner_ifsc: "BANK0987654",
827 partner_bank_name: "Merchant Bank Ltd",
828 inserted_at: ~N[2024-01-15 10:00:00],
829 scheduled_at: ~N[2024-01-15 19:00:00],
830 started_at: ~N[2024-01-15 19:00:00],
831 approved_at: ~N[2024-01-15 19:05:00],
832 bank_processed_at: nil,
833 completed_at: nil,
834 settlement_window_start: ~N[2024-01-15 19:00:00],
835 settlement_window_end: ~N[2024-01-15 23:00:00],
836 failure_code: nil,
837 failure_reason: nil,
838 retry_count: 0,
839 reconciliation_status: "pending",
840 reconciled_at: nil,
841 reconciliation_reference: nil
842 }
843 ]
844
845 # Apply filters
846
:-(
filtered_settlements = apply_settlement_filters(settlements, filters)
847
848 # Simulate pagination
849
:-(
offset = (page - 1) * per_page
850
:-(
Enum.slice(filtered_settlements, offset, per_page)
851 end
852
853 @doc """
854 Gets settlement statistics.
855 """
856
:-(
def get_settlement_stats(filters \\ %{}) do
857
:-(
settlements = list_settlements_with_filters(Map.put(filters, :per_page, 1000))
858
859
:-(
total_count = length(settlements)
860
:-(
completed_count = Enum.count(settlements, &(&1.status == "completed"))
861
:-(
processing_count = Enum.count(settlements, &(&1.status == "processing"))
862
:-(
total_volume = settlements |> Enum.map(& &1.settlement_amount) |> Enum.sum()
863
864
:-(
type_breakdown = settlements
865
:-(
|> Enum.group_by(& &1.type)
866 |> Enum.map(fn {type, type_settlements} ->
867
:-(
count = length(type_settlements)
868
:-(
volume = type_settlements |> Enum.map(& &1.settlement_amount) |> Enum.sum()
869 {type, %{count: count, volume: volume}}
870 end)
871
:-(
|> Map.new()
872
873
:-(
%{
874 total_count: total_count,
875 completed_count: completed_count,
876 processing_count: processing_count,
877 total_volume: total_volume,
878 type_breakdown: type_breakdown
879 }
880 end
881
882 @doc """
883 Gets settlement by ID.
884 """
885 def get_settlement_by_id(id) do
886 list_settlements_with_filters()
887
:-(
|> Enum.find(&(&1.id == id))
888 end
889
890 @doc """
891 Lists active settlement partners.
892 """
893
:-(
def list_active_settlement_partners do
894 [
895 %{id: 1, name: "Partner Bank A"},
896 %{id: 2, name: "Merchant Services Inc"},
897 %{id: 3, name: "International Gateway Ltd"}
898 ]
899 end
900
901 defp apply_settlement_filters(settlements, filters) do
902
:-(
search_term = Map.get(filters, :search, "")
903
:-(
status_filter = Map.get(filters, :status, :all)
904
:-(
type_filter = Map.get(filters, :type, :all)
905
906 settlements
907
:-(
|> Enum.filter(fn settlement ->
908
:-(
search_match = search_term == "" or
909
:-(
String.contains?(String.downcase(settlement.id), String.downcase(search_term)) or
910
:-(
String.contains?(String.downcase(settlement.reference_id || ""), String.downcase(search_term))
911
912
:-(
status_match = status_filter == :all or settlement.status == status_filter
913
:-(
type_match = type_filter == :all or settlement.type == type_filter
914
915
:-(
search_match and status_match and type_match
916 end)
917 end
918 end
Line Hits Source