cover/Elixir.DaProductApp.ForeignExchange.FxRate.html

1 defmodule DaProductApp.ForeignExchange.FxRate do
2 @moduledoc """
3 FX Rate schema for UPI International transactions.
4
5 Stores exchange rates between INR and foreign currencies with PSP markup.
6 Used for real-time currency conversion in international corridors.
7 """
8 use Ecto.Schema
9 import Ecto.Changeset
10 import Ecto.Query
11
12 @primary_key {:id, :binary_id, autogenerate: true}
13 @foreign_key_type :binary_id
14
15
:-(
schema "fx_rates" do
16 field :base_currency, :string # "INR" (always INR for UPI International)
17 field :target_currency, :string # "USD", "EUR", "SGD", "AED"
18 field :fx_rate, :decimal # Exchange rate (e.g., 0.0165 for INR to SGD)
19 field :markup_rate, :decimal # PSP markup percentage (e.g., 2.50%)
20 field :effective_from, :utc_datetime
21 field :effective_until, :utc_datetime
22 field :active, :boolean, default: true
23 field :source, :string # "RBI", "PARTNER_API", "MANUAL"
24 field :corridor, :string # "SINGAPORE", "UAE", "USA"
25 field :last_modified_ts, :utc_datetime
26 field :created_by, :string # System user who created/updated
27 field :rate_type, :string # "SPOT", "FORWARD", "FIXED"
28
29 timestamps(type: :utc_datetime)
30 end
31
32 def changeset(fx_rate, attrs) do
33 fx_rate
34 |> cast(attrs, [
35 :base_currency, :target_currency, :fx_rate, :markup_rate,
36 :effective_from, :effective_until, :active, :source, :corridor,
37 :last_modified_ts, :created_by, :rate_type
38 ])
39 |> validate_required([:base_currency, :target_currency, :fx_rate, :corridor])
40 |> validate_inclusion(:base_currency, ["INR"])
41 |> validate_inclusion(:target_currency, ["USD", "EUR", "SGD", "AED", "GBP", "JPY", "AUD"])
42 |> validate_inclusion(:corridor, ["SINGAPORE", "UAE", "USA", "UK", "JAPAN", "AUSTRALIA"])
43 |> validate_inclusion(:source, ["RBI", "PARTNER_API", "MANUAL", "EXTERNAL_FEED"])
44 |> validate_inclusion(:rate_type, ["SPOT", "FORWARD", "FIXED"])
45 |> validate_number(:fx_rate, greater_than: 0)
46 |> validate_number(:markup_rate, greater_than_or_equal_to: 0)
47
:-(
|> unique_constraint([:base_currency, :target_currency, :corridor, :active])
48 end
49
50 @doc """
51 Get current active rate for a currency pair in specific corridor
52 """
53 def get_active_rate_query(from_currency, to_currency, corridor) do
54
:-(
now = DateTime.utc_now()
55
56
:-(
from r in __MODULE__,
57 where: r.base_currency == ^from_currency and
58 r.target_currency == ^to_currency and
59 r.corridor == ^corridor and
60 r.active == true and
61 r.effective_from <= ^now and
62 (is_nil(r.effective_until) or r.effective_until > ^now),
63 order_by: [desc: r.last_modified_ts],
64 limit: 1
65 end
66
67 @doc """
68 Calculate INR amount from foreign currency amount
69 """
70 def calculate_inr_amount(foreign_amount, %__MODULE__{} = fx_rate) do
71 # INR = Foreign Amount ÷ FX Rate × (1 + Markup)
72
:-(
inr_before_markup = Decimal.div(foreign_amount, fx_rate.fx_rate)
73
:-(
markup_multiplier = Decimal.add(1, Decimal.div(fx_rate.markup_rate, 100))
74
:-(
Decimal.mult(inr_before_markup, markup_multiplier)
75 end
76
77 @doc """
78 Calculate foreign currency amount from INR amount
79 """
80 def calculate_foreign_amount(inr_amount, %__MODULE__{} = fx_rate) do
81 # Remove markup first, then convert
82
:-(
markup_multiplier = Decimal.add(1, Decimal.div(fx_rate.markup_rate, 100))
83
:-(
inr_without_markup = Decimal.div(inr_amount, markup_multiplier)
84
:-(
Decimal.mult(inr_without_markup, fx_rate.fx_rate)
85 end
86 end
Line Hits Source