Compare commits

..

1 Commits

Author SHA1 Message Date
ي
d1ff406d03 Feature-flag pricing tier availability for alpha/demo modes 2026-03-30 07:49:56 +05:30
5 changed files with 85 additions and 67 deletions

View File

@@ -16,22 +16,15 @@ class RouterManager:
self.app = app self.app = app
self.included_routers = [] self.included_routers = []
self.failed_routers = [] self.failed_routers = []
self._included_router_names = set()
def include_router_safely(self, router, router_name: str = None) -> bool: def include_router_safely(self, router, router_name: str = None) -> bool:
"""Include a router safely with error handling.""" """Include a router safely with error handling."""
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
router_name = router_name or getattr(router, 'prefix', 'unknown')
if router_name in self._included_router_names:
if verbose:
logger.info(f"↩️ Router already included, skipping duplicate: {router_name}")
return True
try: try:
self.app.include_router(router) self.app.include_router(router)
router_name = router_name or getattr(router, 'prefix', 'unknown')
self.included_routers.append(router_name) self.included_routers.append(router_name)
self._included_router_names.add(router_name)
if verbose: if verbose:
logger.info(f"✅ Router included successfully: {router_name}") logger.info(f"✅ Router included successfully: {router_name}")
return True return True
@@ -47,21 +40,19 @@ class RouterManager:
# Import os locally to avoid UnboundLocalError if it's shadowed # Import os locally to avoid UnboundLocalError if it's shadowed
import os import os
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true" verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
demo_mode = os.getenv("ALWRITY_DEMO_MODE", "false").lower() == "true"
try: try:
if verbose: if verbose:
logger.info(f"Including core routers (demo_mode={demo_mode})...") logger.info("Including core routers...")
# Subscription router MUST always be included (including demo mode) so
# payment/preflight/subscription endpoints remain available.
from api.subscription import router as subscription_router
self.include_router_safely(subscription_router, "subscription")
# Component logic router # Component logic router
from api.component_logic import router as component_logic_router from api.component_logic import router as component_logic_router
self.include_router_safely(component_logic_router, "component_logic") self.include_router_safely(component_logic_router, "component_logic")
# Subscription router
from api.subscription import router as subscription_router
self.include_router_safely(subscription_router, "subscription")
# Step 3 Research router (core onboarding functionality) # Step 3 Research router (core onboarding functionality)
from api.onboarding_utils.step3_routes import router as step3_research_router from api.onboarding_utils.step3_routes import router as step3_research_router
self.include_router_safely(step3_research_router, "step3_research") self.include_router_safely(step3_research_router, "step3_research")

View File

@@ -260,9 +260,6 @@ async def onboarding_status():
# Include routers using modular utilities # Include routers using modular utilities
router_manager.include_core_routers() router_manager.include_core_routers()
# Safety net: keep subscription routes available even if core inclusion flow changes
# in special modes (e.g., demo mode). De-dup is handled by RouterManager.
router_manager.include_router_safely(subscription_router, "subscription")
router_manager.include_optional_routers() router_manager.include_optional_routers()
# Include assets serving router (must be mounted to serve generated images) # Include assets serving router (must be mounted to serve generated images)

View File

@@ -244,9 +244,6 @@ async def onboarding_status():
# Include routers using modular utilities # Include routers using modular utilities
router_manager.include_core_routers() router_manager.include_core_routers()
# Safety net: keep subscription routes available even if core inclusion flow changes
# in special modes (e.g., demo mode). De-dup is handled by RouterManager.
router_manager.include_router_safely(subscription_router, "subscription")
router_manager.include_optional_routers() router_manager.include_optional_routers()
# SEO Dashboard endpoints # SEO Dashboard endpoints

View File

@@ -52,6 +52,27 @@ export interface SubscriptionPlan {
} }
const PricingPage: React.FC = () => { const PricingPage: React.FC = () => {
const pricingMode = (process.env.REACT_APP_PRICING_MODE || 'alpha').toLowerCase();
const isAlphaMode = pricingMode === 'alpha';
const tierPolicyByMode: Record<string, { visible: string[]; selectable: string[]; unavailableLabels: Record<string, string> }> = {
alpha: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic'],
unavailableLabels: { pro: 'Coming Soon', enterprise: 'Contact Sales' },
},
demo: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic', 'pro'],
unavailableLabels: { enterprise: 'Contact Sales' },
},
production: {
visible: ['free', 'basic', 'pro', 'enterprise'],
selectable: ['free', 'basic', 'pro'],
unavailableLabels: { enterprise: 'Contact Sales' },
},
};
const activeTierPolicy = tierPolicyByMode[pricingMode] || tierPolicyByMode.alpha;
const navigate = useNavigate(); const navigate = useNavigate();
const [plans, setPlans] = useState<SubscriptionPlan[]>([]); const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -76,9 +97,11 @@ const PricingPage: React.FC = () => {
try { try {
setLoading(true); setLoading(true);
const response = await apiClient.get('/api/subscription/plans'); const response = await apiClient.get('/api/subscription/plans');
// Filter out any alpha plans and ensure we only show the 4 main tiers // Filter out legacy alpha-named plans and enforce tier visibility policy.
const filteredPlans = response.data.data.plans.filter( const filteredPlans = response.data.data.plans.filter(
(plan: SubscriptionPlan) => !plan.name.toLowerCase().includes('alpha') (plan: SubscriptionPlan) =>
!plan.name.toLowerCase().includes('alpha') &&
activeTierPolicy.visible.includes(plan.tier)
); );
setPlans(filteredPlans); setPlans(filteredPlans);
} catch (err) { } catch (err) {
@@ -111,10 +134,13 @@ const PricingPage: React.FC = () => {
return; return;
} }
// For alpha testing, only allow Free and Basic plans (Pro features not ready) if (!activeTierPolicy.selectable.includes(plan.tier)) {
if (plan.tier !== 'free' && plan.tier !== 'basic') {
console.error('[PricingPage] Plan tier not available:', plan.tier); console.error('[PricingPage] Plan tier not available:', plan.tier);
setError('This plan is not available for alpha testing'); setError(
isAlphaMode
? 'This plan is not available during alpha testing'
: 'This plan is currently not available for self-serve checkout'
);
return; return;
} }
@@ -351,6 +377,8 @@ const PricingPage: React.FC = () => {
yearlyBilling={yearlyBilling} yearlyBilling={yearlyBilling}
selectedPlanId={selectedPlan} selectedPlanId={selectedPlan}
subscribing={subscribing} subscribing={subscribing}
canSelectPlan={activeTierPolicy.selectable.includes(plan.tier)}
unavailableLabel={activeTierPolicy.unavailableLabels[plan.tier]}
onSelectPlan={setSelectedPlan} onSelectPlan={setSelectedPlan}
onSubscribe={handleSubscribe} onSubscribe={handleSubscribe}
openKnowMoreModal={openKnowMoreModal} openKnowMoreModal={openKnowMoreModal}
@@ -392,10 +420,11 @@ const PricingPage: React.FC = () => {
}}> }}>
<Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ color: 'warning.main' }} /> <Warning sx={{ color: 'warning.main' }} />
Alpha Testing Subscription {isAlphaMode ? 'Alpha Testing Subscription' : 'Confirm Subscription'}
</Typography> </Typography>
{/* Alpha Testing Notice */} {isAlphaMode ? (
<>
<Alert severity="warning" sx={{ mb: 2 }}> <Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}> <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
⚠️ Alpha Testing Mode - No Payment Required ⚠️ Alpha Testing Mode - No Payment Required
@@ -406,10 +435,9 @@ const PricingPage: React.FC = () => {
</Alert> </Alert>
<Typography variant="body1" sx={{ mb: 2 }}> <Typography variant="body1" sx={{ mb: 2 }}>
Thank you for participating in our alpha testing! We're crediting the Basic plan ($29 value) to your account. Thank you for participating in our alpha testing! We&apos;re crediting this plan to your account.
</Typography> </Typography>
{/* TODO: Payment Integration Notice */}
<Box sx={{ <Box sx={{
p: 2, p: 2,
mb: 3, mb: 3,
@@ -428,6 +456,12 @@ const PricingPage: React.FC = () => {
• Upgrade/downgrade options • Upgrade/downgrade options
</Typography> </Typography>
</Box> </Box>
</>
) : (
<Typography variant="body1" sx={{ mb: 3 }}>
Please confirm to continue with your selected subscription plan.
</Typography>
)}
{/* Note: Current behavior allows renewal without payment verification */} {/* Note: Current behavior allows renewal without payment verification */}
{/* This is intentional for alpha testing but will be secured in production */} {/* This is intentional for alpha testing but will be secured in production */}

View File

@@ -69,6 +69,8 @@ interface PlanCardProps {
yearlyBilling: boolean; yearlyBilling: boolean;
selectedPlanId: number | null; selectedPlanId: number | null;
subscribing: boolean; subscribing: boolean;
canSelectPlan: boolean;
unavailableLabel?: string;
onSelectPlan: (planId: number) => void; onSelectPlan: (planId: number) => void;
onSubscribe: (planId: number) => void; onSubscribe: (planId: number) => void;
openKnowMoreModal: (title: string, content: React.ReactNode) => void; openKnowMoreModal: (title: string, content: React.ReactNode) => void;
@@ -79,6 +81,8 @@ const PlanCard: React.FC<PlanCardProps> = ({
yearlyBilling, yearlyBilling,
selectedPlanId, selectedPlanId,
subscribing, subscribing,
canSelectPlan,
unavailableLabel,
onSelectPlan, onSelectPlan,
onSubscribe, onSubscribe,
openKnowMoreModal, openKnowMoreModal,
@@ -909,13 +913,9 @@ const PlanCard: React.FC<PlanCardProps> = ({
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}> <CardActions sx={{ justifyContent: 'center', pb: 3, flexDirection: 'column', gap: 1 }}>
{plan.tier === 'pro' ? ( {!canSelectPlan ? (
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}> <Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
Coming Soon {unavailableLabel || 'Unavailable'}
</Button>
) : plan.tier === 'enterprise' ? (
<Button variant="outlined" size="large" fullWidth disabled sx={{ mb: 1 }}>
Contact Sales
</Button> </Button>
) : ( ) : (
<> <>
@@ -951,4 +951,3 @@ const PlanCard: React.FC<PlanCardProps> = ({
}; };
export default PlanCard; export default PlanCard;