partial — the current public catalog and localized page are source-backed; self-service billing, pilot lifecycle, browser execution, and strategy realignment remain open

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:

  1. 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.
  2. 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.
  3. 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/pricing is 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: -1 renders 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.

Plan Feature Matrix (Current — ADR-030, 2026-06-25)

Per-AI-licensed-user USD pricing. Workspace membership is always free and unlimited.

FeatureLumi Workplace FreeAI StarterAI ProAI 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 usersUnlimitedUnlimitedUnlimitedUnlimited
AI licence requiredNoYesYesYes
Daily Pulse✓✓✓✓
CEO Brief✗✓✓✓
AI extraction✗✓✓✓
Projects/Tasks✓✓✓✓
CRM✓✓✓✓
Internal Chat✓✓✓✓
Approvals✓✓✓✓
AI capabilitiesNoneStarterProFull
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.

FeatureFreeStarterTeamBusiness
Monthly price (VND)0290,0001,490,0004,900,000
Annual price (VND)0230,000/mo1,190,000/mo3,920,000/mo
Included users31025Unlimited
Daily Pulse✓✓✓✓
CEO Brief✗✓✓✓
AI extraction✗✓✓✓
Projects/Tasks1 project10 projects50 projectsUnlimited
Teams✗3 teams10 teamsUnlimited
Commitments✓✓✓✓
Risks✓✓✓✓
Decisions✗✓✓✓
Chat with Lumi✗50 msgs/moUnlimitedUnlimited
Internal Chat (F29)✗✗✓✓
CRM (F27)✗✗100 companiesUnlimited
Approvals (F26)✗✗✓✓
Signal Inbox (F32)✗✗✓✓
Connected Sources (F33)✗✗1 source5 sources
File uploads (F34)✗100MB1GB10GB
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/billing shows 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.

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.

3. Scope Boundaries

  • In:
    • Public /store/pricing page 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=.
  • 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 Plan into a PlanPrice child keyed by (plan, band, currency) — two PPP bands: emerging (global-south wedge: VN/ID/PH, local currency, ~0.4× PPP) and international (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_price is retained read-only/deprecated and backfilled into PlanPrice. 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

MethodPathAuthDescription
GET/api/plans/NoneReturns all active plans with features list. Public endpoint — no auth.
GET/api/plans/features/NoneReturns feature-by-feature comparison matrix.

5.2 Changed Endpoints

MethodPathChange
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, default international) and returns that band’s PlanPrice — USD for international, local currency (VND) for emerging — with included_active_users and extra_active_user_amount replacing 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_save
  • pricing.free, pricing.starter, pricing.team, pricing.business
  • pricing.most_popular, pricing.per_month, pricing.per_month_annual
  • pricing.users, pricing.projects, pricing.teams, pricing.data_history
  • pricing.feature_* — one key per feature
  • pricing.cta_free, pricing.cta_pilot, pricing.cta_enterprise
  • pricing.compare_all, pricing.enterprise_title, pricing.enterprise_desc
  • pricing.pilot_banner, pricing.pilot_banner_warning, pricing.pilot_upgrade
  • pricing.upgrade, pricing.downgrade, pricing.cancel, pricing.reactivate
  • pricing.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) or 0 ₫ (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.app mailto 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), and test_billing.py (TestSubscriptionLifecycle for upgrade/downgrade/cancel/reactivate). Rows below use original per-case naming; those files are authoritative.

#CaseTypeWhere
1Public /api/plans/ returns 4 plans with correct featuresbackendtests/test_billing.py
2/api/plans/ returns correct annual prices with 20% discount appliedbackendtests/test_billing.py
3/api/plans/features/ returns feature comparison matrixbackendtests/test_billing.py
4Pricing page renders all 4 plan cards with correct pricese2edescribe("Pricing")
5Monthly/annual toggle switches prices globallye2esame block
6Annual toggle shows “Save 20%” badge on paid planse2esame block
7Free plan has “Get started free” CTA → navigates to registere2esame block
8Paid plan CTA navigates to register with ?plan= query parame2esame block
9?plan= param pre-selects plan after registration (survives auth handoff)e2esame block
10Registered user on Free sees upgrade options in Settingse2edescribe("Billing")
11Pilot banner shows correct days remaininge2edescribe("Today")
12Pilot banner at 48h shows warning (yellow) stylingunitBillingCard.test.tsx
13Pilot expired → grace banner, org enters read-only after gracee2edescribe("Billing")
14Cancel subscription preserves data through period endbackendtests/test_billing.py
15Downgrade takes effect at period end, warns about feature lossbackendtests/test_billing.py
16Upgrade from annual → pro-rata calculation correctbackendtests/test_billing.py
17Reactivate from read-only → data preserved, subscription resumesbackendtests/test_billing.py
18”Need more than Business?” CTA renders with contact linke2edescribe("Pricing")
19URL remembers period state: /store/pricing?period=annuale2esame block
20Compare all features expandable table renders correctlye2esame block
21?plan=invalid_value defaults to no pre-selection (graceful fallback)e2esame block

2026-06-25 rebuild — new unit test coverage (static, no API mock):

#CaseTypeWhere
22Lumi Workplace free section renders with $0 priceunitPricingPage.test.tsx · describe(‘PricingPage’)
23All 3 AI plan cards render (ai_starter, ai_pro, ai_ultimate)unitsame
24AI Pro highlighted with “Most popular” Badgeunitsame
25Monthly/Annual toggle buttons renderunitsame
26Monthly USD prices shown by default (Starter = $2)unitsame
27Annual toggle switches prices and shows save-% badge (Starter → $1, Save 50%)unitsame
28Billing rules section rendersunitsame
29Compare capabilities toggle rendersunitsame
30Compare table expands on toggle click; shows 4 column headersunitsame
31Enterprise section renders at bottomunitsame
32No banned design tokens (indigo-600, bg-orange-100) in rendered HTMLunitsame
33VND prices for vi locale ($2 → 52,000 ₫)unitdescribe(‘PricingPage — Vietnamese locale’)
34Free Workplace plan shows 0 ₫ for vi localeunitsame

8. Current Behavior

  • /store/:lang/pricing is 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 idempotent seed_plans ownership.
  • The old five-Intel-plan, VND-card, and persistent ?period=annual behavior is superseded by ADR-030 and is not part of the current acceptance boundary.

9. Error States

#ScenarioUser-visibleInternalLogRetry
1/api/plans/ fails to loadPricing page shows skeleton cards + “Could not load plans. [Retry]“API error loggedERROR “plans_load_failed”User clicks Retry
2Checkout fails to create PaymentIntentToast: “Could not start payment. [Try again]“Payment service errorERROR “payment_intent_creation_failed”User retries
3Payment succeeds but subscription activation failsToast: “Payment received but subscription could not be activated. Contact support.”Subscription creation errorCRITICAL “payment_orphaned”Manual intervention — admin panel
4Upgrade blocked by plan limit (users/projects)Form validation: “Your current plan limits prevent this upgrade.”Limit check failsWARNING “upgrade_limit_blocked”User adjusts usage
5Downgrade would exceed new plan’s limitsModal: “You have 15 users but Starter allows 10. Remove 5 users before downgrading.”Limit check failsWARNING “downgrade_limit_blocked”User adjusts usage
6Pilot already exists for this org400: “You already have an active pilot.”Duplicate pilot checkINFO “pilot_already_exists”n/a
7All payment gateways unavailable503: “Payment system is temporarily unavailable.”All gateways return errorCRITICAL “all_gateways_down”User retries later
8Cancel fails due to active PaymentIntentWarning: “You have a pending payment. Cancel it first.”Active PI existsWARNING “cancel_with_pending_payment”User resolves pending payment
9Invalid plan code in upgrade request400: “Plan ‘invalid_code’ not found.”Unknown plan codeWARNING “upgrade_unknown_plan”User selects valid plan
10Reactivation called on non-read-only subscription400: “Subscription is not in read-only state.”State check failsWARNING “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.

#ClaimStatusEvidence
1Plan seeding (Free/Starter/Team/Business) via seed_plans.py✅ matchT3-A R11 BL (2026-06-13); backend/apps/billing/management/commands/seed_plans.py
2GET /api/plans/ (public, AllowAny) with formatted prices, features, CTA✅ matchT3-A R11 BL; backend/apps/billing/views.py::plans_list + urls_plans.py
3GET /api/plans/features/ (public feature comparison matrix)✅ matchT3-A R11 BL; backend/apps/billing/views.py::plan_features
4PLAN_FEATURES dict (billing/features.py — PLAN_CATALOG + PLAN_FEATURES_MATRIX)✅ matchT3-A R11 BL; backend/apps/billing/features.py
5Backend tests: test_billing_plans.py (16 tests) + test_pricing.py; coverage 99.06%✅ matchT3-A R11 BL; 2158 tests pass
6PricingPage.tsx (public: plan cards, monthly/annual toggle, compare-all-features table)✅ matchT3-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)
7usePlans TanStack Query hook (/api/plans/, /api/plans/features/, public)✅ matchT3-E R11 BL; frontend/src/hooks/queries/usePlans.ts
8pricing i18n namespace (en + vi with all §6.4 keys)✅ matchT3-E R11 BL; frontend/src/i18n/locales/en/pricing.json + vi/pricing.json
9Route /store/pricing in App.tsx (public, outside <Layout />)✅ matchT3-E R11 BL; App.tsx
10E2E f53-pricing.spec.ts (4 tests: public access, auth, toggle URL state, plan cards)✅ matchT3-E R11 BL; frontend/tests/e2e/f53-pricing.spec.ts
182026-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✅ match2026-06-25 PR; frontend/src/pages/store/PricingPage.tsx
192026-06-25 rebuild — PricingPage.test.tsx rewritten: 12 unit tests (2 describe blocks); no QueryClientProvider/MSW; static data; Vietnamese VND locale tests✅ match2026-06-25 PR; frontend/src/test/PricingPage.test.tsx
11Pilot countdown banner on Today page (reads Subscription.expires_at)⚠️ gapListed as “Still missing (P1)” in §12 audit
12Enhanced subscription endpoint (plan_features, feature_diff_on_upgrade)⚠️ gapListed as “Still missing (P1)” in §12 audit
13Annual billing in POST /api/billing/pay/ (period: "annual" param)⚠️ gapListed as “Still missing (P1)” in §12 audit
14?plan= pre-selection on /register + BillingCard feature matrix enhancement⚠️ gapListed 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 FIXStrategy 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 builtADR-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 FIXStrategy 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.

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. Feature flag storage: Hardcoded PLAN_FEATURES dict vs model field? Decision: hardcoded dict for v1 (no migration, no admin UI overhead). Must be versioned with effective_date to 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.tsx corrected to frontend/src/pages/store/PricingPage.tsx — the page was moved to pages/store/ in the 2026-06-25 rebuild (row 18 already recorded the correct path).
  • PR-3 plan: frontend/src/pages/PricingPage.tsx corrected to frontend/src/pages/store/PricingPage.tsx for the same reason.
  • PR-1 plan: backend/apps/billing/views_plans.py — this file was never created. Plan views were implemented directly in backend/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 usePlans API 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 vi locale 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.app mailto CTA.
  • AIPlanCard local component replaces old PlanCard; all shadcn/ui + Lumi design tokens.
  • frontend/src/test/PricingPage.test.tsx rewritten: 12 tests across 2 describe blocks; no QueryClientProvider or 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/pricing public page. No backend API changes. usePlans hook 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.tsx top 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 — subtitle updated to “Your whole company, free. Pay only for AI.”
  • frontend/src/i18n/locales/vi/pricing.json — subtitle updated 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 PlanPrice child keyed by (plan, band, currency), with two PPP bands — emerging (VN/ID/PH, local currency, ~0.35× PPP) and international (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.tsx confirmed connected to usePlans hook (fetches GET /api/plans/). No hardcoded plan data.
  • Route /store/pricing added to App.tsx as public page outside <Layout />.
  • plansApi confirmed in endpoints.ts with list() and features().
  • E2E tests updated in f53-pricing.spec.ts to use correct /store/pricing URL.
  • §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 plan
  • TestPlans::test_annual_price_calculation — 290K × 12 × 0.8 = 2,784,000
  • TestPlans::test_annual_discount_20_percent — annual_monthly_equivalent = 232K for Starter
  • TestPlanFeatures::test_all_features_mapped — PLAN_FEATURES dict has entries for all 4 plans
  • TestPlanFeatures::test_free_plan_has_correct_features — daily_pulse, tasks, commitments, risks
  • TestPlanFeatures::test_business_plan_has_all_features — all features including export, api_access, priority_support
  • TestPlansApi::test_public_no_auth — GET /api/plans/ returns 200 without auth header
  • TestPlansApi::test_plan_response_structure — correct JSON shape with formatted prices, features, highlight, cta
  • TestPlanFeaturesApi::test_feature_matrix — GET /api/plans/features/ returns 4×N matrix
  • TestPlansApi::test_inactive_plans_excluded — plans with is_active=False not returned

Backend:

  • Create backend/apps/billing/features.py — PLAN_FEATURES dict with 4 plan codes → list of feature strings. Add to PLAN_FEATURES_V2 comments 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/PlanFeaturesView were implemented directly in backend/apps/billing/views.py as plans_list and plan_features; see §11 rows 2–3) — PlanListView (returns serialized plans with computed annual pricing, features list, formatted prices, highlight flag, CTA) and PlanFeaturesView (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 includes plan_features
  • TestSubscriptionEndpoint::test_feature_diff_on_upgrade — response shows feature_diff_on_upgrade when a higher plan exists
  • TestPayEndpoint::test_annual_period_accepted — POST with period: "annual" creates PaymentIntent correctly
  • TestPayEndpoint::test_annual_pricing_returned — response includes annual amount
  • TestPayEndpoint::test_invalid_period_400 — period not in choices → 400

Backend:

  • Modify backend/apps/billing/views.py::billing_subscription — append plan_features (from PLAN_FEATURES) and feature_diff_on_upgrade (computed by comparing current plan’s features to next-tier plan)
  • Modify backend/apps/billing/views.py::billing_pay — accept and validate period param, 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 cards
    • monthly toggle shows correct prices
    • annual toggle shows 20% badge + correct prices
    • Free plan shows Get started free CTA
    • paid plans show Start 14-day pilot CTA
    • CTA navigates to /register?plan=X
    • compare all features table renders
    • ?period=annual URL state remembered
    • skeleton while loading
    • retry on load error
    • enterprise CTA renders contact link
  • frontend/tests/e2e/lumi.spec.ts — describe("Pricing"):
    • renders all 4 plan cards
    • monthly/annual toggle switches prices
    • CTA 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 features expandable table. Enterprise CTA at bottom. (Originally planned as pages/PricingPage.tsx; moved to pages/store/ in the 2026-06-25 rebuild.)
  • Route /store/pricing in App.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.tsx or 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.tsx does not exist) — enhanced: shows current plan feature list, shows feature_diff_on_upgrade, shows upgrade options as cards
  • frontend/src/test/TodayPage.test.tsx — pilot banner shows correct days remaining, pilot banner warning yellow at <48h
  • frontend/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.tsx component — reads Subscription.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 plan param on POST /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 → 503
  • TestPricingPage::test_all_gateways_down_503 — all gateways return error → 503
  • TestPricingPage::test_upgrade_limit_blocked — included_users exceeded → error message
  • TestPricingPage::test_downgrade_limit_blocked — too many users for target plan → validation
  • TestPricingPage::test_pilot_already_exists — duplicate pilot → 400
  • TestPricingPage::test_cancel_with_pending_payment — active PaymentIntent blocks cancel
  • TestPricingPage::test_cross_tenant_pricing — pricing API is public, no tenant leak
  • TestPricingPage::test_invalid_plan_code_400 — unknown plan in upgrade request
  • TestPricingPage::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