mirror of
https://github.com/plausible/analytics.git
synced 2024-08-17 05:40:24 +03:00
Implement CRM enterprise plan definition QoL improvements (#4230)
* Implement autoprefill of enterprise plan fields on user change * Implement sanitizing input attrs in enterprise plan CRM form * Implement number formatting for monthly pageview limit input in CRM form
This commit is contained in:
parent
aadf528459
commit
e65e37afc0
@ -1,6 +1,15 @@
|
||||
defmodule Plausible.Billing.EnterprisePlanAdmin do
|
||||
use Plausible.Repo
|
||||
|
||||
@numeric_fields [
|
||||
"user_id",
|
||||
"paddle_plan_id",
|
||||
"monthly_pageview_limit",
|
||||
"site_limit",
|
||||
"team_member_limit",
|
||||
"hourly_api_request_limit"
|
||||
]
|
||||
|
||||
def search_fields(_schema) do
|
||||
[
|
||||
:paddle_plan_id,
|
||||
@ -40,8 +49,42 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do
|
||||
|
||||
defp get_user_email(plan), do: plan.user.email
|
||||
|
||||
def create_changeset(schema, attrs) do
|
||||
attrs = sanitize_attrs(attrs)
|
||||
Plausible.Billing.EnterprisePlan.changeset(schema, attrs)
|
||||
end
|
||||
|
||||
def update_changeset(enterprise_plan, attrs) do
|
||||
attrs = Map.put_new(attrs, "features", [])
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.put_new("features", [])
|
||||
|> sanitize_attrs()
|
||||
|
||||
Plausible.Billing.EnterprisePlan.changeset(enterprise_plan, attrs)
|
||||
end
|
||||
|
||||
defp sanitize_attrs(attrs) do
|
||||
attrs
|
||||
|> Enum.map(&clear_attr/1)
|
||||
|> Enum.reject(&(&1 == ""))
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
defp clear_attr({key, value}) when key in @numeric_fields do
|
||||
value =
|
||||
value
|
||||
|> to_string()
|
||||
|> String.replace(~r/[^0-9-]/, "")
|
||||
|> String.trim()
|
||||
|
||||
{key, value}
|
||||
end
|
||||
|
||||
defp clear_attr({key, value}) when is_binary(value) do
|
||||
{key, String.trim(value)}
|
||||
end
|
||||
|
||||
defp clear_attr(other) do
|
||||
other
|
||||
end
|
||||
end
|
||||
|
@ -24,6 +24,83 @@ defmodule Plausible.CrmExtensions do
|
||||
""")
|
||||
]
|
||||
end
|
||||
|
||||
def javascripts(%{assigns: %{context: "billing", resource: "enterprise_plan", changeset: %{}}}) do
|
||||
[
|
||||
Phoenix.HTML.raw("""
|
||||
<script type="text/javascript">
|
||||
(() => {
|
||||
const monthlyPageviewLimitField = document.getElementById("enterprise_plan_monthly_pageview_limit")
|
||||
|
||||
monthlyPageviewLimitField.type = "input"
|
||||
monthlyPageviewLimitField.addEventListener("keyup", numberFormatCallback)
|
||||
monthlyPageviewLimitField.addEventListener("change", numberFormatCallback)
|
||||
|
||||
monthlyPageviewLimitField.dispatchEvent(new Event("change"))
|
||||
|
||||
function numberFormatCallback(e) {
|
||||
const numeric = Number(e.target.value.replace(/[^0-9]/g, ''))
|
||||
const value = numeric > 0 ? new Intl.NumberFormat("en-GB").format(numeric) : ''
|
||||
e.target.value = value
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
"""),
|
||||
Phoenix.HTML.raw("""
|
||||
<script type="text/javascript">
|
||||
(async () => {
|
||||
const userIdField = document.getElementById("enterprise_plan_user_id")
|
||||
let planRequest
|
||||
let lastValue = Number(userIdField.value)
|
||||
let scheduledCheck
|
||||
|
||||
userIdField.addEventListener("change", async () => {
|
||||
if (scheduledCheck) clearTimeout(scheduledCheck)
|
||||
|
||||
scheduledCheck = setTimeout(async () => {
|
||||
const currentValue = Number(userIdField.value)
|
||||
if (Number.isInteger(currentValue)
|
||||
&& currentValue > 0
|
||||
&& currentValue != lastValue
|
||||
&& !planRequest) {
|
||||
planRequest = await fetch("/crm/billing/user/" + currentValue + "/current_plan")
|
||||
const result = await planRequest.json()
|
||||
|
||||
fillForm(result)
|
||||
|
||||
lastValue = currentValue
|
||||
planRequest = null
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
userIdField.dispatchEvent(new Event("change"))
|
||||
|
||||
function fillForm(result) {
|
||||
[
|
||||
'billing_interval',
|
||||
'monthly_pageview_limit',
|
||||
'site_limit',
|
||||
'team_member_limit',
|
||||
'hourly_api_request_limit'
|
||||
].forEach(name => {
|
||||
const prefillValue = result[name] || ""
|
||||
const field = document.getElementById('enterprise_plan_' + name)
|
||||
|
||||
field.value = prefillValue
|
||||
field.dispatchEvent(new Event("change"))
|
||||
});
|
||||
|
||||
['stats_api', 'props', 'funnels', 'revenue_goals'].forEach(feature => {
|
||||
const checked = result.features.includes(feature)
|
||||
document.getElementById('enterprise_plan_features_' + feature).checked = checked
|
||||
});
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
""")
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def javascripts(_) do
|
||||
|
@ -24,6 +24,38 @@ defmodule PlausibleWeb.AdminController do
|
||||
|> send_resp(200, html_response)
|
||||
end
|
||||
|
||||
def current_plan(conn, params) do
|
||||
user =
|
||||
params["user_id"]
|
||||
|> String.to_integer()
|
||||
|> Plausible.Users.with_subscription()
|
||||
|
||||
plan =
|
||||
case user && user.subscription &&
|
||||
Plausible.Billing.Plans.get_subscription_plan(user.subscription) do
|
||||
%{} = plan ->
|
||||
plan
|
||||
|> Map.take([
|
||||
:billing_interval,
|
||||
:monthly_pageview_limit,
|
||||
:site_limit,
|
||||
:team_member_limit,
|
||||
:hourly_api_request_limit,
|
||||
:features
|
||||
])
|
||||
|> Map.update(:features, [], fn features -> Enum.map(features, & &1.name()) end)
|
||||
|
||||
_ ->
|
||||
%{features: []}
|
||||
end
|
||||
|
||||
json_response = Jason.encode!(plan)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(200, json_response)
|
||||
end
|
||||
|
||||
defp usage_and_limits_html(user, usage, limits, embed?) do
|
||||
content = """
|
||||
<ul>
|
||||
|
@ -76,6 +76,7 @@ defmodule PlausibleWeb.Router do
|
||||
scope "/crm", PlausibleWeb do
|
||||
pipe_through :flags
|
||||
get "/auth/user/:user_id/usage", AdminController, :usage
|
||||
get "/billing/user/:user_id/current_plan", AdminController, :current_plan
|
||||
end
|
||||
end
|
||||
|
||||
|
52
test/plausible/billing/enterprise_plan_admin_test.exs
Normal file
52
test/plausible/billing/enterprise_plan_admin_test.exs
Normal file
@ -0,0 +1,52 @@
|
||||
defmodule Plausible.Billing.EnterprisePlanAdminTest do
|
||||
use Plausible.DataCase, async: true
|
||||
|
||||
alias Plausible.Billing.EnterprisePlan
|
||||
alias Plausible.Billing.EnterprisePlanAdmin
|
||||
|
||||
@moduletag :ee_only
|
||||
|
||||
test "sanitizes number inputs and whitespace" do
|
||||
user = insert(:user)
|
||||
|
||||
changeset =
|
||||
EnterprisePlanAdmin.create_changeset(%EnterprisePlan{}, %{
|
||||
"user_id" => to_string(user.id),
|
||||
"paddle_plan_id" => " . 123456 ",
|
||||
"billing_interval" => "monthly",
|
||||
"monthly_pageview_limit" => "100,000,000",
|
||||
"site_limit" => " 10 ",
|
||||
"team_member_limit" => "-1 ",
|
||||
"hourly_api_request_limit" => " 1,000",
|
||||
"features" => ["goals"]
|
||||
})
|
||||
|
||||
assert changeset.valid?
|
||||
assert changeset.changes.user_id == user.id
|
||||
assert changeset.changes.paddle_plan_id == "123456"
|
||||
assert changeset.changes.billing_interval == :monthly
|
||||
assert changeset.changes.monthly_pageview_limit == 100_000_000
|
||||
assert changeset.changes.site_limit == 10
|
||||
assert changeset.changes.hourly_api_request_limit == 1000
|
||||
assert changeset.changes.features == [Plausible.Billing.Feature.Goals]
|
||||
end
|
||||
|
||||
test "scrubs empty attrs" do
|
||||
user = insert(:user)
|
||||
|
||||
changeset =
|
||||
EnterprisePlanAdmin.create_changeset(%EnterprisePlan{}, %{
|
||||
"user_id" => to_string(user.id),
|
||||
"paddle_plan_id" => " ,. ",
|
||||
"billing_interval" => "monthly",
|
||||
"monthly_pageview_limit" => "100,000,000",
|
||||
"site_limit" => " 10 ",
|
||||
"team_member_limit" => "-1 ",
|
||||
"hourly_api_request_limit" => " 1,000",
|
||||
"features" => ["goals"]
|
||||
})
|
||||
|
||||
refute changeset.valid?
|
||||
assert {_, validation: :required} = changeset.errors[:paddle_plan_id]
|
||||
end
|
||||
end
|
@ -62,4 +62,59 @@ defmodule PlausibleWeb.AdminControllerTest do
|
||||
assert site.stats_start_date == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /crm/billing/user/:user_id/current_plan" do
|
||||
setup [:create_user, :log_in]
|
||||
|
||||
@tag :ee_only
|
||||
test "returns 403 if the logged in user is not a super admin", %{conn: conn} do
|
||||
conn = get(conn, "/crm/billing/user/0/current_plan")
|
||||
assert response(conn, 403) == "Not allowed"
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns empty state for non-existent user", %{conn: conn, user: user} do
|
||||
patch_env(:super_admin_user_ids, [user.id])
|
||||
|
||||
conn = get(conn, "/crm/billing/user/0/current_plan")
|
||||
assert json_response(conn, 200) == %{"features" => []}
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns empty state for user without subscription", %{conn: conn, user: user} do
|
||||
patch_env(:super_admin_user_ids, [user.id])
|
||||
|
||||
conn = get(conn, "/crm/billing/user/#{user.id}/current_plan")
|
||||
assert json_response(conn, 200) == %{"features" => []}
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns empty state for user with subscription with non-existent paddle plan ID", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
patch_env(:super_admin_user_ids, [user.id])
|
||||
|
||||
insert(:subscription, user: user)
|
||||
|
||||
conn = get(conn, "/crm/billing/user/#{user.id}/current_plan")
|
||||
assert json_response(conn, 200) == %{"features" => []}
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "returns plan data for user with subscription", %{conn: conn, user: user} do
|
||||
patch_env(:super_admin_user_ids, [user.id])
|
||||
|
||||
insert(:subscription, user: user, paddle_plan_id: "857104")
|
||||
|
||||
conn = get(conn, "/crm/billing/user/#{user.id}/current_plan")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"features" => ["goals"],
|
||||
"monthly_pageview_limit" => 10_000_000,
|
||||
"site_limit" => 10,
|
||||
"team_member_limit" => 3
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user