Add TenantCustomTier system and fix BusinessEditModal feature loading

Backend:
- Add TenantCustomTier model for per-tenant feature overrides
- Update EntitlementService to check custom tier before plan features
- Add custom_tier action on TenantViewSet (GET/PUT/DELETE)
- Add Celery task for grace period management (30-day expiry)

Frontend:
- Add DynamicFeaturesEditor component for dynamic feature management
- Fix BusinessEditModal to load features from plan defaults when no custom tier
- Update limits (max_users, max_resources, etc.) to use featureValues
- Remove outdated canonical feature check from FeaturePicker (removes warning icons)
- Add useBillingPlans hook for accessing billing system data
- Add custom tier API functions to platform.ts

Features now follow consistent rules:
- Load from plan defaults when no custom tier exists
- Load from custom tier when one exists
- Reset to plan defaults when plan changes
- Save to custom tier on edit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-12 21:00:54 -05:00
parent d25c578e59
commit b384d9912a
183 changed files with 47627 additions and 3955 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import PricingTable from '../../components/marketing/PricingTable';
import DynamicPricingCards from '../../components/marketing/DynamicPricingCards';
import FeatureComparisonTable from '../../components/marketing/FeatureComparisonTable';
import FAQAccordion from '../../components/marketing/FAQAccordion';
import CTASection from '../../components/marketing/CTASection';
@@ -19,9 +20,25 @@ const PricingPage: React.FC = () => {
</p>
</div>
{/* Pricing Table */}
{/* Dynamic Pricing Cards */}
<div className="pb-20">
<PricingTable />
<DynamicPricingCards />
</div>
{/* Feature Comparison Table */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-4">
{t('marketing.pricing.featureComparison.title', 'Compare Plans')}
</h2>
<p className="text-center text-gray-600 dark:text-gray-400 mb-12 max-w-2xl mx-auto">
{t(
'marketing.pricing.featureComparison.subtitle',
'See exactly what you get with each plan'
)}
</p>
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<FeatureComparisonTable />
</div>
</div>
{/* FAQ Section */}
@@ -29,24 +46,26 @@ const PricingPage: React.FC = () => {
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-12">
{t('marketing.pricing.faq.title')}
</h2>
<FAQAccordion items={[
{
question: t('marketing.pricing.faq.needPython.question'),
answer: t('marketing.pricing.faq.needPython.answer')
},
{
question: t('marketing.pricing.faq.exceedLimits.question'),
answer: t('marketing.pricing.faq.exceedLimits.answer')
},
{
question: t('marketing.pricing.faq.customDomain.question'),
answer: t('marketing.pricing.faq.customDomain.answer')
},
{
question: t('marketing.pricing.faq.dataSafety.question'),
answer: t('marketing.pricing.faq.dataSafety.answer')
}
]} />
<FAQAccordion
items={[
{
question: t('marketing.pricing.faq.needPython.question'),
answer: t('marketing.pricing.faq.needPython.answer'),
},
{
question: t('marketing.pricing.faq.exceedLimits.question'),
answer: t('marketing.pricing.faq.exceedLimits.answer'),
},
{
question: t('marketing.pricing.faq.customDomain.question'),
answer: t('marketing.pricing.faq.customDomain.answer'),
},
{
question: t('marketing.pricing.faq.dataSafety.question'),
answer: t('marketing.pricing.faq.dataSafety.answer'),
},
]}
/>
</div>
{/* CTA */}