mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-08 18:35:00 +03:00
Coverage-matrix operation x entitlement/quota x transport in INVENTORY-CUSTOCRM-0009. Enforcement infrastructure: - HasEntitlement DRF permission (org-level feature gate, orthogonal to HasCapability); centralized api_exception_handler mapping EntitlementRequired->403, QuotaExceeded HARD->409 / RATE+CONCURRENT->429 (+Retry-After), PolicyUnavailable->503, UsageConflict->409. - quota_service.check() advisory pre-check. - UsageReservation lease model + reserve_usage/release_usage/ expire_stale_reservations for CONCURRENT quotas (SPEC-HUB-0022 section 7); worker sweep reaps lapsed leases. QuotaExceeded now carries mode. Cumulative quota gates (record_usage, mirror ai_agent_slots): - products (create_product), client_connections (create_integration), new_dialogs_per_period (new dialog only, is_new branch in ingest + support), managed_ai_credits (successful LlmInvocation, best-effort accounting). Storage gate: assert_storage_quota pre-write check blocks overage on writes, reads never gated (PLAN section 12). Concurrent p2p_calls: reserve on CallSession create, release on terminal transition in calls/lifecycle.py. Entitlement gates on views: sales_department, support_department, product_sales_api (inline), p2p_calls, knowledge_base, managed_ai (inline handoff on deny). Deferred (no target module): crm_api, voice_ai, sso_saml, concurrent_voice_sessions, customer_audit (tracks B/C/C12). bootstrap_edevs_owner now provisions an active STARTUP subscription (ensure_default_subscription) since post-C07 every tenant operation requires a subscription; create_test_subscription reconciles ai_agent_slot counter for tests that create active agents directly. Gate (PLAN section 12): coverage-matrix exists; storage overage blocks writes not reads; downgrade/suspension zero-limit clears entitlements without delete; Free counts only a brand-new dialog. Targeted tests: reservations 5, quota_service 4, enforcement mapping 6. Full backend regression green except one pre-existing SOCKS5 test and one pre-existing membership-migration ProtectedError (both fail on clean HEAD). UI unchanged. Production migration/deploy and publication of production PlanVersion are not part of this commit.
92 lines
3.6 KiB
Python
92 lines
3.6 KiB
Python
from hub_platform.subscriptions.keys import PlanCode
|
|
from hub_platform.subscriptions.models import (
|
|
EntitlementDefinition,
|
|
EntitlementGrant,
|
|
Plan,
|
|
PlanVersion,
|
|
QuotaDefinition,
|
|
QuotaGrant,
|
|
QuotaLimitSource,
|
|
QuotaMode,
|
|
Subscription,
|
|
)
|
|
from hub_platform.subscriptions.plan_service import publish_plan_version
|
|
from hub_platform.subscriptions.subscription_service import create_subscription
|
|
from hub_platform.testing import system_tenant_context
|
|
|
|
|
|
def create_test_subscription(organization, *, quantity: int = 1, plan_code=PlanCode.STARTUP):
|
|
# Idempotent: if bootstrap or a prior call already created a subscription,
|
|
# return it (C07 bootstrap now provisions a default subscription).
|
|
existing = Subscription.objects.filter(organization=organization).first()
|
|
if existing is not None:
|
|
# Reconcile the ai_agent_slot counter with agents that may have been
|
|
# activated directly (test fixtures bypass the gate) and align the
|
|
# subscription quantity with what the test expects.
|
|
_resync_agent_slots(organization, existing, quantity)
|
|
return existing
|
|
plan, _ = Plan.objects.get_or_create(
|
|
code=plan_code,
|
|
defaults={"name": "Test plan", "saleable": True},
|
|
)
|
|
version, created = PlanVersion.objects.get_or_create(
|
|
plan=plan,
|
|
version=1,
|
|
defaults={
|
|
"agent_unit_price_minor": 290_000,
|
|
"currency": "RUB",
|
|
"billing_period": "MONTH",
|
|
},
|
|
)
|
|
if created:
|
|
entitlement, _ = EntitlementDefinition.objects.get_or_create(
|
|
key="byok_ai",
|
|
defaults={"name": "BYOK AI"},
|
|
)
|
|
EntitlementGrant.objects.create(
|
|
plan_version=version,
|
|
definition=entitlement,
|
|
)
|
|
quota, _ = QuotaDefinition.objects.get_or_create(
|
|
key="ai_agent_slots",
|
|
defaults={"name": "AI agent slots", "unit": "slots"},
|
|
)
|
|
QuotaGrant.objects.create(
|
|
plan_version=version,
|
|
definition=quota,
|
|
mode=QuotaMode.HARD,
|
|
limit_source=QuotaLimitSource.SUBSCRIPTION_AI_AGENT_QUANTITY,
|
|
)
|
|
if version.published_at is None:
|
|
version = publish_plan_version(version)
|
|
return create_subscription(
|
|
context=system_tenant_context(organization),
|
|
plan_version=version,
|
|
ai_agent_quantity=quantity,
|
|
)
|
|
|
|
|
|
def _resync_agent_slots(organization, subscription, quantity: int) -> None:
|
|
"""Reconcile ai_agent_slots for an existing subscription so tests that create
|
|
ACTIVE agents directly (bypassing the slot gate) keep the counter consistent,
|
|
and align the subscription quantity with the requested value."""
|
|
from hub_platform.ai.models import AIAgent, AIAgentStatus
|
|
from hub_platform.subscriptions.models import QuotaDefinition, UsageCounter
|
|
|
|
active = AIAgent.objects.filter(
|
|
organization=organization, status=AIAgentStatus.ACTIVE
|
|
).count()
|
|
target_quantity = max(quantity, active)
|
|
if subscription.ai_agent_quantity != target_quantity:
|
|
subscription.ai_agent_quantity = target_quantity
|
|
subscription.save(update_fields=["ai_agent_quantity", "updated_at"])
|
|
definition = QuotaDefinition.objects.filter(key="ai_agent_slots").first()
|
|
if definition is not None:
|
|
period = subscription.usage_periods.filter(status="OPEN").first()
|
|
if period is not None:
|
|
counter, _ = UsageCounter.objects.update_or_create(
|
|
period=period,
|
|
quota_definition=definition,
|
|
defaults={"used_value": active},
|
|
)
|