logicspike/docs

Billing

Billing & Subscription — User Journeys

Narrative flows for every billing touchpoint a user encounters on LogicSpike.


Finalized Decisions

# Decision Detail
1 Monthly / Yearly toggle Pricing page has two tabs. Yearly discount % is customizable per plan (stored in plans.yearly_discount_pct).
2 Free trial 1-month free trial of Pro on first upgrade. Card/mandate captured upfront (Razorpay). Cancellable anytime during trial.
3 Predefined plans + Coin system Fixed plans (Free / Starter / Pro / Business). Tenants can top-up LogicSpike Coins to buy extras (storage, seats, email sends) without upgrading the whole plan.
4 Currency INR (Razorpay, current). USD/international via Stripe (Phase 8).
5 Coupon / promo codes Razorpay: create coupons as subscription discounts. Stripe: Promotion Codes at checkout (Phase 8).
6 Hybrid seat model Free & Starter = flat with fixed seats. Pro = flat base + ₹400/extra seat/mo. Business = per-seat (₹1,000/seat/mo).
7 Tax Razorpay GST (configured in Dashboard). Stripe Tax (automatic_tax: true) when Stripe is added (Phase 8).
8 Invoice customization v1: Company Name + Tax ID (VAT/GST) in billing settings → provider puts on invoice PDF. v2: billing address, PO number for enterprise.

Personas

Name Role Context
Ayva Owner of "TechStartup" Free tier, growing team, ready to upgrade
Raj Admin of "AgencyHub" Pro plan, managing multiple clients
Sam New signup Just created an account, first workspace

Plans & Pricing

Pricing Table (Monthly / Yearly Toggle)

Free Starter ($12/mo) Pro ($29/mo) Business ($79/mo)
Yearly price Free $10/mo (billed $120/yr) $24/mo (billed $288/yr) $65/mo (billed $780/yr)
Savings 17% off 17% off 18% off
Blog posts 10 50 Unlimited Unlimited
Media storage 500 MB 5 GB 25 GB 100 GB
Team seats (included) 2 5 10 25
Extra seat cost $5/seat/mo $12/seat/mo (pure per-seat)
Custom roles
API keys 1 3 10 Unlimited
Custom domain
Priority support
Free trial ✅ 1 month ✅ 1 month

LogicSpike Coins (Top-Up System)

Workspace owners can purchase coins to buy extras without upgrading their plan:

Coin Pack Price Coins
Small $5 500 coins
Medium $20 2,200 coins (10% bonus)
Large $50 6,000 coins (20% bonus)
Extra Coin Cost Equivalent
+1 GB storage 100 coins ~$1
+1 team seat (monthly) 250 coins ~$2.50
+100 email sends 50 coins ~$0.50
+10 blog posts 75 coins ~$0.75
Custom domain add-on 500 coins/mo ~$5/mo

Design: Coins never expire. Coins belong to the workspace, not to individual users. Seat/domain add-ons purchased with coins are monthly recurring (deducted automatically). When coins run out → add-on pauses, workspace owner notified to top-up.


Journey 1 — New User Gets a Free Plan Automatically

Persona: Sam
Trigger: Created first workspace "SamBlog".

  1. System auto-provisions on Free plan. No card required.
  2. Dashboard shows "Free Plan" badge in sidebar.
  3. Limits active from Day 1 (10 posts, 500 MB, 2 seats).
  4. Subtle "Upgrade" button in sidebar (not aggressive).

System: tenants.plan_id = 'free', subscriptions created with status: 'active', current_period_end: null. tenant_services populated from plan_service_limits.


Journey 2 — Owner Explores Pricing (Monthly / Yearly Toggle)

Persona: Ayva
Trigger: Hits a limit → toast: "Your Free plan allows 2 members. Upgrade to add more."

  1. Toast has "View Plans" button → /dashboard/settings/billing.
  2. Plans tab shows pricing cards with Monthly / Yearly toggle at the top.
  3. Switching to Yearly shows discounted prices + "Save X%" badges (% read from plans.yearly_discount_pct).
  4. Current plan highlighted with "Current Plan" badge.
  5. Pro & Business show "Start Free Trial" (if never trialed before) instead of "Upgrade".
  6. Ayva clicks "Start Free Trial" on Pro.

Journey 3 — Free Trial + Checkout (Card Required Upfront)

Persona: Ayva (continued)
Trigger: Clicked "Start Free Trial" on Pro.

  1. Info modal: "Try Pro free for 30 days. You won't be charged until the trial ends. Cancel anytime."
  2. Backend calls POST /billing/checkout → returns Razorpay modal params (subscription_id, key_id).
  3. Razorpay Checkout modal opens on the same page (no redirect). Ayva sees:
    • Plan description + amount
    • UPI / Card / Net Banking options
    • Trial notice
  4. Ayva pays (or sets up mandate for deferred trial charge).
  5. Frontend calls POST /billing/payment/verify with razorpay_signature → backend verifies HMAC and activates subscription.
  6. Webhook subscription.activated fires asynchronously (idempotent).
  7. Backend:
    • subscriptions row: status: 'trialing', trial_end: <30 days out>, plan_id: 'pro'.
    • tenant_services rebuilt with Pro limits.
  8. UI: "Pro Plan (Trial)" badge, "Trial ends Feb 27" notice in billing settings.
  9. Day 28: System shows banner: "Your trial ends in 2 days. You'll be charged ₹2,499/mo after."
  10. Day 30: Razorpay auto-charges → subscription.charged webhook → status: 'active'. Seamless.

Trial Cancellation:

  • Ayva cancels during trial → webhook subscription.cancelled → downgrade to Free immediately.
  • No charge. Mandate released.

Phase 8 (Stripe): For international customers, POST /billing/checkout will return { checkout_url } instead. Frontend redirects to Stripe, then returns.


Journey 4 — Coin Top-Up

Persona: Ayva
Trigger: Ayva is on Pro but needs 5 GB more storage without upgrading to Business.

  1. Goes to billing → "Coins & Add-ons" tab.
  2. Sees current coin balance: 0 coins.
  3. Clicks "Buy Coins" → coin pack selector (Small / Medium / Large).
  4. Selects Medium (2,200 coins for $20) → Stripe Checkout (one-time payment).
  5. Webhook checkout.session.completed → backend credits 2,200 coins to tenant_coins.balance.
  6. Back to Coins tab → clicks "Buy Extra" → "+5 GB storage (500 coins)".
  7. Confirms → 500 coins deducted → tenant_services storage limit increased by 5 GB.
  8. Balance: 1,700 coins remaining.

System: New table tenant_coins tracks balance + transaction history. coin_transactions table logs every credit/debit with reason.


Journey 5 — Monthly Renewal (Happy Path)

Persona: Raj
Trigger: Monthly auto-charge by Stripe.

  1. Webhook invoice.payment_succeeded fires.
  2. Backend updates subscriptions.current_period_end.
  3. No UI interruption — seamless.
  4. Invoice available in billing → "Invoices" tab (fetched from Stripe).
  5. Invoice includes Company Name + Tax ID if set by tenant.

Journey 6 — Payment Fails (Past Due)

Persona: Raj
Trigger: Card expired, Stripe retry fails.

  1. Webhook invoice.payment_failedsubscriptions.status = 'past_due'.
  2. Sitewide warning banner:

    ⚠️ "Your payment failed. Update your payment method to avoid service interruption." [Update Payment →]

  3. Grace period: 7 days. All features remain active.
  4. "Update Payment" → Stripe Customer Portal (billing_portal/sessions).
  5. Stripe retries 3 times over 7 days.
  6. Resolved: invoice.payment_succeeded → status back to 'active' → banner gone.
  7. Not resolved: → Journey 7 (downgrade).

Journey 7 — Involuntary Downgrade

Trigger: Stripe exhausts retries, subscription canceled.

  1. Webhook customer.subscription.deleted fires.
  2. Backend: subscriptions.status = 'canceled', tenants.plan_id = 'free', tenant_services rebuilt.
  3. Red banner:

    🔴 "Your subscription was canceled due to payment failure. Workspace downgraded to Free."

  4. Soft enforcement — NO data deleted:
    • 50 posts but Free allows 10 → existing preserved, can't create new.
    • 8 members but Free allows 2 → existing stay, can't invite more.
    • Coin-purchased add-ons paused until re-subscription or coin top-up.
  5. Re-subscribe button available.

Principle: We NEVER delete user data on downgrade. Only block new creation.


Journey 8 — Voluntary Plan Change

8a — Upgrade (Starter → Pro)

  1. Billing settings → "Change Plan" → selects Pro.
  2. Stripe prorates — charges difference for remaining cycle.
  3. Webhook → tenant_services rebuilt with Pro limits. Immediate effect.

8b — Downgrade (Pro → Starter)

  1. Billing settings → "Change Plan" → selects Starter.
  2. Warning: "Your plan changes to Starter at the end of this billing period. No refund for unused time."
  3. subscriptions.cancel_at_period_end = true, pending_plan_id = 'starter'.
  4. At period end → Stripe webhook → plan changes → limits adjusted. Soft enforcement applies.

8c — Switch Billing Cycle (Monthly ↔ Yearly)

  1. Billing settings → "Switch to Yearly" / "Switch to Monthly".
  2. Yearly → immediate charge of discounted annual amount (prorated credit for unused monthly).
  3. Monthly → changes at end of current annual period.

Journey 9 — Billing Dashboard

Persona: Ayva at /dashboard/settings/billing.

Tab 1 — Overview

  • Current plan + badge (Free / Starter / Pro / Business)
  • Billing cycle: Monthly or Yearly
  • Next billing date + amount
  • Usage meters (posts used/limit, storage, members, API keys)
  • "Manage Subscription" → Stripe Customer Portal
  • "Change Plan" → plan comparison with Monthly/Yearly toggle

Tab 2 — Plans

  • Pricing cards rendered from GET /billing/plans response
  • Monthly / Yearly toggle — shows yearly_discount_pct as "Save X%" badge
  • Current plan highlighted with "Current Plan" badge
  • "Start Free Trial" vs "Upgrade" CTA logic (based on has_used_trial)
  • Click "Upgrade" → POST /billing/checkout → backend returns Razorpay modal params
  • Razorpay checkout modal opens inline (no redirect)
  • After payment → POST /billing/payment/verify → subscription activated
  • Downgrade → warning modal → POST /billing/change-plan

Tab 3 — Invoices

  • Table: Date, Amount, Status (Paid/Failed/Pending), Tax, Download PDF
  • Fetched from Stripe API — PDF hosted by Stripe
  • Tenant's Company Name + Tax ID shown on invoices

Tab 4 — Coins & Add-ons

  • Current coin balance (workspace-level)
  • "Buy Coins" button → coin packs
  • Active add-ons list (extra storage, seats, etc.)
  • "Buy Extra" button → add-on marketplace
  • Transaction history: credits (purchases) and debits (add-ons)

Tab 5 — Billing Info

  • Company Name (editable)
  • Tax ID / VAT / GST number (editable)
  • Saved to Stripe Customer object → appears on invoices automatically
  • v2: Billing address, PO number

Journey 10 — Usage Limit Enforcement (Soft Walls)

Strategy: Soft Walls, Not Hard Blocks

Scenario Behavior
Create 11th post on Free (limit: 10) Modal: "Post limit reached. Upgrade or buy extra with coins."
Invite 3rd member on Free (limit: 2) Toast: "Seat limit reached. Upgrade or buy a seat with coins."
Storage at 95%+ Warning banner: "Storage almost full (480 MB / 500 MB)."
Storage at 100% Upload blocked. Existing files untouched.
2nd API key on Free (limit: 1) Block with upgrade prompt.

Technical enforcement:

  1. Service checks tenant_services.limits before allowing action.
  2. Counts current usage (SELECT COUNT(*) FROM blog_posts WHERE tenant_id = ?).
  3. If current >= limit → return { error: "PLAN_LIMIT_REACHED", limit, current, upgrade_url }.
  4. Frontend catches this → shows upgrade prompt with plan comparison + coin purchase option.

Journey 11 — Promo Code at Checkout

Trigger: Ayva has a promo code "LAUNCH50".

  1. During Stripe Checkout → "Add promotion code" field (enabled via allow_promotion_codes: true).
  2. Enters "LAUNCH50" → Stripe validates → shows "50% off first 3 months".
  3. Completes checkout → discount applied automatically.
  4. Promo codes managed entirely in Stripe Dashboard — no backend code needed.

Journey 12 — Non-Owner Billing Access

Trigger: Team member navigates to /dashboard/settings/billing.

Permission Access
No billing permissions 🚫 Access Denied page
billing:invoices.read View-only: see invoices + current plan
billing:plans.read View-only: see plans comparison
Owner Full access: upgrade, downgrade, manage subscription, buy coins

Journey 13 — Re-subscription After Cancellation

Persona: Raj
Trigger: Raj's subscription was canceled (involuntary downgrade via Journey 7). He wants to re-subscribe.

  1. Billing dashboard shows 🔴 banner: "Your workspace was downgraded to Free. Re-subscribe to restore your plan."
  2. Clicks "Re-subscribe" → Plans tab → selects Pro again.
  3. System reuses the existing stripe_customer_id (no new Stripe Customer created).
  4. Trial eligibility: has_used_trial = true → no free trial offered. Goes straight to paid checkout.
  5. Stripe Checkout → new sub_... created → new subscriptions row.
  6. Webhook checkout.session.completedrebuildEntitlements() → Pro limits restored.
  7. Paused add-ons: Remain paused. Raj must manually re-enable them from the Coins & Add-ons tab (coins must be sufficient).

System notes:

  • Old subscription ID is kept in subscriptions with status: 'canceled' (historical record).
  • New subscription gets a new row with status: 'active'.
  • Existing data (posts, files, members) is immediately accessible again once limits are restored.

Journey 14 — Email Notifications (via Communication Service)

Billing events trigger emails to the workspace owner (not all members).

Trigger Email Timing
Successful checkout "Welcome to [Plan Name]!" Immediate
Trial started "Your free trial of [Plan] has started. You have 30 days." Immediate
Trial ending "Your trial ends in 3 days. You'll be charged [amount]." 3 days before trial end
Payment succeeded (none — handled by Stripe receipt)
Payment failed "Your payment failed. Update your payment method." Immediate
Subscription canceled "Your workspace has been downgraded to Free." Immediate
Coin balance low "Your coin balance is below 100. Top up to keep add-ons active." When balance < 100
Add-on paused "Your [addon] add-on has been paused due to insufficient coins." Immediate

Implementation: Stripe sends its own dunning emails for payment failures. LogicSpike emails are supplementary and sent via the Communication service.


DB Schema

All billing tables (subscriptions, tenant_services, tenant_coins, coin_transactions, tenant_addons, processed_stripe_events, plans, services, service_limits, plan_service_limits, coin_packs, addon_catalog) are fully defined in architecture.md.

Billing