Spec: F53 — Pricing Page & Self-Service Plans
Updated 2026-06-25 per ADR-030 (ai-per-user-licensing-model). The pricing model has moved to per-AI-licensed-user billing in USD. Workspace membership is free and unlimited. AI capabilities are gated per user by an AI licence. 4 tiers: Lumi Workplace Free · AI Starter ($2/user/mo) · AI Pro ($5/user/mo, Most Popular) · AI Ultimate ($10/user/mo). Sections below that predate this change are marked Legacy (pre-ADR-030). The as-built page is documented in §6.5 and §13 (2026-06-25 rebuild entry).
Status: partial — the current public catalog and localized page are source-backed; self-service billing, pilot lifecycle, browser execution, and strategy realignment remain open
1. Problem & CEO Job
Job: Decide fast, but a CEO cannot decide whether to use Lumi without a pricing page.
runlumi.app/store/marketing currently offers only “14-day free pilot. Full features. No credit
card.” That is a landing-page bullet, not pricing.
The footer’s Pricing link must show a clear per-tier comparison, annual/monthly toggle, and self-service plan-selection-to-checkout-to-active-subscription path. Without it, sales starts from zero, prospects repeat the same questions, and self-service adoption cannot start.
This spec builds that public page on F24’s existing multi-gateway billing flow.
Pricing doctrine (strategy v2, 2026-06-14) — binding, overrides any older framing in this spec. Two hard rules and one anchor:
- No AI credits, no token language — ever (hard rule). AI is table-stakes and must disappear into the work (strategy v2 Inversion 6, §11.1: “no AI credits; no token terms”). Baseline AI — including chat with Lumi — is included in every paid plan; margin is protected by routing models intelligently behind the scenes, not by exposing message caps or credits the owner has to ration. Any per-message or per-token cap in this spec (e.g. the §2 “Chat with Lumi | 50 msgs/mo” line) contradicts this doctrine and is flagged in §11 as a gap to remove — not a shipped decision.
- Simple flat plans, not an enterprise-style grid. The owner we price for runs on Zalo and Excel and is trust-sensitive and low-ACV; pricing must be simple, predictable, cheap enough to try, high enough to signal seriousness (§11.1), with no complex enterprise-style plan grid in the first market. The current multi-tier Business grid with a long per-feature matrix is heavier than the doctrine allows — tier simplification is flagged in §11.
- Anchor against the free incumbent + Zoho-style bundle, not Jira/Asana. Prices anchor against Zalo/Excel (free baseline), the cost of a lost customer / missed follow-up, owner time, and a Zoho-like bundle — never US point-tool SaaS (Inversion 6, §11.2). VND reference anchors from field-tests §16: Starter ~299–499k, Team ~799k–1.5M, Pro ~1.5–3M / month. (These are reference targets for the §11 simplification work, not a claim that the page already uses them.)
2. User Stories & Acceptance Criteria
Public Pricing Page — Legacy pre-ADR-030 checklist
The checklist in this subsection is retained for historical traceability only. It is superseded by the current ADR-030 four-plan contract in the next subsection and is not evidence that the old five-Intel-plan, VND-card, or URL-persistence behavior ships today.
-
As a visiting CEO, I want to see a pricing page at
/store/pricing, so that I can compare plans without signing up.-
/store/pricingis a public page (no auth required), linked from footer, landing CTA, and/store/marketing. - Plan comparison table: 5 plans — Free, Intel Starter, Intel Growth, Intel Business, Intel Scale.
- Each plan card shows: monthly price (VND), annual price (with “Save 20%” badge), key feature limits (users unlimited on all plans, storage), and included features (daily brief ✓, audit log ✓, export ✗, etc.).
- Monthly/annual toggle switches prices globally. URL remembers state:
/store/pricing?period=annual. - Free plan is always displayed. Intel Starter is highlighted as “Most popular” with a subtle accent.
- All plans show “Unlimited users” —
included_users: -1renders as “Unlimited users”, never as “-1”.
-
-
As a visiting CEO, I want to start a pilot directly from the pricing page, so that I can go from research to trial in one click.
- Each paid plan has a “Start 14-day pilot” CTA → navigates to
/register?plan=starter(pre-selects plan for post-signup checkout). - Free plan has “Get started free” CTA → navigates to
/register?plan=free. - “Compare all features” expandable table below the cards shows every feature row-by-row across all 5 plans.
- Each paid plan has a “Start 14-day pilot” CTA → navigates to
Plan Feature Matrix (Current — ADR-030, 2026-06-25)
Per-AI-licensed-user USD pricing. Workspace membership is always free and unlimited.
| Feature | Lumi Workplace Free | AI Starter | AI Pro | AI Ultimate |
|---|---|---|---|---|
| Monthly price | $0 | $2/user/mo | $5/user/mo | $10/user/mo |
| Annual price | $0 | $1/user/mo ($12/user/yr) — Save 50% | $3/user/mo ($36/user/yr) — Save 40% | $7/user/mo ($84/user/yr) — Save 30% |
| Workspace users | Unlimited | Unlimited | Unlimited | Unlimited |
| AI licence required | No | Yes | Yes | Yes |
| Daily Pulse | ✓ | ✓ | ✓ | ✓ |
| CEO Brief | ✗ | ✓ | ✓ | ✓ |
| AI extraction | ✗ | ✓ | ✓ | ✓ |
| Projects/Tasks | ✓ | ✓ | ✓ | ✓ |
| CRM | ✓ | ✓ | ✓ | ✓ |
| Internal Chat | ✓ | ✓ | ✓ | ✓ |
| Approvals | ✓ | ✓ | ✓ | ✓ |
| AI capabilities | None | Starter | Pro | Full |
| Audit log | ✗ | ✗ | ✓ | ✓ |
| Data export | ✗ | ✗ | ✗ | ✓ |
| API access | ✗ | ✗ | ✗ | ✓ |
| Priority support | ✗ | ✗ | ✗ | ✓ |
Plan Feature Matrix — Legacy (pre-ADR-030)
The table below documents the original VND flat-plan structure. Superseded by the per-AI-user USD model above. Retained for historical reference.
| Feature | Free | Starter | Team | Business |
|---|---|---|---|---|
| Monthly price (VND) | 0 | 290,000 | 1,490,000 | 4,900,000 |
| Annual price (VND) | 0 | 230,000/mo | 1,190,000/mo | 3,920,000/mo |
| Included users | 3 | 10 | 25 | Unlimited |
| Daily Pulse | ✓ | ✓ | ✓ | ✓ |
| CEO Brief | ✗ | ✓ | ✓ | ✓ |
| AI extraction | ✗ | ✓ | ✓ | ✓ |
| Projects/Tasks | 1 project | 10 projects | 50 projects | Unlimited |
| Teams | ✗ | 3 teams | 10 teams | Unlimited |
| Commitments | ✓ | ✓ | ✓ | ✓ |
| Risks | ✓ | ✓ | ✓ | ✓ |
| Decisions | ✗ | ✓ | ✓ | ✓ |
| Chat with Lumi | ✗ | 50 msgs/mo | Unlimited | Unlimited |
| Internal Chat (F29) | ✗ | ✗ | ✓ | ✓ |
| CRM (F27) | ✗ | ✗ | 100 companies | Unlimited |
| Approvals (F26) | ✗ | ✗ | ✓ | ✓ |
| Signal Inbox (F32) | ✗ | ✗ | ✓ | ✓ |
| Connected Sources (F33) | ✗ | ✗ | 1 source | 5 sources |
| File uploads (F34) | ✗ | 100MB | 1GB | 10GB |
| Audit log | ✗ | ✗ | ✓ | ✓ |
| Data export | ✗ | ✗ | ✗ | ✓ |
| API access | ✗ | ✗ | ✗ | ✓ |
| Priority support | ✗ | ✗ | ✗ | ✓ |
Self-Service Checkout
-
As a registered CEO on the Free plan, I want to upgrade to a paid plan from Settings, so that I don’t need to contact anyone.
-
/settings/billingshows current plan, upgrade options (same card layout as pricing page), and payment history. - “Upgrade to Starter” → selects gateway (VietQR shown first, Stripe/PayPal/Airwallex as alternatives) → creates
PaymentIntent→ redirects to payment or shows QR. - Payment success → subscription activates immediately. Dashboard shows new plan with remaining period.
- Upgrade from annual → annual prorated calculation (remaining value from current period applied as discount to new period).
-
-
As a registered CEO on a paid plan, I want to downgrade or cancel, so that I’m not locked in.
- Cancel: subscription stays active until period end → then transitions to read-only. “Your data will not be deleted.”
- Downgrade: takes effect at end of current period. Notifications warn about feature loss (e.g., “You’ll lose access to CRM at the end of your billing period”).
- Reactivate from read-only: re-subscribe to any plan. Data is preserved during read-only.
Pilot Management
- As a piloted CEO, I want to see my pilot status prominently, so that I know when it ends and how to subscribe.
- Pilot banner on Today page: “Your 14-day pilot ends in 5 days. Upgrade to keep Lumi.” with CTA to
/settings/billing. - Pilot banner at 48 hours remaining: yellow warning “2 days remaining.”
- Pilot expired → grace banner “Your pilot has ended. Subscribe to restore access.” Organization enters read-only after grace period.
- Pilot organizations that subscribe keep all their data. No migration, no export/import.
- Pilot banner on Today page: “Your 14-day pilot ends in 5 days. Upgrade to keep Lumi.” with CTA to
Enterprise / Custom Plans
- As a enterprise buyer, I want to see that custom plans are available, so that I contact sales instead of bouncing.
- “Need more than Business?” CTA at the bottom of the pricing page: “Contact us for custom pricing, SSO, dedicated support, and data residency options.” Links to
support@runlumi.app.
- “Need more than Business?” CTA at the bottom of the pricing page: “Contact us for custom pricing, SSO, dedicated support, and data residency options.” Links to
3. Scope Boundaries
- In:
- Public
/store/pricingpage with plan comparison + monthly/annual toggle. - Plan seeding in database (Free, Starter, Team, Business) — via migration or seed command.
- Self-service upgrade/downgrade/cancel flow in
/settings/billing. - Pilot banner with countdown on Today page.
- Plan pre-selection from URL param
?plan=.
- Public
- Out:
- Custom coupon/discount codes — manual override via admin panel only.
- Seats-based metering (pay-per-user beyond included users) — v1 is flat-rate per plan.
- Usage-based pricing (pay-per-signal, pay-per-API-call).
- Invoicing system — manual on request per F24 out-of-scope.
- Multi-currency pricing — all plans priced in VND. Gateway handles conversion.
- Affiliate/referral tracking — pricing.md §10–14 exists but out of scope.
- Trial extension (manual from admin panel only).
- Plan comparison tool (interactive “which plan is right for you?” quiz).
4. Data Model
4.1 Plan Seeding
No model changes — the Plan model already supports everything needed. The seed creates 4 plans:
Plan(code="free", name="Free", monthly_price=0, included_users=3, max_projects=1, max_teams=0, has_daily_brief=False, has_audit_log=False, has_export=False, sort_order=10)
Plan(code="starter", name="Starter", monthly_price=290000, included_users=10, max_projects=10, max_teams=3, data_history_days=90, has_daily_brief=True, has_audit_log=False, has_export=False, sort_order=20)
Plan(code="team", name="Team", monthly_price=1490000, included_users=25, max_projects=50, max_teams=10, data_history_days=365, has_daily_brief=True, has_audit_log=True, has_export=False, sort_order=30)
Plan(code="business", name="Business", monthly_price=4900000, included_users=None, max_projects=None, max_teams=None, data_history_days=0, has_daily_brief=True, has_audit_log=True, has_export=True, sort_order=40)
included_users=None = Unlimited. data_history_days=0 = Unlimited.
v3 direction (2026-06-23 — not yet built; ADR-024 + pricing.md v3). The “no model changes” claim above holds for the shipped page only. The agreed global-pricing direction moves price out of
Planinto aPlanPricechild keyed by(plan, band, currency)— two PPP bands:emerging(global-south wedge: VN/ID/PH, local currency, ~0.4× PPP) andinternational(USD default). Billing unit is the active user (an org’s band-priced plan includes a headroom of active users + cheap per-extra-active-user), not the provisioned seat.Plan.monthly_priceis retained read-only/deprecated and backfilled intoPlanPrice. Active-user measurement (ActiveUserMonth) + committed-prepay collection live in F24 / ADR-024. This supersedes gap row 16 below (it specifies the “re-anchor pricing” fix).
4.2 Plan Feature Map
Features not expressed as model flags are documented in a PLAN_FEATURES dict used by the /api/plans/ endpoint:
PLAN_FEATURES = {
"free": ["daily_pulse", "tasks", "commitments", "risks", "telegram"],
"starter": ["daily_pulse", "tasks", "commitments", "risks", "ceo_brief", "ai_extraction", "decisions", "chat", "telegram", "file_uploads"],
"team": ["daily_pulse", "tasks", "commitments", "risks", "ceo_brief", "ai_extraction", "decisions", "chat", "internal_chat", "crm", "approvals", "signal_inbox", "connected_sources", "file_uploads", "telegram", "audit_log"],
"business": ["daily_pulse", "tasks", "commitments", "risks", "ceo_brief", "ai_extraction", "decisions", "chat", "internal_chat", "crm", "approvals", "signal_inbox", "connected_sources", "file_uploads", "telegram", "audit_log", "export", "api_access", "priority_support"],
}
4.3 Organization Model Changes
# Organization — new field (already exists conceptually but needs explicit tracking)
pilot_expires_at = models.DateTimeField(null=True, blank=True)
Existing: Subscription.status = "trialing" handles pilot. No model change needed — Subscription.expires_at already stores pilot end date. The PLAN_FEATURES dict is the only addition.
5. API Contract
5.1 New Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/plans/ | None | Returns all active plans with features list. Public endpoint — no auth. |
| GET | /api/plans/features/ | None | Returns feature-by-feature comparison matrix. |
5.2 Changed Endpoints
| Method | Path | Change |
|---|---|---|
| GET | /api/billing/subscription/ | Now includes plan_features: ["daily_pulse", "tasks", ...] and feature_diff_on_upgrade: {...} |
| POST | /api/billing/pay/ | Accepts period: "annual" param. Respects plan param for upgrades. |
5.3 Plan Response (Current — ADR-030, 2026-06-25)
GET /api/plans/
{
"plans": [
{
"code": "ai_pro",
"name": "AI Pro",
"monthly_amount": 5,
"currency": "USD",
"annual_amount": 36,
"annual_monthly_equivalent": 3,
"discount_percent": 40,
"ai_tier": "pro",
"included_users": -1,
"features": ["daily_pulse", "tasks", "commitments", "risks", "ceo_brief", "ai_extraction", "decisions", "chat", "internal_chat", "crm", "approvals", "audit_log"],
"highlight": true,
"cta": "Get AI licence",
"cta_href": "/register?plan=ai_pro"
},
...
]
}
Fields monthly_amount, currency, and ai_tier replace the legacy monthly_price (VND integer),
annual_price, and max_users fields. included_users: -1 = unlimited. ai_tier values:
null (Workplace Free), "starter", "pro", "ultimate".
5.3 Plan Response — Legacy (pre-ADR-030)
The response shape below used VND amounts and seat limits. Superseded. Retained for reference.
GET /api/plans/
{
"plans": [
{
"code": "starter",
"name": "Starter",
"monthly_price": 290000,
"annual_price": 2784000,
"monthly_price_formatted": "290.000 ₫",
"annual_price_formatted": "2.784.000 ₫",
"annual_monthly_equivalent": "232.000 ₫ / tháng",
"discount_percent": 20,
"included_users": 10,
"max_projects": 10,
"max_teams": 3,
"data_history_days": 90,
"features": ["daily_pulse", "tasks", "commitments", "risks", "ceo_brief", "ai_extraction", "decisions", "chat", "telegram", "file_uploads"],
"highlight": true,
"cta": "Start 14-day pilot",
"cta_href": "/register?plan=starter"
},
...
]
}
v3 direction (band-aware response — not yet built; ADR-024).
GET /api/plans/resolves the viewer’s band (from billing market, defaultinternational) and returns that band’sPlanPrice— USD forinternational, local currency (VND) foremerging— withincluded_active_usersandextra_active_user_amountreplacing the seat fields. The pricing page renders the resolved band and a Zoho/Workspace comparison block (pricing.md v3 §17).
6. UI / UX
6.1 Pricing Page (/store/pricing)
Grid layout: grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 — one column per plan at desktop.
Subtitle: “Your whole company, free. Pay only for AI.” — no per-seat/per-user language.
All plans show “Unlimited users” (included_users: -1). Single VND price book (emerging focus).
┌─ Lumi AI Workspace — Pricing ─────────────────────────────────────────────────┐
│ │
│ Plans that scale with your company │
│ Your whole company, free. Pay only for AI. │
│ │
│ [Monthly] [Annual — Save 20%] │
│ │
│ ┌─ Free ──┐ ┌─ Intel Starter─┐ ┌─ Intel Growth─┐ ┌─Intel Business┐ ┌─Intel Scale─┐ │
│ │ │ │ ★ Most popular │ │ │ │ │ │ │ │
│ │ 0 ₫/mo │ │ 199K ₫/mo │ │ 499K ₫/mo │ │ 990K ₫/mo │ │ 1.99M ₫/mo │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │✓Unlimited│ │ ✓ Unlimited │ │ ✓ Unlimited │ │ ✓ Unlimited │ │ ✓ Unlimited │ │
│ │ users │ │ users │ │ users │ │ users │ │ users │ │
│ │ ✓ Tasks │ │ ✓ CEO Brief │ │ ✓ CRM │ │ ✓ Audit log │ │ ✓ API │ │
│ │ ✓ Pulse │ │ ✓ AI extract │ │ ✓ Approvals │ │ ✓ Export │ │ ✓ Priority │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │[Get free]│ │ [Start pilot] │ │ [Start pilot] │ │ [Start pilot] │ │[Start pilot]│ │
│ └─────────┘ └───────────────┘ └───────────────┘ └───────────────┘ └─────────────┘ │
│ │
│ [Compare all features ↓] │
│ │
│ Need more than Business? Contact us for custom plans. │
│ support@runlumi.app │
└────────────────────────────────────────────────────────────────────────────────┘
6.2 Settings → Billing (/settings/billing)
┌─ Settings / Billing ───────────────────────────────────────────────┐
│ │
│ Current plan: Team │
│ Status: Active · Next payment: Jul 15, 2026 · 1.49M ₫ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Your plan includes: 25 users · 50 projects · 10 teams │ │
│ │ ✓ AI extraction · ✓ CRM · ✓ Approvals · ✓ Internal Chat │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Payment history │
│ ┌──────┬──────┬────────┬──────────┬────────┐ │
│ │ Date │ Plan │ Period │ Amount │ Status │ │
│ ├──────┼──────┼────────┼──────────┼────────┤ │
│ │ 6/15 │ Team │ Monthly│ 1.49M ₫ │ ✓ Paid │ │
│ │ 6/01 │ Pil. │ 14-day │ 2.9M ₫ │ ✓ Paid │ │
│ └──────┴──────┴────────┴──────────┴────────┘ │
│ │
│ [Upgrade plan ▾] [Cancel subscription] │
└─────────────────────────────────────────────────────────────────────┘
6.3 Pilot Banner (Today Page)
┌─ Today ─────────────────────────────────────────────────────────────┐
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ⏳ Your 14-day pilot ends in 5 days. │ │
│ │ Upgrade to Starter to keep your briefs, tasks, and team data. │ │
│ │ [Upgrade now →] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 3 Decisions · 2 Overdue · 1 Risk │
│ ... │
└──────────────────────────────────────────────────────────────────────┘
6.4 i18n
New namespace: pricing. Keys:
pricing.title,pricing.subtitle,pricing.monthly,pricing.annual,pricing.annual_savepricing.free,pricing.starter,pricing.team,pricing.businesspricing.most_popular,pricing.per_month,pricing.per_month_annualpricing.users,pricing.projects,pricing.teams,pricing.data_historypricing.feature_*— one key per featurepricing.cta_free,pricing.cta_pilot,pricing.cta_enterprisepricing.compare_all,pricing.enterprise_title,pricing.enterprise_descpricing.pilot_banner,pricing.pilot_banner_warning,pricing.pilot_upgradepricing.upgrade,pricing.downgrade,pricing.cancel,pricing.reactivatepricing.payment_history,pricing.next_payment
Both en and vi updated. All prices formatted per locale (VND formatting in vi, ₫ in en).
6.5 Rebuilt Layout (2026-06-25 — as-built; supersedes §6.1 for the shipped page)
The pricing page was rebuilt as fully static (no API call). Route: /store/:lang/pricing (public,
no auth). Locale read from :lang param via useStoreLocale. Two product tiers:
Section 1 — Lumi Workplace (free forever)
- “Free forever” badge; price =
$0(en) or0 ₫(vi locale) - 9 bullet features: workspace users, tasks & projects, guests, comments, views, forms, mobile, reporting, storage
- “Get started free” CTA →
/register?plan=free
Section 2 — Lumi AI Workplace (3 paid plans)
- Monthly / Annual toggle (default: monthly); annual variant shows save-%
Badge - AI Starter: $2/mo · $1/mo annual ($12/yr) · Save 50%
- AI Pro (highlighted, “Most popular”
Badge): $5/mo · $3/mo annual ($36/yr) · Save 40% - AI Ultimate: $10/mo · $7/mo annual ($84/yr) · Save 30%
- Vietnamese (vi locale): USD × 26,000 VND_RATE (e.g. $2 → 52,000 ₫; $0 → 0 ₫)
- Billing rules block: 6 items (
billing_rule_1–billing_rule_6)
Section 3 — Compare capabilities (collapsible)
- Toggle button (
ChevronDown/ChevronUp); default collapsed - 17-row table across 4 columns: Workplace Free · AI Starter · AI Pro · AI Ultimate
- Cell values:
check|none|limited|basic|full|advanced|standard
Enterprise CTA
- “Need more than AI Ultimate?” heading +
support@runlumi.appmailto link
Components: Button, Badge, Table/TableBody/TableCell/TableHead/TableHeader/TableRow
from shadcn; AIPlanCard local component; Lumi design tokens throughout (no stock Tailwind palette).
7. Test Plan
Coverage note (2026-06-14): Pricing/plan tests are split across
backend/tests/test_billing_plans.py(TestPlansListPublic,TestPlanFeaturesMatrix,TestAnnualPriceComputed,TestSeedPlansIdempotent),test_pricing.py(TestPlansAPI), andtest_billing.py(TestSubscriptionLifecyclefor upgrade/downgrade/cancel/reactivate). Rows below use original per-case naming; those files are authoritative.
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Public /api/plans/ returns 4 plans with correct features | backend | tests/test_billing.py |
| 2 | /api/plans/ returns correct annual prices with 20% discount applied | backend | tests/test_billing.py |
| 3 | /api/plans/features/ returns feature comparison matrix | backend | tests/test_billing.py |
| 4 | Pricing page renders all 4 plan cards with correct prices | e2e | describe("Pricing") |
| 5 | Monthly/annual toggle switches prices globally | e2e | same block |
| 6 | Annual toggle shows “Save 20%” badge on paid plans | e2e | same block |
| 7 | Free plan has “Get started free” CTA → navigates to register | e2e | same block |
| 8 | Paid plan CTA navigates to register with ?plan= query param | e2e | same block |
| 9 | ?plan= param pre-selects plan after registration (survives auth handoff) | e2e | same block |
| 10 | Registered user on Free sees upgrade options in Settings | e2e | describe("Billing") |
| 11 | Pilot banner shows correct days remaining | e2e | describe("Today") |
| 12 | Pilot banner at 48h shows warning (yellow) styling | unit | BillingCard.test.tsx |
| 13 | Pilot expired → grace banner, org enters read-only after grace | e2e | describe("Billing") |
| 14 | Cancel subscription preserves data through period end | backend | tests/test_billing.py |
| 15 | Downgrade takes effect at period end, warns about feature loss | backend | tests/test_billing.py |
| 16 | Upgrade from annual → pro-rata calculation correct | backend | tests/test_billing.py |
| 17 | Reactivate from read-only → data preserved, subscription resumes | backend | tests/test_billing.py |
| 18 | ”Need more than Business?” CTA renders with contact link | e2e | describe("Pricing") |
| 19 | URL remembers period state: /store/pricing?period=annual | e2e | same block |
| 20 | Compare all features expandable table renders correctly | e2e | same block |
| 21 | ?plan=invalid_value defaults to no pre-selection (graceful fallback) | e2e | same block |
2026-06-25 rebuild — new unit test coverage (static, no API mock):
| # | Case | Type | Where |
|---|---|---|---|
| 22 | Lumi Workplace free section renders with $0 price | unit | PricingPage.test.tsx · describe(‘PricingPage’) |
| 23 | All 3 AI plan cards render (ai_starter, ai_pro, ai_ultimate) | unit | same |
| 24 | AI Pro highlighted with “Most popular” Badge | unit | same |
| 25 | Monthly/Annual toggle buttons render | unit | same |
| 26 | Monthly USD prices shown by default (Starter = $2) | unit | same |
| 27 | Annual toggle switches prices and shows save-% badge (Starter → $1, Save 50%) | unit | same |
| 28 | Billing rules section renders | unit | same |
| 29 | Compare capabilities toggle renders | unit | same |
| 30 | Compare table expands on toggle click; shows 4 column headers | unit | same |
| 31 | Enterprise section renders at bottom | unit | same |
| 32 | No banned design tokens (indigo-600, bg-orange-100) in rendered HTML | unit | same |
| 33 | VND prices for vi locale ($2 → 52,000 ₫) | unit | describe(‘PricingPage — Vietnamese locale’) |
| 34 | Free Workplace plan shows 0 ₫ for vi locale | unit | same |
8. Current Behavior
/store/:lang/pricingis a public static page with Workplace Free and AI Starter, AI Pro, and AI Ultimate cards.- The page owns a local monthly/annual toggle, renders the current capability comparison, formats VND for
vi, and provides registration/enterprise CTAs; it does not claim checkout or payment completion. /api/plans/and/api/plans/features/remain the public catalog API boundary, with four current plan IDs and idempotentseed_plansownership.- The old five-Intel-plan, VND-card, and persistent
?period=annualbehavior is superseded by ADR-030 and is not part of the current acceptance boundary.
9. Error States
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 1 | /api/plans/ fails to load | Pricing page shows skeleton cards + “Could not load plans. [Retry]“ | API error logged | ERROR “plans_load_failed” | User clicks Retry |
| 2 | Checkout fails to create PaymentIntent | Toast: “Could not start payment. [Try again]“ | Payment service error | ERROR “payment_intent_creation_failed” | User retries |
| 3 | Payment succeeds but subscription activation fails | Toast: “Payment received but subscription could not be activated. Contact support.” | Subscription creation error | CRITICAL “payment_orphaned” | Manual intervention — admin panel |
| 4 | Upgrade blocked by plan limit (users/projects) | Form validation: “Your current plan limits prevent this upgrade.” | Limit check fails | WARNING “upgrade_limit_blocked” | User adjusts usage |
| 5 | Downgrade would exceed new plan’s limits | Modal: “You have 15 users but Starter allows 10. Remove 5 users before downgrading.” | Limit check fails | WARNING “downgrade_limit_blocked” | User adjusts usage |
| 6 | Pilot already exists for this org | 400: “You already have an active pilot.” | Duplicate pilot check | INFO “pilot_already_exists” | n/a |
| 7 | All payment gateways unavailable | 503: “Payment system is temporarily unavailable.” | All gateways return error | CRITICAL “all_gateways_down” | User retries later |
| 8 | Cancel fails due to active PaymentIntent | Warning: “You have a pending payment. Cancel it first.” | Active PI exists | WARNING “cancel_with_pending_payment” | User resolves pending payment |
| 9 | Invalid plan code in upgrade request | 400: “Plan ‘invalid_code’ not found.” | Unknown plan code | WARNING “upgrade_unknown_plan” | User selects valid plan |
| 10 | Reactivation called on non-read-only subscription | 400: “Subscription is not in read-only state.” | State check fails | WARNING “reactivate_invalid_state” | n/a |
10. Diagrams & Docs Touched
- Public-site route diagrams
- F24 billing docs and payment-flow sequence diagrams
- Marketing docs that reference CTA paths
11. Implementation Audit
Edge Cases Catalog (formerly §9, 2026-06-14; 10 items): plan/period URL param tampering, overlapping pilots, annual discount rounding consistency (backend is source of truth), feature list changes mid-subscription (snapshot at creation), free plan limit enforcement at API level, upgrade/downgrade same billing period (non-refundable proration), multi-org single user (subscription per-org), payment gateway offline during upgrade, VAT display change (backend computes at current rate), currency switch for expansion markets. Full catalog in git history.
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Plan seeding (Free/Starter/Team/Business) via seed_plans.py | ✅ match | T3-A R11 BL (2026-06-13); backend/apps/billing/management/commands/seed_plans.py |
| 2 | GET /api/plans/ (public, AllowAny) with formatted prices, features, CTA | ✅ match | T3-A R11 BL; backend/apps/billing/views.py::plans_list + urls_plans.py |
| 3 | GET /api/plans/features/ (public feature comparison matrix) | ✅ match | T3-A R11 BL; backend/apps/billing/views.py::plan_features |
| 4 | PLAN_FEATURES dict (billing/features.py — PLAN_CATALOG + PLAN_FEATURES_MATRIX) | ✅ match | T3-A R11 BL; backend/apps/billing/features.py |
| 5 | Backend tests: test_billing_plans.py (16 tests) + test_pricing.py; coverage 99.06% | ✅ match | T3-A R11 BL; 2158 tests pass |
| 6 | PricingPage.tsx (public: plan cards, monthly/annual toggle, compare-all-features table) | ✅ match | T3-E R11 BL; frontend/src/pages/store/PricingPage.tsx (moved from pages/PricingPage.tsx to pages/store/ in the 2026-06-25 rebuild — see row 18) |
| 7 | usePlans TanStack Query hook (/api/plans/, /api/plans/features/, public) | ✅ match | T3-E R11 BL; frontend/src/hooks/queries/usePlans.ts |
| 8 | pricing i18n namespace (en + vi with all §6.4 keys) | ✅ match | T3-E R11 BL; frontend/src/i18n/locales/en/pricing.json + vi/pricing.json |
| 9 | Route /store/pricing in App.tsx (public, outside <Layout />) | ✅ match | T3-E R11 BL; App.tsx |
| 10 | E2E f53-pricing.spec.ts (4 tests: public access, auth, toggle URL state, plan cards) | ✅ match | T3-E R11 BL; frontend/tests/e2e/f53-pricing.spec.ts |
| 18 | 2026-06-25 rebuild — static PricingPage.tsx at /store/:lang/pricing: Workplace Free + AI Starter/Pro/Ultimate; monthly/annual toggle; VND at VND_RATE=26,000 for vi locale; 17-row collapsible capability table; 6-item billing rules block; enterprise CTA | ✅ match | 2026-06-25 PR; frontend/src/pages/store/PricingPage.tsx |
| 19 | 2026-06-25 rebuild — PricingPage.test.tsx rewritten: 12 unit tests (2 describe blocks); no QueryClientProvider/MSW; static data; Vietnamese VND locale tests | ✅ match | 2026-06-25 PR; frontend/src/test/PricingPage.test.tsx |
| 11 | Pilot countdown banner on Today page (reads Subscription.expires_at) | ⚠️ gap | Listed as “Still missing (P1)” in §12 audit |
| 12 | Enhanced subscription endpoint (plan_features, feature_diff_on_upgrade) | ⚠️ gap | Listed as “Still missing (P1)” in §12 audit |
| 13 | Annual billing in POST /api/billing/pay/ (period: "annual" param) | ⚠️ gap | Listed as “Still missing (P1)” in §12 audit |
| 14 | ?plan= pre-selection on /register + BillingCard feature matrix enhancement | ⚠️ gap | Listed as “Still missing (P1)” in §12 audit |
| 15 | [Strategy v2 realignment — TO FIX, not shipped] Remove AI metering / token language (hard doctrine). The §2 feature matrix meters AI (“Chat with Lumi” = ✗ / 50 msgs/mo / Unlimited / Unlimited) and PLAN_FEATURES/the page expose chat as a capped feature. Doctrine (Inversion 6, §11.1: “no AI credits; no token terms”) requires baseline AI — including chat with Lumi — included in every paid plan, with no per-message cap and no token/credit language anywhere in pricing copy, the matrix, the /api/plans/ response, or PLAN_FEATURES. To fix: drop the “50 msgs/mo” cap and any metered-AI wording; mark chat simply “included” on all paid plans. | 🔲 gap — TO FIX | Strategy v2 Inversion 6 + §11.1; registry Positioning doctrine (strategy v2, 2026-06-14) rule 3 (no AI credits / no token language — hard rule). Not done — current as-built page/matrix meter chat (rows 4, 6). |
| 17 | [v3 global-pricing direction — agreed, not built] Per-active-user billing on two PPP bands. Move price out of Plan into PlanPrice(plan, band, currency); no-brainer-vs-Zoho reset (2026-06-23): international (USD) Free $0 / Starter $19·5 / Team $49·15 / Business $99·40 / Scale $199·100, extra $5/user; emerging (VN/ID/PH local currency, ~0.4× PPP) Starter 199k VND·5 / Team 499k·15 / Business 990k·40 / Scale 1.99M·100, extra 49k/user. Bill on active users (ActiveUserMonth meter, committed-prepay + soft overage, measurement via Cloudflare Cron — no Celery). Band derived-from-market + locked + pinned to payment instrument (anti-arbitrage). Payment methods: PayPal + Airwallex (card) + Sepay QR (VN only); Stripe removed. Band-aware /api/plans/ + Zoho comparison block on the page. This is the concrete shape of the row-16 “re-anchor pricing” fix and supersedes it. | 🔲 agreed — not built | ADR-024; pricing.md v3 §15–§19; engine work lives in F24. |
| 16 | [Strategy v2 realignment — TO FIX, not shipped] Simplify tiers; re-anchor pricing. The current 4-tier grid with a long per-feature matrix and a heavy “Business” tier is closer to an enterprise-style grid than doctrine allows (§11.1: “no complex enterprise-style plan grid in the first market”). To fix: simplify to a small set of flat plans (Free/Trial · Starter · Team · Pro per strategy v2 §11.2), priced against the free incumbent (Zalo/Excel) + a Zoho-style bundle and the cost of a lost customer — not Jira/Asana/US point tools. VND reference anchors (field-tests §16): Starter ~299–499k, Team ~799k–1.5M, Pro ~1.5–3M / month. | 🔲 gap — TO FIX | Strategy v2 Inversion 6 + §11.1–11.2; field-tests.md §16 Pricing signal test; registry doctrine rule 1 (competitor = Zalo/Excel, not US SaaS). Not done — current as-built grid is Free/Starter/Team/Business with the §2 matrix (rows 1, 6). §4 plan-seeding fields, §5 API contract, §7 tests unchanged by this note. |
12. Open Questions / Decisions
Contract Graph reconciliation (2026-07-16)
The current Contract Graph owns the partial public boundary: the public plan catalog and feature
matrix, idempotent seed_plans records, and the localized static pricing page. Executable source
and focused tests confirm that boundary, including a negative proof for superseded Intel plan names.
The older checklist is explicitly labeled historical and superseded by ADR-030; it is no longer a
contradiction against the current spec status.
Self-service billing, pilot lifecycle, strategy realignment, and browser execution remain explicit gaps. No browser, payment-provider, production, Redis, Sentry, or native evidence is inferred from source or proof collection. See F53 contract, implementation audit, and test plan.
- Free plan limits: 1 project, 3 users, no brief, no AI. Is the Free plan generous enough to be useful but constrained enough to drive upgrades? Decision: yes — the Free plan shows the concept (pulses, tasks, risks) but withholds the value (CEO brief, AI extraction). The activation moment (“CEO sees a brief”) requires an upgrade → this is the conversion trigger.
- Pilot pricing: F24 defines pilot periods at 2.9M VND (14-day) and 9M VND (30-day). Should these be shown on the pricing page or kept as “14-day free trial”? Decision: show as “14-day free pilot” — consistent with current landing page language. The 30-day pilot is not advertised publicly (internal/design partner offer only).
- Annual pricing display: Show “232K ₫/tháng (billed annually 2,784K ₫)” or just “232K ₫/tháng”? Decision: show both. Vietnamese consumers are used to seeing the per-month equivalent with the annual total in small print. This matches how VietJet, Tiki, and FPT Shop display annual pricing.
- VAT display: Vietnamese businesses expect prices with VAT. Should plan prices include VAT? Decision: yes, all listed prices include 10% VAT. This is documented in a small “Prices include VAT” footnote.
- Feature flag storage: Hardcoded
PLAN_FEATURESdict vs model field? Decision: hardcoded dict for v1 (no migration, no admin UI overhead). Must be versioned witheffective_dateto handle mid-subscription feature changes.
13. Bug / Change Log
2026-07-02 — Docs: correct stale file paths (batch 2)
Type: Documentation Fix
Summary:
- §11 row 6:
frontend/src/pages/PricingPage.tsxcorrected tofrontend/src/pages/store/PricingPage.tsx— the page was moved topages/store/in the 2026-06-25 rebuild (row 18 already recorded the correct path). - PR-3 plan:
frontend/src/pages/PricingPage.tsxcorrected tofrontend/src/pages/store/PricingPage.tsxfor the same reason. - PR-1 plan:
backend/apps/billing/views_plans.py— this file was never created. Plan views were implemented directly inbackend/apps/billing/views.py. Annotated as not created. - PR-4 plan:
frontend/src/test/BillingCard.test.tsx— file does not exist; BillingCard enhancement is an open ⚠️ gap (§11 row 14). Annotated as not created. - No code changes.
2026-07-02 — Spec drift corrected: 10 §2 ACs checked per §11 audit
Type: Documentation Change
Summary:
- 10
[ ]acceptance criteria changed to[x]: the 6 Public Pricing Page ACs, the 3 CTA/compare-table ACs, and the Enterprise CTA AC. All confirmed by §11 ✅ rows 6, 9, 18, 19 (public route, plan cards, monthly/annual toggle, unlimited users, save-% badges, CTAs, 17-row collapsible capability table, enterprise CTA, unit tests 22–34). - 11 ACs remain
[ ]: self-service checkout (/settings/billing, gateway selection, payment success, proration — §11 rows 12/13 ⚠️ missing), pilot management (countdown banner, 48h warning, expired/grace/data-preservation — §11 row 11 ⚠️ missing), and the v2/strategy gap rows (15–17: AI metering removal, tier simplification, per-active-user PPP bands — 🔲 not built). These are genuinely unimplemented and correctly stay unchecked.
2026-06-25 — Pricing page rebuilt: static Workplace + AI Workplace tiers
Type: Feature (replaces the API-driven page)
Summary:
- Removed
usePlansAPI dependency; page is now fully static (no network call at render time). - Two-tier layout: Lumi Workplace (free forever) + Lumi AI Workplace (AI Starter $2/mo · AI Pro $5/mo · AI Ultimate $10/mo).
- Monthly/annual toggle (default monthly); annual shows Save 50%/40%/30% badges.
- VND displayed for
vilocale at VND_RATE = 26,000 (e.g. $2 → 52,000 ₫; free → 0 ₫). - 17-row collapsible capability comparison table; 6-item billing rules block.
- Enterprise section: “Need more than AI Ultimate?” +
support@runlumi.appmailto CTA. AIPlanCardlocal component replaces oldPlanCard; all shadcn/ui + Lumi design tokens.frontend/src/test/PricingPage.test.tsxrewritten: 12 tests across 2 describe blocks; noQueryClientProvideror MSW — static data only.docs/pricing-payment/pricing-final.md(new): Workplace + AI Workplace tier definitions.
Reason:
- Pricing strategy consolidated to static Workplace/AI Workplace tiers; live
/api/plans/not needed for the public page; removes API failure mode on the highest-traffic public surface.
Impact:
/store/:lang/pricingpublic page. No backend API changes.usePlanshook and/api/plans/endpoint retained for Settings/billing flows.
Tests:
- 12 unit tests:
PricingPage(10) +PricingPage — Vietnamese locale(2). See §7 rows 22–34.
2026-06-25 — VND single-book pricing + Intel plan names + 5-card grid (ADR-029)
Type: Feature Update
Summary:
PricingPage.tsxtop comment updated: “Shows 5 plan cards” (was 4).- Plan card grid changed to
grid-cols-1 sm:grid-cols-2 lg:grid-cols-5— one column per plan at desktop. - Skeleton count updated from 4 to 5.
frontend/src/i18n/locales/en/pricing.json—subtitleupdated to “Your whole company, free. Pay only for AI.”frontend/src/i18n/locales/vi/pricing.json—subtitleupdated to “Toàn bộ công ty của bạn, miễn phí. Chỉ trả tiền cho AI.”- §2 acceptance criteria updated: 5 plans with Intel names, unlimited users on all plans.
- §6.1 UI/UX wireframe and layout note updated to reflect 5-column grid and new subtitle.
Reason:
- ADR-029 (single global VND price book, “emerging focus”): Free / Intel Starter (199k) / Intel Growth (499k) / Intel Business (990k) / Intel Scale (1,990k). All plans include unlimited users — pay for AI intelligence, not headcount.
2026-06-23 — v3 global pricing direction: per-active-user + PPP bands (global-south wedge)
Type: Documentation Change (direction; not yet implemented)
Summary:
- Recorded the agreed v3 pricing direction for the global south, ratified in
ADR-024 and detailed in
pricing.md v3 §15–§18: bill on active users (not seats), price via
a
PlanPricechild keyed by(plan, band, currency), with two PPP bands —emerging(VN/ID/PH, local currency, ~0.35× PPP) andinternational(USD default). Added the band tables and a Zoho comparison to the pricing strategy doc. - §4.1 + §5.3 annotated with the v3 direction (band-aware
/api/plans/,PlanPrice/ActiveUserMonth). - §11 added gap row 17 (per-active-user PPP bands — agreed, not built) which supersedes row 16’s “re-anchor pricing” by specifying the concrete shape.
Reason:
- A flat USD book (pricing.md v2) prices Lumi for developed markets; the wedge is global-south SMEs who compare Lumi to Zoho — which won the global south on PPP pricing. To be the no-brainer wedge there, Lumi must localize price for the wedge markets (two bands), keeping the per-active-user unit and 7-day trial unchanged.
Impact:
- Pricing strategy + future F24/F53 implementation. No shipped behavior changed (this is direction). Engine work (model, metering, collection) is F24; the page becomes band-aware in F53 when built.
Tests:
- Docs-only — no code tests affected.
Notes:
- Numbers are launch anchors, tunable from WTP evidence without a new ADR; the structure (unit, bands, derived-and-locked band, committed-prepay) is fixed by ADR-024.
2026-06-14 — Strategy v2 realignment (positioning)
Type: Documentation Change
Summary:
- §1 added a binding Pricing doctrine block (strategy v2): no AI credits / no token language (hard rule); simple flat plans, no enterprise-style grid; anchor against the free incumbent (Zalo/Excel) + Zoho-style bundle, not Jira/Asana. VND reference anchors from field-tests §16 (Starter ~299–499k, Team ~799k–1.5M, Pro ~1.5–3M/mo) cited as targets for the simplification work, not as a claim the page already uses them.
- §11 added gap rows 15 (remove the “Chat with Lumi | 50 msgs/mo” AI cap + all token/credit language; chat included on all paid plans) and 16 (simplify the 4-tier Business grid; re-anchor pricing) — both marked TO FIX — not shipped.
Reason:
- Align with competitive-strategy-v2.md Inversion 6 + §11.1–11.2, field-tests.md §16, and the registry Positioning doctrine (strategy v2, 2026-06-14) rule 3 (no AI credits / no token language — hard rule) and rule 1 (competitor = Zalo/Excel, not US SaaS).
Impact:
- Positioning prose only in §1. The AI-message-cap removal and tier simplification are recorded as §11 audit gaps to fix — not claimed as already changed. No §4 data model (plan seeding fields), §5 API contract, §7 test plan, §11 prior audit-verdict rows, or Status change.
Tests:
- None (spec-only).
Notes:
- The §2 feature matrix (including the “50 msgs/mo” line) and the §6 4-tier UI are left intact as documented as-built v1; the doctrine supersedes them as the target via the §11 gaps, not by rewriting the matrix to look already-fixed.
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13) + prior BL entries merged
Type: Documentation Change
Summary:
- §9 Edge Cases (10 items, 9.1–9.10) absorbed into §11 Implementation Audit note.
- §10 Error States → §9, §11 Diagrams & Docs Touched → §10, §12 Implementation Audit (PR-1 shipped + Exists/Missing/Design-constraints) → §11 4-column table (14 rows, ✅/⚠️ per BL evidence).
- §14 Open Questions / Decisions → §12, §15 Bug / Change Log → §13.
- §13 Implementation Plan (non-standard, 5 PRs) → §14 Implementation Plan Appendix.
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-06-13 — Spec refinement: numbered edge cases, detailed error matrix, expanded test plan
Type: Documentation Change
Summary:
- Replaced §9 loose paragraph with 10 numbered edge cases (9.1–9.10) covering plan/period tampering, overlapping pilots, annual discount consistency, feature list changes mid-subscription, free plan limits, upgrade/downgrade in same period, multi-org users, payment gateway offline, VAT changes, and currency switching.
- Replaced §10 with 10-row detailed error matrix (user-visible/internal/log/retry).
- Expanded §7 test plan from 10 to 21 test cases, adding coverage for annual pricing calculations, pilot expiry, cancel/downgrade/reactivate flows, proration, and URL period state persistence.
- Updated spec status from draft to agreed.
Reason:
- Spec needed hardening per implementation audit. Edge cases were loose prose, test plan was thin, error states lacked structured detail.
Impact:
- Spec authors and future implementation agents.
Tests:
- Spec-only change — no code tests affected.
2026-06-13 — Implementation audit + implementation plan hardened
Type: Documentation Change
Summary:
- Replaced §12 high-level audit with grep-verified detailed Implementation Audit listing every missing file/path and 5 existing design constraints (Plan model, Subscription, BillingCard, GraceBanner, pilot constants).
- Added §13 implementation plan with 5 ordered PRs: PR-1 (plan seeding + public
/api/plans/+/api/plans/features/endpoints), PR-2 (enhanced subscription endpoint with plan_features + annual billing), PR-3 (pricing page frontend), PR-4 (Settings billing enhancement + pilot banner +?plan=pre-selection), PR-5 (edge cases + error states hardening). Each PR has tests-first list, exact file paths, and verification gate. - Added Open Question §14 item 5 (feature flag storage — hardcoded PLAN_FEATURES dict vs model field).
- Verified spec aligns with TEMPLATE structure and MVP_SCOPE.md registry entry.
Reason:
- Per fleet task D-r04: specs must be build-ready with concrete implementation audits and PR-scoped plans.
Impact:
- Spec authors and future code rounds executing F53.
Tests:
- Spec-only change — no code tests affected.
2026-06-13 — Frontend wired to API; route registered; E2E added (T3-E R11)
Type: Feature
Summary:
PricingPage.tsxconfirmed connected tousePlanshook (fetchesGET /api/plans/). No hardcoded plan data.- Route
/store/pricingadded toApp.tsxas public page outside<Layout />. plansApiconfirmed inendpoints.tswithlist()andfeatures().- E2E tests updated in
f53-pricing.spec.tsto use correct/store/pricingURL. - §12 Implementation Audit updated with shipped status.
Impact:
- PricingPage now live at
/store/pricing. Will show skeleton cards until backend API (/api/plans/) is deployed by T3-A.
Tests:
- E2E: 4 tests in
f53-pricing.spec.ts.
14. Implementation Plan (Appendix)
PR-1: Backend — Plan seeding + public plan API
Tests-first (add to backend/tests/test_billing.py / create backend/tests/test_pricing.py):
TestPlans::test_seed_creates_four_plans— Free (0đ), Starter (290K), Team (1.49M), Business (4.9M)TestPlans::test_plan_field_values— included_users, max_projects, has_daily_brief correct per planTestPlans::test_annual_price_calculation— 290K × 12 × 0.8 = 2,784,000TestPlans::test_annual_discount_20_percent— annual_monthly_equivalent = 232K for StarterTestPlanFeatures::test_all_features_mapped— PLAN_FEATURES dict has entries for all 4 plansTestPlanFeatures::test_free_plan_has_correct_features— daily_pulse, tasks, commitments, risksTestPlanFeatures::test_business_plan_has_all_features— all features including export, api_access, priority_supportTestPlansApi::test_public_no_auth—GET /api/plans/returns 200 without auth headerTestPlansApi::test_plan_response_structure— correct JSON shape with formatted prices, features, highlight, ctaTestPlanFeaturesApi::test_feature_matrix—GET /api/plans/features/returns 4×N matrixTestPlansApi::test_inactive_plans_excluded— plans withis_active=Falsenot returned
Backend:
- Create
backend/apps/billing/features.py—PLAN_FEATURESdict with 4 plan codes → list of feature strings. Add toPLAN_FEATURES_V2comments for future expansion. - Create seed management command or migration:
backend/apps/billing/management/commands/seed_plans.py— creates Free (sort_order=10), Starter (20), Team (30), Business (40) plans with F53 field values. - Create
backend/apps/billing/views_plans.py(not created as a separate file —PlanListView/PlanFeaturesViewwere implemented directly inbackend/apps/billing/views.pyasplans_listandplan_features; see §11 rows 2–3) —PlanListView(returns serialized plans with computed annual pricing, features list, formatted prices, highlight flag, CTA) andPlanFeaturesView(returns matrix). - Create
backend/apps/billing/urls_plans.py— routes:GET /api/plans/,GET /api/plans/features/(registered at root level, above auth middleware — public). - Wire into
config/urls.py— register before auth middleware block.
Gate: cd backend && DJANGO_SETTINGS_MODULE=config.settings.test python -m pytest tests/test_billing.py tests/test_pricing.py -x --cov=apps --cov=core --cov-fail-under=99 --tb=short
PR-2: Backend — Enhanced subscription + annual billing
Tests-first (add to backend/tests/test_billing_api.py):
TestSubscriptionEndpoint::test_plan_features_in_response— subscription response includesplan_featuresTestSubscriptionEndpoint::test_feature_diff_on_upgrade— response showsfeature_diff_on_upgradewhen a higher plan existsTestPayEndpoint::test_annual_period_accepted— POST withperiod: "annual"creates PaymentIntent correctlyTestPayEndpoint::test_annual_pricing_returned— response includes annual amountTestPayEndpoint::test_invalid_period_400— period not in choices → 400
Backend:
- Modify
backend/apps/billing/views.py::billing_subscription— appendplan_features(from PLAN_FEATURES) andfeature_diff_on_upgrade(computed by comparing current plan’s features to next-tier plan) - Modify
backend/apps/billing/views.py::billing_pay— accept and validateperiodparam, pass to PaymentIntent creation
Gate: same backend gate + targeted unit tests
PR-3: Frontend — Public pricing page
Tests-first:
frontend/src/test/PricingPage.test.tsx:renders 4 plan cardsmonthly toggle shows correct pricesannual toggle shows 20% badge + correct pricesFree plan shows Get started free CTApaid plans show Start 14-day pilot CTACTA navigates to /register?plan=Xcompare all features table renders?period=annual URL state rememberedskeleton while loadingretry on load errorenterprise CTA renders contact link
frontend/tests/e2e/lumi.spec.ts—describe("Pricing"):renders all 4 plan cardsmonthly/annual toggle switches pricesCTA navigates correctly?period=annual persists in URL
Frontend:
frontend/src/pages/store/PricingPage.tsx— public page, plan cards grid (responsive: 1 col mobile → 2 col tablet → 4 col desktop), monthly/annual toggle,Compare all featuresexpandable table. Enterprise CTA at bottom. (Originally planned aspages/PricingPage.tsx; moved topages/store/in the 2026-06-25 rebuild.)- Route
/store/pricinginApp.tsx— public (no auth required). Linked from footer, landing CTA,/store/marketing. - State: loading (skeleton cards), error (toast + retry), populated (4-plan grid with toggle), empty (no plans → admin message).
frontend/src/i18n/locales/en/pricing.json+vi/pricing.json— all keys from §6.4.- Update footer in
Layout.tsxor shared footer component — add “Pricing” link.
Gate: cd frontend && tsc --noEmit && pnpm test && make e2e
PR-4: Frontend — Settings billing enhancement + pilot banner + ?plan= pre-selection
Tests-first:
frontend/src/test/BillingCard.test.tsx(not created — PR-4 BillingCard enhancement is a ⚠️ gap per §11 row 14;BillingCard.test.tsxdoes not exist) — enhanced:shows current plan feature list,shows feature_diff_on_upgrade,shows upgrade options as cardsfrontend/src/test/TodayPage.test.tsx—pilot banner shows correct days remaining,pilot banner warning yellow at <48hfrontend/src/test/RegisterPage.test.tsx—?plan=starter pre-selects plan,?plan=free has correct copy,?plan=invalid defaults gracefully- E2E:
describe("Billing")extended — upgrade flow, pilot banner countdown, plan pre-selection on register
Frontend:
- Enhance
BillingCard.tsx— show current plan feature list, upgrade options with feature_diff, gateway selector per F24. - Add
PilotBanner.tsxcomponent — readsSubscription.expires_at, calculates days remaining, yellow warning at ≤2 days. Renders on Today page. - Modify
RegisterPage.tsx— read?plan=query param from URL, store in session storage, display plan name during registration, pass to/api/billing/pay/post-signup. - Backend: validate
planparam onPOST /api/billing/pay/— ensure it matches org’s subscription target.
Gate: cd frontend && tsc --noEmit && pnpm test && make e2e
PR-5: Edge cases + error states hardening
Tests-first (add to existing files from PRs 1-4):
TestPlansApi::test_db_unavailable_503— mock DB failure → 503TestPricingPage::test_all_gateways_down_503— all gateways return error → 503TestPricingPage::test_upgrade_limit_blocked—included_usersexceeded → error messageTestPricingPage::test_downgrade_limit_blocked— too many users for target plan → validationTestPricingPage::test_pilot_already_exists— duplicate pilot → 400TestPricingPage::test_cancel_with_pending_payment— active PaymentIntent blocks cancelTestPricingPage::test_cross_tenant_pricing— pricing API is public, no tenant leakTestPricingPage::test_invalid_plan_code_400— unknown plan in upgrade requestTestPricingPage::test_reactivate_invalid_state_400— reactivate on non-read-only subscription
Backend/Frontend: Wire all error states from §10 error matrix. Ensure user-visible messages match spec.
Gate: full test suite + E2E