feat: dynamic global billing defaults from ApplicationSettings
This commit is contained in:
parent
a16ce2b9dd
commit
448ed20401
3 changed files with 109 additions and 11 deletions
|
|
@ -3,21 +3,55 @@ defmodule Towerops.Billing do
|
|||
Billing context for subscription and payment management.
|
||||
|
||||
Integrates with Stripe for metered billing based on device count.
|
||||
Pricing: $1/device/month after first 10 free devices.
|
||||
Pricing: $2/device/month after first 10 free devices (configurable via ApplicationSettings).
|
||||
"""
|
||||
|
||||
alias Towerops.Billing.StripeClient
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Organizations.SubscriptionLimits
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Settings
|
||||
|
||||
require Logger
|
||||
|
||||
@default_free_devices 10
|
||||
@default_price_per_device Decimal.new("1.00")
|
||||
@default_price_per_device Decimal.new("2.00")
|
||||
|
||||
# Public API
|
||||
|
||||
@doc """
|
||||
Returns the default number of free devices from ApplicationSettings.
|
||||
Falls back to module attribute (@default_free_devices = 10) if not configured.
|
||||
|
||||
Expected `value_type`: "integer"
|
||||
"""
|
||||
def default_free_devices do
|
||||
Settings.get_setting("default_free_devices") || @default_free_devices
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the default price per device from ApplicationSettings.
|
||||
Falls back to module attribute (@default_price_per_device = $2.00) if not configured.
|
||||
|
||||
Expected `value_type`: "decimal"
|
||||
"""
|
||||
def default_price_per_device do
|
||||
case Settings.get_setting("default_price_per_device") do
|
||||
nil -> @default_price_per_device
|
||||
%Decimal{} = decimal -> decimal
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the Stripe Price ID from ApplicationSettings.
|
||||
Falls back to STRIPE_PRICE_ID environment variable if not configured.
|
||||
|
||||
Expected `value_type`: "string"
|
||||
"""
|
||||
def stripe_price_id do
|
||||
Settings.get_setting("stripe_price_id") || Application.get_env(:towerops, :stripe_price_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create Stripe checkout session for organization to start paid subscription.
|
||||
|
||||
|
|
@ -79,18 +113,18 @@ defmodule Towerops.Billing do
|
|||
|
||||
@doc """
|
||||
Returns the effective number of free devices for an organization.
|
||||
Uses custom override if set, otherwise the default (10).
|
||||
Uses custom override if set, otherwise the global default from ApplicationSettings.
|
||||
"""
|
||||
def effective_free_device_count(%Organization{} = org) do
|
||||
org.custom_free_device_limit || @default_free_devices
|
||||
org.custom_free_device_limit || default_free_devices()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the effective price per device for an organization.
|
||||
Uses custom override if set, otherwise the default ($1.00).
|
||||
Uses custom override if set, otherwise the global default from ApplicationSettings.
|
||||
"""
|
||||
def effective_price_per_device(%Organization{} = org) do
|
||||
org.custom_price_per_device || @default_price_per_device
|
||||
org.custom_price_per_device || default_price_per_device()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Towerops.Repo.Migrations.SeedBillingSettings do
|
|||
VALUES
|
||||
(gen_random_uuid(), 'default_free_devices', '10', 'integer',
|
||||
'Number of free devices included in all subscriptions', NOW(), NOW()),
|
||||
(gen_random_uuid(), 'default_price_per_device', '2.00', 'string',
|
||||
(gen_random_uuid(), 'default_price_per_device', '2.00', 'decimal',
|
||||
'Price per device per month (USD) after free tier', NOW(), NOW())
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ defmodule Towerops.BillingTest do
|
|||
alias Towerops.Billing.StripeClient
|
||||
alias Towerops.Organizations
|
||||
alias Towerops.Organizations.Organization
|
||||
alias Towerops.Settings.ApplicationSetting
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
|
|
@ -204,7 +205,7 @@ defmodule Towerops.BillingTest do
|
|||
|
||||
describe "estimated_monthly_cost/1" do
|
||||
test "returns correct cost calculation", %{organization: organization} do
|
||||
# Create 25 devices (15 billable)
|
||||
# Create 25 devices (15 billable at $2.00/device = $30.00)
|
||||
for _ <- 1..25 do
|
||||
device_fixture(%{organization_id: organization.id})
|
||||
end
|
||||
|
|
@ -212,7 +213,7 @@ defmodule Towerops.BillingTest do
|
|||
assert {:ok, %{devices: 25, billable: 15, cost_usd: cost}} =
|
||||
Billing.estimated_monthly_cost(organization)
|
||||
|
||||
assert Decimal.equal?(cost, Decimal.new(15))
|
||||
assert Decimal.equal?(cost, Decimal.new("30.00"))
|
||||
end
|
||||
|
||||
test "returns zero cost for free tier usage", %{organization: organization} do
|
||||
|
|
@ -257,7 +258,7 @@ defmodule Towerops.BillingTest do
|
|||
end
|
||||
|
||||
test "returns effective values in estimated cost map", %{organization: organization} do
|
||||
# Default org (no overrides): 10 free, $1.00/device
|
||||
# Default org (no overrides): 10 free, $2.00/device
|
||||
for _ <- 1..15 do
|
||||
device_fixture(%{organization_id: organization.id})
|
||||
end
|
||||
|
|
@ -268,7 +269,7 @@ defmodule Towerops.BillingTest do
|
|||
price_per_device: price
|
||||
}} = Billing.estimated_monthly_cost(organization)
|
||||
|
||||
assert Decimal.equal?(price, Decimal.new("1.00"))
|
||||
assert Decimal.equal?(price, Decimal.new("2.00"))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -387,4 +388,67 @@ defmodule Towerops.BillingTest do
|
|||
assert updated_org.payment_method_status == "requires_action"
|
||||
end
|
||||
end
|
||||
|
||||
describe "default_free_devices/0" do
|
||||
test "returns value from ApplicationSettings when set" do
|
||||
# Seed setting
|
||||
{:ok, _} =
|
||||
Repo.insert(%ApplicationSetting{
|
||||
key: "default_free_devices",
|
||||
value: "15",
|
||||
value_type: "integer"
|
||||
})
|
||||
|
||||
assert Billing.default_free_devices() == 15
|
||||
end
|
||||
|
||||
test "falls back to module attribute when setting not found" do
|
||||
# Ensure setting doesn't exist
|
||||
Repo.delete_all(from s in ApplicationSetting, where: s.key == "default_free_devices")
|
||||
|
||||
assert Billing.default_free_devices() == 10
|
||||
end
|
||||
end
|
||||
|
||||
describe "default_price_per_device/0" do
|
||||
test "returns value from ApplicationSettings when set" do
|
||||
{:ok, _} =
|
||||
Repo.insert(%ApplicationSetting{
|
||||
key: "default_price_per_device",
|
||||
value: "3.50",
|
||||
value_type: "decimal"
|
||||
})
|
||||
|
||||
assert Billing.default_price_per_device() == Decimal.new("3.50")
|
||||
end
|
||||
|
||||
test "falls back to module attribute when setting not found" do
|
||||
Repo.delete_all(
|
||||
from s in ApplicationSetting,
|
||||
where: s.key == "default_price_per_device"
|
||||
)
|
||||
|
||||
assert Billing.default_price_per_device() == Decimal.new("2.00")
|
||||
end
|
||||
end
|
||||
|
||||
describe "stripe_price_id/0" do
|
||||
test "returns value from ApplicationSettings when set" do
|
||||
{:ok, _} =
|
||||
Repo.insert(%ApplicationSetting{
|
||||
key: "stripe_price_id",
|
||||
value: "price_test123",
|
||||
value_type: "string"
|
||||
})
|
||||
|
||||
assert Billing.stripe_price_id() == "price_test123"
|
||||
end
|
||||
|
||||
test "falls back to env var when setting not found" do
|
||||
Repo.delete_all(from s in ApplicationSetting, where: s.key == "stripe_price_id")
|
||||
|
||||
# Env var is set in test config to "price_test_fake"
|
||||
assert Billing.stripe_price_id() == "price_test_fake"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue