Packages

phoenix_kit

1.7.49
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules billing web currencies.ex
Raw

lib/modules/billing/web/currencies.ex

defmodule PhoenixKit.Modules.Billing.Web.Currencies do
@moduledoc """
Currencies management LiveView for the billing module.
Provides currency configuration interface with CRUD operations
and bulk import from the BeamLabCountries library.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Billing
alias PhoenixKit.Modules.Billing.Currency
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
if Billing.enabled?() do
project_title = Settings.get_project_title()
socket =
socket
|> assign(:page_title, "Currencies")
|> assign(:project_title, project_title)
|> assign(:url_path, Routes.path("/admin/billing/currencies"))
|> assign(:currencies, [])
|> assign(:loading, true)
|> assign(:show_form, false)
|> assign(:editing_currency, nil)
|> assign(:form, nil)
|> assign(:show_import, false)
|> assign(:available_currencies, [])
|> assign(:selected_imports, MapSet.new())
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Billing module is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, load_currencies(socket)}
end
defp load_currencies(socket) do
currencies = Billing.list_currencies(order_by: [asc: :sort_order, asc: :code])
socket
|> assign(:currencies, currencies)
|> assign(:loading, false)
end
# --- Toggle / Default / Refresh ---
@impl true
def handle_event("toggle_enabled", %{"uuid" => uuid}, socket) do
currency = Enum.find(socket.assigns.currencies, &(&1.uuid == uuid))
case Billing.update_currency(currency, %{enabled: !currency.enabled}) do
{:ok, _currency} ->
{:noreply,
socket
|> load_currencies()
|> put_flash(:info, "Currency updated")}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to update currency")}
end
end
@impl true
def handle_event("set_default", %{"uuid" => uuid}, socket) do
currency = Enum.find(socket.assigns.currencies, &(&1.uuid == uuid))
case Billing.set_default_currency(currency) do
{:ok, _currency} ->
{:noreply,
socket
|> load_currencies()
|> put_flash(:info, "#{currency.code} set as default currency")}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Failed to set default currency")}
end
end
@impl true
def handle_event("refresh", _params, socket) do
{:noreply, socket |> assign(:loading, true) |> load_currencies()}
end
# --- Currency Form (Add / Edit) ---
@impl true
def handle_event("show_add_form", _params, socket) do
changeset = Currency.changeset(%Currency{}, %{})
{:noreply,
socket
|> assign(:show_form, true)
|> assign(:editing_currency, nil)
|> assign(:form, to_form(changeset))}
end
@impl true
def handle_event("show_edit_form", %{"uuid" => uuid}, socket) do
currency = Enum.find(socket.assigns.currencies, &(&1.uuid == uuid))
changeset = Currency.changeset(currency, %{})
{:noreply,
socket
|> assign(:show_form, true)
|> assign(:editing_currency, currency)
|> assign(:form, to_form(changeset))}
end
@impl true
def handle_event("close_form", _params, socket) do
{:noreply,
socket
|> assign(:show_form, false)
|> assign(:editing_currency, nil)
|> assign(:form, nil)}
end
@impl true
def handle_event("validate", %{"currency" => params}, socket) do
changeset =
(socket.assigns.editing_currency || %Currency{})
|> Currency.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset))}
end
@impl true
def handle_event("save", %{"currency" => params}, socket) do
result =
case socket.assigns.editing_currency do
nil -> Billing.create_currency(params)
currency -> Billing.update_currency(currency, params)
end
case result do
{:ok, _currency} ->
action = if socket.assigns.editing_currency, do: "updated", else: "created"
{:noreply,
socket
|> load_currencies()
|> assign(:show_form, false)
|> assign(:editing_currency, nil)
|> assign(:form, nil)
|> put_flash(:info, "Currency #{action} successfully")}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
# --- Delete ---
@impl true
def handle_event("delete_currency", %{"uuid" => uuid}, socket) do
currency = Enum.find(socket.assigns.currencies, &(&1.uuid == uuid))
case Billing.delete_currency(currency) do
{:ok, _currency} ->
{:noreply,
socket
|> load_currencies()
|> put_flash(:info, "#{currency.code} deleted")}
{:error, :is_default} ->
{:noreply, put_flash(socket, :error, "Cannot delete the default currency")}
{:error, :currency_in_use} ->
{:noreply,
put_flash(socket, :error, "Cannot delete currency — it is used by existing orders")}
{:error, _other} ->
{:noreply, put_flash(socket, :error, "Failed to delete currency")}
end
end
# --- Import from BeamLabCountries ---
@impl true
def handle_event("show_import", _params, socket) do
existing_codes =
socket.assigns.currencies
|> Enum.map(& &1.code)
|> MapSet.new()
# Get company country's currencies to prioritize them (primary + alternative)
company_country_code = Settings.get_setting("billing_company_country", "")
priority_currency_codes = get_country_currency_codes(company_country_code)
available =
BeamLabCountries.Currencies.all()
|> Enum.reject(&MapSet.member?(existing_codes, &1.code))
|> sort_currencies_with_priority(priority_currency_codes)
{:noreply,
socket
|> assign(:show_import, true)
|> assign(:available_currencies, available)
|> assign(:selected_imports, MapSet.new())}
end
@impl true
def handle_event("close_import", _params, socket) do
{:noreply,
socket
|> assign(:show_import, false)
|> assign(:available_currencies, [])
|> assign(:selected_imports, MapSet.new())}
end
@impl true
def handle_event("toggle_import_selection", %{"code" => code}, socket) do
selected = socket.assigns.selected_imports
updated =
if MapSet.member?(selected, code),
do: MapSet.delete(selected, code),
else: MapSet.put(selected, code)
{:noreply, assign(socket, :selected_imports, updated)}
end
@impl true
def handle_event("select_all_imports", _params, socket) do
all_codes = MapSet.new(socket.assigns.available_currencies, & &1.code)
{:noreply, assign(socket, :selected_imports, all_codes)}
end
@impl true
def handle_event("deselect_all_imports", _params, socket) do
{:noreply, assign(socket, :selected_imports, MapSet.new())}
end
@impl true
def handle_event("import_selected", _params, socket) do
selected = socket.assigns.selected_imports
to_import =
Enum.filter(socket.assigns.available_currencies, &MapSet.member?(selected, &1.code))
{ok_count, fail_count} =
Enum.reduce(to_import, {0, 0}, fn cur, {ok, fail} ->
attrs = %{
code: cur.code,
name: cur.name,
symbol: cur.symbol_native,
decimal_places: cur.decimal_digits,
exchange_rate: "1.0",
enabled: false
}
case Billing.create_currency(attrs) do
{:ok, _} -> {ok + 1, fail}
{:error, _} -> {ok, fail + 1}
end
end)
message =
case {ok_count, fail_count} do
{ok, 0} -> "#{ok} currencies imported"
{0, fail} -> "Import failed for #{fail} currencies"
{ok, fail} -> "#{ok} imported, #{fail} failed"
end
{:noreply,
socket
|> load_currencies()
|> assign(:show_import, false)
|> assign(:available_currencies, [])
|> assign(:selected_imports, MapSet.new())
|> put_flash(:info, message)}
end
# --- Helpers ---
def error_to_string([]), do: ""
def error_to_string(errors) when is_list(errors) do
Enum.map_join(errors, ", ", fn
{msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
msg when is_binary(msg) ->
msg
end)
end
# Gets currency codes for a country from BeamLabCountries (primary + alternative)
defp get_country_currency_codes(country_code)
when is_binary(country_code) and country_code != "" do
case BeamLabCountries.get(country_code) do
%{currency_code: primary, alt_currency: alt} ->
[primary, alt]
|> Enum.reject(&(is_nil(&1) or &1 == ""))
_ ->
[]
end
end
defp get_country_currency_codes(_), do: []
# Sorts currencies with priority currencies first, then alphabetically
defp sort_currencies_with_priority(currencies, []) do
Enum.sort_by(currencies, & &1.code)
end
defp sort_currencies_with_priority(currencies, priority_codes) when is_list(priority_codes) do
priority_set = MapSet.new(priority_codes)
{priority, rest} = Enum.split_with(currencies, &MapSet.member?(priority_set, &1.code))
# Sort priority currencies in the order they appear in priority_codes
sorted_priority =
Enum.sort_by(priority, fn cur ->
Enum.find_index(priority_codes, &(&1 == cur.code)) || 999
end)
sorted_priority ++ Enum.sort_by(rest, & &1.code)
end
end