cover/Elixir.DaProductAppWeb.InternationalPaymentsLive.html

1
:-(
defmodule DaProductAppWeb.InternationalPaymentsLive do
2 @moduledoc """
3 LiveView for International Payments monitoring and analytics.
4
5 Features:
6 - Read-only international payment monitoring
7 - Multi-corridor tracking and analytics
8 - FX rate monitoring and markup tracking
9 - Partner integration status
10 - Settlement tracking and reconciliation
11 """
12 use DaProductAppWeb, :live_view
13
14 alias DaProductApp.Accounts
15 alias DaProductApp.Monitoring
16
17 @impl true
18
:-(
def mount(_params, session, socket) do
19
:-(
case get_current_user(session) do
20
:-(
nil ->
21 {:ok, redirect(socket, to: ~p"/login")}
22
23 user ->
24
:-(
if has_access?(user) do
25
:-(
if connected?(socket) do
26 # Subscribe to international payment events for real-time updates
27
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "international_payments")
28
:-(
Phoenix.PubSub.subscribe(DaProductApp.PubSub, "transactions")
29 end
30
31
:-(
socket =
32 socket
33 |> assign(:current_user, user)
34 |> assign(:page_title, "International Payments")
35 |> assign(:current_page, :international_payments)
36 |> assign(:international_payments, [])
37 |> assign(:search_term, "")
38 |> assign(:corridor_filter, :all)
39 |> assign(:status_filter, :all)
40 |> assign(:partner_filter, :all)
41 |> assign(:date_from, nil)
42 |> assign(:date_to, nil)
43 |> assign(:selected_payment, nil)
44 |> assign(:show_details_modal, false)
45 |> assign(:stats, %{})
46 |> assign(:fx_rates, %{})
47 |> load_international_payments()
48 |> load_stats()
49 |> load_fx_rates()
50
51 {:ok, socket}
52 else
53 {:ok,
54 socket
55 |> put_flash(:error, "Access denied")
56 |> redirect(to: ~p"/login")}
57 end
58 end
59 end
60
61 @impl true
62
:-(
def handle_params(params, _url, socket) do
63
:-(
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
64 end
65
66 defp apply_action(socket, :index, _params) do
67 socket
68 |> assign(:page_title, "International Payments")
69
:-(
|> assign(:show_details_modal, false)
70 end
71
72 defp apply_action(socket, :show, %{"id" => id}) do
73
:-(
payment = Monitoring.get_transaction!(id)
74
75 socket
76
:-(
|> assign(:page_title, "International Payment #{payment.org_txn_id}")
77 |> assign(:show_details_modal, true)
78
:-(
|> assign(:selected_payment, payment)
79 end
80
81 @impl true
82
:-(
def handle_event("search", %{"search_term" => search_term}, socket) do
83
:-(
socket =
84 socket
85 |> assign(:search_term, search_term)
86 |> load_international_payments()
87
88 {:noreply, socket}
89 end
90
91
:-(
def handle_event("filter_corridor", %{"corridor" => corridor}, socket) do
92
:-(
corridor_filter = if corridor == "all", do: :all, else: corridor
93
94
:-(
socket =
95 socket
96 |> assign(:corridor_filter, corridor_filter)
97 |> load_international_payments()
98
99 {:noreply, socket}
100 end
101
102
:-(
def handle_event("filter_status", %{"status" => status}, socket) do
103
:-(
status_filter = if status == "all", do: :all, else: String.to_atom(status)
104
105
:-(
socket =
106 socket
107 |> assign(:status_filter, status_filter)
108 |> load_international_payments()
109
110 {:noreply, socket}
111 end
112
113
:-(
def handle_event("filter_partner", %{"partner" => partner}, socket) do
114
:-(
partner_filter = if partner == "all", do: :all, else: partner
115
116
:-(
socket =
117 socket
118 |> assign(:partner_filter, partner_filter)
119 |> load_international_payments()
120
121 {:noreply, socket}
122 end
123
124
:-(
def handle_event("filter_date_range", %{"date_from" => date_from, "date_to" => date_to}, socket) do
125
:-(
parsed_date_from = if date_from != "", do: Date.from_iso8601!(date_from), else: nil
126
:-(
parsed_date_to = if date_to != "", do: Date.from_iso8601!(date_to), else: nil
127
128
:-(
socket =
129 socket
130 |> assign(:date_from, parsed_date_from)
131 |> assign(:date_to, parsed_date_to)
132 |> load_international_payments()
133 |> load_stats()
134
135 {:noreply, socket}
136 end
137
138
:-(
def handle_event("close_details", _params, socket) do
139 {:noreply, push_patch(socket, to: ~p"/international-payments")}
140 end
141
142
:-(
def handle_event("refresh", _params, socket) do
143
:-(
socket =
144 socket
145 |> load_international_payments()
146 |> load_stats()
147 |> load_fx_rates()
148 |> put_flash(:info, "Data refreshed")
149
150 {:noreply, socket}
151 end
152
153 # Handle real-time updates from PubSub
154 @impl true
155
:-(
def handle_info({:transaction_created, _transaction}, socket) do
156 {:noreply, load_international_payments(socket)}
157 end
158
159
:-(
def handle_info({:transaction_updated, _transaction}, socket) do
160 {:noreply, load_international_payments(socket)}
161 end
162
163
:-(
def handle_info({:international_payment_updated, _payment}, socket) do
164 {:noreply, load_international_payments(socket)}
165 end
166
167 defp load_international_payments(socket) do
168
:-(
filters = [
169
:-(
search_term: socket.assigns.search_term,
170
:-(
status: socket.assigns.status_filter,
171
:-(
date_from: socket.assigns.date_from,
172
:-(
date_to: socket.assigns.date_to,
173 limit: 100
174 ]
175
176 # Filter for international transactions only
177
:-(
international_payments = get_international_payments(filters, socket.assigns)
178
:-(
assign(socket, :international_payments, international_payments)
179 end
180
181 defp load_stats(socket) do
182
:-(
stats = get_international_payment_stats(socket.assigns.date_from, socket.assigns.date_to)
183
:-(
assign(socket, :stats, stats)
184 end
185
186 defp load_fx_rates(socket) do
187
:-(
fx_rates = get_current_fx_rates()
188
:-(
assign(socket, :fx_rates, fx_rates)
189 end
190
191 defp get_international_payments(filters, assigns) do
192 # Use database-level filtering for better performance
193
:-(
enhanced_filters = filters ++ [transaction_type: "INTERNATIONAL"]
194
195
:-(
international_transactions = Monitoring.list_transactions(enhanced_filters)
196
:-(
|> filter_by_corridor(assigns.corridor_filter)
197
:-(
|> filter_by_partner(assigns.partner_filter)
198
199
:-(
international_transactions
200 end
201
202
:-(
defp filter_by_corridor(transactions, :all), do: transactions
203 defp filter_by_corridor(transactions, corridor) do
204
:-(
Enum.filter(transactions, &(&1.corridor == corridor))
205 end
206
207
:-(
defp filter_by_partner(transactions, :all), do: transactions
208 defp filter_by_partner(transactions, partner_id) do
209
:-(
Enum.filter(transactions, &(to_string(&1.partner_id) == partner_id))
210 end
211
212 defp get_international_payment_stats(date_from, date_to) do
213 # Get transaction stats and filter for international
214
:-(
all_stats = Monitoring.get_transaction_stats(date_from, date_to)
215
216 # Get corridor performance
217
:-(
corridor_performance = Monitoring.get_corridor_performance(date_from, date_to)
218
219
:-(
%{
220 total_count: calculate_international_count(all_stats),
221
:-(
status_counts: all_stats[:status_counts] || %{},
222 corridor_performance: corridor_performance,
223 fx_volume: calculate_fx_volume(corridor_performance)
224 }
225 end
226
227 defp calculate_international_count(stats) do
228 # This is a simplified calculation - in a real implementation,
229 # you'd query specifically for international transactions
230
:-(
total = stats[:total_count] || 0
231 # Assuming ~30% are international (adjust based on your data)
232
:-(
round(total * 0.3)
233 end
234
235 defp calculate_fx_volume(corridor_performance) do
236 corridor_performance
237
:-(
|> Enum.reduce(Decimal.new(0), fn {_corridor, stats}, acc ->
238
:-(
Decimal.add(acc, stats[:volume] || Decimal.new(0))
239 end)
240 end
241
242 defp get_current_fx_rates do
243 # Mock FX rates - in real implementation, fetch from your FX service
244
:-(
%{
245 "USD" => %{rate: Decimal.new("82.50"), last_updated: DateTime.utc_now()},
246 "SGD" => %{rate: Decimal.new("61.20"), last_updated: DateTime.utc_now()},
247 "AED" => %{rate: Decimal.new("22.45"), last_updated: DateTime.utc_now()}
248 }
249 end
250
251 # Helper functions for the template
252
:-(
defp corridor_badge_class("SINGAPORE"), do: "badge-info"
253
:-(
defp corridor_badge_class("UAE"), do: "badge-warning"
254
:-(
defp corridor_badge_class("USA"), do: "badge-accent"
255
:-(
defp corridor_badge_class(_), do: "badge-ghost"
256
257
:-(
defp status_badge_class("success"), do: "badge-success"
258
:-(
defp status_badge_class("failure"), do: "badge-error"
259
:-(
defp status_badge_class("pending"), do: "badge-warning"
260
:-(
defp status_badge_class("processing"), do: "badge-info"
261
:-(
defp status_badge_class("deemed"), do: "badge-secondary"
262
:-(
defp status_badge_class("reversed"), do: "badge-neutral"
263
:-(
defp status_badge_class(_), do: "badge-ghost"
264
265
:-(
defp format_amount(nil), do: "N/A"
266 defp format_amount(amount) when is_binary(amount) do
267
:-(
case Decimal.parse(amount) do
268
:-(
{decimal, _} -> format_decimal(decimal)
269
:-(
:error -> amount
270 end
271 end
272
:-(
defp format_amount(%Decimal{} = amount), do: format_decimal(amount)
273
:-(
defp format_amount(amount) when is_number(amount), do: :erlang.float_to_binary(amount, decimals: 2)
274
275 defp format_decimal(%Decimal{} = decimal) do
276 decimal
277 |> Decimal.round(2)
278
:-(
|> Decimal.to_string()
279 end
280
281 defp format_currency(amount, currency) when currency in ["USD", "SGD", "AED"] do
282
:-(
case currency do
283
:-(
"USD" -> "$#{format_amount(amount)}"
284
:-(
"SGD" -> "S$#{format_amount(amount)}"
285
:-(
"AED" -> "AED #{format_amount(amount)}"
286
:-(
_ -> "#{format_amount(amount)} #{currency}"
287 end
288 end
289
:-(
defp format_currency(amount, currency), do: "#{format_amount(amount)} #{currency || "INR"}"
290
291
:-(
defp corridor_options do
292 [
293 {"All Corridors", "all"},
294 {"Singapore", "SINGAPORE"},
295 {"UAE", "UAE"},
296 {"USA", "USA"}
297 ]
298 end
299
300
:-(
defp status_options do
301 [
302 {"All Statuses", "all"},
303 {"Success", "success"},
304 {"Failure", "failure"},
305 {"Pending", "pending"},
306 {"Processing", "processing"},
307 {"Deemed", "deemed"},
308 {"Reversed", "reversed"}
309 ]
310 end
311
312
:-(
defp partner_options do
313 [
314 {"All Partners", "all"},
315 {"Partner 1", "1"},
316 {"Partner 2", "2"},
317 {"Partner 3", "3"}
318 ]
319 end
320
321 defp format_fx_rate(%Decimal{} = rate) do
322 rate
323 |> Decimal.round(4)
324
:-(
|> Decimal.to_string()
325 end
326
:-(
defp format_fx_rate(rate), do: to_string(rate)
327
328 # Helper functions for authentication and access control
329
:-(
defp get_current_user(session) do
330
:-(
case session do
331 %{"user_id" => user_id} when is_binary(user_id) or is_integer(user_id) ->
332
:-(
Accounts.get_user!(user_id)
333
:-(
_ ->
334 nil
335 end
336 rescue
337
:-(
_ -> nil
338 end
339
340
:-(
defp has_access?(_user), do: true
341 end
Line Hits Source