7.2 KiB
Dynamic Global Pricing Configuration
Date: 2026-03-06 Status: Approved Author: System Design
Overview
Add superadmin UI to dynamically modify global billing defaults (free device count and price per device) with automatic Stripe Price creation and subscription migration. Update default pricing from $1 to $2/device/month.
Goals
- Allow superadmins to change global pricing without code deployment
- Automatically create new Stripe Prices and migrate active subscriptions
- Update marketing materials to reflect $2/device/month pricing
- Maintain per-organization override capability
- Provide audit trail and migration reporting
Design Decisions
Storage Strategy (Hybrid Approach)
ApplicationSettings entries:
default_free_devices(integer): Number of free devices (default: 10)default_price_per_device(string): Price per device/month USD (default: "2.00")stripe_price_id(string): Current Stripe Price object ID
Fallback behavior:
- Check ApplicationSettings first
- Fall back to module attributes if not set
- Stripe Price ID falls back to env var
STRIPE_PRICE_ID
Stripe Integration
Price changes trigger:
- Create new Stripe Price object at new unit amount
- Update
stripe_price_idin ApplicationSettings - Migrate all active subscriptions to new Price (best-effort)
- Report successes/failures
Migration behavior:
- Update subscriptions with
proration_behavior: "none"(change at next billing cycle) - Best-effort: continue on individual failures, report at end
- Failed migrations logged for manual review
UI Placement
Location: /admin/organizations page
Component: "Global Billing Defaults" card at top of organization list
Workflow:
- Superadmin clicks "Edit Defaults"
- Modal shows form with current values
- On save → confirmation dialog: "This will update X active subscriptions. Continue?"
- After confirmation → updates DB + Stripe + migrates subscriptions
- Success message shows migration results (e.g., "Updated 45/47 subscriptions, 2 failed")
Safety & Audit
Confirmation dialog:
- Shows current values vs new values
- Shows count of active subscriptions that will be affected
- Requires explicit confirmation before execution
Audit logging:
- Action:
"global_pricing_updated" - Metadata includes:
- Old/new values for free devices and price
- Migration results (succeeded/failed counts)
- Failed organization IDs and error reasons
- IP address and superuser ID captured
Data Model
New ApplicationSettings Records
INSERT INTO application_settings (id, key, value, value_type, description, inserted_at, updated_at)
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',
'Price per device per month (USD) after free tier', NOW(), NOW());
Note: stripe_price_id will be populated on first price update via admin UI.
Schema Changes
No schema changes required. Uses existing application_settings table.
Implementation Components
1. Billing Module Updates
New functions:
default_free_devices/0- Get from Settings or fallbackdefault_price_per_device/0- Get from Settings or fallbackstripe_price_id/0- Get from Settings or fallback to env varmigrate_all_subscriptions_to_price/1- Migrate subscriptions with reporting
Updated functions:
effective_free_device_count/1- Usedefault_free_devices/0effective_price_per_device/1- Usedefault_price_per_device/0
Module attribute updates:
- Change
@default_price_per_devicefromDecimal.new("1.00")toDecimal.new("2.00")
2. StripeClient Updates
New functions:
create_price/1- Create metered billing Price objectupdate_subscription_price/2- Switch subscription to new Pricelist_active_subscriptions/1- Paginated list of active subs
Price creation parameters:
unit_amount_decimal: e.g., "2.00" for $2/device/monthrecurring.usage_type: "metered"recurring.aggregate_usage: "max" (bill for max devices in period)billing_scheme: "per_unit"
3. Admin Context
New function:
Admin.update_global_pricing/3- Orchestrates full update
Flow:
- Validate new values (positive numbers, reasonable ranges)
- Get count of active subscriptions for confirmation
- If price changed: create new Stripe Price
- Update ApplicationSettings
- If price changed: migrate subscriptions
- Create audit log with results
- Return migration report
4. Admin LiveView UI
Location: lib/towerops_web/live/admin/org_live/index.ex
New components:
- Global Defaults card with current values
- Edit modal with form
- Confirmation dialog with impact preview
- Success/error messages with migration results
Validation:
default_free_devices: integer, 1-10000default_price_per_device: decimal, 0.01-999.99
5. Marketing Updates
Files to update:
lib/towerops_web/controllers/page_html/home.html.heex: Change $3 → $2lib/towerops_web/controllers/user_registration_html/new.html.heex: Update copy
Error Handling
Partial Migration Failures
Scenario: Stripe API fails for some subscriptions
Behavior:
- Continue migrating remaining subscriptions
- Collect failed org IDs and error messages
- Return summary:
%{total: 50, succeeded: 48, failed: 2, failures: [...]} - Display warning in UI with failure details
- Log failures in audit_logs for manual follow-up
Failed subscriptions:
- Remain on old price until manually updated
- No disruption to billing
- Superadmin can retry migration or update individually
Complete Failures
Scenario: Stripe Price creation fails
Behavior:
- Abort entire operation
- Don't update ApplicationSettings
- Don't migrate any subscriptions
- Show error message with Stripe error details
- No audit log created (operation didn't complete)
Testing Strategy
Unit tests:
Billing.default_free_devices/0with/without ApplicationSettingBilling.default_price_per_device/0with/without ApplicationSettingBilling.migrate_all_subscriptions_to_price/1with mocked Stripe
Integration tests:
StripeClient.create_price/1(mocked in test)StripeClient.update_subscription_price/2(mocked in test)- Admin context with full flow (mocked Stripe)
LiveView tests:
- Render Global Defaults card
- Open edit modal
- Validation errors
- Confirmation dialog shows impact
- Success message with results
Security Considerations
- Only accessible by superusers (existing
require_superuserplug) - Audit log captures all changes with IP and user ID
- Confirmation dialog prevents accidental mass updates
- Rate limiting already applied to
/admin/*routes (100 req/min)
Rollout Plan
- Deploy code with $2 default in module attributes
- Run migration to seed ApplicationSettings
- Existing orgs continue using per-org overrides or new $2 default
- Superadmin can adjust pricing via UI going forward
- Marketing pages immediately show $2 pricing
Future Enhancements
- Volume pricing tiers (e.g., 10% off at 500+ devices)
- Scheduled price changes (effective date in future)
- Price change preview/dry-run mode
- Email notification to affected org owners before price change