Packages
phoenix_kit
1.7.9
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
Current section
Files
lib/phoenix_kit_web/live/modules/billing/settings.ex
defmodule PhoenixKitWeb.Live.Modules.Billing.Settings do
@moduledoc """
Billing settings LiveView for the billing module.
Provides configuration interface for billing module settings.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Billing
alias PhoenixKit.Billing.CountryData
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
project_title = Settings.get_setting("project_title", "PhoenixKit")
billing_enabled = Billing.enabled?()
socket =
socket
|> assign(:page_title, "Billing Settings")
|> assign(:project_title, project_title)
|> assign(:url_path, Routes.path("/admin/billing/settings"))
|> assign(:billing_enabled, billing_enabled)
|> load_settings()
{:ok, socket}
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp load_settings(socket) do
socket
# General settings
|> assign(:default_currency, Settings.get_setting("billing_default_currency", "EUR"))
|> assign(:invoice_prefix, Settings.get_setting("billing_invoice_prefix", "INV"))
|> assign(:order_prefix, Settings.get_setting("billing_order_prefix", "ORD"))
|> assign(:receipt_prefix, Settings.get_setting("billing_receipt_prefix", "RCP"))
|> assign(:invoice_due_days, Settings.get_setting("billing_invoice_due_days", "14"))
|> assign(:tax_enabled, Settings.get_setting("billing_tax_enabled", "false") == "true")
|> assign(:tax_rate, Settings.get_setting("billing_default_tax_rate", "0"))
# Company information with address breakdown
|> assign(:company_name, Settings.get_setting("billing_company_name", ""))
|> assign(:company_vat, Settings.get_setting("billing_company_vat", ""))
|> assign(:company_address_line1, Settings.get_setting("billing_company_address_line1", ""))
|> assign(:company_address_line2, Settings.get_setting("billing_company_address_line2", ""))
|> assign(:company_city, Settings.get_setting("billing_company_city", ""))
|> assign(:company_state, Settings.get_setting("billing_company_state", ""))
|> assign(:company_postal_code, Settings.get_setting("billing_company_postal_code", ""))
|> assign(:company_country, Settings.get_setting("billing_company_country", ""))
# Country dropdown data
|> assign(:countries, CountryData.countries_for_select())
|> assign_suggested_tax_rate()
# Bank details
|> assign(:bank_name, Settings.get_setting("billing_bank_name", ""))
|> assign(:bank_iban, Settings.get_setting("billing_bank_iban", ""))
|> assign(:bank_swift, Settings.get_setting("billing_bank_swift", ""))
end
@impl true
def handle_event("toggle_billing", _params, socket) do
new_enabled = !socket.assigns.billing_enabled
result =
if new_enabled do
Billing.enable_system()
else
Billing.disable_system()
end
case result do
{:ok, _} ->
{:noreply,
socket
|> assign(:billing_enabled, new_enabled)
|> put_flash(:info, if(new_enabled, do: "Billing enabled", else: "Billing disabled"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update billing status")}
end
end
@impl true
def handle_event("save_general", params, socket) do
# Convert checkbox value to "true"/"false" string
tax_enabled = if params["tax_enabled"] == "true", do: "true", else: "false"
settings = [
{"billing_default_currency", params["default_currency"]},
{"billing_invoice_prefix", params["invoice_prefix"]},
{"billing_order_prefix", params["order_prefix"]},
{"billing_receipt_prefix", params["receipt_prefix"]},
{"billing_invoice_due_days", params["invoice_due_days"]},
{"billing_tax_enabled", tax_enabled},
{"billing_default_tax_rate", params["tax_rate"]}
]
Enum.each(settings, fn {key, value} ->
Settings.update_setting(key, value)
end)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, "General settings saved")}
end
@impl true
def handle_event("country_changed", %{"company_country" => country_code}, socket) do
current_rate = parse_tax_rate(socket.assigns.tax_rate)
suggested_rate =
if country_code != "" do
rate = CountryData.get_standard_vat_percent(country_code)
# Hide suggestion if it matches current rate
if rate == current_rate, do: nil, else: rate
else
nil
end
{:noreply,
socket
|> assign(:company_country, country_code)
|> assign(:suggested_tax_rate, suggested_rate)}
end
@impl true
def handle_event("tax_rate_changed", %{"tax_rate" => tax_rate}, socket) do
current_rate = parse_tax_rate(tax_rate)
country_code = socket.assigns.company_country
suggested_rate =
if country_code != "" do
rate = CountryData.get_standard_vat_percent(country_code)
if rate == current_rate, do: nil, else: rate
else
nil
end
{:noreply,
socket
|> assign(:tax_rate, tax_rate)
|> assign(:suggested_tax_rate, suggested_rate)}
end
@impl true
def handle_event("apply_suggested_tax", _params, socket) do
case socket.assigns.suggested_tax_rate do
nil ->
{:noreply, socket}
rate ->
{:noreply,
socket
|> assign(:tax_rate, to_string(rate))
|> assign(:suggested_tax_rate, nil)}
end
end
@impl true
def handle_event("save_company", params, socket) do
data = extract_company_data(params)
case validate_company_data(data) do
[] ->
save_company_settings(data, params)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, "Company information saved")}
errors ->
{:noreply, put_flash(socket, :error, Enum.join(errors, ". "))}
end
end
@impl true
def handle_event("save_bank", params, socket) do
iban = (params["bank_iban"] || "") |> String.trim()
swift = (params["bank_swift"] || "") |> String.trim()
country_code = socket.assigns.company_country
errors =
[]
|> validate_bank_iban(iban, country_code)
|> validate_bank_swift(swift)
case errors do
[] ->
settings = [
{"billing_bank_name", params["bank_name"]},
{"billing_bank_iban", normalize_iban(iban)},
{"billing_bank_swift", String.upcase(swift)}
]
Enum.each(settings, fn {key, value} ->
Settings.update_setting(key, value)
end)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, "Bank details saved")}
errors ->
{:noreply, put_flash(socket, :error, Enum.join(Enum.reverse(errors), ". "))}
end
end
# Company data validation helpers
defp extract_company_data(params) do
%{
name: (params["company_name"] || "") |> String.trim(),
country: params["company_country"] || "",
vat: (params["company_vat"] || "") |> String.trim(),
address_line1: (params["company_address_line1"] || "") |> String.trim(),
city: (params["company_city"] || "") |> String.trim()
}
end
defp validate_company_data(data) do
[]
|> validate_required(data.name, "Company name is required")
|> validate_required(data.country, "Country is required")
|> validate_required(data.vat, "VAT number is required")
|> validate_required(data.address_line1, "Street address is required")
|> validate_required(data.city, "City is required")
|> validate_eu_vat(data.vat, data.country)
|> Enum.reverse()
end
defp validate_required(errors, "", message), do: [message | errors]
defp validate_required(errors, _value, _message), do: errors
defp validate_eu_vat(errors, vat, country) when vat != "" and country != "" do
if CountryData.eu_member?(country) do
if Regex.match?(~r/^[A-Z]{2}[0-9A-Z]{2,12}$/, String.upcase(vat)) do
errors
else
["VAT number must be in EU format (e.g., #{country}123456789)" | errors]
end
else
errors
end
end
defp validate_eu_vat(errors, _vat, _country), do: errors
defp save_company_settings(data, params) do
settings = [
{"billing_company_name", data.name},
{"billing_company_vat", String.upcase(data.vat)},
{"billing_company_address_line1", data.address_line1},
{"billing_company_address_line2", params["company_address_line2"] || ""},
{"billing_company_city", data.city},
{"billing_company_state", params["company_state"] || ""},
{"billing_company_postal_code", params["company_postal_code"] || ""},
{"billing_company_country", data.country}
]
Enum.each(settings, fn {key, value} ->
Settings.update_setting(key, value)
end)
end
# Suggested tax rate helper
defp assign_suggested_tax_rate(socket) do
country_code = socket.assigns.company_country
current_rate = parse_tax_rate(socket.assigns.tax_rate)
suggested_rate =
if country_code != "" do
rate = CountryData.get_standard_vat_percent(country_code)
# Hide suggestion if it matches current rate
if rate == current_rate, do: nil, else: rate
else
nil
end
assign(socket, :suggested_tax_rate, suggested_rate)
end
defp parse_tax_rate(rate) when is_binary(rate) do
case Float.parse(rate) do
{value, _} -> if value == trunc(value), do: trunc(value), else: value
:error -> 0
end
end
defp parse_tax_rate(rate) when is_number(rate), do: rate
defp parse_tax_rate(_), do: 0
# Bank validation helpers
defp validate_bank_iban(errors, iban, country_code) do
case CountryData.validate_iban_format(iban, country_code) do
:ok -> errors
{:error, msg} -> [msg | errors]
end
end
defp validate_bank_swift(errors, swift) do
case CountryData.validate_swift_format(swift) do
:ok -> errors
{:error, msg} -> [msg | errors]
end
end
defp normalize_iban(iban) do
iban |> String.replace(~r/\s/, "") |> String.upcase()
end
end