"""CDR Storage Tier handler. Storage tier purchases (tier1/2/3) are not filings — they're subscription allocations that bump the customer's `cdr_ingestion_profiles.storage_plan` quota. No Playwright, no external portal, no artifacts generated. Tiers correspond to (storage bytes, classified calls) caps: tier1: 50 GB / 50M calls tier2: 250 GB / 250M calls tier3: 1,000 GB / 1,000M calls A new purchase replaces the existing tier (doesn't stack). Subsequent ingestion enforces the new cap via `scripts/workers/cdr_ingester.py`. """ from __future__ import annotations import logging import os from typing import Optional import psycopg2 from .base_handler import BaseServiceHandler logger = logging.getLogger(__name__) class CDRStorageTierHandler(BaseServiceHandler): SERVICE_SLUG = "cdr-storage-tier" # set per subclass SERVICE_NAME = "CDR Storage Tier" REQUIRES_LLM = False TIER_LABEL = "" # set per subclass: 'tier1'|'tier2'|'tier3' async def process(self, order_data: dict) -> list[str]: order_number = order_data["name"] entity = order_data.get("entity", {}) or {} entity_id = entity.get("id") if not entity_id: self._create_admin_todo( order_number, f"{self.SERVICE_NAME}: no telecom_entity_id on order. " "Link the entity + re-dispatch, or bump the storage plan " "manually in cdr_ingestion_profiles.storage_plan.", ) return [] try: conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) with conn.cursor() as cur: cur.execute( """ UPDATE cdr_ingestion_profiles SET storage_plan = %s, storage_plan_purchased_at = NOW(), storage_plan_order_ref = %s WHERE telecom_entity_id = %s RETURNING id """, (self.TIER_LABEL, order_number, entity_id), ) row = cur.fetchone() conn.commit() conn.close() except psycopg2.errors.UndefinedColumn: # Columns don't exist yet — fall back to setting just storage_plan try: conn = psycopg2.connect(os.environ.get("DATABASE_URL", "")) with conn.cursor() as cur: cur.execute( "UPDATE cdr_ingestion_profiles SET storage_plan = %s " "WHERE telecom_entity_id = %s RETURNING id", (self.TIER_LABEL, entity_id), ) row = cur.fetchone() conn.commit() conn.close() except Exception as exc: logger.warning( "%s: storage_plan column missing + fallback failed: %s", self.SERVICE_SLUG, exc, ) row = None except Exception as exc: logger.exception("%s: DB error: %s", self.SERVICE_SLUG, exc) row = None if not row: self._create_admin_todo( order_number, f"{self.SERVICE_NAME} for entity {entity_id}: no CDR profile " "found. Customer needs to enable CDR ingestion first, OR " "admin should provision the profile manually.", ) return [] logger.info( "%s: set storage_plan=%s on profile for entity %s (order %s)", self.SERVICE_SLUG, self.TIER_LABEL, entity_id, order_number, ) return [] # no artifacts — tier updates are quiet def _create_admin_todo(self, order_number: str, description: str) -> None: try: from scripts.workers.erpnext_client import ERPNextClient ERPNextClient().create_resource( "ToDo", { "description": f"[{self.SERVICE_SLUG}] {order_number}\n\n{description}", "priority": "Low", "role": "Accounting Advisor", }, ) except Exception as exc: logger.error("Could not create admin ToDo: %s", exc) class CDRStorageTier1Handler(CDRStorageTierHandler): SERVICE_SLUG = "cdr-storage-tier1" SERVICE_NAME = "CDR Storage Tier 1 (50 GB / 50M calls)" TIER_LABEL = "tier1" class CDRStorageTier2Handler(CDRStorageTierHandler): SERVICE_SLUG = "cdr-storage-tier2" SERVICE_NAME = "CDR Storage Tier 2 (250 GB / 250M calls)" TIER_LABEL = "tier2" class CDRStorageTier3Handler(CDRStorageTierHandler): SERVICE_SLUG = "cdr-storage-tier3" SERVICE_NAME = "CDR Storage Tier 3 (1 TB / 1B calls)" TIER_LABEL = "tier3"