feat: Quota overage system, updated tier pricing, and communication addons
Quota Overage System: - Add QuotaOverage model for tracking resource/user quota overages - Implement 30-day grace period with email notifications (immediate, 7-day, 1-day) - Add QuotaWarningBanner component in BusinessLayout - Add QuotaSettings page for managing overages and archiving resources - Add Celery tasks for automated quota checks and expiration handling - Add quota management API endpoints Updated Tier Pricing (Stripe: 2.9% + $0.30): - Free: No payments (requires addon) - Starter: 4% + $0.40 - Professional: 3.5% + $0.35 - Business: 3.25% + $0.32 - Enterprise: 3% + $0.30 New Subscription Addons: - Online Payments ($5/mo + 5% + $0.50) - for Free tier - SMS Notifications ($10/mo) - enables SMS reminders - Masked Calling ($15/mo) - enables anonymous calling BusinessEditModal Improvements: - Increased width to match PlanModal (max-w-3xl) - Added all tier options with auto-update on tier change - Added limits configuration and permissions sections Backend Fixes: - Fixed SubscriptionPlan serializer to include all communication fields - Allow blank business_tier for addon plans - Added migration for business_tier field changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
131
frontend/src/components/QuotaWarningBanner.tsx
Normal file
131
frontend/src/components/QuotaWarningBanner.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, X, ExternalLink } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { QuotaOverage } from '../api/auth';
|
||||
|
||||
interface QuotaWarningBannerProps {
|
||||
overages: QuotaOverage[];
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const QuotaWarningBanner: React.FC<QuotaWarningBannerProps> = ({ overages, onDismiss }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!overages || overages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the most urgent overage (least days remaining)
|
||||
const mostUrgent = overages.reduce((prev, curr) =>
|
||||
curr.days_remaining < prev.days_remaining ? curr : prev
|
||||
);
|
||||
|
||||
const isUrgent = mostUrgent.days_remaining <= 7;
|
||||
const isCritical = mostUrgent.days_remaining <= 1;
|
||||
|
||||
const getBannerStyles = () => {
|
||||
if (isCritical) {
|
||||
return 'bg-red-600 text-white border-red-700';
|
||||
}
|
||||
if (isUrgent) {
|
||||
return 'bg-amber-500 text-white border-amber-600';
|
||||
}
|
||||
return 'bg-amber-100 text-amber-900 border-amber-300';
|
||||
};
|
||||
|
||||
const getIconColor = () => {
|
||||
if (isCritical || isUrgent) {
|
||||
return 'text-white';
|
||||
}
|
||||
return 'text-amber-600';
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`border-b ${getBannerStyles()}`}>
|
||||
<div className="max-w-7xl mx-auto px-4 py-3 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className={`h-5 w-5 flex-shrink-0 ${getIconColor()}`} />
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
||||
<span className="font-medium">
|
||||
{isCritical
|
||||
? t('quota.banner.critical', 'URGENT: Automatic archiving tomorrow!')
|
||||
: isUrgent
|
||||
? t('quota.banner.urgent', 'Action Required: {{days}} days left', { days: mostUrgent.days_remaining })
|
||||
: t('quota.banner.warning', 'Quota exceeded for {{count}} item(s)', { count: overages.length })
|
||||
}
|
||||
</span>
|
||||
<span className="text-sm opacity-90">
|
||||
{t('quota.banner.details',
|
||||
'You have {{overage}} {{type}} over your plan limit. Grace period ends {{date}}.',
|
||||
{
|
||||
overage: mostUrgent.overage_amount,
|
||||
type: mostUrgent.display_name,
|
||||
date: formatDate(mostUrgent.grace_period_ends_at)
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to="/settings/quota"
|
||||
className={`inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
isCritical || isUrgent
|
||||
? 'bg-white/20 hover:bg-white/30 text-white'
|
||||
: 'bg-amber-600 hover:bg-amber-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{t('quota.banner.manage', 'Manage Quota')}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Link>
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className={`p-1 rounded-md transition-colors ${
|
||||
isCritical || isUrgent
|
||||
? 'hover:bg-white/20'
|
||||
: 'hover:bg-amber-200'
|
||||
}`}
|
||||
aria-label={t('common.dismiss', 'Dismiss')}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show additional overages if there are more than one */}
|
||||
{overages.length > 1 && (
|
||||
<div className="mt-2 text-sm opacity-90">
|
||||
<span className="font-medium">{t('quota.banner.allOverages', 'All overages:')}</span>
|
||||
<ul className="ml-4 mt-1 space-y-0.5">
|
||||
{overages.map((overage) => (
|
||||
<li key={overage.id}>
|
||||
{overage.display_name}: {overage.current_usage}/{overage.allowed_limit}
|
||||
({t('quota.banner.overBy', 'over by {{amount}}', { amount: overage.overage_amount })})
|
||||
{' - '}
|
||||
{overage.days_remaining <= 0
|
||||
? t('quota.banner.expiredToday', 'expires today!')
|
||||
: t('quota.banner.daysLeft', '{{days}} days left', { days: overage.days_remaining })
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuotaWarningBanner;
|
||||
Reference in New Issue
Block a user