multi sector clustering erroro rasing, adn tasks page word coutn monthly limits removal
This commit is contained in:
@@ -1032,7 +1032,7 @@ from rest_framework.decorators import api_view, permission_classes
|
||||
def get_usage_summary(request):
|
||||
"""
|
||||
Get comprehensive usage summary for current account.
|
||||
Includes hard limits (sites, users, keywords, clusters) and monthly limits (ideas, words, images).
|
||||
Includes hard limits (sites, users, keywords) and monthly limits (ahrefs queries only).
|
||||
|
||||
GET /api/v1/billing/usage-summary/
|
||||
"""
|
||||
|
||||
@@ -257,12 +257,11 @@ class BillingConfiguration(models.Model):
|
||||
|
||||
class PlanLimitUsage(AccountBaseModel):
|
||||
"""
|
||||
Track monthly usage of plan limits (ideas, words, images, prompts)
|
||||
Track monthly usage of plan limits (ideas, images, prompts)
|
||||
Resets at start of each billing period
|
||||
"""
|
||||
LIMIT_TYPE_CHOICES = [
|
||||
('content_ideas', 'Content Ideas'),
|
||||
('content_words', 'Content Words'),
|
||||
('images_basic', 'Basic Images'),
|
||||
('images_premium', 'Premium Images'),
|
||||
('image_prompts', 'Image Prompts'),
|
||||
|
||||
@@ -358,31 +358,8 @@ class Content(SoftDeletableModel, SiteSectorBaseModel):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Increment usage for new content or if word count increased
|
||||
if self.content_html and self.word_count:
|
||||
# Only count newly generated words
|
||||
new_words = self.word_count - old_word_count if not is_new else self.word_count
|
||||
|
||||
if new_words > 0:
|
||||
from igny8_core.business.billing.services.limit_service import LimitService
|
||||
try:
|
||||
# Get account from site
|
||||
account = self.site.account if self.site else None
|
||||
if account:
|
||||
LimitService.increment_usage(
|
||||
account=account,
|
||||
limit_type='content_words',
|
||||
amount=new_words,
|
||||
metadata={
|
||||
'content_id': self.id,
|
||||
'content_title': self.title,
|
||||
'site_id': self.site.id if self.site else None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Error incrementing word usage for content {self.id}: {str(e)}")
|
||||
# NOTE: Content words no longer tracked as a monthly plan limit.
|
||||
# Credits are the only enforcement for content generation.
|
||||
|
||||
def soft_delete(self, user=None, reason=None, retention_days=None):
|
||||
"""
|
||||
|
||||
@@ -30,20 +30,12 @@ class ContentGenerationService:
|
||||
Raises:
|
||||
InsufficientCreditsError: If account doesn't have enough credits
|
||||
"""
|
||||
from igny8_core.business.billing.services.limit_service import LimitService, MonthlyLimitExceededError
|
||||
|
||||
# Get tasks
|
||||
tasks = Tasks.objects.filter(id__in=task_ids, account=account)
|
||||
|
||||
# Calculate estimated credits needed based on word count
|
||||
total_word_count = sum(task.word_count or 1000 for task in tasks)
|
||||
|
||||
# Check monthly word count limit
|
||||
try:
|
||||
LimitService.check_monthly_limit(account, 'content_words', amount=total_word_count)
|
||||
except MonthlyLimitExceededError as e:
|
||||
raise InsufficientCreditsError(str(e))
|
||||
|
||||
# Check credits
|
||||
try:
|
||||
self.credit_service.check_credits(account, 'content_generation', total_word_count)
|
||||
|
||||
@@ -292,7 +292,6 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
content_type_filter = request.query_params.get('content_type', '')
|
||||
content_structure_filter = request.query_params.get('content_structure', '')
|
||||
cluster_filter = request.query_params.get('cluster', '')
|
||||
source_filter = request.query_params.get('source', '')
|
||||
search = request.query_params.get('search', '')
|
||||
|
||||
# Apply search to base queryset
|
||||
@@ -310,8 +309,6 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
status_qs = status_qs.filter(content_structure=content_structure_filter)
|
||||
if cluster_filter:
|
||||
status_qs = status_qs.filter(cluster_id=cluster_filter)
|
||||
if source_filter:
|
||||
status_qs = status_qs.filter(source=source_filter)
|
||||
statuses = list(set(status_qs.values_list('status', flat=True)))
|
||||
statuses = sorted([s for s in statuses if s])
|
||||
status_labels = {
|
||||
@@ -333,8 +330,6 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
type_qs = type_qs.filter(content_structure=content_structure_filter)
|
||||
if cluster_filter:
|
||||
type_qs = type_qs.filter(cluster_id=cluster_filter)
|
||||
if source_filter:
|
||||
type_qs = type_qs.filter(source=source_filter)
|
||||
content_types = list(set(type_qs.values_list('content_type', flat=True)))
|
||||
content_types = sorted([t for t in content_types if t])
|
||||
type_labels = {
|
||||
@@ -356,8 +351,6 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
structure_qs = structure_qs.filter(content_type=content_type_filter)
|
||||
if cluster_filter:
|
||||
structure_qs = structure_qs.filter(cluster_id=cluster_filter)
|
||||
if source_filter:
|
||||
structure_qs = structure_qs.filter(source=source_filter)
|
||||
structures = list(set(structure_qs.values_list('content_structure', flat=True)))
|
||||
structures = sorted([s for s in structures if s])
|
||||
structure_labels = {
|
||||
@@ -381,8 +374,6 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
cluster_qs = cluster_qs.filter(content_type=content_type_filter)
|
||||
if content_structure_filter:
|
||||
cluster_qs = cluster_qs.filter(content_structure=content_structure_filter)
|
||||
if source_filter:
|
||||
cluster_qs = cluster_qs.filter(source=source_filter)
|
||||
from igny8_core.modules.planner.models import Clusters
|
||||
cluster_ids = list(set(
|
||||
cluster_qs.exclude(cluster_id__isnull=True)
|
||||
@@ -394,35 +385,12 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
for c in clusters
|
||||
]
|
||||
|
||||
# Get sources (filtered by other fields)
|
||||
source_qs = base_qs
|
||||
if status_filter:
|
||||
source_qs = source_qs.filter(status=status_filter)
|
||||
if content_type_filter:
|
||||
source_qs = source_qs.filter(content_type=content_type_filter)
|
||||
if content_structure_filter:
|
||||
source_qs = source_qs.filter(content_structure=content_structure_filter)
|
||||
if cluster_filter:
|
||||
source_qs = source_qs.filter(cluster_id=cluster_filter)
|
||||
sources = list(set(source_qs.values_list('source', flat=True)))
|
||||
sources = sorted([s for s in sources if s])
|
||||
source_labels = {
|
||||
'manual': 'Manual',
|
||||
'planner': 'Planner',
|
||||
'ai': 'AI',
|
||||
}
|
||||
source_options = [
|
||||
{'value': s, 'label': source_labels.get(s, s.title())}
|
||||
for s in sources
|
||||
]
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'statuses': status_options,
|
||||
'content_types': content_type_options,
|
||||
'content_structures': content_structure_options,
|
||||
'clusters': cluster_options,
|
||||
'sources': source_options,
|
||||
},
|
||||
request=request
|
||||
)
|
||||
@@ -970,16 +938,6 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
word_count = calculate_word_count(html_content)
|
||||
serializer.validated_data['word_count'] = word_count
|
||||
|
||||
# Check monthly word count limit (enforces ALL entry points: manual, import, AI, automation)
|
||||
if account and word_count > 0:
|
||||
from igny8_core.business.billing.services.limit_service import LimitService, MonthlyLimitExceededError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
try:
|
||||
LimitService.check_monthly_limit(account, 'content_words', amount=word_count)
|
||||
except MonthlyLimitExceededError as e:
|
||||
raise ValidationError(str(e))
|
||||
|
||||
if account:
|
||||
serializer.save(account=account)
|
||||
else:
|
||||
|
||||
@@ -22,11 +22,7 @@ def reset_monthly_plan_limits():
|
||||
It finds all accounts where the billing period has ended and resets their monthly usage.
|
||||
|
||||
Monthly limits that get reset:
|
||||
- content_ideas
|
||||
- content_words
|
||||
- images_basic
|
||||
- images_premium
|
||||
- image_prompts
|
||||
- ahrefs_queries
|
||||
|
||||
Hard limits (sites, users, keywords, clusters) are NOT reset.
|
||||
"""
|
||||
|
||||
@@ -667,7 +667,7 @@ export default function ProgressModal({
|
||||
className="max-w-lg"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<div className="p-6 min-h-[200px]">
|
||||
<div className={`p-6 ${status === 'error' ? 'min-h-[140px]' : 'min-h-[200px]'}`}>
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1 text-center">
|
||||
@@ -735,7 +735,7 @@ export default function ProgressModal({
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{!showSuccess && status !== 'completed' && (
|
||||
{!showSuccess && status !== 'completed' && status !== 'error' && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
{(() => {
|
||||
const funcName = (functionId || title || '').toLowerCase();
|
||||
@@ -791,7 +791,29 @@ export default function ProgressModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Checklist-style Progress Steps - Always visible */}
|
||||
{/* Error Alert (shown when status is error) */}
|
||||
{status === 'error' && (
|
||||
<div className="mb-6">
|
||||
{/* Big centered error icon */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="w-16 h-16 rounded-full bg-error-600 dark:bg-error-700 flex items-center justify-center">
|
||||
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dark error alert box with centered text */}
|
||||
<div className="p-5 rounded-lg bg-error-600 dark:bg-error-700 border border-error-700 dark:border-error-600">
|
||||
<div className="text-base font-semibold text-white text-center whitespace-pre-line">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Checklist-style Progress Steps - Hide on error */}
|
||||
{status !== 'error' && (
|
||||
<div className="mb-6 space-y-3">
|
||||
{checklistItems.map((item, index) => (
|
||||
<div
|
||||
@@ -826,6 +848,7 @@ export default function ProgressModal({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
{showSuccess && onClose && (
|
||||
|
||||
@@ -304,6 +304,67 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
|
||||
if (response.state === 'PROGRESS') {
|
||||
const meta = response.meta || {};
|
||||
const result = (meta as any).result || {};
|
||||
const details = (meta as any).details || {};
|
||||
const failedSuccess = meta.success === false || result.success === false || details.success === false;
|
||||
|
||||
// Check if this is an error in progress (validation failed during execution)
|
||||
if (failedSuccess || meta.phase === 'ERROR' || (meta.error && meta.error_type)) {
|
||||
const errorMsg = meta.error || result.error || details.error || meta.message || 'Operation failed';
|
||||
const errorType = meta.error_type || result.error_type || details.error_type || 'Error';
|
||||
setProgress({
|
||||
percentage: 0,
|
||||
message: `${errorType}: ${errorMsg}`,
|
||||
status: 'error',
|
||||
details: meta.details,
|
||||
});
|
||||
|
||||
// Update step logs from error response
|
||||
if (meta.request_steps || meta.response_steps) {
|
||||
const allSteps: Array<{
|
||||
stepNumber: number;
|
||||
stepName: string;
|
||||
status: string;
|
||||
message: string;
|
||||
timestamp?: number;
|
||||
}> = [];
|
||||
|
||||
if (meta.request_steps && Array.isArray(meta.request_steps)) {
|
||||
meta.request_steps.forEach((step: any) => {
|
||||
allSteps.push({
|
||||
stepNumber: step.stepNumber || 0,
|
||||
stepName: step.stepName || 'Unknown',
|
||||
status: step.status || 'error',
|
||||
message: step.message || step.error || '',
|
||||
timestamp: step.timestamp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (meta.response_steps && Array.isArray(meta.response_steps)) {
|
||||
meta.response_steps.forEach((step: any) => {
|
||||
allSteps.push({
|
||||
stepNumber: step.stepNumber || 0,
|
||||
stepName: step.stepName || 'Unknown',
|
||||
status: step.status || 'error',
|
||||
message: step.message || step.error || '',
|
||||
timestamp: step.timestamp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
||||
setStepLogs(allSteps);
|
||||
}
|
||||
|
||||
// Stop polling on validation error
|
||||
isStopped = true;
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine current step from request_steps/response_steps first (most accurate)
|
||||
let currentStep: string | null = null;
|
||||
@@ -519,6 +580,69 @@ export function useProgressModal(): UseProgressModalReturn {
|
||||
}
|
||||
} else if (response.state === 'SUCCESS') {
|
||||
const meta = response.meta || {};
|
||||
const result = (meta as any).result || {};
|
||||
const details = (meta as any).details || {};
|
||||
const failedSuccess = meta.success === false || result.success === false || details.success === false;
|
||||
|
||||
// Check if the task completed but with a validation/business logic error
|
||||
// (success: false can be returned inside meta.result/meta.details)
|
||||
if (failedSuccess) {
|
||||
// This is a validation or business logic error - treat as failure
|
||||
const errorMsg = meta.error || result.error || details.error || meta.message || 'Operation failed';
|
||||
const errorType = meta.error_type || result.error_type || details.error_type || 'Error';
|
||||
setProgress({
|
||||
percentage: 0,
|
||||
message: `${errorType}: ${errorMsg}`,
|
||||
status: 'error',
|
||||
details: meta.details,
|
||||
});
|
||||
|
||||
// Update step logs from failure response
|
||||
if (meta.request_steps || meta.response_steps) {
|
||||
const allSteps: Array<{
|
||||
stepNumber: number;
|
||||
stepName: string;
|
||||
status: string;
|
||||
message: string;
|
||||
timestamp?: number;
|
||||
}> = [];
|
||||
|
||||
if (meta.request_steps && Array.isArray(meta.request_steps)) {
|
||||
meta.request_steps.forEach((step: any) => {
|
||||
allSteps.push({
|
||||
stepNumber: step.stepNumber || 0,
|
||||
stepName: step.stepName || 'Unknown',
|
||||
status: step.status || 'error',
|
||||
message: step.message || step.error || '',
|
||||
timestamp: step.timestamp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (meta.response_steps && Array.isArray(meta.response_steps)) {
|
||||
meta.response_steps.forEach((step: any) => {
|
||||
allSteps.push({
|
||||
stepNumber: step.stepNumber || 0,
|
||||
stepName: step.stepName || 'Unknown',
|
||||
status: step.status || 'error',
|
||||
message: step.message || step.error || '',
|
||||
timestamp: step.timestamp,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
allSteps.sort((a, b) => a.stepNumber - b.stepNumber);
|
||||
setStepLogs(allSteps);
|
||||
}
|
||||
|
||||
// Stop polling on validation error
|
||||
isStopped = true;
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing transition timeout
|
||||
if (stepTransitionTimeoutRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user