analytics/lib/mix/tasks/create_paddle_sandbox_plans.ex

179 lines
5.8 KiB
Elixir

defmodule Mix.Tasks.CreatePaddleSandboxPlans do
@moduledoc """
Utility for creating Sandbox plans that are used on staging. The product of
this task is a `sandbox_plans_v*.json` file matching with the production
plans, just with the monthly/yearly product_id's of the sandbox plans.
In principle, this task works like `Mix.Tasks.CreatePaddleProdPlans`, with
the following differences:
* The `filename` argument should be the name of the JSON file containing the
production plans (meaning that those should be created as the first step).
No special "input file" required.
* To copy the curl command from the browser, you need to add a plan from
https://sandbox-vendors.paddle.com/subscriptions/plans. Everything else is
the same - please see `create_paddle_prod_plans.ex` for instructions.
* This task can be executed multiple times in a row - it will not create
duplicates in Sandbox Paddle. On staging we can use a specific plan-naming
structure to determine whether a plan has been created already. On prod we
cannot do that since the plan names need to look nice.
* No need to copy paddle API credentials.
## Usage example:
```
mix create_paddle_sandbox_plans plans_v5.json
```
"""
alias Mix.Tasks.CreatePaddleProdPlans
use Mix.Task
@requirements ["app.config"]
def run([filename]) do
{:ok, _} = Application.ensure_all_started(:telemetry)
Finch.start_link(name: MyFinch)
prod_plans =
Application.app_dir(:plausible, ["priv", filename])
|> File.read!()
|> JSON.decode!()
to_be_created =
prod_plans
|> put_prices()
|> Enum.flat_map(fn priced_plan ->
[
create_paddle_plan_attrs(priced_plan, "monthly"),
create_paddle_plan_attrs(priced_plan, "yearly")
]
end)
IO.puts("Fetching all sandbox plans before we get started...")
paddle_plans_before = fetch_all_sandbox_plans()
created =
Enum.filter(to_be_created, fn attrs ->
if Enum.any?(paddle_plans_before, &(&1["name"] == attrs.name)) do
IO.puts("⚠️ The plan #{attrs.name} already exists in Sandbox Paddle")
false
else
create_paddle_plan(attrs)
true
end
end)
paddle_plans_after =
if created != [] do
IO.puts("⏳ waiting 3s before fetching the newly created plans...")
Process.sleep(3000)
IO.puts("Fetching all sandbox plans after creation...")
fetch_all_sandbox_plans()
else
IO.puts("All plans have been created already.")
paddle_plans_before
end
file_path_to_write = Path.join("priv", "sandbox_" <> filename)
write_sandbox_plans_json_file(prod_plans, file_path_to_write, paddle_plans_after)
IO.puts("✅ All done! Wrote #{length(prod_plans)} new plans into #{file_path_to_write}!")
end
defp create_paddle_plan(%{name: name, price: price, type: type, interval_index: interval_index}) do
your_unique_token = "abc"
# Replace this curl command. You might be able to reuse
# the request body after replacing your unique token.
curl_command = """
... REPLACE ME
--data-raw '_token=#{your_unique_token}&plan-id=&default-curr=USD&tmpicon=false&name=#{name}&checkout_custom_message=&taxable_type=standard&interval=#{interval_index}&period=1&type=#{type}&trial_length=&price_USD=#{price}&active_EUR=on&price_EUR=#{price}&active_GBP=on&price_GBP=#{price}'
"""
case CreatePaddleProdPlans.curl_quietly(curl_command) do
:ok ->
IO.puts("✅ Created #{name}")
{:error, reason} ->
IO.puts("❌ Halting. The plan #{name} could not be created. Error: #{reason}")
System.halt(1)
end
end
defp fetch_all_sandbox_plans() do
url = "https://sandbox-vendors.paddle.com/api/2.0/subscription/plans"
paddle_config = Application.get_env(:plausible, :paddle)
paddle_credentials = %{
vendor_id: paddle_config[:vendor_id],
vendor_auth_code: paddle_config[:vendor_auth_code]
}
CreatePaddleProdPlans.fetch_all_paddle_plans(url, paddle_credentials)
end
@paddle_interval_indexes %{"monthly" => 2, "yearly" => 5}
defp create_paddle_plan_attrs(plan_with_price, type) do
%{
name: plan_name(plan_with_price, type),
price: plan_with_price["#{type}_price"],
type: type,
interval_index: @paddle_interval_indexes[type]
}
end
defp plan_name(plan, type) do
generation = "v#{plan["generation"]}"
kind = plan["kind"]
volume = plan["monthly_pageview_limit"] |> PlausibleWeb.StatsView.large_number_format()
[generation, type, kind, volume] |> Enum.join("_")
end
defp write_sandbox_plans_json_file(prod_plans, filepath, paddle_plans) do
sandbox_plans =
prod_plans
|> Enum.map(fn prod_plan ->
monthly_plan_name = plan_name(prod_plan, "monthly")
yearly_plan_name = plan_name(prod_plan, "yearly")
%{"id" => sandbox_monthly_product_id} =
Enum.find(paddle_plans, &(&1["name"] == monthly_plan_name))
%{"id" => sandbox_yearly_product_id} =
Enum.find(paddle_plans, &(&1["name"] == yearly_plan_name))
Map.merge(prod_plan, %{
"monthly_product_id" => to_string(sandbox_monthly_product_id),
"yearly_product_id" => to_string(sandbox_yearly_product_id)
})
|> CreatePaddleProdPlans.order_keys()
end)
content = Jason.encode!(sandbox_plans, pretty: true)
File.write!(filepath, content)
end
defp put_prices(plans) do
prices =
Application.app_dir(:plausible, ["priv", "plan_prices.json"])
|> File.read!()
|> JSON.decode!()
plans
|> Enum.map(fn plan ->
Map.merge(plan, %{
"monthly_price" => prices[plan["monthly_product_id"]],
"yearly_price" => prices[plan["yearly_product_id"]]
})
end)
end
end