cover/Elixir.DaProductApp.ForeignExchange.FxRateService.html

1 defmodule DaProductApp.ForeignExchange.FxRateService do
2 @moduledoc """
3 Service for managing FX rates and currency conversions in UPI International.
4
5 Handles:
6 - Real-time rate fetching and caching
7 - Currency conversion with markup
8 - Rate validation and expiry
9 - Integration with external FX providers
10 """
11
12 import Ecto.Query
13 alias DaProductApp.Repo
14 alias DaProductApp.ForeignExchange.FxRate
15
16 @doc """
17 Get current FX rate for currency pair in specific corridor
18 """
19 def get_current_rate(from_currency, to_currency, corridor) do
20
:-(
case Repo.one(FxRate.get_active_rate_query(from_currency, to_currency, corridor)) do
21
:-(
nil -> fetch_and_cache_live_rate(from_currency, to_currency, corridor)
22
:-(
rate -> {:ok, rate}
23 end
24 end
25
26 @doc """
27 Convert INR amount to foreign currency with current rates
28 """
29 def convert_inr_to_foreign(inr_amount, target_currency, corridor) do
30
:-(
case get_current_rate("INR", target_currency, corridor) do
31 {:ok, fx_rate} ->
32
:-(
foreign_amount = FxRate.calculate_foreign_amount(inr_amount, fx_rate)
33 {:ok, %{
34 foreign_amount: foreign_amount,
35 foreign_currency: target_currency,
36
:-(
fx_rate: fx_rate.fx_rate,
37
:-(
markup_rate: fx_rate.markup_rate,
38 inr_amount: inr_amount,
39 corridor: corridor,
40
:-(
rate_timestamp: fx_rate.last_modified_ts
41 }}
42
43
:-(
{:error, reason} -> {:error, reason}
44 end
45 end
46
47 @doc """
48 Convert foreign currency amount to INR with current rates
49 """
50 def convert_foreign_to_inr(foreign_amount, from_currency, corridor) do
51
:-(
case get_current_rate("INR", from_currency, corridor) do
52 {:ok, fx_rate} ->
53
:-(
inr_amount = FxRate.calculate_inr_amount(foreign_amount, fx_rate)
54 {:ok, %{
55 inr_amount: inr_amount,
56 foreign_amount: foreign_amount,
57 foreign_currency: from_currency,
58
:-(
fx_rate: fx_rate.fx_rate,
59
:-(
markup_rate: fx_rate.markup_rate,
60 corridor: corridor,
61
:-(
rate_timestamp: fx_rate.last_modified_ts
62 }}
63
64
:-(
{:error, reason} -> {:error, reason}
65 end
66 end
67
68 @doc """
69 Validate if FX rates are still current and not expired
70 """
71
:-(
def validate_rate_freshness(fx_timestamp, max_age_minutes \\ 5) do
72
:-(
case DateTime.diff(DateTime.utc_now(), fx_timestamp, :minute) do
73
:-(
diff when diff <= max_age_minutes -> {:ok, :fresh}
74
:-(
diff -> {:error, {:rate_expired, diff}}
75 end
76 end
77
78 @doc """
79 Get supported corridors and their currencies
80 """
81 def get_supported_corridors() do
82
:-(
%{
83 "SINGAPORE" => %{currency: "SGD", country: "Singapore"},
84 "UAE" => %{currency: "AED", country: "United Arab Emirates"},
85 "USA" => %{currency: "USD", country: "United States"},
86 "UK" => %{currency: "GBP", country: "United Kingdom"},
87 "JAPAN" => %{currency: "JPY", country: "Japan"},
88 "AUSTRALIA" => %{currency: "AUD", country: "Australia"}
89 }
90 end
91
92 @doc """
93 Create or update FX rate
94 """
95 def upsert_fx_rate(attrs) do
96 # Deactivate existing rates for the same currency pair and corridor
97
:-(
deactivate_existing_rates(attrs.base_currency, attrs.target_currency, attrs.corridor)
98
99 # Create new rate
100 %FxRate{}
101 |> FxRate.changeset(Map.put(attrs, :active, true))
102
:-(
|> Repo.insert()
103 end
104
105 # Private functions
106
107 defp fetch_and_cache_live_rate(from_currency, to_currency, corridor) do
108
:-(
case fetch_live_rate_from_provider(from_currency, to_currency, corridor) do
109 {:ok, rate_data} ->
110
:-(
case upsert_fx_rate(%{
111 base_currency: from_currency,
112 target_currency: to_currency,
113 corridor: corridor,
114
:-(
fx_rate: rate_data.rate,
115
:-(
markup_rate: rate_data.markup || get_default_markup(corridor),
116
:-(
source: rate_data.source,
117 rate_type: "SPOT",
118 effective_from: DateTime.utc_now(),
119 last_modified_ts: DateTime.utc_now(),
120 created_by: "SYSTEM"
121 }) do
122
:-(
{:ok, fx_rate} -> {:ok, fx_rate}
123
:-(
{:error, reason} -> {:error, {:cache_failed, reason}}
124 end
125
126 {:error, reason} ->
127
:-(
get_fallback_rate(from_currency, to_currency, corridor, reason)
128 end
129 end
130
131 defp fetch_live_rate_from_provider(from_currency, to_currency, corridor) do
132 # Simulate fetching from external FX provider
133 # In production: integrate with RBI, partner APIs, or commercial FX providers
134
:-(
case corridor do
135
:-(
"SINGAPORE" when from_currency == "INR" and to_currency == "SGD" ->
136 {:ok, %{rate: Decimal.new("0.0165"), markup: Decimal.new("2.50"), source: "RBI_REFERENCE"}}
137
138
:-(
"UAE" when from_currency == "INR" and to_currency == "AED" ->
139 {:ok, %{rate: Decimal.new("0.0446"), markup: Decimal.new("2.25"), source: "RBI_REFERENCE"}}
140
141
:-(
"USA" when from_currency == "INR" and to_currency == "USD" ->
142 {:ok, %{rate: Decimal.new("0.0121"), markup: Decimal.new("2.75"), source: "RBI_REFERENCE"}}
143
144
:-(
_ ->
145 {:error, {:unsupported_corridor, corridor}}
146 end
147 end
148
149 defp get_fallback_rate(from_currency, to_currency, corridor, _original_error) do
150 # Use cached rate even if slightly stale, or return error
151
:-(
query = from r in FxRate,
152 where: r.base_currency == ^from_currency and
153 r.target_currency == ^to_currency and
154 r.corridor == ^corridor,
155 order_by: [desc: r.last_modified_ts],
156 limit: 1
157
158
:-(
case Repo.one(query) do
159
:-(
nil -> {:error, {:no_rate_available, corridor}}
160
:-(
stale_rate -> {:ok, stale_rate} # Use stale rate as fallback
161 end
162 end
163
164 defp deactivate_existing_rates(base_currency, target_currency, corridor) do
165 from(r in FxRate,
166 where: r.base_currency == ^base_currency and
167 r.target_currency == ^target_currency and
168 r.corridor == ^corridor and
169 r.active == true
170 )
171
:-(
|> Repo.update_all(set: [active: false, effective_until: DateTime.utc_now()])
172 end
173
174 defp get_default_markup(corridor) do
175
:-(
case corridor do
176
:-(
"SINGAPORE" -> Decimal.new("2.50")
177
:-(
"UAE" -> Decimal.new("2.25")
178
:-(
"USA" -> Decimal.new("2.75")
179
:-(
_ -> Decimal.new("2.00")
180 end
181 end
182 end
Line Hits Source