cover/Elixir.WalletLimitsFees.PolicyEngine.html

1 defmodule WalletLimitsFees.PolicyEngine do
2 @moduledoc """
3 Pure policy evaluation engine for transfer limits and fee calculation.
4
5 No side effects — reads from PolicyStore and returns deterministic decisions.
6 Decision evidence should be captured by the calling query handler.
7 """
8
9 alias WalletLimitsFees.PolicyStore
10
11 @doc """
12 Evaluates whether a transfer amount is within configured limits.
13
14 `context` may include:
15 - `:daily_used` — amount already spent today (default 0)
16 - `:monthly_used` — amount already spent this month (default 0)
17
18 Returns:
19 - `{:ok, :no_policy}` — no policy configured; permissive.
20 - `{:ok, :within_limits}` — all limits satisfied.
21 - `{:error, :transaction_limit_exceeded, details}` — single transaction too large.
22 - `{:error, :daily_limit_exceeded, details}` — daily spend would be exceeded.
23 - `{:error, :monthly_limit_exceeded, details}` — monthly spend would be exceeded.
24 """
25 @spec evaluate_limits(
26 tier :: atom(),
27 currency :: String.t(),
28 amount :: pos_integer(),
29 context :: map()
30 ) ::
31 {:ok, :no_policy | :within_limits}
32 | {:error, :transaction_limit_exceeded | :daily_limit_exceeded | :monthly_limit_exceeded, map()}
33
:-(
def evaluate_limits(tier, currency, amount, context \\ %{}) do
34 10 case PolicyStore.get_limit_policy(tier, currency) do
35 2 {:error, :not_found} ->
36 {:ok, :no_policy}
37
38 {:ok, policy} ->
39 8 daily_used = Map.get(context, :daily_used, 0)
40 8 monthly_used = Map.get(context, :monthly_used, 0)
41
42 8 cond do
43 8 amount > policy.max_transaction_amount ->
44 2 {:error, :transaction_limit_exceeded,
45 2 %{limit: policy.max_transaction_amount, amount: amount}}
46
47 6 daily_used + amount > policy.daily_limit ->
48 1 {:error, :daily_limit_exceeded,
49 1 %{limit: policy.daily_limit, used: daily_used, amount: amount}}
50
51 5 monthly_used + amount > policy.monthly_limit ->
52 1 {:error, :monthly_limit_exceeded,
53 1 %{limit: policy.monthly_limit, used: monthly_used, amount: amount}}
54
55 4 true ->
56 {:ok, :within_limits}
57 end
58 end
59 end
60
61 @doc """
62 Calculates the applicable fee for a transfer.
63
64 Fee = flat_fee + trunc(amount * percentage_fee), subject to min_fee and max_fee.
65
66 Returns `{:ok, fee_amount}` where fee_amount is 0 when no policy exists.
67 """
68 @spec calculate_fee(
69 tier :: atom(),
70 currency :: String.t(),
71 transfer_type :: atom(),
72 amount :: non_neg_integer()
73 ) :: {:ok, non_neg_integer()}
74 def calculate_fee(tier, currency, transfer_type, amount) do
75 9 case PolicyStore.get_fee_policy(tier, currency, transfer_type) do
76 1 {:error, :not_found} ->
77 {:ok, 0}
78
79 {:ok, policy} ->
80 8 pct_part = trunc(amount * policy.percentage_fee)
81 8 fee = policy.flat_fee + pct_part
82 8 fee = max(fee, policy.min_fee)
83
84 8 fee =
85 8 if policy.max_fee > 0 do
86 2 min(fee, policy.max_fee)
87 else
88 6 fee
89 end
90
91 {:ok, fee}
92 end
93 end
94 end
Line Hits Source