docs: design for dynamic global pricing configuration
This commit is contained in:
parent
2ae7a2680a
commit
ffb61641bb
1 changed files with 224 additions and 0 deletions
224
docs/plans/2026-03-06-dynamic-global-pricing-design.md
Normal file
224
docs/plans/2026-03-06-dynamic-global-pricing-design.md
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
1. Allow superadmins to change global pricing without code deployment
|
||||||
|
2. Automatically create new Stripe Prices and migrate active subscriptions
|
||||||
|
3. Update marketing materials to reflect $2/device/month pricing
|
||||||
|
4. Maintain per-organization override capability
|
||||||
|
5. 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:**
|
||||||
|
1. Create new Stripe Price object at new unit amount
|
||||||
|
2. Update `stripe_price_id` in ApplicationSettings
|
||||||
|
3. Migrate all active subscriptions to new Price (best-effort)
|
||||||
|
4. 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:**
|
||||||
|
1. Superadmin clicks "Edit Defaults"
|
||||||
|
2. Modal shows form with current values
|
||||||
|
3. On save → confirmation dialog: "This will update X active subscriptions. Continue?"
|
||||||
|
4. After confirmation → updates DB + Stripe + migrates subscriptions
|
||||||
|
5. 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
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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 fallback
|
||||||
|
- `default_price_per_device/0` - Get from Settings or fallback
|
||||||
|
- `stripe_price_id/0` - Get from Settings or fallback to env var
|
||||||
|
- `migrate_all_subscriptions_to_price/1` - Migrate subscriptions with reporting
|
||||||
|
|
||||||
|
**Updated functions:**
|
||||||
|
- `effective_free_device_count/1` - Use `default_free_devices/0`
|
||||||
|
- `effective_price_per_device/1` - Use `default_price_per_device/0`
|
||||||
|
|
||||||
|
**Module attribute updates:**
|
||||||
|
- Change `@default_price_per_device` from `Decimal.new("1.00")` to `Decimal.new("2.00")`
|
||||||
|
|
||||||
|
### 2. StripeClient Updates
|
||||||
|
|
||||||
|
**New functions:**
|
||||||
|
- `create_price/1` - Create metered billing Price object
|
||||||
|
- `update_subscription_price/2` - Switch subscription to new Price
|
||||||
|
- `list_active_subscriptions/1` - Paginated list of active subs
|
||||||
|
|
||||||
|
**Price creation parameters:**
|
||||||
|
- `unit_amount_decimal`: e.g., "2.00" for $2/device/month
|
||||||
|
- `recurring.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:**
|
||||||
|
1. Validate new values (positive numbers, reasonable ranges)
|
||||||
|
2. Get count of active subscriptions for confirmation
|
||||||
|
3. If price changed: create new Stripe Price
|
||||||
|
4. Update ApplicationSettings
|
||||||
|
5. If price changed: migrate subscriptions
|
||||||
|
6. Create audit log with results
|
||||||
|
7. 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-10000
|
||||||
|
- `default_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 → $2
|
||||||
|
- `lib/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/0` with/without ApplicationSetting
|
||||||
|
- `Billing.default_price_per_device/0` with/without ApplicationSetting
|
||||||
|
- `Billing.migrate_all_subscriptions_to_price/1` with 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_superuser` plug)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
1. Deploy code with $2 default in module attributes
|
||||||
|
2. Run migration to seed ApplicationSettings
|
||||||
|
3. Existing orgs continue using per-org overrides or new $2 default
|
||||||
|
4. Superadmin can adjust pricing via UI going forward
|
||||||
|
5. 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
|
||||||
Loading…
Add table
Reference in a new issue