Packages

phoenix_kit

1.7.52
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 order_form.ex
Raw

lib/modules/billing/web/order_form.ex

defmodule PhoenixKit.Modules.Billing.Web.OrderForm do
@moduledoc """
Order form LiveView for creating and editing orders.
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Billing
alias PhoenixKit.Modules.Billing.CountryData
alias PhoenixKit.Modules.Billing.Order
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth
alias PhoenixKit.Utils.Routes
@impl true
def mount(params, _session, socket) do
if Billing.enabled?() do
project_title = Settings.get_project_title()
%{users: users} = Auth.list_users_paginated(limit: 100)
currencies = Billing.list_currencies(enabled: true)
default_currency = Settings.get_setting("billing_default_currency", "EUR")
socket =
socket
|> assign(:project_title, project_title)
|> assign(:users, users)
|> assign(:currencies, currencies)
|> assign(:default_currency, default_currency)
|> assign(:billing_profiles, [])
|> load_order(params["id"])
{:ok, socket}
else
{:ok,
socket
|> put_flash(:error, "Billing module is not enabled")
|> push_navigate(to: Routes.path("/admin"))}
end
end
defp load_order(socket, nil) do
# New order
changeset =
Billing.change_order(%Billing.Order{
currency: socket.assigns.default_currency,
line_items: [%{"name" => "", "quantity" => 1, "unit_price" => "0.00", "total" => "0.00"}]
})
socket
|> assign(:page_title, "New Order")
|> assign(:url_path, Routes.path("/admin/billing/orders/new"))
|> assign(:order, nil)
|> assign(:form, to_form(changeset))
|> assign(:line_items, [%{id: 0, name: "", description: "", quantity: 1, unit_price: "0.00"}])
|> assign(:selected_user_uuid, nil)
|> assign(:selected_billing_profile_uuid, nil)
|> assign(:country_tax_rate, nil)
|> assign(:country_name, nil)
|> assign(:country_vat_percent, nil)
end
defp load_order(socket, id) do
case Billing.get_order(id, preload: [:user, :billing_profile]) do
nil ->
socket
|> put_flash(:error, "Order not found")
|> push_navigate(to: Routes.path("/admin/billing/orders"))
order ->
changeset = Billing.change_order(order)
line_items = parse_line_items(order.line_items)
billing_profiles =
if order.user_uuid, do: Billing.list_user_billing_profiles(order.user_uuid), else: []
# Get country tax info from billing profile
{country_tax_rate, country_name, country_vat_percent} =
if order.billing_profile do
get_country_tax_info(order.billing_profile.country)
else
{nil, nil, nil}
end
socket
|> assign(:page_title, "Edit Order #{order.order_number}")
|> assign(:url_path, Routes.path("/admin/billing/orders/#{order.uuid}/edit"))
|> assign(:order, order)
|> assign(:form, to_form(changeset))
|> assign(:line_items, line_items)
|> assign(:selected_user_uuid, order.user_uuid)
|> assign(:billing_profiles, billing_profiles)
|> assign(:selected_billing_profile_uuid, order.billing_profile_uuid)
|> assign(:country_tax_rate, country_tax_rate)
|> assign(:country_name, country_name)
|> assign(:country_vat_percent, country_vat_percent)
end
end
defp parse_line_items(nil),
do: [%{id: 0, name: "", description: "", quantity: 1, unit_price: "0.00"}]
defp parse_line_items([]),
do: [%{id: 0, name: "", description: "", quantity: 1, unit_price: "0.00"}]
defp parse_line_items(items) do
items
|> Enum.with_index()
|> Enum.map(fn {item, idx} ->
%{
id: idx,
name: item["name"] || "",
description: item["description"] || "",
quantity: item["quantity"] || 1,
unit_price: item["unit_price"] || "0.00"
}
end)
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("select_user", %{"user_id" => user_id}, socket) do
user_id = if user_id == "", do: nil, else: user_id
billing_profiles = if user_id, do: Billing.list_user_billing_profiles(user_id), else: []
# Auto-select default profile if available, otherwise select first profile
default_profile = Enum.find(billing_profiles, & &1.is_default)
selected_profile = default_profile || List.first(billing_profiles)
selected_profile_id = if selected_profile, do: selected_profile.uuid, else: nil
# Get country tax info for selected profile
{country_tax_rate, country_name, country_vat_percent} =
if selected_profile do
get_country_tax_info(selected_profile.country)
else
{nil, nil, nil}
end
{:noreply,
socket
|> assign(:selected_user_uuid, user_id)
|> assign(:billing_profiles, billing_profiles)
|> assign(:selected_billing_profile_uuid, selected_profile_id)
|> assign(:country_tax_rate, country_tax_rate)
|> assign(:country_name, country_name)
|> assign(:country_vat_percent, country_vat_percent)}
end
@impl true
def handle_event(
"select_billing_profile",
%{"order" => %{"billing_profile_uuid" => profile_id}},
socket
) do
handle_billing_profile_selection(profile_id, socket)
end
@impl true
def handle_event("select_billing_profile", %{"profile_id" => profile_id}, socket) do
handle_billing_profile_selection(profile_id, socket)
end
@impl true
def handle_event("add_line_item", _params, socket) do
new_id = length(socket.assigns.line_items)
new_item = %{id: new_id, name: "", description: "", quantity: 1, unit_price: "0.00"}
{:noreply, assign(socket, :line_items, socket.assigns.line_items ++ [new_item])}
end
@impl true
def handle_event("remove_line_item", %{"id" => id}, socket) do
id = String.to_integer(id)
items = Enum.reject(socket.assigns.line_items, &(&1.id == id))
items =
if Enum.empty?(items),
do: [%{id: 0, name: "", description: "", quantity: 1, unit_price: "0.00"}],
else: items
{:noreply, assign(socket, :line_items, items)}
end
@impl true
def handle_event("update_line_item", params, socket) do
id = String.to_integer(params["id"])
field = String.to_existing_atom(params["field"])
value = params["value"]
items =
Enum.map(socket.assigns.line_items, fn item ->
if item.id == id do
Map.put(item, field, value)
else
item
end
end)
{:noreply, assign(socket, :line_items, items)}
end
@impl true
def handle_event("save", %{"order" => order_params}, socket) do
# Get tax rate - prefer country-based rate from billing profile, fallback to config
tax_rate =
case socket.assigns.country_tax_rate do
%Decimal{} = rate ->
rate
_ ->
config = Billing.get_config()
get_tax_rate_decimal(config)
end
line_items =
socket.assigns.line_items
|> Enum.filter(&(&1.name != ""))
|> Enum.map(fn item ->
quantity = parse_number(item.quantity, 1)
unit_price = parse_decimal(item.unit_price)
total = Decimal.mult(unit_price, quantity)
%{
"name" => item.name,
"description" => item.description,
"quantity" => quantity,
"unit_price" => Decimal.to_string(unit_price),
"total" => Decimal.to_string(total)
}
end)
# Calculate totals with tax using Order.calculate_totals
{subtotal, tax_amount, total} = Order.calculate_totals(line_items, tax_rate, Decimal.new("0"))
order_params =
order_params
|> Map.put("line_items", line_items)
|> Map.put("subtotal", Decimal.to_string(subtotal))
|> Map.put("tax_rate", Decimal.to_string(tax_rate))
|> Map.put("tax_amount", Decimal.to_string(tax_amount))
|> Map.put("total", Decimal.to_string(total))
|> Map.put("user_uuid", socket.assigns.selected_user_uuid)
|> Map.put("billing_profile_uuid", socket.assigns.selected_billing_profile_uuid)
save_order(socket, order_params)
end
defp save_order(socket, params) do
result =
if socket.assigns.order do
Billing.update_order(socket.assigns.order, params)
else
Billing.create_order(params)
end
case result do
{:ok, order} ->
{:noreply,
socket
|> put_flash(:info, "Order saved successfully")
|> push_navigate(to: Routes.path("/admin/billing/orders/#{order.uuid}"))}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
rescue
e ->
require Logger
Logger.error("Order save failed: #{Exception.message(e)}")
{:noreply, put_flash(socket, :error, gettext("Something went wrong. Please try again."))}
end
defp handle_billing_profile_selection(profile_id, socket) do
profile_id = if profile_id == "", do: nil, else: profile_id
{country_tax_rate, country_name, country_vat_percent} =
if profile_id do
case Billing.get_billing_profile(profile_id) do
nil -> {nil, nil, nil}
profile -> get_country_tax_info(profile.country)
end
else
{nil, nil, nil}
end
{:noreply,
socket
|> assign(:selected_billing_profile_uuid, profile_id)
|> assign(:country_tax_rate, country_tax_rate)
|> assign(:country_name, country_name)
|> assign(:country_vat_percent, country_vat_percent)}
end
defp parse_number(value, _default) when is_integer(value), do: value
defp parse_number(value, default) when is_binary(value) do
case Integer.parse(value) do
{num, _} -> num
:error -> default
end
end
defp parse_number(_, default), do: default
defp parse_decimal(value) when is_binary(value) do
case Decimal.parse(value) do
{decimal, _} -> decimal
:error -> Decimal.new(0)
end
end
defp parse_decimal(_), do: Decimal.new(0)
defp get_tax_rate_decimal(config) do
if config.tax_enabled do
# Settings stores "20" for 20%, schema needs 0.20
config.default_tax_rate
|> Decimal.new()
|> Decimal.div(Decimal.new(100))
else
Decimal.new("0")
end
end
defp get_country_tax_info(nil), do: {nil, nil, nil}
defp get_country_tax_info(country_code) when is_binary(country_code) do
tax_rate = CountryData.get_standard_vat_rate(country_code)
vat_percent = CountryData.get_standard_vat_percent(country_code)
country_name = CountryData.get_country_name(country_code)
{tax_rate, country_name, vat_percent}
end
defp get_country_tax_info(_), do: {nil, nil, nil}
end