Packages
phoenix_kit
1.7.23
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 plan
- 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_setting("project_title", "PhoenixKit")
plans = Billing.list_subscription_plans(active_only: true)
socket =
socket
|> assign(:page_title, "Create Subscription")
|> assign(:project_title, project_title)
|> assign(:plans, plans)
|> assign(:user_search, "")
|> assign(:user_results, [])
|> assign(:selected_user, nil)
|> assign(:selected_plan_id, nil)
|> assign(:payment_methods, [])
|> assign(:selected_payment_method_id, 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_id}, socket) do
case Auth.get_user(user_id) do
nil ->
{:noreply, put_flash(socket, :error, "User not found")}
user ->
payment_methods = Billing.list_payment_methods(user.id, 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_id, 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_id, nil)}
end
@impl true
def handle_event("select_plan", %{"plan_id" => plan_id}, socket) do
plan_id = if plan_id == "", do: nil, else: String.to_integer(plan_id)
# Get plan's default trial days
trial_days =
if plan_id do
case Enum.find(socket.assigns.plans, &(&1.id == plan_id)) do
%{trial_days: days} when is_integer(days) and days > 0 -> to_string(days)
_ -> ""
end
else
""
end
{:noreply,
socket
|> assign(:selected_plan_id, plan_id)
|> assign(:trial_days, trial_days)
|> assign(:enable_trial, trial_days != "")}
end
@impl true
def handle_event("select_payment_method", %{"payment_method_id" => pm_id}, socket) do
pm_id = if pm_id == "", do: nil, else: String.to_integer(pm_id)
{:noreply, assign(socket, :selected_payment_method_id, pm_id)}
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_plan_id: plan_id,
selected_payment_method_id: pm_id,
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(plan_id) ->
{:noreply, assign(socket, :error, "Please select a plan")}
true ->
attrs = %{
plan_id: plan_id,
payment_method_id: pm_id,
trial_days:
if(enable_trial && trial_days != "", do: String.to_integer(trial_days), else: 0)
}
case Billing.create_subscription(user.id, attrs) do
{:ok, subscription} ->
{:noreply,
socket
|> put_flash(:info, "Subscription created successfully")
|> push_navigate(to: Routes.path("/admin/billing/subscriptions/#{subscription.id}"))}
{: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
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