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:
@@ -151,7 +151,7 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
|
||||
if (process.env.NODE_ENV === 'development') console.log('SubscriptionContext: Checking subscription for user:', userId);
|
||||
const response = await apiClient.get(`/api/subscription/status/${userId}`);
|
||||
let subscriptionData = response.data.data;
|
||||
const subscriptionData = response.data.data;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') console.log('SubscriptionContext: Subscription data received:', { active: subscriptionData?.active, plan: subscriptionData?.plan });
|
||||
|
||||
@@ -191,21 +191,6 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
// Update ref immediately so callbacks can access latest value
|
||||
subscriptionRef.current = subscriptionData;
|
||||
|
||||
if (subscriptionData && (subscriptionData.plan === 'free' || subscriptionData.plan === 'none')) {
|
||||
try {
|
||||
const verifyResponse = await apiClient.get(`/api/subscription/verify-checkout/${userId}`);
|
||||
const verifiedData = verifyResponse.data?.data;
|
||||
if (verifiedData && verifiedData.plan && verifiedData.plan !== 'free' && verifiedData.plan !== 'none') {
|
||||
subscriptionData = { ...subscriptionData, ...verifiedData };
|
||||
setSubscription(subscriptionData);
|
||||
subscriptionRef.current = subscriptionData;
|
||||
console.log('SubscriptionContext: Plan corrected via Stripe re-verification:', verifiedData.plan);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — Stripe may not be configured or user has no Stripe customer
|
||||
}
|
||||
}
|
||||
|
||||
// Check if subscription is expired/inactive and show modal
|
||||
// Show modal if subscription is inactive on initial load (when subscription was null before)
|
||||
// This ensures the modal shows when an end user navigates to the app
|
||||
@@ -393,6 +378,12 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
}
|
||||
}, [planSignature]);
|
||||
|
||||
// Ref so mount effect always calls latest verifyCheckout
|
||||
const verifyCheckoutRef = useRef(verifyCheckout);
|
||||
useEffect(() => {
|
||||
verifyCheckoutRef.current = verifyCheckout;
|
||||
}, [verifyCheckout]);
|
||||
|
||||
const showExpiredModal = useCallback(() => {
|
||||
setIsUsageLimitModal(false);
|
||||
setShowModal(true);
|
||||
@@ -721,6 +712,32 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
};
|
||||
}, []); // Remove checkSubscription dependency to prevent loop
|
||||
|
||||
// One-time Stripe sync after initial checkSubscription
|
||||
// Handles: Customer Portal returns, new subscriptions with delayed webhooks
|
||||
useEffect(() => {
|
||||
const pendingChange = sessionStorage.getItem('pending_subscription_change');
|
||||
if (pendingChange === 'true') {
|
||||
sessionStorage.removeItem('pending_subscription_change');
|
||||
}
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
const current = subscriptionRef.current;
|
||||
if (!current) return;
|
||||
const plan = (current.plan || '').toLowerCase();
|
||||
if (pendingChange === 'true' || plan === 'free' || plan === 'none') {
|
||||
console.log('[StripeSync] Syncing with Stripe after mount, reason:',
|
||||
pendingChange ? 'Customer Portal return' : 'free plan check');
|
||||
try {
|
||||
await verifyCheckoutRef.current();
|
||||
} catch {
|
||||
// verifyCheckout already logs errors internally
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // Only run on mount
|
||||
|
||||
const value: SubscriptionContextType = {
|
||||
subscription,
|
||||
loading,
|
||||
|
||||
Reference in New Issue
Block a user