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

@@ -98,7 +98,7 @@ const PluginMarketplace: React.FC = () => {
const hasPluginsFeature = canUse('plugins');
const isLocked = !hasPluginsFeature;
// Fetch marketplace plugins
// Fetch marketplace plugins - only when user has the feature
const { data: plugins = [], isLoading, error } = useQuery<PluginTemplate[]>({
queryKey: ['plugin-templates', 'marketplace'],
queryFn: async () => {
@@ -121,6 +121,8 @@ const PluginMarketplace: React.FC = () => {
pluginCode: p.plugin_code,
}));
},
// Don't fetch if user doesn't have the plugins feature
enabled: hasPluginsFeature && !permissionsLoading,
});
// Fetch installed plugins to check which are already installed
@@ -130,6 +132,8 @@ const PluginMarketplace: React.FC = () => {
const { data } = await api.get('/plugin-installations/');
return data;
},
// Don't fetch if user doesn't have the plugins feature
enabled: hasPluginsFeature && !permissionsLoading,
});
// Create a set of installed template IDs for quick lookup
@@ -223,7 +227,10 @@ const PluginMarketplace: React.FC = () => {
);
}
if (error) {
// Check if error is a 403 (plan restriction) - show upgrade prompt instead
const is403Error = error && (error as any)?.response?.status === 403;
if (error && !is403Error) {
return (
<div className="p-8">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
@@ -235,8 +242,11 @@ const PluginMarketplace: React.FC = () => {
);
}
// If 403 error, treat as locked
const effectivelyLocked = isLocked || is403Error;
return (
<LockedSection feature="plugins" isLocked={isLocked} variant="overlay">
<LockedSection feature="plugins" isLocked={effectivelyLocked} variant="overlay">
<div className="p-8 space-y-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between">