fix(ui): update plan descriptions to reflect per-user pricing
- Rewrite taglines and feature lists for all four tiers with concrete, use-case-driven copy (freelancers, knowledge workers, power users — no team or enterprise framing) - Rename 'Business' display name to 'Power' (plan_id stays 'business' for backwards DB compatibility) - Replace 'enterprise' language in pricing page hero with 'per person, per month' copy - Swap fa-building icon for fa-bolt on the Power tier - Fix support level for Power tier to 'Priority' (was 'Dedicated') - Update docs/SubscriptionTiers.md with new names, table, and intended-use-case section - Add test_business_tier_display_name_is_power assertion Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+18
-14
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Subscription tier definitions and enforcement utilities for DocuElevate SaaS.
|
Subscription tier definitions and enforcement utilities for DocuElevate SaaS.
|
||||||
|
|
||||||
|
All plans are priced per user per month (or per year with ~20 % discount).
|
||||||
Four tiers (prices ex-VAT; German customers +19 % MwSt):
|
Four tiers (prices ex-VAT; German customers +19 % MwSt):
|
||||||
- free $0/mo — 50 lifetime docs, 150 lifetime OCR pages, 1 dest
|
- free $0/mo — 50 lifetime docs, 150 lifetime OCR pages, 1 dest
|
||||||
- starter $2.99/mo — 50/mo, 300 OCR pp/mo, 2 dests, 1 mailbox
|
- starter $2.99/mo — 50/mo, 300 OCR pp/mo, 2 dests, 1 mailbox
|
||||||
- professional $5.99/mo — 150/mo, 750 OCR pp/mo, 5 dests, 3 mailboxes
|
- professional $5.99/mo — 150/mo, 750 OCR pp/mo, 5 dests, 3 mailboxes
|
||||||
- business $7.99/mo — 300/mo, 1500 OCR pp/mo, 10 dests, unlimited mailboxes
|
- power $7.99/mo — 300/mo, 1500 OCR pp/mo, 10 dests, unlimited mailboxes
|
||||||
|
|
||||||
Limits use 0 to represent "unlimited".
|
Limits use 0 to represent "unlimited".
|
||||||
All paid tiers include a 30-day free trial (trial_days field).
|
All paid tiers include a 30-day free trial (trial_days field).
|
||||||
@@ -14,14 +15,14 @@ All paid tiers include a 30-day free trial (trial_days field).
|
|||||||
Infrastructure: CX32 (app+Redis €7.59) + CX22 (worker €3.79) + BX21 (storage €7.22) ≈ $24/mo
|
Infrastructure: CX32 (app+Redis €7.59) + CX22 (worker €3.79) + BX21 (storage €7.22) ≈ $24/mo
|
||||||
At 100 users infra share ≈ $0.24/user/mo.
|
At 100 users infra share ≈ $0.24/user/mo.
|
||||||
|
|
||||||
Starter : OCR $0.45 + AI $0.012 + infra $0.24 + Stripe $0.34 = $1.04 → 65 % gross margin
|
Starter : OCR $0.45 + AI $0.012 + infra $0.24 + Stripe $0.34 = $1.04 → 65 % gross margin
|
||||||
Professional: OCR $1.13 + AI $0.035 + infra $0.24 + Stripe $0.42 = $1.82 → 70 % gross margin
|
Professional: OCR $1.13 + AI $0.035 + infra $0.24 + Stripe $0.42 = $1.82 → 70 % gross margin
|
||||||
Business : OCR $2.25 + AI $0.069 + infra $0.24 + Stripe $0.48 = $3.04 → 62 % gross margin
|
Power : OCR $2.25 + AI $0.069 + infra $0.24 + Stripe $0.48 = $3.04 → 62 % gross margin
|
||||||
|
|
||||||
After ~30 % German corporate tax: Starter 45 %, Professional 49 %, Business 43 %.
|
After ~30 % German corporate tax: Starter 45 %, Professional 49 %, Power 43 %.
|
||||||
At average usage (~40 % of quota) margins improve to 55-65 % after tax.
|
At average usage (~40 % of quota) margins improve to 55-65 % after tax.
|
||||||
|
|
||||||
⚠ If GPT-4o (not mini) is configured, Business AI cost at max rises to ~$1.92/user,
|
⚠ If GPT-4o (not mini) is configured, Power AI cost at max rises to ~$1.92/user,
|
||||||
reducing after-tax margin to ~33 %. Recommend GPT-4o mini as default in production.
|
reducing after-tax margin to ~33 %. Recommend GPT-4o mini as default in production.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"free": {
|
"free": {
|
||||||
"id": "free",
|
"id": "free",
|
||||||
"name": "Free",
|
"name": "Free",
|
||||||
"tagline": "Explore DocuElevate at no cost",
|
"tagline": "Try DocuElevate free — no credit card needed",
|
||||||
"price_monthly": 0,
|
"price_monthly": 0,
|
||||||
"price_yearly": 0,
|
"price_yearly": 0,
|
||||||
"trial_days": 0,
|
"trial_days": 0,
|
||||||
@@ -75,7 +76,8 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"starter": {
|
"starter": {
|
||||||
"id": "starter",
|
"id": "starter",
|
||||||
"name": "Starter",
|
"name": "Starter",
|
||||||
"tagline": "Perfect for individuals getting started",
|
# Use case: freelancer sending ~50 invoices, contracts, or scanned receipts a month
|
||||||
|
"tagline": "Perfect for freelancers and side-project owners",
|
||||||
"price_monthly": 2.99,
|
"price_monthly": 2.99,
|
||||||
"price_yearly": 28.99, # ≈ 80 % of monthly × 12 — save ~19 % (≈ 2½ months free)
|
"price_yearly": 28.99, # ≈ 80 % of monthly × 12 — save ~19 % (≈ 2½ months free)
|
||||||
"trial_days": 30,
|
"trial_days": 30,
|
||||||
@@ -89,7 +91,7 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"max_mailboxes": 1,
|
"max_mailboxes": 1,
|
||||||
"api_access": True,
|
"api_access": True,
|
||||||
"features": [
|
"features": [
|
||||||
"50 documents / month",
|
"50 documents / month — invoices, contracts, receipts",
|
||||||
"2 storage destinations",
|
"2 storage destinations",
|
||||||
"300 OCR pages / month",
|
"300 OCR pages / month",
|
||||||
"25 MB max file size",
|
"25 MB max file size",
|
||||||
@@ -104,7 +106,8 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"professional": {
|
"professional": {
|
||||||
"id": "professional",
|
"id": "professional",
|
||||||
"name": "Professional",
|
"name": "Professional",
|
||||||
"tagline": "For growing teams that need more power",
|
# Use case: consultant or knowledge worker handling ~150 docs/month across multiple platforms
|
||||||
|
"tagline": "For knowledge workers managing documents daily",
|
||||||
"price_monthly": 5.99,
|
"price_monthly": 5.99,
|
||||||
"price_yearly": 57.99, # ≈ 80 % of monthly × 12 — save ~19 %
|
"price_yearly": 57.99, # ≈ 80 % of monthly × 12 — save ~19 %
|
||||||
"trial_days": 30,
|
"trial_days": 30,
|
||||||
@@ -118,7 +121,7 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"max_mailboxes": 3,
|
"max_mailboxes": 3,
|
||||||
"api_access": True,
|
"api_access": True,
|
||||||
"features": [
|
"features": [
|
||||||
"150 documents / month",
|
"150 documents / month — reports, contracts, invoices",
|
||||||
"5 storage destinations",
|
"5 storage destinations",
|
||||||
"750 OCR pages / month",
|
"750 OCR pages / month",
|
||||||
"100 MB max file size",
|
"100 MB max file size",
|
||||||
@@ -133,8 +136,9 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
},
|
},
|
||||||
"business": {
|
"business": {
|
||||||
"id": "business",
|
"id": "business",
|
||||||
"name": "Business",
|
"name": "Power",
|
||||||
"tagline": "High-volume processing for organisations",
|
# Use case: power user — real estate agent, bookkeeper, or researcher processing ~10 docs/day
|
||||||
|
"tagline": "For power users with high-volume document workflows",
|
||||||
"price_monthly": 7.99,
|
"price_monthly": 7.99,
|
||||||
"price_yearly": 76.99, # ≈ 80 % of monthly × 12 — save ~20 %
|
"price_yearly": 76.99, # ≈ 80 % of monthly × 12 — save ~20 %
|
||||||
"trial_days": 30,
|
"trial_days": 30,
|
||||||
@@ -148,7 +152,7 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"max_mailboxes": 0, # unlimited mailboxes
|
"max_mailboxes": 0, # unlimited mailboxes
|
||||||
"api_access": True,
|
"api_access": True,
|
||||||
"features": [
|
"features": [
|
||||||
"300 documents / month",
|
"300 documents / month — ~10 documents per day",
|
||||||
"10 storage destinations",
|
"10 storage destinations",
|
||||||
"1,500 OCR pages / month",
|
"1,500 OCR pages / month",
|
||||||
"Unlimited file size",
|
"Unlimited file size",
|
||||||
@@ -156,7 +160,7 @@ TIER_DEFAULTS: dict[str, dict[str, Any]] = {
|
|||||||
"Unlimited email ingestion mailboxes",
|
"Unlimited email ingestion mailboxes",
|
||||||
"All ingestion methods",
|
"All ingestion methods",
|
||||||
"Webhooks & full API access",
|
"Webhooks & full API access",
|
||||||
"Dedicated support",
|
"Priority support",
|
||||||
],
|
],
|
||||||
"cta": "Start free trial",
|
"cta": "Start free trial",
|
||||||
"badge": "Best Value",
|
"badge": "Best Value",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
DocuElevate uses database-backed subscription plans that are fully configurable by admins via the **Plan Designer** at `/admin/plans`. Four default tiers are seeded automatically on first startup.
|
DocuElevate uses database-backed subscription plans that are fully configurable by admins via the **Plan Designer** at `/admin/plans`. Four default tiers are seeded automatically on first startup.
|
||||||
|
|
||||||
|
All plans are priced **per user, per month** (or per year with ~20 % discount). There are no team, business, or enterprise tiers — every plan is a single-user subscription.
|
||||||
|
|
||||||
## Default Plans
|
## Default Plans
|
||||||
|
|
||||||
| Plan | Monthly | Yearly | Docs/Month | Lifetime Docs | OCR Pages/Mo | Max File | Mailboxes | Destinations |
|
| Plan | Monthly | Yearly | Docs/Month | Lifetime Docs | OCR Pages/Mo | Max File | Mailboxes | Destinations |
|
||||||
@@ -9,12 +11,21 @@ DocuElevate uses database-backed subscription plans that are fully configurable
|
|||||||
| **Free** | $0 | $0 | — | 50 total | 150 total | 5 MB | 0 | 1 |
|
| **Free** | $0 | $0 | — | 50 total | 150 total | 5 MB | 0 | 1 |
|
||||||
| **Starter** | $2.99 | $28.99 | 50 | — | 300 | 25 MB | 1 | 2 |
|
| **Starter** | $2.99 | $28.99 | 50 | — | 300 | 25 MB | 1 | 2 |
|
||||||
| **Professional** | $5.99 | $57.99 | 150 | — | 750 | 100 MB | 3 | 5 |
|
| **Professional** | $5.99 | $57.99 | 150 | — | 750 | 100 MB | 3 | 5 |
|
||||||
| **Business** | $7.99 | $76.99 | 300 | — | 1,500 | Unlimited | Unlimited | 10 |
|
| **Power** | $7.99 | $76.99 | 300 | — | 1,500 | Unlimited | Unlimited | 10 |
|
||||||
|
|
||||||
> Prices ex-VAT. German customers add 19% MwSt.
|
> Prices ex-VAT. German customers add 19% MwSt.
|
||||||
|
|
||||||
All paid plans include a **30-day free trial**.
|
All paid plans include a **30-day free trial**.
|
||||||
|
|
||||||
|
### Intended Use Cases
|
||||||
|
|
||||||
|
- **Free** — Try DocuElevate with no commitment. Good for one-off experiments or evaluating the service.
|
||||||
|
- **Starter** — Freelancers and side-project owners sending ~50 invoices, contracts, or scanned receipts a month.
|
||||||
|
- **Professional** — Knowledge workers (consultants, paralegals, accountants) handling ~150 multi-page documents a month across several cloud destinations.
|
||||||
|
- **Power** — Power users with heavy daily workloads: real estate agents, bookkeepers, or researchers processing ~10 documents a day (≈ 300/month) with no file-size restrictions.
|
||||||
|
|
||||||
|
> The **plan_id** in the database remains `"business"` for the Power tier to preserve backwards compatibility. The display name shown to users is "Power".
|
||||||
|
|
||||||
## How Plans Are Stored
|
## How Plans Are Stored
|
||||||
|
|
||||||
Plans are stored in the `subscription_plans` database table. On application startup, `seed_default_plans()` is called automatically — if the table is empty, the four built-in defaults are inserted. If plans already exist, the seed is a no-op.
|
Plans are stored in the `subscription_plans` database table. On application startup, `seed_default_plans()` is called automatically — if the table is empty, the four built-in defaults are inserted. If plans already exist, the seed is a no-op.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
Choose the plan that's right for you
|
Choose the plan that's right for you
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-indigo-100 text-lg max-w-2xl mx-auto">
|
<p class="text-indigo-100 text-lg max-w-2xl mx-auto">
|
||||||
From free exploration to unlimited enterprise processing — scale as your document workflows grow.
|
One price per person, per month — from casual exploration to power-user workflows. No team plans, no per-seat tiers.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Annual / Monthly toggle (cosmetic — actual billing handled separately) -->
|
<!-- Annual / Monthly toggle (cosmetic — actual billing handled separately) -->
|
||||||
@@ -318,7 +318,7 @@
|
|||||||
<td class="px-4 py-3 text-center text-xs text-gray-500">Community</td>
|
<td class="px-4 py-3 text-center text-xs text-gray-500">Community</td>
|
||||||
<td class="px-4 py-3 text-center text-xs text-gray-700">Email</td>
|
<td class="px-4 py-3 text-center text-xs text-gray-700">Email</td>
|
||||||
<td class="px-4 py-3 text-center text-xs text-gray-700 font-medium">Priority email</td>
|
<td class="px-4 py-3 text-center text-xs text-gray-700 font-medium">Priority email</td>
|
||||||
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Dedicated</td>
|
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Priority</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
{% if tier_id == 'free' %}fa-seedling text-gray-500
|
{% if tier_id == 'free' %}fa-seedling text-gray-500
|
||||||
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
|
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
|
||||||
{% elif tier_id == 'professional' %}fa-star text-indigo-600
|
{% elif tier_id == 'professional' %}fa-star text-indigo-600
|
||||||
{% else %}fa-building text-purple-600{% endif %}
|
{% else %}fa-bolt text-purple-600{% endif %}
|
||||||
text-2xl" aria-hidden="true"></i>
|
text-2xl" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">Current Plan</p>
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">Current Plan</p>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ def test_free_tier_has_no_mailboxes():
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_business_tier_has_highest_limits():
|
def test_business_tier_has_highest_limits():
|
||||||
"""Business tier must have the highest limits of all paid tiers."""
|
"""Power tier (plan_id 'business') must have the highest limits of all paid tiers."""
|
||||||
t = TIERS["business"]
|
t = TIERS["business"]
|
||||||
# lifetime: no hard cap (0 = unlimited)
|
# lifetime: no hard cap (0 = unlimited)
|
||||||
assert t["lifetime_file_limit"] == 0
|
assert t["lifetime_file_limit"] == 0
|
||||||
@@ -121,9 +121,15 @@ def test_business_tier_has_highest_limits():
|
|||||||
assert t["max_file_size_mb"] == 0
|
assert t["max_file_size_mb"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_business_tier_display_name_is_power():
|
||||||
|
"""The 'business' plan_id must display as 'Power'."""
|
||||||
|
assert TIERS["business"]["name"] == "Power"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_mailbox_limits_increase_by_tier():
|
def test_mailbox_limits_increase_by_tier():
|
||||||
"""Mailbox limits must increase across tiers: free=0, starter=1, professional=3, business=0(inf)."""
|
"""Mailbox limits must increase across tiers: free=0, starter=1, professional=3, power/business=0(inf)."""
|
||||||
assert TIERS["free"]["max_mailboxes"] == 0
|
assert TIERS["free"]["max_mailboxes"] == 0
|
||||||
assert TIERS["starter"]["max_mailboxes"] == 1
|
assert TIERS["starter"]["max_mailboxes"] == 1
|
||||||
assert TIERS["professional"]["max_mailboxes"] == 3
|
assert TIERS["professional"]["max_mailboxes"] == 3
|
||||||
@@ -144,7 +150,7 @@ def test_free_tier_has_no_trial():
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_pricing_order():
|
def test_pricing_order():
|
||||||
"""Paid tier prices must increase in order: starter < professional < business."""
|
"""Paid tier prices must increase in order: starter < professional < power."""
|
||||||
assert TIERS["starter"]["price_monthly"] < TIERS["professional"]["price_monthly"]
|
assert TIERS["starter"]["price_monthly"] < TIERS["professional"]["price_monthly"]
|
||||||
assert TIERS["professional"]["price_monthly"] < TIERS["business"]["price_monthly"]
|
assert TIERS["professional"]["price_monthly"] < TIERS["business"]["price_monthly"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user