Packages
phoenix_kit
1.7.53
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/modules/billing/web/subscription_form.ex
defmodule PhoenixKit.Modules.Billing.Web.SubscriptionForm do
@moduledoc """
Subscription form LiveView for creating subscriptions manually.
Allows administrators to:
- Search and select a user by email
- Choose a subscription type
- Optionally assign a payment method
- Configure trial period
"""
use PhoenixKitWeb, :live_view
alias PhoenixKit.Modules.Billing
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()
types = Billing.list_subscription_types(active_only: true)
socket =
socket
|> assign(:page_title, "Create Subscription")
|> assign(:project_title, project_title)
|> assign(:subscription_types, types)
|> assign(:user_search, "")
|> assign(:user_results, [])
|> assign(:selected_user, nil)
|> assign(:selected_subscription_type_uuid, nil)
|> assign(:payment_methods, [])
|> assign(:selected_payment_method_uuid, nil)
|> assign(:enable_trial, false)
|> assign(:trial_days, "")
|> assign(:error, nil)
{: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, socket}
end
@impl true
def handle_event("search_user", %{"query" => query}, socket) do
if String.length(query) >= 2 do
results = search_users(query)
{:noreply, assign(socket, user_search: query, user_results: results)}
else
{:noreply, assign(socket, user_search: query, user_results: [])}
end
end
@impl true
def handle_event("select_user", %{"id" => user_uuid}, socket) do
case Auth.get_user(user_uuid) do
nil ->
{:noreply, put_flash(socket, :error, "User not found")}
user ->
payment_methods = Billing.list_payment_methods(user.uuid, status: "active")
{:noreply,
socket
|> assign(:selected_user, user)
|> assign(:user_search, user.email)
|> assign(:user_results, [])
|> assign(:payment_methods, payment_methods)
|> assign(:selected_payment_method_uuid, nil)}
end
end
@impl true
def handle_event("clear_user", _params, socket) do
{:noreply,
socket
|> assign(:selected_user, nil)
|> assign(:user_search, "")
|> assign(:user_results, [])
|> assign(:payment_methods, [])
|> assign(:selected_payment_method_uuid, nil)}
end
@impl true
def handle_event("select_subscription_type", %{"subscription_type_uuid" => type_uuid}, socket) do
type_uuid = if type_uuid == "", do: nil, else: type_uuid
# Get subscription type's default trial days
trial_days =
if type_uuid do
case Enum.find(socket.assigns.subscription_types, &(to_string(&1.uuid) == type_uuid)) do
%{trial_days: days} when is_integer(days) and days > 0 -> to_string(days)
_ -> ""
end
else
""
end
{:noreply,
socket
|> assign(:selected_subscription_type_uuid, type_uuid)
|> assign(:trial_days, trial_days)
|> assign(:enable_trial, trial_days != "")}
end
@impl true
def handle_event("select_payment_method", %{"payment_method_uuid" => pm_uuid}, socket) do
pm_uuid = if pm_uuid == "", do: nil, else: pm_uuid
{:noreply, assign(socket, :selected_payment_method_uuid, pm_uuid)}
end
@impl true
def handle_event("toggle_trial", %{"enable" => enable}, socket) do
enable = enable == "true"
{:noreply, assign(socket, :enable_trial, enable)}
end
@impl true
def handle_event("update_trial_days", %{"days" => days}, socket) do
{:noreply, assign(socket, :trial_days, days)}
end
@impl true
def handle_event("clear_error", _params, socket) do
{:noreply, assign(socket, :error, nil)}
end
@impl true
def handle_event("save", _params, socket) do
%{
selected_user: user,
selected_subscription_type_uuid: type_uuid,
selected_payment_method_uuid: pm_uuid,
enable_trial: enable_trial,
trial_days: trial_days
} = socket.assigns
cond do
is_nil(user) ->
{:noreply, assign(socket, :error, "Please select a customer")}
is_nil(type_uuid) ->
{:noreply, assign(socket, :error, "Please select a subscription type")}
true ->
attrs = %{
subscription_type_uuid: type_uuid,
payment_method_uuid: pm_uuid,
trial_days:
if(enable_trial && trial_days != "", do: String.to_integer(trial_days), else: 0)
}
try do
case Billing.create_subscription(user.uuid, attrs) do
{:ok, subscription} ->
{:noreply,
socket
|> put_flash(:info, "Subscription created successfully")
|> push_navigate(
to: Routes.path("/admin/billing/subscriptions/#{subscription.uuid}")
)}
{:error, %Ecto.Changeset{} = changeset} ->
error_msg = format_changeset_errors(changeset)
{:noreply, assign(socket, :error, error_msg)}
{:error, reason} ->
{:noreply,
assign(socket, :error, "Failed to create subscription: #{inspect(reason)}")}
end
rescue
e ->
require Logger
Logger.error("Subscription save failed: #{Exception.message(e)}")
{:noreply, put_flash(socket, :error, "Something went wrong. Please try again.")}
end
end
end
# Private helpers
defp search_users(query) do
# Use paginated search with small page size
%{users: users} = Auth.list_users_paginated(search: query, page_size: 10)
users
end
defp format_changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
|> Enum.map_join("; ", fn {field, errors} -> "#{field}: #{Enum.join(errors, ", ")}" end)
end
# Helper functions for template
def format_interval(interval, interval_count) do
case {interval, interval_count} do
{"month", 1} -> "Monthly"
{"month", n} -> "Every #{n} months"
{"year", 1} -> "Yearly"
{"year", n} -> "Every #{n} years"
{"week", 1} -> "Weekly"
{"week", n} -> "Every #{n} weeks"
{"day", 1} -> "Daily"
{"day", n} -> "Every #{n} days"
_ -> "#{interval_count} #{interval}(s)"
end
end
def format_payment_method(pm) do
case pm.type do
"card" ->
brand = pm.brand || "Card"
last4 = pm.last4 || "****"
"#{String.capitalize(brand)} ending in #{last4}"
type ->
String.capitalize(type)
end
end
end