Skip to content

Desktop Billing QA Checklist

Manual testing checklist for subscription, upgrade, downgrade, cancellation, and credit flows on desktop.

Prerequisites

  1. Running local stack: docker compose up --build
  2. Stripe CLI forwarding webhooks: stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe
  3. Stripe test card: 4242 4242 4242 4242 (any future expiry, any CVC)
  4. A verified account (email verified or OAuth)
  5. Fresh DB recommended: docker compose down -v && docker compose up --build

1. Trial Flow

  • New user lands on plan picker after signup (no dashboard access)
  • Creator card shows "START FREE TRIAL" button
  • Click START FREE TRIAL → dashboard loads with "TRIAL · 7d LEFT" in sidebar
  • Sidebar shows "30 AI MIN"
  • Settings → Credits tab shows 30 balance with progress bar
  • Settings → Credits tab shows trial grant in transaction history
  • Second account with same email cannot get another trial (409)

2. New Subscription (No Existing Sub)

  • From plan picker, click SUBSCRIBE on Hobby → redirects to Stripe Checkout
  • Stripe Checkout shows correct price ($9/mo or $86/yr depending on toggle)
  • Complete payment with test card → redirects to "Processing Payment" screen
  • Processing screen polls and transitions to dashboard within ~10s
  • Sidebar credits update (120 AI MIN for Hobby)
  • Settings → Billing tab shows "Hobby" plan with "active" status
  • Settings → Billing tab shows payment in history table
  • Settings → Credits tab shows subscription grant in transaction history
  • Stripe CLI shows checkout.session.completed webhook 200

3. Upgrade (Hobby → Creator)

  • From plan picker or Settings → Billing → UPGRADE PLAN
  • Creator card shows "UPGRADE" button
  • Click UPGRADE → confirmation screen appears (NOT Stripe Checkout)
  • Confirmation shows prorated charge amount (should be < Creator full price)
  • Confirmation shows next billing date
  • Click GO BACK → returns to plan picker
  • Click CONFIRM UPGRADE → "Plan Updated" success screen
  • Dashboard shows updated tier in sidebar
  • Credits update: old tier credits + new tier credits (check Credits tab)
  • Settings → Billing tab shows "Creator" with "active" status
  • Payment history shows prorated charge
  • Stripe CLI shows invoice.payment_succeeded webhook

Proration Math Verification

  • Monthly Hobby (\(9) → Monthly Creator (\)19) mid-cycle: charge should be < $19
  • Annual Hobby (\(86) → Annual Creator (\)182) mid-cycle: charge should be ~$96
  • Amount shown in confirmation matches actual Stripe charge

4. Downgrade (Creator → Hobby)

  • From plan picker, Hobby card shows "DOWNGRADE" button
  • Click DOWNGRADE → "What You'll Lose" modal appears (NOT immediate change)
  • Modal lists features being lost (AI Director, storage, etc.)
  • Modal shows effective date (end of current billing period)
  • Click KEEP MY CURRENT PLAN → modal closes, no change
  • Click CONFIRM DOWNGRADE → "Plan Updated" success screen
  • Dashboard still shows Creator tier (change not immediate)
  • Settings → Billing still shows Creator as active until period end

5. Cancellation Flow

  • Settings → Billing tab → CANCEL SUBSCRIPTION button visible when active
  • Click CANCEL → Step 1: Reason picker appears
  • Select reason + fill detail (required for some) → CONTINUE
  • Step 2: "What You'll Lose" warning (all reasons, not just too_expensive)
  • Warning lists dashboard, videos, AI features, no-refund notice
  • Click KEEP MY PLAN → modal closes
  • Click I UNDERSTAND → proceeds to step 3 (or discount for too_expensive)

Quality/Features/Competitor Path

  • Select "Quality not what I expected" → after warn step, "Let Us Help" screen appears
  • Select "Missing features" → same support screen with different message
  • Select "Switching to another tool" → same support screen with different message
  • Textarea allows typing a message
  • SEND TO OUR TEAM → sends support request via API, shows "Sent." confirmation
  • User receives confirmation email with FAQ + Discord links (Sapari voice, not corporate)
  • Support team receives alert email with user's tier, credits, paid status, and reply-to
  • JOIN OUR DISCORD → opens Discord link in new tab
  • "Continue cancelling" → proceeds to confirm step

Too Expensive Path

  • Step 3: Discount offer (20% off 3mo for monthly, 30% off next year for annual)
  • Click CLAIM discount → Step 5: "Discount Applied" confirmation with message
  • Click GOT IT → modal closes
  • Verify discount in Stripe dashboard
  • Click "No thanks, continue cancelling" → proceeds to step 4

One-Time-Only Discount Enforcement

  • Accept discount once → cancel again with "too expensive" → discount step is SKIPPED (goes straight to confirm)
  • Billing status discount_used is true after accepting discount
  • API: calling discount_accepted outcome again returns "already used" message (not a new coupon)
  • API: calling discount_accepted with a non-too_expensive reason returns 422 (server enforces reason/outcome coupling)

Final Cancellation

  • Step 4: REMIND ME BEFORE RENEWAL → modal closes, reminder scheduled
  • Step 4: CANCEL NOW → subscription cancelled, redirected to plan picker
  • Refresh page → still on plan picker (not dashboard)
  • Credits preserved (shown on plan picker if resubscribing)
  • Stripe CLI shows customer.subscription.deleted webhook

6. Post-Cancel Resubscribe

  • After cancel, plan picker shows SUBSCRIBE (not START FREE TRIAL)
  • Subtitle text says "Choose a plan to continue" (not trial text)
  • Subscribe to any plan → Stripe Checkout → payment → dashboard access restored
  • Previous credits still available + new credits added

7. Billing Tab (Settings)

  • Current plan card shows tier name, status badge, billing interval
  • Trial users see trial end date
  • UPGRADE PLAN button opens plan picker overlay
  • UPDATE PAYMENT METHOD → redirects to Stripe Customer Portal (or shows error message)
  • CANCEL SUBSCRIPTION only visible when status is "active"
  • Payment history table: paginated, shows date/type/amount/status
  • PREV/NEXT pagination works

8. Credits Tab (Settings)

  • Balance card shows total AI MIN as big number
  • Per-pool bars: "Beta" (X/240), "Subscription" (X/600), or "Credits" (fallback)
  • Beta bar shows renewal date below
  • Progress bar colors: green >60%, yellow >20%, red >0%, gray empty
  • Reserved credits shown when analysis in progress
  • Transaction history shows: trial grants, subscription grants, beta grants, usage deductions, period resets
  • Transaction amounts: green +N for grants, red -N for usage
  • Running balance shown per transaction

9. Sidebar Credit Badge

  • Shows "X AI MIN" in orange
  • Shows "0 AI MIN" in red when empty
  • Shows "TRIAL · Xd LEFT" during trial
  • Shows bolt icon + "BETA" for beta users
  • Updates after: subscribing, analyzing, cancelling (no manual refresh needed)

10. Beta Access

  • Beta user sees "Welcome to the Beta" modal on first login (not trial modal)
  • Modal mentions 240 AI minutes, 30-day renewal, watermark-free, stacking
  • Dismissing modal persists (refresh doesn't show it again)
  • Sidebar CreditBadge shows bolt icon + "BETA" below balance
  • Settings → Billing tab shows orange "BETA" badge next to tier name
  • Settings → Credits tab shows "Beta" bar with X/240 and renewal date
  • Settings → Credits tab shows total balance as big number above the bar
  • Settings -> Billing shows beta fields (credits total, used, renewal date)

Beta + Subscription Stacking

  • Beta user subscribes to Hobby → Credits tab shows two bars: "Beta" (X/240) + "Subscription" (X/120)
  • Total balance = beta remaining + subscription remaining
  • Using credits drains beta bar first (RENEWABLE priority)
  • Cancelling subscription → beta credits survive, subscription bar disappears
  • Billing tab shows both "BETA" badge and "active" subscription status

Beta Credit Renewal

  • After 30 days, credits reset to 240 (verify via API or wait)
  • Transaction history shows "Renewable period reset" entry after renewal
  • Progress bar resets to full after renewal

Beta Invite Flow (Admin)

  • Admin Beta page: invite existing user by email -> user gets beta + welcome email
  • Admin Beta page: invite new email -> invite email sent with signup link
  • Invite email contains signup URL with ?beta_invite=TOKEN
  • Email/password signup via invite link -> account created with beta access automatically
  • Google OAuth signup via invite link -> account created with beta access automatically (token forwarded through OAuthState)
  • Canonical email match: invite User@example.com, signup user@example.com -> beta granted (case)
  • Canonical email match: invite user.name@gmail.com, signup username@gmail.com -> beta granted (Gmail dot-stripping)
  • Canonical email match: invite user+beta@example.com, signup user@example.com -> beta granted (+tag alias)
  • Signup with different email than invite -> no beta granted (non-canonical, truly different mailbox)
  • Invite link after 24h -> token expired, no auto-grant

Admin User Management

  • User Lookup: search by username prefix finds user, emails are masked
  • User Lookup: search by full email finds exact match
  • User Lookup: search by user ID number finds exact match
  • User Lookup: selecting user loads card with tier, credits, storage, projects
  • User Lookup: grant credits form requires min 10 char reason, credits appear after grant
  • User Lookup: "Rebuild Credits" button rebuilds balance from ledger
  • Overview page: health dots show DB/Redis/Storage status

Admin Panel UI

  • Admin nav item visible only to superusers (Lock icon in sidebar)
  • Admin panel loads via React.lazy (check DevTools Network -- separate chunk)
  • Overview page shows real stats (beta users, recent events, DB/Redis health, queue depths when non-zero)
  • Overview page: "Credit Audit" button triggers audit (shows sudo prompt), result displayed inline
  • Overview page: "Cleanup Exports" button runs cleanup, shows count of deleted exports
  • User Lookup: search works, user card shows real data, quick actions work (grant beta, grant credits, change tier, notify, rebuild, deactivate, reactivate, force logout, GDPR delete)
  • User Lookup: expand "Payment History" shows payments with status badges and amounts
  • User Lookup: expand "Projects" shows project names with clip counts
  • User Lookup: expand "Analysis Runs" shows runs with project name, mode, credits, status
  • User Lookup: expand "Entitlements" shows type, status, granted/used, grant reason, expiry
  • User Lookup: expand "Active Sessions" shows device, IP, last activity
  • User Lookup: change tier dropdown shows all tiers, current tier highlighted
  • User Lookup: send notification inline form works
  • User Lookup: force logout and GDPR delete require sudo (password prompt)
  • User Lookup: as super admin, "Promote to Admin" visible on non-admin users (requires sudo)
  • User Lookup: as super admin, "Demote from Admin" visible on other admins (requires sudo, can't self-demote)
  • User Lookup: promote/demote not visible to regular admins (only super admins)
  • Beta Management: invite form, users table with search/sort, masked emails with reveal
  • Feature Flags: create, toggle, percentage slider, allowlist add/remove, delete
  • Audit Log: 5 filters (event type, resource type, admin ID, target user, since), expandable rows
  • Notifications: compose with target modes (user/tier/everyone), confirmation popup, recent list with filters
  • Discounts: create, active/scope filters, deactivate
  • Support Triage: tickets/cancellations toggle, assignment filters, claim/unclaim, inline reply
  • Payments: stuck payments table loads (empty = "all payments healthy"), Sync from Stripe button
  • Dark mode works on all admin pages
  • Non-superuser navigating to admin view redirected to pipeline

User Reactivation (Admin)

  • User Lookup: deactivated user shows "Reactivate" button -> click it -> user can log in again
  • User Lookup: active user has no "Reactivate" button

Audit Logging (Admin)

  • After any admin action that mutates state (beta grant, credit rebuild, etc.), Audit Log page shows the event
  • After admin reads that return PII (user list, user-by-username, entitlement lookups, discount list, notification list, beta user list, audit queries), Audit Log page shows a search event
  • Searching for a non-existent username/ID still emits a search event (emit-on-miss — forensic record of what admins probed for)
  • Audit Log: filter by event type (e.g. "grant" or "search") shows only matching events
  • Audit Log: filter by admin ID shows only that admin's actions
  • Audit Log: expanding a row shows detail JSON (for PII reads, details.target_user_id identifies the affected user when applicable)
  • Audit Log: pagination works (Newer/Older buttons)

Admin Security

  • Clicking "Force Logout" or "GDPR Delete" shows password prompt (sudo modal)
  • Entering correct password -> action executes
  • Entering wrong password -> error message, action does not execute
  • After 3 wrong sudo attempts -> locked out message
  • Idle for >2 hours on admin pages -> next action returns error (session timeout)

Support System

  • Paid/beta user: Settings > Support shows create ticket form
  • Free user: Settings > Support hides the form and shows "Support is available to paid subscribers and beta testers. Subscribe to any plan to contact support."
  • Trial user: Settings > Support hides the form (trial is not support-eligible, even though they have creator-tier features)
  • Submit failure surfaces the backend's specific detail (e.g. "You already have an open support conversation"), not a generic "Failed to send"
  • User creates ticket -> appears in admin Support Triage with correct priority
  • Admin default view shows "Mine + Unclaimed" conversations
  • Admin replies from triage -> auto-assigns, user sees reply in real-time
  • Admin can't reply to another admin's conversation without clicking "Claim"
  • Cancellation flow with "poor_quality" reason -> creates critical support conversation
  • Admin can filter triage by status, priority, category, assignment
  • Admin Cancellations tab shows feedback with reason/outcome filters
  • Email masked everywhere in admin, click eye icon to reveal (logged in audit)

Payment Recovery (Admin)

  • Payments page: shows stuck payments table (or "all payments healthy" if none)
  • Payments page: summary badges show counts per failure mode
  • Payments page: "Materialize" button opens sudo prompt, then grants credits
  • Payments page: "Wait" button is gray and non-actionable (informational)
  • Payments page: mail icon opens compose modal with pre-filled subject/message based on failure mode
  • Payments page: admin can edit subject + message before sending
  • Payments page: "Sync from Stripe" button shows result (X records checked, Y discrepancies)
  • Non-superuser cannot access Payments page (redirected or 403)

11. Edge Cases

  • Double-click UPGRADE button → only one request sent
  • Upgrade while analysis running → works, credits correct
  • Cancel then immediately resubscribe → works, no duplicate entitlements
  • Switch billing period (monthly ↔ annual) on plan picker → prices update
  • Stripe webhook delay (>10s) → payment success page shows "Processing..." then resolves
  • Stripe webhook timeout (>30s) → payment success page shows refresh button
  • Invalid/expired card → Stripe Checkout handles error (we don't see it)

12. Storage Quota

  • Upload clip → Settings → Billing → Usage shows storage bar incrementing
  • Upload second clip → counter accumulates correctly
  • Delete clip → storage bar decrements
  • Upload that would exceed quota → file shows inline error message (not just "FAILED")
  • ClipUploader header shows storage label ("1.2 GB / 2 GB")
  • Storage bar below header: orange normal, yellow > 80%, red > 95%
  • When storage full → upload zone replaced with "Storage full" message
  • YouTube import → does NOT count toward storage
  • Sidebar CreditBadge shows "STORAGE 85%" warning when > 80% (hidden below 80%)
  • Asset Manager sidebar shows storage bar with label
  • Asset cards show file size below name (e.g. "40 MB")
  • Asset group headers show total size (e.g. "3 ITEMS · 120 MB")
  • Sort assets by "Largest First" / "Smallest First" → correct order
  • Asset upload exceeding quota → red error banner with backend message
  • Storage bar/labels update without page refresh after upload/delete

13. Project Limits

  • Sidebar shows project count "⅔" next to PROJECTS nav item
  • At project limit → NEW PROJECT button disabled (opacity-50, cursor-not-allowed)
  • At project limit → hover shows tooltip "Project limit reached (3/3)"
  • At project limit → uploading files shows creation error inline in ClipUploader
  • At project limit → does NOT retry endlessly (check server logs for repeated 422s)
  • Settings → Billing → Usage shows "Active Projects: 2 / 3" (red at limit)
  • Delete a project → count updates, NEW PROJECT re-enabled

14. AI Director Gating

  • Hobby user → Director Notes textarea is disabled with "Creator+" label
  • Hobby user → Director Notes card has reduced opacity
  • Creator user → Director Notes works normally, shows "Optional"
  • Hobby user → typing in Director Notes not possible (disabled)

15. Export Retention

  • Export a video → export list shows "Xd left" next to READY status
  • Hobby export → expires after 14 days (check API response expires_at)
  • Creator export → expires after 30 days
  • Expired export → shows "EXPIRED" in red instead of "READY"
  • Expired export → download button hidden
  • Expired export → re-exporting the same project works fine
  • Expiry warning: ≤ 3 days remaining → "Xd left" in yellow

16. Watermark

  • Trial user: exported video has Sapari watermark (bottom-right, semi-transparent)
  • Hobby paid user: exported video has NO watermark
  • Creator paid user: exported video has NO watermark
  • Beta user (granted via admin invite, even with prior trial entitlement): exported video has NO watermark
  • Trial user: video preview shows watermark overlay
  • Hover over watermark in preview → "Remove watermark" label appears
  • Click watermark in preview → plan picker opens
  • Paid user: no watermark overlay in video preview
  • Beta user: no watermark overlay in video preview (preview must match export — both gate on billing.can_watermark_free)

Stripe CLI Reference

# Forward webhooks to local backend
stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe

# Trigger specific webhook for testing
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failed

# View recent events
stripe events list --limit 10

Test Cards

Card Behavior
4242 4242 4242 4242 Succeeds
4000 0000 0000 3220 Requires 3D Secure
4000 0000 0000 0002 Declined
4000 0000 0000 9995 Insufficient funds