feat: validate podcast cost estimation accuracy, document per-token costs, and fix subscription/plan enforcement

Issue #543 — Validate Estimated Cost Accuracy (UI vs Backend)

Backend:
- cost_estimator.py uses pricing catalog (APIProviderPricing) as single source of truth
- All 7 cost components: analysis, research (search+LLM), script, TTS, voice clone, avatar, video
- initialize_default_pricing() runs on every app startup for auto-sync

Frontend cost estimation fixes:
- Added missing analysisCost, scriptCost, voiceCloneCost to PodcastEstimate type
- toPodcastEstimate() now extracts all 7 backend fields (was dropping 3)
- headerCostEst maps analysisCost->Analyze, scriptCost->Write, voiceCloneCost->Produce
- EstimateCard shows 5 chips: Analysis, Research, Script, Voice(TTS+clone), Visuals(avatar+video)
- Chip sum now equals backend total for all configurations

Subscription & plan fixes:
- Removed Stripe re-verification from checkSubscription() (downgrade regression fix #539)
- Added verifyCheckoutRef pattern for reliable mount-time checkout polling
- One-time Stripe sync effect with pending_subscription_change flag for Customer Portal returns
- Free plan limits: stability_calls 3->10, audio_calls 5->10 (supports 2 podcasts)
- Image enforcement uses actual provider (GPT_PROVIDER), not hardcoded Stability
- Billing/pricing pages bypass onboarding check in ProtectedRoute
- Gradient buttons + loading spinner on plan chip in UserBadge
- Added metadata-based Stripe lookup fallback (Issue #538)

Documentation:
- TESTING_GUIDE.md: comprehensive testing instructions for non-technical testers
  - Free plan limits, usage tracking, cost estimation formulas
  - 10 test cases for UI verification
  - Troubleshooting guide
  - Quick-reference cost formulas with all default rates

Cleanup: removed legacy ToBeMigrated directory (70+ files, ~22K LOC)
GSC Brainstorm: service, hook, modal, and UI components for blog topic brainstorming
This commit is contained in:
ajaysi
2026-05-27 08:46:38 +05:30
parent 96fa469fe8
commit aaf94049da
100 changed files with 2953 additions and 22118 deletions

View File

@@ -39,7 +39,12 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const isFeatureLimited = shouldSkipOnboarding();
const defaultRoute = getDefaultLandingRoute();
const isOnDefaultRoute = typeof location?.pathname === 'string' && location.pathname.startsWith(defaultRoute);
const allowAccess = isOnboardingComplete || localComplete || (isFeatureLimited && isOnDefaultRoute);
// Allow access to utility pages regardless of onboarding status
const bypassRoutes = ['/billing', '/pricing', '/onboarding'];
const isBypassRoute = typeof location?.pathname === 'string' && bypassRoutes.some(route => location.pathname.startsWith(route));
const allowAccess = isOnboardingComplete || localComplete || (isFeatureLimited && isOnDefaultRoute) || isBypassRoute;
// Wait for Clerk to load before any redirect decisions
if (!isLoaded) {

View File

@@ -20,7 +20,7 @@ interface UserBadgeProps {
const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
const { user, isSignedIn } = useUser();
const { signOut } = useClerk();
const { subscription, refreshSubscription } = useSubscription();
const { subscription, refreshSubscription, loading } = useSubscription();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [systemStatus, setSystemStatus] = useState<'healthy' | 'warning' | 'critical' | 'unknown'>('unknown');
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -131,12 +131,18 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
label={getPlanLabel()}
size="small"
sx={{
bgcolor: `${getPlanColor()}20`,
border: `1px solid ${getPlanColor()}`,
color: getPlanColor(),
bgcolor: loading ? '#e5e7eb' : `${getPlanColor()}20`,
border: loading ? '1px solid #d1d5db' : `1px solid ${getPlanColor()}`,
color: loading ? '#9ca3af' : getPlanColor(),
fontWeight: 700,
fontSize: '0.75rem',
height: 24,
minWidth: loading ? 60 : 'auto',
animation: loading ? 'plan-pulse 1.5s ease-in-out infinite' : 'none',
'@keyframes plan-pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.4 },
},
}}
/>
@@ -236,13 +242,13 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
<IconButton
onClick={handleRefreshPlan}
size="small"
disabled={isRefreshing}
disabled={isRefreshing || loading}
sx={{
color: '#6b7280',
'&:hover': { bgcolor: '#e5e7eb' },
}}
>
{isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
{(isRefreshing || loading) ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
</IconButton>
</Tooltip>
</Box>
@@ -289,10 +295,10 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
<Divider sx={{ mx: 2 }} />
<MenuItem onClick={() => { handleClose(); saveNavigationState(window.location.pathname); window.location.href = '/pricing'; }} sx={{ mx: 1, borderRadius: 1, color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
<MenuItem onClick={() => { handleClose(); saveNavigationState(window.location.pathname); sessionStorage.setItem('pending_subscription_change', 'true'); window.location.href = '/pricing'; }} sx={{ mx: 1, borderRadius: 1, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', color: '#ffffff', fontWeight: 600, mb: 0.5, '&:hover': { background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)', boxShadow: '0 2px 8px rgba(99,102,241,0.4)' } }}>
Manage Subscription
</MenuItem>
<MenuItem onClick={() => { handleClose(); window.location.href = '/billing'; }} sx={{ mx: 1, borderRadius: 1, color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
<MenuItem onClick={() => { handleClose(); window.location.href = '/billing'; }} sx={{ mx: 1, borderRadius: 1, background: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%)', color: '#ffffff', fontWeight: 600, '&:hover': { background: 'linear-gradient(135deg, #0891b2 0%, #2563eb 100%)', boxShadow: '0 2px 8px rgba(6,182,212,0.4)' } }}>
View Costing Details
</MenuItem>
<MenuItem onClick={handleSignOut} sx={{ mx: 1, borderRadius: 1, color: '#6b7280', '&:hover': { bgcolor: '#fef2f2', color: '#ef4444' } }}>