Packages
phoenix_kit
1.7.113
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/settings/organization.ex
defmodule PhoenixKitWeb.Live.Settings.Organization do
@moduledoc """
Organization settings management LiveView for PhoenixKit.
Provides a unified interface for company information shared between
Legal and Billing modules. This includes company details and bank information.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.PubSub.Manager, as: PubSubManager
alias PhoenixKit.Settings
alias PhoenixKit.Utils.CountryData
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.Routes
@default_company_info %{
"name" => "",
"address_line1" => "",
"address_line2" => "",
"city" => "",
"state" => "",
"postal_code" => "",
"country" => "",
"vat_number" => "",
"registration_number" => ""
}
@default_bank_details %{
"bank_name" => "",
"iban" => "",
"swift" => ""
}
def mount(_params, _session, socket) do
# Subscribe to organization settings updates for real-time sync
if connected?(socket) do
PubSubManager.subscribe("organization:settings")
end
project_title = Settings.get_project_title()
socket =
socket
|> assign(:page_title, gettext("Organization Settings"))
|> assign(:project_title, project_title)
|> assign(:current_path, get_current_path(socket.assigns.current_locale_base))
|> load_settings()
{:ok, socket}
end
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
defp load_settings(socket) do
company_info = get_company_info()
bank_details = get_bank_details()
socket
|> assign_company_info(company_info)
|> assign_country_data(company_info["country"])
|> assign_tax_settings(company_info)
|> assign_bank_details(bank_details)
|> assign(:site_url, Settings.get_setting("site_url", ""))
end
defp assign_company_info(socket, info) do
socket
|> assign(:company_name, info["name"] || "")
|> assign(:company_vat, info["vat_number"] || "")
|> assign(:company_registration, info["registration_number"] || "")
|> assign(:company_address_line1, info["address_line1"] || "")
|> assign(:company_address_line2, info["address_line2"] || "")
|> assign(:company_city, info["city"] || "")
|> assign(:company_state, info["state"] || "")
|> assign(:company_postal_code, info["postal_code"] || "")
|> assign(:company_country, info["country"] || "")
end
defp assign_country_data(socket, country) do
socket
|> assign(:countries, CountryData.countries_for_select())
|> assign(:subdivision_label, get_subdivision_label(country))
|> assign(:eu_country, eu_country?(country))
end
defp assign_tax_settings(socket, _company_info) do
tax_config = CountryData.get_tax_config()
country = socket.assigns.company_country
suggested_rate =
if country != "" do
rate = CountryData.get_standard_vat_percent(country)
current = parse_tax_rate(tax_config.rate)
if rate != 0 and rate != current, do: rate
end
socket
|> assign(:tax_enabled, tax_config.enabled)
|> assign(:tax_rate, tax_config.rate)
|> assign(:suggested_tax_rate, suggested_rate)
end
defp assign_bank_details(socket, details) do
socket
|> assign(:bank_name, details["bank_name"] || "")
|> assign(:bank_iban, details["iban"] || "")
|> assign(:bank_swift, details["swift"] || "")
end
# ===================================
# EVENT HANDLERS
# ===================================
def handle_event("country_changed", %{"company_country" => country_code}, socket) do
# Update suggested tax rate when country changes
current_rate = parse_tax_rate(socket.assigns.tax_rate)
suggested_rate =
if country_code != "" do
rate = CountryData.get_standard_vat_percent(country_code)
if rate != 0 and rate != current_rate, do: rate
end
{:noreply,
socket
|> assign(:company_country, country_code)
|> assign(:subdivision_label, get_subdivision_label(country_code))
|> assign(:eu_country, eu_country?(country_code))
|> assign(:suggested_tax_rate, suggested_rate)}
end
def handle_event("save_company", params, socket) do
data = extract_company_data(params)
case validate_company_data(data) do
[] ->
save_company_info(data, params)
# Broadcast to all admin sessions
broadcast_settings_change(:company_info_updated)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, gettext("Organization information saved"))}
errors ->
{:noreply, put_flash(socket, :error, Enum.join(errors, ". "))}
end
end
def handle_event("save_tax", params, socket) do
tax_enabled = params["tax_enabled"] == "true"
tax_rate = (params["tax_rate"] || "0") |> String.trim()
# Save tax settings into company_info JSON
company_info = get_company_info()
updated_info =
company_info
|> Map.put("tax_enabled", tax_enabled)
|> Map.put("tax_rate", tax_rate)
Settings.update_json_setting("company_info", updated_info)
# Also sync to legacy keys for backward compatibility with Billing/Shop
Settings.update_setting(
"billing_tax_enabled",
if(tax_enabled, do: "true", else: "false")
)
Settings.update_setting("billing_default_tax_rate", tax_rate)
Settings.update_setting("shop_tax_enabled", if(tax_enabled, do: "true", else: "false"))
Settings.update_setting("shop_tax_rate", tax_rate)
broadcast_settings_change(:tax_settings_updated)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, gettext("Tax settings saved"))}
end
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 != 0 and rate != current_rate, do: rate
end
{:noreply,
socket
|> assign(:tax_rate, tax_rate)
|> assign(:suggested_tax_rate, suggested_rate)}
end
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
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
[] ->
save_bank_details(params, iban, swift)
# Broadcast to all admin sessions
broadcast_settings_change(:bank_details_updated)
{:noreply,
socket
|> load_settings()
|> put_flash(:info, gettext("Bank details saved"))}
errors ->
{:noreply, put_flash(socket, :error, Enum.join(Enum.reverse(errors), ". "))}
end
end
# Handle PubSub messages for settings sync
def handle_info({:organization_settings_changed, _data}, socket) do
{:noreply, load_settings(socket)}
end
# ===================================
# DATA ACCESS (with fallback to legacy keys)
# ===================================
@doc """
Gets company info from consolidated key with fallback to legacy keys.
"""
def get_company_info do
Map.merge(@default_company_info, CountryData.get_company_info())
end
@doc """
Gets bank details from consolidated key with fallback to legacy keys.
"""
def get_bank_details do
Map.merge(@default_bank_details, CountryData.get_bank_details())
end
# ===================================
# VALIDATION
# ===================================
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, gettext("Company name is required"))
|> validate_required(data.country, gettext("Country is required"))
|> validate_required(data.vat, gettext("VAT number is required"))
|> validate_required(data.address_line1, gettext("Street address is required"))
|> validate_required(data.city, gettext("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 eu_country?(country) do
if Regex.match?(~r/^[A-Z]{2}[0-9A-Z]{2,12}$/, String.upcase(vat)) do
errors
else
[
gettext("VAT number must be in EU format (e.g., %{country}123456789)", country: country)
| errors
]
end
else
errors
end
end
defp validate_eu_vat(errors, _vat, _country), do: errors
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
# ===================================
# SAVE OPERATIONS
# ===================================
defp save_company_info(data, params) do
# Merge with existing company_info to preserve tax_enabled/tax_rate keys
existing = get_company_info()
company_info =
Map.merge(existing, %{
"name" => data.name,
"address_line1" => data.address_line1,
"address_line2" => (params["company_address_line2"] || "") |> String.trim(),
"city" => data.city,
"state" => (params["company_state"] || "") |> String.trim(),
"postal_code" => (params["company_postal_code"] || "") |> String.trim(),
"country" => data.country,
"vat_number" => String.upcase(data.vat),
"registration_number" => (params["company_registration"] || "") |> String.trim()
})
Settings.update_json_setting("company_info", company_info)
end
defp save_bank_details(params, iban, swift) do
bank_details = %{
"bank_name" => (params["bank_name"] || "") |> String.trim(),
"iban" => normalize_iban(iban),
"swift" => String.upcase(swift)
}
Settings.update_json_setting("company_bank_details", bank_details)
end
defp normalize_iban(iban) do
iban |> String.replace(~r/\s/, "") |> String.upcase()
end
# ===================================
# HELPERS
# ===================================
defp get_subdivision_label(nil), do: gettext("State/Province")
defp get_subdivision_label(""), do: gettext("State/Province")
defp get_subdivision_label(country_code) do
CountryData.get_subdivision_label(country_code)
end
defp eu_country?(nil), do: false
defp eu_country?(""), do: false
defp eu_country?(country_code), do: CountryData.eu_member?(country_code)
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(_), do: 0
defp get_current_path(locale) do
Routes.path("/admin/settings/organization", locale: locale)
end
# Broadcast settings change to all connected admin sessions
defp broadcast_settings_change(type) do
PubSubManager.broadcast(
"organization:settings",
{:organization_settings_changed, %{type: type, timestamp: UtilsDate.utc_now()}}
)
rescue
# PubSub may not be available in all environments
_ -> :ok
end
end