Waiting for deployment "maya-pipecat-v5" rollout to finish: 76 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 76 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 76 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 76 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 77 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 77 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 78 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 78 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 78 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 78 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 80 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 83 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 87 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 90 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 90 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 90 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 90 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 91 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 93 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 96 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 96 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 99 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 99 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 99 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 100 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 100 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 100 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 100 out of 101 new replicas have been updated...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 26 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 25 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 25 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 25 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 24 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 24 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 24 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 23 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 23 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 23 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 22 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 22 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 21 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 21 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 21 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 20 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 20 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 20 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 20 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 19 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 19 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 18 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 18 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 14 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 13 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 13 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 13 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 12 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 12 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 12 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 11 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 11 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 11 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 10 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 10 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 9 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 9 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 9 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 8 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 8 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 8 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 8 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 7 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 7 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 7 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 6 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 6 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 4 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 4 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 4 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 4 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 3 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 3 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 3 old replicas are pending termination...
Waiting for deployment "maya-pipecat-v5" rollout to finish: 1 old replicas are pending termination...
deployment "maya-pipecat-v5" successfully rolled out


Rollout complete. Let me check the distribution now.


Bash

kubectl get pods -l app=maya-pipecat-v5 -o wide --no-headers | awk '{print $7}' | sort | uniq -c | sort -rn

Check pod distribution across nodes


     23 gke-maya-pipecat-default-pool-f76748ed-l70x
     22 gke-maya-pipecat-pool-v2-ca055238-xw71
     22 gke-maya-pipecat-pool-v2-ca055238-s01j
     22 gke-maya-pipecat-default-pool-f76748ed-vks4
     21 gke-maya-pipecat-default-pool-f76748ed-trnd
     21 gke-maya-pipecat-default-pool-f76748ed-24sr


Evenly distributed now — ~21-23 pods per node across all 6 nodes, both pools.


can u check when we kept all defalut to gemini live ..when exactly


git log --all --oneline --diff-filter=M -- src/bot/core/main.py | head -20

Check git history for main.py changes


6627007 version 5 done still phase 1
988c26d working still here
689d8a3 updated some handlers
e4b6816 added 2 supprts small and daily
13999e0 v4: Migrate from Daily to SmallWebRTC transport
86dbff5 updated
c094b0a updated
ce05259 updated
1c8cea4 updated
f204954 added music gen
46ba7da updated
c0aacae added gpt image
80fca3e updated hindi v2
4a58d3c udpated


git log --all -p -S "use_gemini_live = True" -- src/bot/core/main.py --format="%h %ad %s" --date=short | head -30

Find when use_gemini_live was set to True


commit 662700714c6e2fb82dd6b2be60d5beb73ae16dc6
Author: saicherry93479 <saicherry93479@gmail.com>
Date:   Tue Mar 31 09:22:38 2026 +0530

    version 5 done still phase 1

diff --git a/src/bot/core/main.py b/src/bot/core/main.py
index 9ec5e18..801cadd 100644
--- a/src/bot/core/main.py
+++ b/src/bot/core/main.py
@@ -20,20 +20,14 @@ from src.bot.services.music_service import create_music_handler
 load_dotenv()

-async def run_bot_webrtc_global_agent(webrtc_connection, device_id: str = None, session_id: str = None, version: str = None, character_id: str = None, app_key: str = None, language: str = None, source: str = None, persona_id: str = None, transport_type: str = "smallwebrtc", room_url: str = None, token: str = None):
+async def run_bot_webrtc_global_agent(webrtc_connection, device_id: str = None, session_id: str = None, version: str = None, character_id: str = None, app_key: str = None, language: str = None, source: str = None, persona_id: str = None, transport_type: str = "smallwebrtc", room_url: str = None, token: str = None, app_version: int = None):
     bot_start_time = time.time()
     device_id = device_id or "common"
-    is_web = (source == "web")

     # Accept character_id as persona_id for backward compat
     if not persona_id and character_id:
         persona_id = character_id

-    # Web beta: force Telugu language
-    if is_web:
-        language = "te"
-        logger.info(f"WEB_SESSION: device_id={device_id} forcing language=te, no limits")
-
     session_state = SessionState()


It was set in commit `6627007` on **March 31, 2026** by saicherry93479 — "version 5 done still phase 1". That's when `use_gemini_live = True` was hardcoded for all users, removing the web-only check that previously only used Gemini Live for Telugu web sessions.


can we keep a flag to turn of payments showing to users and turning on to users with a config or something .. i mean we experimneting now with giving all free but i doont want to gve full unlimited ..i want to restrict them by some limit like some coll down period


Let me check the current limit flow to design this properly.


1	"""Usage limit checker — 3-layer credits: plan → topup → free."""
2
3	from loguru import logger
4
5
6	def can_generate(bot_session) -> str:
7	    """Check if user can generate content.
8
9	    Returns:
10	        "" — allowed
11	        "subscribe" — no plan, free exhausted → show plans popup
12	        "topup" — has plan but exhausted → show topup popup
13	    """
14	    if bot_session.plan_remaining > 0:
15	        return ""
16	    if bot_session.topup_remaining > 0:
17	        return ""
18	    if bot_session.free_remaining > 0:
19	        return ""
20
21	    # Blocked
22	    if bot_session.subscription_id:
23	        return "topup"
24	    return "subscribe"
25
26
27	def use_credit(bot_session):
28	    """Decrement one credit after successful generation. Plan first, then topup, then free."""
29	    if bot_session.plan_remaining > 0:
30	        bot_session.plan_remaining -= 1
31	        bot_session.credits_used_plan += 1
32	        logger.debug(f"CREDIT_USED_PLAN: device={bot_session.device_id} plan_remaining={bot_session.plan_remaining}")
33	    elif bot_session.topup_remaining > 0:
34	        bot_session.topup_remaining -= 1
35	        bot_session.credits_used_topup += 1
36	        logger.debug(f"CREDIT_USED_TOPUP: device={bot_session.device_id} topup_remaining={bot_session.topup_remaining}")
37	    elif bot_session.free_remaining > 0:
38	        bot_session.free_remaining -= 1
39	        logger.debug(f"CREDIT_USED_FREE: device={bot_session.device_id} free_remaining={bot_session.free_remaining}")
40
41
42	async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
43	    """Send limit reached popup with plan/topup data to the frontend.
44
45	    Fetches plan data from DB so the pricing screen shows actual prices.
46	    """
47	    from src.service.usage_service import get_plans_for_device, get_topup_for_device, get_topups_for_content
48
49	    content_type = bot_session.content_type or "image_gen"
50	    device_id = bot_session.device_id
51
52	    # Determine payment provider from region
53	    from src.service.usage_service import _get_device_region, _get_payment_provider
54	    region = await _get_device_region(device_id)
55	    payment_provider = _get_payment_provider(region)
56
57	    try:
58	        if block_reason == "topup":
59	            plan_data = await get_topup_for_device(device_id, content_type, payment_provider)
60	            topups_list = await get_topups_for_content(content_type, payment_provider)
61	        else:
62	            plan_data = await get_plans_for_device(device_id, content_type, payment_provider)
63	            topups_list = []
64
65	        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
66	        await rtvi_processor.send_server_message({
67	            "type": popup_type,
68	            "data": {
69	                "content_type": content_type,
70	                "reason": block_reason,
71	                "plan": plan_data,
72	                "topups": topups_list,
73	            },
74	        })
75	        logger.info(f"LIMIT_POPUP_SENT: device={device_id} reason={block_reason} content={content_type} provider={payment_provider} plan_id={plan_data.get('plan_id') or plan_data.get('topup_id', 'none')}")
76	    except Exception as e:
77	        logger.error(f"LIMIT_POPUP_FAILED: device={device_id} error={e}")
78	        # Fallback: send without data (old behavior)
79	        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
80	        await rtvi_processor.send_server_message({"type": popup_type, "content_type": content_type})
81

"""Usage limit checker — 3-layer credits: plan → topup → free."""

from loguru import logger

def can_generate(bot_session) -> str:
    """Check if user can generate content.

    Returns:
        "" — allowed
        "subscribe" — no plan, free exhausted → show plans popup
        "topup" — has plan but exhausted → show topup popup
    """
    if bot_session.plan_remaining > 0:
        return ""
    if bot_session.topup_remaining > 0:
        return ""
    if bot_session.free_remaining > 0:
        return ""

    # Blocked
    if bot_session.subscription_id:
        return "topup"
    return "subscribe"

def use_credit(bot_session):
    """Decrement one credit after successful generation. Plan first, then topup, then free."""
    if bot_session.plan_remaining > 0:
        bot_session.plan_remaining -= 1
        bot_session.credits_used_plan += 1
        logger.debug(f"CREDIT_USED_PLAN: device={bot_session.device_id} plan_remaining={bot_session.plan_remaining}")
    elif bot_session.topup_remaining > 0:
        bot_session.topup_remaining -= 1
        bot_session.credits_used_topup += 1
        logger.debug(f"CREDIT_USED_TOPUP: device={bot_session.device_id} topup_remaining={bot_session.topup_remaining}")
    elif bot_session.free_remaining > 0:
        bot_session.free_remaining -= 1
        logger.debug(f"CREDIT_USED_FREE: device={bot_session.device_id} free_remaining={bot_session.free_remaining}")

async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
    """Send limit reached popup with plan/topup data to the frontend.

    Fetches plan data from DB so the pricing screen shows actual prices.
    """
    from src.service.usage_service import get_plans_for_device, get_topup_for_device, get_topups_for_content

    content_type = bot_session.content_type or "image_gen"
    device_id = bot_session.device_id

    # Determine payment provider from region
    from src.service.usage_service import _get_device_region, _get_payment_provider
    region = await _get_device_region(device_id)
    payment_provider = _get_payment_provider(region)

    try:
        if block_reason == "topup":
            plan_data = await get_topup_for_device(device_id, content_type, payment_provider)
            topups_list = await get_topups_for_content(content_type, payment_provider)
        else:
            plan_data = await get_plans_for_device(device_id, content_type, payment_provider)
            topups_list = []

        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
        await rtvi_processor.send_server_message({
            "type": popup_type,
            "data": {
                "content_type": content_type,
                "reason": block_reason,
                "plan": plan_data,
                "topups": topups_list,
            },
        })
        logger.info(f"LIMIT_POPUP_SENT: device={device_id} reason={block_reason} content={content_type} provider={payment_provider} plan_id={plan_data.get('plan_id') or plan_data.get('topup_id', 'none')}")
    except Exception as e:
        logger.error(f"LIMIT_POPUP_FAILED: device={device_id} error={e}")
        # Fallback: send without data (old behavior)
        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
        await rtvi_processor.send_server_message({"type": popup_type, "content_type": content_type})


1	"""Service layer for usage state, generation checks, and plan assignment."""
2
3	import hashlib
4	from typing import Dict, Any, List
5
6	from loguru import logger
7
8	from src.repository.plan_repository import plan_repo
9	from src.repository.subscription_repository import subscription_repo
10	from src.repository.topup_repository import topup_repo
11	from src.repository.usage_repository import usage_repo
12
13
14	def _device_hash(device_id: str) -> int:
15	    """Deterministic 0-99 hash for A/B assignment."""
16	    return int(hashlib.sha256(device_id.encode()).hexdigest(), 16) % 100
17
18
19	async def _get_device_info(device_id: str) -> Dict[str, Any]:
20	    """Get device region and country, defaulting to india for legacy devices."""
21	    from src.repository.device_repository import device_repo
22	    device = await device_repo.get_device(device_id)
23	    if device:
24	        return {
25	            "region": device.get("region") or "india",
26	            "country": device.get("country"),
27	        }
28	    return {"region": "india", "country": None}
29
30
31	async def _get_device_region(device_id: str) -> str:
32	    """Get device region, defaulting to 'india' for legacy devices."""
33	    info = await _get_device_info(device_id)
34	    return info["region"]
35
36
37	def _get_payment_provider(region: str) -> str:
38	    """Map region to payment provider."""
39	    return "google_play" if region == "global" else "razorpay"
40
41
42	async def get_usage_state(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
43	    """Build usage state for a device + content type (plan/topup level).
44
45	    Free limits are persona-level now, handled separately in can_generate_persona().
46	    Subscription and topup repos already handle 'all' fallback internally.
47	    If payment_provider is given, only checks subs from that provider.
48	    """
49	    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
50	    plan_remaining = sub["credits_remaining"] if sub else 0
51	    subscription_id = sub["id"] if sub else None
52	    plan_content_type = sub["plan_content_type"] if sub else None
53
54	    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)
55
56	    return {
57	        "plan_remaining": plan_remaining,
58	        "topup_remaining": topup_remaining,
59	        "has_plan": sub is not None,
60	        "subscription_id": subscription_id,
61	        "plan_content_type": plan_content_type,
62	    }
63
64
65	async def can_generate_persona(device_id: str, persona_id: str, payment_provider: str = None) -> Dict[str, Any]:
66	    """Check if the device can use a persona.
67
68	    Credit check order: plan → topup → region free limit (A/B) → persona free limit.
69
70	    For global users, region_free_limits with ab_percentage controls what fraction
71	    of users get free access. Users outside the A/B percentage see the paywall.
72
73	    Args:
74	        device_id: The device identifier.
75	        persona_id: The persona to check.
76	        payment_provider: If set, filters plans/topups shown when blocked.
77
78	    Returns:
79	        {"allowed": True} — allowed
80	        {"allowed": False, "reason": "subscribe"|"topup", ...} — blocked
81	    """
82	    from src.repository.persona_repository import persona_repo
83
84	    persona = await persona_repo.get_persona_by_id(persona_id)
85	    if not persona:
86	        return {"allowed": True}
87
88	    content_type = persona.get("content_type", "")
89
90	    # Resolve region, country, and payment provider if not provided
91	    device_info = await _get_device_info(device_id)
92	    region = device_info["region"]
93	    country = device_info["country"]
94	    if not payment_provider:
95	        payment_provider = _get_payment_provider(region)
96
97	    # 1. Check plan credits (content_type level, with 'all' fallback in repo)
98	    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
99	    plan_remaining = sub["credits_remaining"] if sub else 0
100	    if plan_remaining > 0:
101	        return {"allowed": True}
102
103	    # 2. Check topup credits (content_type level, with 'all' fallback in repo)
104	    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)
105	    if topup_remaining > 0:
106	        return {"allowed": True}
107
108	    # 3. Free limits: India uses persona-level (preserves old behavior),
109	    #    Global uses region_free_limits with A/B testing.
110	    if region == "india":
111	        # India: always use persona-level free limits
112	        free_count = persona.get("free_count", 0)
113	        free_window_hours = persona.get("free_window_hours", 24)
114	        if free_count > 0:
115	            used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
116	            if used < free_count:
117	                return {"allowed": True}
118	    else:
119	        # Global: use region_free_limits with A/B percentage
120	        # Group A (hash < ab_percentage): fully free, no limits, no paywall
121	        # Group B (hash >= ab_percentage): gets free_count free, then paywall
122	        region_limits = await plan_repo.get_region_free_limits(region)
123	        region_limit = region_limits.get(content_type)
124	        if region_limit:
125	            ab_pct = region_limit.get("ab_percentage", 100)
126	            device_h = _device_hash(device_id)
127
128	            if device_h < ab_pct:
129	                # Group A: fully free — always allowed
130	                return {"allowed": True}
131	            else:
132	                # Group B: gets free_count free, then paywall
133	                free_count = region_limit["free_count"]
134	                free_window = region_limit["free_window_hours"]
135	                if free_count > 0:
136	                    used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window)
137	                    if used < free_count:
138	                        return {"allowed": True}
139	        else:
140	            # No region limits configured — fall back to persona-level
141	            free_count = persona.get("free_count", 0)
142	            free_window_hours = persona.get("free_window_hours", 24)
143	            if free_count > 0:
144	                used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
145	                if used < free_count:
146	                    return {"allowed": True}
147
148	    # Blocked — show payment options filtered by provider
149	    # Apply country-level price overrides for global users
150	    country_prices = {}
151	    if country and region != "india":
152	        from src.repository.country_prices_repository import country_prices_repo
153	        country_prices = await country_prices_repo.get_prices_for_country(country)
154
155	    if sub:
156	        topup = await get_topup_for_device(device_id, content_type, payment_provider)
157	        if country_prices and topup:
158	            from src.repository.country_prices_repository import country_prices_repo
159	            topup = country_prices_repo.apply_country_price(topup, country_prices)
160	        return {
161	            "allowed": False,
162	            "reason": "topup",
163	            "content_type": content_type,
164	            "persona_id": persona_id,
165	            "plan": topup,
166	        }
167	    else:
168	        plan = await get_plans_for_device(device_id, content_type, payment_provider)
169	        if country_prices and plan:
170	            from src.repository.country_prices_repository import country_prices_repo
171	            plan = country_prices_repo.apply_country_price(plan, country_prices)
172	        return {
173	            "allowed": False,
174	            "reason": "subscribe",
175	            "content_type": content_type,
176	            "persona_id": persona_id,
177	            "plan": plan,
178	        }
179
180
181	def _assign_plan(device_id: str, plans: List[Dict[str, Any]]) -> Dict[str, Any]:
182	    """Deterministically assign a plan variant to a device using weighted hash."""
183	    if not plans:
184	        return {}
185	    h = _device_hash(device_id)
186	    cumulative = 0
187	    for p in plans:
188	        cumulative += p.get("percentage", 0)
189	        if h < cumulative:
190	            plan = dict(p)
191	            plan.pop("percentage", None)
192	            return plan
193	    plan = dict(plans[-1])
194	    plan.pop("percentage", None)
195	    return plan
196
197
198	async def get_plans_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
199	    """Get the A/B-assigned plan for a device + content type.
200
201	    Tries specific content_type first, falls back to 'all' if no plans found.
202	    """
203	    plans = await plan_repo.get_plans_for_content(content_type, payment_provider)
204	    if not plans and content_type != "all":
205	        plans = await plan_repo.get_plans_for_content("all", payment_provider)
206	    return _assign_plan(device_id, plans)
207
208
209	async def get_topup_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
210	    """Get the A/B-assigned topup for a device + content type.
211
212	    Tries specific content_type first, falls back to 'all' if no topups found.
213	    """
214	    topups = await plan_repo.get_topups_for_content(content_type, payment_provider)
215	    if not topups and content_type != "all":
216	        topups = await plan_repo.get_topups_for_content("all", payment_provider)
217	    return _assign_plan(device_id, topups)
218
219
220	async def get_topups_for_content(content_type: str, payment_provider: str = None) -> List[Dict[str, Any]]:
221	    """Get available topup packs for a content type."""
222	    return await plan_repo.get_topups_for_content(content_type, payment_provider)
223
224
225	async def get_all_usage(device_id: str) -> Dict[str, Dict[str, Any]]:
226	    """Get usage state for all content types."""
227	    content_types = ["image_gen", "video_gen", "ad_video", "ppt_gen", "music_gen"]
228	    result = {}
229	    for ct in content_types:
230	        result[ct] = await get_usage_state(device_id, ct)
231	    return result
232

"""Service layer for usage state, generation checks, and plan assignment."""

import hashlib
from typing import Dict, Any, List

from loguru import logger

from src.repository.plan_repository import plan_repo
from src.repository.subscription_repository import subscription_repo
from src.repository.topup_repository import topup_repo
from src.repository.usage_repository import usage_repo

def _device_hash(device_id: str) -> int:
    """Deterministic 0-99 hash for A/B assignment."""
    return int(hashlib.sha256(device_id.encode()).hexdigest(), 16) % 100

async def _get_device_info(device_id: str) -> Dict[str, Any]:
    """Get device region and country, defaulting to india for legacy devices."""
    from src.repository.device_repository import device_repo
    device = await device_repo.get_device(device_id)
    if device:
        return {
            "region": device.get("region") or "india",
            "country": device.get("country"),
        }
    return {"region": "india", "country": None}

async def _get_device_region(device_id: str) -> str:
    """Get device region, defaulting to 'india' for legacy devices."""
    info = await _get_device_info(device_id)
    return info["region"]

def _get_payment_provider(region: str) -> str:
    """Map region to payment provider."""
    return "google_play" if region == "global" else "razorpay"

async def get_usage_state(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Build usage state for a device + content type (plan/topup level).

    Free limits are persona-level now, handled separately in can_generate_persona().
    Subscription and topup repos already handle 'all' fallback internally.
    If payment_provider is given, only checks subs from that provider.
    """
    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
    plan_remaining = sub["credits_remaining"] if sub else 0
    subscription_id = sub["id"] if sub else None
    plan_content_type = sub["plan_content_type"] if sub else None

    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)

    return {
        "plan_remaining": plan_remaining,
        "topup_remaining": topup_remaining,
        "has_plan": sub is not None,
        "subscription_id": subscription_id,
        "plan_content_type": plan_content_type,
    }

async def can_generate_persona(device_id: str, persona_id: str, payment_provider: str = None) -> Dict[str, Any]:
    """Check if the device can use a persona.

    Credit check order: plan → topup → region free limit (A/B) → persona free limit.

    For global users, region_free_limits with ab_percentage controls what fraction
    of users get free access. Users outside the A/B percentage see the paywall.

    Args:
        device_id: The device identifier.
        persona_id: The persona to check.
        payment_provider: If set, filters plans/topups shown when blocked.

    Returns:
        {"allowed": True} — allowed
        {"allowed": False, "reason": "subscribe"|"topup", ...} — blocked
    """
    from src.repository.persona_repository import persona_repo

    persona = await persona_repo.get_persona_by_id(persona_id)
    if not persona:
        return {"allowed": True}

    content_type = persona.get("content_type", "")

    # Resolve region, country, and payment provider if not provided
    device_info = await _get_device_info(device_id)
    region = device_info["region"]
    country = device_info["country"]
    if not payment_provider:
        payment_provider = _get_payment_provider(region)

    # 1. Check plan credits (content_type level, with 'all' fallback in repo)
    sub = await subscription_repo.get_active(device_id, content_type, payment_provider)
    plan_remaining = sub["credits_remaining"] if sub else 0
    if plan_remaining > 0:
        return {"allowed": True}

    # 2. Check topup credits (content_type level, with 'all' fallback in repo)
    topup_remaining = await topup_repo.get_credits(device_id, content_type, payment_provider)
    if topup_remaining > 0:
        return {"allowed": True}

    # 3. Free limits: India uses persona-level (preserves old behavior),
    #    Global uses region_free_limits with A/B testing.
    if region == "india":
        # India: always use persona-level free limits
        free_count = persona.get("free_count", 0)
        free_window_hours = persona.get("free_window_hours", 24)
        if free_count > 0:
            used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
            if used < free_count:
                return {"allowed": True}
    else:
        # Global: use region_free_limits with A/B percentage
        # Group A (hash < ab_percentage): fully free, no limits, no paywall
        # Group B (hash >= ab_percentage): gets free_count free, then paywall
        region_limits = await plan_repo.get_region_free_limits(region)
        region_limit = region_limits.get(content_type)
        if region_limit:
            ab_pct = region_limit.get("ab_percentage", 100)
            device_h = _device_hash(device_id)

            if device_h < ab_pct:
                # Group A: fully free — always allowed
                return {"allowed": True}
            else:
                # Group B: gets free_count free, then paywall
                free_count = region_limit["free_count"]
                free_window = region_limit["free_window_hours"]
                if free_count > 0:
                    used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window)
                    if used < free_count:
                        return {"allowed": True}
        else:
            # No region limits configured — fall back to persona-level
            free_count = persona.get("free_count", 0)
            free_window_hours = persona.get("free_window_hours", 24)
            if free_count > 0:
                used = await usage_repo.count_persona_generations(device_id, persona_id, content_type, free_window_hours)
                if used < free_count:
                    return {"allowed": True}

    # Blocked — show payment options filtered by provider
    # Apply country-level price overrides for global users
    country_prices = {}
    if country and region != "india":
        from src.repository.country_prices_repository import country_prices_repo
        country_prices = await country_prices_repo.get_prices_for_country(country)

    if sub:
        topup = await get_topup_for_device(device_id, content_type, payment_provider)
        if country_prices and topup:
            from src.repository.country_prices_repository import country_prices_repo
            topup = country_prices_repo.apply_country_price(topup, country_prices)
        return {
            "allowed": False,
            "reason": "topup",
            "content_type": content_type,
            "persona_id": persona_id,
            "plan": topup,
        }
    else:
        plan = await get_plans_for_device(device_id, content_type, payment_provider)
        if country_prices and plan:
            from src.repository.country_prices_repository import country_prices_repo
            plan = country_prices_repo.apply_country_price(plan, country_prices)
        return {
            "allowed": False,
            "reason": "subscribe",
            "content_type": content_type,
            "persona_id": persona_id,
            "plan": plan,
        }

def _assign_plan(device_id: str, plans: List[Dict[str, Any]]) -> Dict[str, Any]:
    """Deterministically assign a plan variant to a device using weighted hash."""
    if not plans:
        return {}
    h = _device_hash(device_id)
    cumulative = 0
    for p in plans:
        cumulative += p.get("percentage", 0)
        if h < cumulative:
            plan = dict(p)
            plan.pop("percentage", None)
            return plan
    plan = dict(plans[-1])
    plan.pop("percentage", None)
    return plan

async def get_plans_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Get the A/B-assigned plan for a device + content type.

    Tries specific content_type first, falls back to 'all' if no plans found.
    """
    plans = await plan_repo.get_plans_for_content(content_type, payment_provider)
    if not plans and content_type != "all":
        plans = await plan_repo.get_plans_for_content("all", payment_provider)
    return _assign_plan(device_id, plans)

async def get_topup_for_device(device_id: str, content_type: str, payment_provider: str = None) -> Dict[str, Any]:
    """Get the A/B-assigned topup for a device + content type.

    Tries specific content_type first, falls back to 'all' if no topups found.
    """
    topups = await plan_repo.get_topups_for_content(content_type, payment_provider)
    if not topups and content_type != "all":
        topups = await plan_repo.get_topups_for_content("all", payment_provider)
    return _assign_plan(device_id, topups)

async def get_topups_for_content(content_type: str, payment_provider: str = None) -> List[Dict[str, Any]]:
    """Get available topup packs for a content type."""
    return await plan_repo.get_topups_for_content(content_type, payment_provider)

async def get_all_usage(device_id: str) -> Dict[str, Dict[str, Any]]:
    """Get usage state for all content types."""
    content_types = ["image_gen", "video_gen", "ad_video", "ppt_gen", "music_gen"]
    result = {}
    for ct in content_types:
        result[ct] = await get_usage_state(device_id, ct)
    return result


1	"""Repository for configurations_v2 table."""
2
3	import json
4	import time
5	from typing import Dict, Any, List
6
7	from loguru import logger
8
9	from src.repository.base import BaseRepository
10
11	# Defaults by region
12	_DEFAULT_LANGUAGES_INDIA = [
13	    {"code": "te", "native_label": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41", "script_char": "\u0c24", "english_label": "Telugu"},
14	    {"code": "hi", "native_label": "\u0939\u093f\u0928\u094d\u0926\u0940", "script_char": "\u0939", "english_label": "Hindi"}
15	]
16	_DEFAULT_LANGUAGES_GLOBAL = [
17	    {"code": "en", "native_label": "English", "script_char": "E", "english_label": "English"},
18	    {"code": "es", "native_label": "Español", "script_char": "E", "english_label": "Spanish"},
19	    {"code": "fr", "native_label": "Français", "script_char": "F", "english_label": "French"},
20	    {"code": "pt", "native_label": "Português", "script_char": "P", "english_label": "Portuguese"},
21	    {"code": "id", "native_label": "Bahasa Indonesia", "script_char": "B", "english_label": "Indonesian"},
22	    {"code": "de", "native_label": "Deutsch", "script_char": "D", "english_label": "German"},
23	    {"code": "ja", "native_label": "日本語", "script_char": "日", "english_label": "Japanese"},
24	    {"code": "ar", "native_label": "العربية", "script_char": "ع", "english_label": "Arabic"},
25	    {"code": "ko", "native_label": "한국어", "script_char": "한", "english_label": "Korean"},
26	    {"code": "vi", "native_label": "Tiếng Việt", "script_char": "V", "english_label": "Vietnamese"},
27	]
28
29
30	class ConfigV2Repository(BaseRepository):
31	    """Reads configuration values from the configurations_v2 table.
32
33	    Table schema:
34	        config_key   TEXT PRIMARY KEY
35	        config_value TEXT (JSON string or plain value)
36	        updated_at   INTEGER (unix timestamp)
37	    """
38
39	    async def get_on_open_config(self) -> Dict[str, Any]:
40	        """Get all on_open config values in a single query."""
41	        client = self.get_client()
42	        if not client:
43	            return {}
44
45	        start_time = time.time()
46	        try:
47	            result = await self.query({
48	                "sql": """
49	                    SELECT config_key, config_value
50	                    FROM configurations_v2
51	                    WHERE config_key IN ('update_enabled', 'force_update', 'show_language_screen')
52	                """,
53	                "args": [],
54	            }, "config_v2_on_open")
55
56	            config = {}
57	            if result.rows:
58	                for row in result.rows:
59	                    key = self.extract_text(row, 0)
60	                    value = self.extract_text(row, 1)
61	                    if key and value is not None:
62	                        config[key] = value.lower() in ("true", "1", "yes")
63
64	            duration_ms = (time.time() - start_time) * 1000
65	            logger.debug(f"CONFIG_V2_ON_OPEN: keys={list(config.keys())} duration_ms={duration_ms:.2f}")
66	            return config
67	        except Exception as e:
68	            duration_ms = (time.time() - start_time) * 1000
69	            logger.error(f"CONFIG_V2_ON_OPEN_FAILED: duration_ms={duration_ms:.2f} error={e}")
70	            return {}
71
72	    async def get_languages(self, region: str = None) -> List[Dict[str, Any]]:
73	        """Get supported languages from config, optionally filtered by region.
74
75	        Languages in DB can have a 'region' field: 'india', 'global', or 'all'.
76	        If region is provided, returns only languages matching that region or 'all'.
77	        If no region field exists on a language, it's treated as 'india' for backward compat.
78	        """
79	        from src.core.cache_layer import cached
80
81	        async def _fetch_languages():
82	            try:
83	                result = await self.query({
84	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
85	                    "args": [{"type": "text", "value": "languages"}],
86	                }, "config_v2_languages")
87	                if result.rows:
88	                    raw = self.extract_text(result.rows[0], 0)
89	                    if raw:
90	                        parsed = json.loads(raw)
91	                        if isinstance(parsed, dict) and "languages" in parsed:
92	                            return parsed
93	                        if isinstance(parsed, list):
94	                            return {"languages": parsed}
95	            except Exception as e:
96	                logger.error(f"CONFIG_V2_LANGUAGES_FETCH_ERROR: {e}")
97	            return None
98
99	        result = await cached(
100	            key="config_v2:languages",
101	            fallback=_fetch_languages,
102	            ttl=300,
103	        )
104
105	        all_languages = []
106	        if result and "languages" in result:
107	            all_languages = result["languages"]
108	        else:
109	            # No DB config — return defaults based on region
110	            if region == "global":
111	                return _DEFAULT_LANGUAGES_GLOBAL
112	            return _DEFAULT_LANGUAGES_INDIA
113
114	        # Filter by region if provided
115	        if region:
116	            filtered = [
117	                lang for lang in all_languages
118	                if lang.get("region", "india") in (region, "all")
119	            ]
120	            return filtered if filtered else all_languages
121
122	        return all_languages
123
124
125	    async def get_language_prompt(self, lang_code: str) -> str:
126	        """Get language instruction for a language code (hi, te)."""
127	        from src.core.cache_layer import cached
128
129	        cache_key = f"config_v2:language_prompt_{lang_code}"
130
131	        async def _fetch():
132	            try:
133	                result = await self.query({
134	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
135	                    "args": [{"type": "text", "value": f"language_prompt_{lang_code}"}],
136	                }, "config_v2_language_prompt")
137	                if result.rows:
138	                    return self.extract_text(result.rows[0], 0)
139	            except Exception as e:
140	                logger.error(f"CONFIG_V2_LANGUAGE_PROMPT_ERROR: {e}")
141	            return None
142
143	        result = await cached(key=cache_key, fallback=_fetch, ttl=300)
144	        if result:
145	            return result
146	        # Fallback defaults
147	        _FALLBACK_PROMPTS = {
148	            "hi": "Speak in Hinglish — Hindi mixed with English words naturally. Use Roman script.",
149	            "te": "Speak in Telugu using Roman script (Tinglish). Mix English words naturally for tech terms.",
150	            "en": "Speak in English. Keep it casual and conversational.",
151	            "es": "Speak in Spanish. Keep it casual and conversational. Use English words for tech terms.",
152	            "fr": "Speak in French. Keep it casual and conversational. Use English words for tech terms.",
153	            "pt": "Speak in Portuguese. Keep it casual and conversational. Use English words for tech terms.",
154	            "id": "Speak in Indonesian. Keep it casual and conversational. Use English words for tech terms.",
155	            "de": "Speak in German. Keep it casual and conversational. Use English words for tech terms.",
156	            "ja": "Speak in Japanese. Keep it casual and conversational. Use English words for tech terms.",
157	            "ar": "Speak in Arabic. Keep it casual and conversational. Use English words for tech terms.",
158	            "ko": "Speak in Korean. Keep it casual and conversational. Use English words for tech terms.",
159	            "vi": "Speak in Vietnamese. Keep it casual and conversational. Use English words for tech terms.",
160	        }
161	        return _FALLBACK_PROMPTS.get(lang_code, "Speak in English. Keep it casual and conversational.")
162
163	    async def get_firered_config(self) -> Dict[str, Any]:
164	        """Get FireRed image editing config.
165
166	        DB value (JSON): {"enabled": true, "percentage": 20, "max_queue": 3}
167	        - enabled: kill switch
168	        - percentage: % of devices routed to FireRed (SHA256 hash)
169	        - max_queue: if FireRed processing >= this, skip to CF
170	        """
171	        from src.core.cache_layer import cached
172
173	        async def _fetch():
174	            try:
175	                result = await self.query({
176	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
177	                    "args": [{"type": "text", "value": "firered_config"}],
178	                }, "config_v2_firered")
179	                if result.rows:
180	                    raw = self.extract_text(result.rows[0], 0)
181	                    if raw:
182	                        return json.loads(raw)
183	            except Exception as e:
184	                logger.error(f"CONFIG_V2_FIRERED_ERROR: {e}")
185	            return None
186
187	        result = await cached(key="config_v2:firered_config", fallback=_fetch, ttl=120)
188	        if result:
189	            return result
190	        return {"enabled": False, "percentage": 0, "max_queue": 3}
191
192	    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
193	        """Get language-specific intro for a persona."""
194	        from src.core.cache_layer import cached
195
196	        async def _fetch():
197	            try:
198	                result = await self.query({
199	                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
200	                    "args": [{"type": "text", "value": "language_intros"}],
201	                }, "config_v2_language_intros")
202	                if result.rows:
203	                    raw = self.extract_text(result.rows[0], 0)
204	                    if raw:
205	                        return json.loads(raw)
206	            except Exception as e:
207	                logger.error(f"CONFIG_V2_LANGUAGE_INTROS_ERROR: {e}")
208	            return None
209
210	        intros = await cached(key="config_v2:language_intros", fallback=_fetch, ttl=300)
211	        if intros and persona_id in intros:
212	            return intros[persona_id].get(lang_code, intros[persona_id].get("hi", ""))
213	        return ""
214
215
216	# Singleton instance
217	config_v2_repo = ConfigV2Repository()
218

"""Repository for configurations_v2 table."""

import json
import time
from typing import Dict, Any, List

from loguru import logger

from src.repository.base import BaseRepository

# Defaults by region
_DEFAULT_LANGUAGES_INDIA = [
    {"code": "te", "native_label": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41", "script_char": "\u0c24", "english_label": "Telugu"},
    {"code": "hi", "native_label": "\u0939\u093f\u0928\u094d\u0926\u0940", "script_char": "\u0939", "english_label": "Hindi"}
]
_DEFAULT_LANGUAGES_GLOBAL = [
    {"code": "en", "native_label": "English", "script_char": "E", "english_label": "English"},
    {"code": "es", "native_label": "Español", "script_char": "E", "english_label": "Spanish"},
    {"code": "fr", "native_label": "Français", "script_char": "F", "english_label": "French"},
    {"code": "pt", "native_label": "Português", "script_char": "P", "english_label": "Portuguese"},
    {"code": "id", "native_label": "Bahasa Indonesia", "script_char": "B", "english_label": "Indonesian"},
    {"code": "de", "native_label": "Deutsch", "script_char": "D", "english_label": "German"},
    {"code": "ja", "native_label": "日本語", "script_char": "日", "english_label": "Japanese"},
    {"code": "ar", "native_label": "العربية", "script_char": "ع", "english_label": "Arabic"},
    {"code": "ko", "native_label": "한국어", "script_char": "한", "english_label": "Korean"},
    {"code": "vi", "native_label": "Tiếng Việt", "script_char": "V", "english_label": "Vietnamese"},
]

class ConfigV2Repository(BaseRepository):
    """Reads configuration values from the configurations_v2 table.

    Table schema:
        config_key   TEXT PRIMARY KEY
        config_value TEXT (JSON string or plain value)
        updated_at   INTEGER (unix timestamp)
    """

    async def get_on_open_config(self) -> Dict[str, Any]:
        """Get all on_open config values in a single query."""
        client = self.get_client()
        if not client:
            return {}

        start_time = time.time()
        try:
            result = await self.query({
                "sql": """
                    SELECT config_key, config_value
                    FROM configurations_v2
                    WHERE config_key IN ('update_enabled', 'force_update', 'show_language_screen')
                """,
                "args": [],
            }, "config_v2_on_open")

            config = {}
            if result.rows:
                for row in result.rows:
                    key = self.extract_text(row, 0)
                    value = self.extract_text(row, 1)
                    if key and value is not None:
                        config[key] = value.lower() in ("true", "1", "yes")

            duration_ms = (time.time() - start_time) * 1000
            logger.debug(f"CONFIG_V2_ON_OPEN: keys={list(config.keys())} duration_ms={duration_ms:.2f}")
            return config
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(f"CONFIG_V2_ON_OPEN_FAILED: duration_ms={duration_ms:.2f} error={e}")
            return {}

    async def get_languages(self, region: str = None) -> List[Dict[str, Any]]:
        """Get supported languages from config, optionally filtered by region.

        Languages in DB can have a 'region' field: 'india', 'global', or 'all'.
        If region is provided, returns only languages matching that region or 'all'.
        If no region field exists on a language, it's treated as 'india' for backward compat.
        """
        from src.core.cache_layer import cached

        async def _fetch_languages():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "languages"}],
                }, "config_v2_languages")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        parsed = json.loads(raw)
                        if isinstance(parsed, dict) and "languages" in parsed:
                            return parsed
                        if isinstance(parsed, list):
                            return {"languages": parsed}
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGES_FETCH_ERROR: {e}")
            return None

        result = await cached(
            key="config_v2:languages",
            fallback=_fetch_languages,
            ttl=300,
        )

        all_languages = []
        if result and "languages" in result:
            all_languages = result["languages"]
        else:
            # No DB config — return defaults based on region
            if region == "global":
                return _DEFAULT_LANGUAGES_GLOBAL
            return _DEFAULT_LANGUAGES_INDIA

        # Filter by region if provided
        if region:
            filtered = [
                lang for lang in all_languages
                if lang.get("region", "india") in (region, "all")
            ]
            return filtered if filtered else all_languages

        return all_languages

    async def get_language_prompt(self, lang_code: str) -> str:
        """Get language instruction for a language code (hi, te)."""
        from src.core.cache_layer import cached

        cache_key = f"config_v2:language_prompt_{lang_code}"

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": f"language_prompt_{lang_code}"}],
                }, "config_v2_language_prompt")
                if result.rows:
                    return self.extract_text(result.rows[0], 0)
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGE_PROMPT_ERROR: {e}")
            return None

        result = await cached(key=cache_key, fallback=_fetch, ttl=300)
        if result:
            return result
        # Fallback defaults
        _FALLBACK_PROMPTS = {
            "hi": "Speak in Hinglish — Hindi mixed with English words naturally. Use Roman script.",
            "te": "Speak in Telugu using Roman script (Tinglish). Mix English words naturally for tech terms.",
            "en": "Speak in English. Keep it casual and conversational.",
            "es": "Speak in Spanish. Keep it casual and conversational. Use English words for tech terms.",
            "fr": "Speak in French. Keep it casual and conversational. Use English words for tech terms.",
            "pt": "Speak in Portuguese. Keep it casual and conversational. Use English words for tech terms.",
            "id": "Speak in Indonesian. Keep it casual and conversational. Use English words for tech terms.",
            "de": "Speak in German. Keep it casual and conversational. Use English words for tech terms.",
            "ja": "Speak in Japanese. Keep it casual and conversational. Use English words for tech terms.",
            "ar": "Speak in Arabic. Keep it casual and conversational. Use English words for tech terms.",
            "ko": "Speak in Korean. Keep it casual and conversational. Use English words for tech terms.",
            "vi": "Speak in Vietnamese. Keep it casual and conversational. Use English words for tech terms.",
        }
        return _FALLBACK_PROMPTS.get(lang_code, "Speak in English. Keep it casual and conversational.")

    async def get_firered_config(self) -> Dict[str, Any]:
        """Get FireRed image editing config.

        DB value (JSON): {"enabled": true, "percentage": 20, "max_queue": 3}
        - enabled: kill switch
        - percentage: % of devices routed to FireRed (SHA256 hash)
        - max_queue: if FireRed processing >= this, skip to CF
        """
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "firered_config"}],
                }, "config_v2_firered")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_FIRERED_ERROR: {e}")
            return None

        result = await cached(key="config_v2:firered_config", fallback=_fetch, ttl=120)
        if result:
            return result
        return {"enabled": False, "percentage": 0, "max_queue": 3}

    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
        """Get language-specific intro for a persona."""
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "language_intros"}],
                }, "config_v2_language_intros")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGE_INTROS_ERROR: {e}")
            return None

        intros = await cached(key="config_v2:language_intros", fallback=_fetch, ttl=300)
        if intros and persona_id in intros:
            return intros[persona_id].get(lang_code, intros[persona_id].get("hi", ""))
        return ""

# Singleton instance
config_v2_repo = ConfigV2Repository()


Now I understand the full flow. Here's what I propose — a single DB config `free_limits_config` in `configurations_v2` that controls everything:

```json
{
  "enabled": true,
  "show_payments": false,
  "free_count": 5,
  "cooldown_minutes": 60
}
```

- **`show_payments`**: `false` = never show payment popup (even when free exhausted, just block with a "try again later" message). `true` = show payment popup as usual.
- **`free_count`**: number of free generations per cooldown window (replaces the 999999 in persona table)
- **`cooldown_minutes`**: cooldown window in minutes (e.g., 60 = 5 generations per hour)
- **`enabled`**: if `false`, falls back to existing persona-level limits

This way you can tweak limits from DB without redeploying. Want me to implement this?


we alredy have limits mentioned in each persona table at persona level .. we should be able to add payments turn on and off .. if on and limits completed as of now shows payemnets page ... and what we need to include a tag .. like if payments off we need to have colldown perdio and agent should say same


Got it. Simple approach:

1. DB config `payments_config` → `{"enabled": false, "cooldown_minutes": 60}`
2. Payments ON + limits hit → show payment popup (current behavior)
3. Payments OFF + limits hit → no popup, agent says "limit reached, try after X minutes"

Let me implement it.


78
79	    # Load usage state (persona-level free limits, content-level plan/topup)
80	    if persona_id and bot_session.content_type and device_id != "common":
81	        from src.service.usage_service import get_usage_state
82	        from src.repository.usage_repository import usage_repo
83	        from src.repository.plan_repository import plan_repo
84	        import hashlib
85
86	        device_region = (device_info.get("region") or "india") if device_info else "india"
87	        payment_provider = "google_play" if device_region == "global" else "razorpay"
88
89	        usage = await get_usage_state(device_id, bot_session.content_type, payment_provider)
90	        bot_session.plan_remaining = usage["plan_remaining"]
91	        bot_session.topup_remaining = usage["topup_remaining"]
92	        bot_session.subscription_id = usage["subscription_id"]
93	        bot_session.plan_content_type = usage.get("plan_content_type")
94
95	        if device_region == "india":
96	            # India: always use persona-level free limits (no change from old behavior)
97	            free_count = persona.get("free_count", 0) if persona else 0
98	            free_window = persona.get("free_window_hours", 24) if persona else 24
99	        else:
100	            # Global: use region_free_limits with A/B percentage
101	            # Group A (hash < ab_percentage): fully free, no limits
102	            # Group B (hash >= ab_percentage): gets free_count free, then paywall
103	            region_limits = await plan_repo.get_region_free_limits(device_region)
104	            region_limit = region_limits.get(bot_session.content_type)
105	            if region_limit:
106	                ab_pct = region_limit.get("ab_percentage", 100)
107	                device_h = int(hashlib.sha256(device_id.encode()).hexdigest(), 16) % 100
108	                if device_h < ab_pct:
109	                    # Group A: fully free — set unlimited
110	                    free_count = 999999
111	                    free_window = 24
112	                else:
113	                    # Group B: limited free
114	                    free_count = region_limit["free_count"]
115	                    free_window = region_limit["free_window_hours"]
116	            else:
117	                free_count = persona.get("free_count", 0) if persona else 0
118	                free_window = persona.get("free_window_hours", 24) if persona else 24
119
120	        if free_count > 0:
121	            if free_count == 999999:
122	                bot_session.free_remaining = 999999  # unlimited
123	            else:
124	                used_free = await usage_repo.count_persona_generations(device_id, persona_id, bot_session.content_type, free_window)
125	                bot_session.free_remaining = max(0, free_count - used_free)
126	        else:
127	            bot_session.free_remaining = 0
128
129	        logger.info(f"USAGE_STATE: session_id={session_id} device_id={device_id} persona={persona_id} content={bot_session.content_type} free={bot_session.free_remaining}/{free_count} plan={bot_session.plan_remaining} topup={bot_session.topup_remaining} transport={transport_type}")
130
131	    # Determine which tools to enable
132	    video_enabled = "generate_video" in persona_tools or "generate_video_from_image" in persona_tools

    # Load usage state (persona-level free limits, content-level plan/topup)
    if persona_id and bot_session.content_type and device_id != "common":
        from src.service.usage_service import get_usage_state
        from src.repository.usage_repository import usage_repo
        from src.repository.plan_repository import plan_repo
        import hashlib

        device_region = (device_info.get("region") or "india") if device_info else "india"
        payment_provider = "google_play" if device_region == "global" else "razorpay"

        usage = await get_usage_state(device_id, bot_session.content_type, payment_provider)
        bot_session.plan_remaining = usage["plan_remaining"]
        bot_session.topup_remaining = usage["topup_remaining"]
        bot_session.subscription_id = usage["subscription_id"]
        bot_session.plan_content_type = usage.get("plan_content_type")

        if device_region == "india":
            # India: always use persona-level free limits (no change from old behavior)
            free_count = persona.get("free_count", 0) if persona else 0
            free_window = persona.get("free_window_hours", 24) if persona else 24
        else:
            # Global: use region_free_limits with A/B percentage
            # Group A (hash < ab_percentage): fully free, no limits
            # Group B (hash >= ab_percentage): gets free_count free, then paywall
            region_limits = await plan_repo.get_region_free_limits(device_region)
            region_limit = region_limits.get(bot_session.content_type)
            if region_limit:
                ab_pct = region_limit.get("ab_percentage", 100)
                device_h = int(hashlib.sha256(device_id.encode()).hexdigest(), 16) % 100
                if device_h < ab_pct:
                    # Group A: fully free — set unlimited
                    free_count = 999999
                    free_window = 24
                else:
                    # Group B: limited free
                    free_count = region_limit["free_count"]
                    free_window = region_limit["free_window_hours"]
            else:
                free_count = persona.get("free_count", 0) if persona else 0
                free_window = persona.get("free_window_hours", 24) if persona else 24

        if free_count > 0:
            if free_count == 999999:
                bot_session.free_remaining = 999999  # unlimited
            else:
                used_free = await usage_repo.count_persona_generations(device_id, persona_id, bot_session.content_type, free_window)
                bot_session.free_remaining = max(0, free_count - used_free)
        else:
            bot_session.free_remaining = 0

        logger.info(f"USAGE_STATE: session_id={session_id} device_id={device_id} persona={persona_id} content={bot_session.content_type} free={bot_session.free_remaining}/{free_count} plan={bot_session.plan_remaining} topup={bot_session.topup_remaining} transport={transport_type}")

    # Determine which tools to enable
    video_enabled = "generate_video" in persona_tools or "generate_video_from_image" in persona_tools


Now let me check where the limit popup is triggered in the image/video handlers.


src/bot/services/limit_checker.py:6:def can_generate(bot_session) -> str:
src/bot/services/limit_checker.py:42:async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
src/bot/services/limit_checker.py:58:        if block_reason == "topup":
src/bot/services/limit_checker.py:65:        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
src/bot/services/limit_checker.py:70:                "reason": block_reason,
src/bot/services/limit_checker.py:75:        logger.info(f"LIMIT_POPUP_SENT: device={device_id} reason={block_reason} content={content_type} provider={payment_provider} plan_id={plan_data.get('plan_id') or plan_data.get('topup_id', 'none')}")
src/bot/services/limit_checker.py:79:        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
src/bot/services/custom_tts.py:47:    def can_generate_metrics(self) -> bool:
src/bot/services/video_service.py:208:        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
src/bot/services/video_service.py:209:        block_reason = _can_gen(bot_session)
src/bot/services/video_service.py:210:        if block_reason:
src/bot/services/video_service.py:211:            await send_limit_popup(bot_session, rtvi_processor, block_reason)
src/bot/services/video_service.py:267:        from src.bot.services.limit_checker import can_generate as _can_gen, send_limit_popup
src/bot/services/video_service.py:268:        block_reason = _can_gen(bot_session)
src/bot/services/video_service.py:269:        if block_reason:
src/bot/services/video_service.py:270:            await send_limit_popup(bot_session, rtvi_processor, block_reason)
src/bot/services/music_service.py:166:        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
src/bot/services/music_service.py:169:            await send_limit_popup(bot_session, rtvi, limit_reason)
src/bot/services/ppt_service.py:192:        from src.bot.services.limit_checker import can_generate as check_limits, send_limit_popup
src/bot/services/ppt_service.py:195:            await send_limit_popup(bot_session, rtvi, limit_reason)
src/bot/services/image_service.py:167:            from src.bot.services.limit_checker import can_generate, send_limit_popup
src/bot/services/image_service.py:168:            block_reason = can_generate(bot_session)
src/bot/services/image_service.py:169:            if block_reason:
src/bot/services/image_service.py:170:                await send_limit_popup(bot_session, rtvi_processor, block_reason)


All services call `can_generate()` → if blocked, call `send_limit_popup()`. I need to:

1. Add `get_payments_config()` to `config_v2_repository.py`
2. Modify `limit_checker.py` — when payments off, return `"cooldown"` instead of `"subscribe"`/`"topup"`
3. Modify `send_limit_popup()` — when cooldown, don't send payment popup, just send a cooldown message
4. Load payments config into `bot_session` at startup

Let me check the bot session state class first.


src/bot/core/config.py:123:class BotSessionState:
src/bot/core/config.py-124-    def __init__(self, session_id: str, device_id: str, bot_start_time: float):
/home/ubuntu/pipecat/src/bot/core/config.py-125-        self.session_id = session_id
/home/ubuntu/pipecat/src/bot/core/config.py-126-        self.device_id = device_id
/home/ubuntu/pipecat/src/bot/core/config.py-127-        self.chunk_counter = 0
src/bot/core/config.py-128-        self.user_stopped_speaking_time: Optional[float] = None
src/bot/core/config.py-129-        self.current_user_id: Optional[str] = None
/home/ubuntu/pipecat/src/bot/core/config.py-130-        self.greeting_sent = False
/home/ubuntu/pipecat/src/bot/core/config.py-131-        self.user_spoke_first = False
/home/ubuntu/pipecat/src/bot/core/config.py-132-        self.audio_recorder = None
/home/ubuntu/pipecat/src/bot/core/config.py-133-        self.user_joined = False
/home/ubuntu/pipecat/src/bot/core/config.py-134-        self.bot_joined = False
/home/ubuntu/pipecat/src/bot/core/config.py-135-        self.bot_idle = False
/home/ubuntu/pipecat/src/bot/core/config.py-136-        self.no_user_timeout_task = None
src/bot/core/config.py-137-        self.participants: dict = {}
/home/ubuntu/pipecat/src/bot/core/config.py-138-        self.session_start_time = bot_start_time
/home/ubuntu/pipecat/src/bot/core/config.py-139-        self.bot_start_time = bot_start_time
/home/ubuntu/pipecat/src/bot/core/config.py-140-        self.app_review_eligible = False
/home/ubuntu/pipecat/src/bot/core/config.py-141-        self.vad_analyzer = None
/home/ubuntu/pipecat/src/bot/core/config.py-142-        self.audio_buffer = None
src/bot/core/config.py-143-        self._last_upload_time: float = 0
src/bot/core/config.py-144-        self.uploaded_image: Optional[dict] = None
src/bot/core/config.py-145-        self.last_image: Optional[dict] = None
src/bot/core/config.py-146-        self.last_generated_image: Optional[dict] = None
src/bot/core/config.py-147-        self.image_batch: list = []  # NEW: Batch of images uploaded together
src/bot/core/config.py-148-        self.max_batch_size: int = 2  # NEW: Max images per batch
/home/ubuntu/pipecat/src/bot/core/config.py-149-        self.session_created_at_start = False
src/bot/core/config.py-150-        self.video_enabled = False  # NEW: Track if video generation is enabled for this session
src/bot/core/config.py-151-        self.is_processing = False  # NEW: In-memory lock for video/image processing (prevents concurrent requests)
src/bot/core/config.py-152-        self.generated_videos: List[Dict] = []  # Store generated videos to save at session end
src/bot/core/config.py-153-        self.generated_ppts: List[Dict] = []    # Store generated PPTs to save at session end


[Request interrupted by user]


can we keep colldown period cotnet level


Yes. DB config like:

```json
{
  "payments_enabled": false,
  "cooldowns": {
    "image_gen": 30,
    "video_gen": 60,
    "music_gen": 60,
    "ppt_gen": 60
  }
}
```

Persona table keeps `free_count` / `free_window_hours` as-is. When limits hit:
- **payments ON** → payment popup (current)
- **payments OFF** → agent says "limit ho gaya, X minutes baad try karo"

Let me implement.


Edit

async def get_language_intro(self, lang_code: str, persona_id: str) -> str:

    async def get_payments_config(self) -> Dict[str, Any]:
        """Get payments config (toggle payments + per-content cooldowns).

        DB value (JSON): {
            "payments_enabled": false,
            "cooldowns": {"image_gen": 30, "video_gen": 60, "music_gen": 60, "ppt_gen": 60}
        }
        - payments_enabled: true = show payment popup when limits hit, false = show cooldown message
        - cooldowns: per content_type cooldown in minutes (agent tells user to wait)
        """
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "payments_config"}],
                }, "config_v2_payments")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_PAYMENTS_ERROR: {e}")
            return None

        result = await cached(key="config_v2:payments_config", fallback=_fetch, ttl=120)
        if result:
            return result
        return {"payments_enabled": True, "cooldowns": {}}

    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:


The file /home/ubuntu/pipecat/src/repository/config_v2_repository.py has been updated successfully.

    async def get_payments_config(self) -> Dict[str, Any]:
        """Get payments config (toggle payments + per-content cooldowns).

        DB value (JSON): {
            "payments_enabled": false,
            "cooldowns": {"image_gen": 30, "video_gen": 60, "music_gen": 60, "ppt_gen": 60}
        }
        - payments_enabled: true = show payment popup when limits hit, false = show cooldown message
        - cooldowns: per content_type cooldown in minutes (agent tells user to wait)
        """
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "payments_config"}],
                }, "config_v2_payments")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_PAYMENTS_ERROR: {e}")
            return None

        result = await cached(key="config_v2:payments_config", fallback=_fetch, ttl=120)
        if result:
            return result
        return {"payments_enabled": True, "cooldowns": {}}

    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:

"""Repository for configurations_v2 table."""

import json
import time
from typing import Dict, Any, List

from loguru import logger

from src.repository.base import BaseRepository

# Defaults by region
_DEFAULT_LANGUAGES_INDIA = [
    {"code": "te", "native_label": "\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41", "script_char": "\u0c24", "english_label": "Telugu"},
    {"code": "hi", "native_label": "\u0939\u093f\u0928\u094d\u0926\u0940", "script_char": "\u0939", "english_label": "Hindi"}
]
_DEFAULT_LANGUAGES_GLOBAL = [
    {"code": "en", "native_label": "English", "script_char": "E", "english_label": "English"},
    {"code": "es", "native_label": "Español", "script_char": "E", "english_label": "Spanish"},
    {"code": "fr", "native_label": "Français", "script_char": "F", "english_label": "French"},
    {"code": "pt", "native_label": "Português", "script_char": "P", "english_label": "Portuguese"},
    {"code": "id", "native_label": "Bahasa Indonesia", "script_char": "B", "english_label": "Indonesian"},
    {"code": "de", "native_label": "Deutsch", "script_char": "D", "english_label": "German"},
    {"code": "ja", "native_label": "日本語", "script_char": "日", "english_label": "Japanese"},
    {"code": "ar", "native_label": "العربية", "script_char": "ع", "english_label": "Arabic"},
    {"code": "ko", "native_label": "한국어", "script_char": "한", "english_label": "Korean"},
    {"code": "vi", "native_label": "Tiếng Việt", "script_char": "V", "english_label": "Vietnamese"},
]

class ConfigV2Repository(BaseRepository):
    """Reads configuration values from the configurations_v2 table.

    Table schema:
        config_key   TEXT PRIMARY KEY
        config_value TEXT (JSON string or plain value)
        updated_at   INTEGER (unix timestamp)
    """

    async def get_on_open_config(self) -> Dict[str, Any]:
        """Get all on_open config values in a single query."""
        client = self.get_client()
        if not client:
            return {}

        start_time = time.time()
        try:
            result = await self.query({
                "sql": """
                    SELECT config_key, config_value
                    FROM configurations_v2
                    WHERE config_key IN ('update_enabled', 'force_update', 'show_language_screen')
                """,
                "args": [],
            }, "config_v2_on_open")

            config = {}
            if result.rows:
                for row in result.rows:
                    key = self.extract_text(row, 0)
                    value = self.extract_text(row, 1)
                    if key and value is not None:
                        config[key] = value.lower() in ("true", "1", "yes")

            duration_ms = (time.time() - start_time) * 1000
            logger.debug(f"CONFIG_V2_ON_OPEN: keys={list(config.keys())} duration_ms={duration_ms:.2f}")
            return config
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(f"CONFIG_V2_ON_OPEN_FAILED: duration_ms={duration_ms:.2f} error={e}")
            return {}

    async def get_languages(self, region: str = None) -> List[Dict[str, Any]]:
        """Get supported languages from config, optionally filtered by region.

        Languages in DB can have a 'region' field: 'india', 'global', or 'all'.
        If region is provided, returns only languages matching that region or 'all'.
        If no region field exists on a language, it's treated as 'india' for backward compat.
        """
        from src.core.cache_layer import cached

        async def _fetch_languages():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "languages"}],
                }, "config_v2_languages")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        parsed = json.loads(raw)
                        if isinstance(parsed, dict) and "languages" in parsed:
                            return parsed
                        if isinstance(parsed, list):
                            return {"languages": parsed}
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGES_FETCH_ERROR: {e}")
            return None

        result = await cached(
            key="config_v2:languages",
            fallback=_fetch_languages,
            ttl=300,
        )

        all_languages = []
        if result and "languages" in result:
            all_languages = result["languages"]
        else:
            # No DB config — return defaults based on region
            if region == "global":
                return _DEFAULT_LANGUAGES_GLOBAL
            return _DEFAULT_LANGUAGES_INDIA

        # Filter by region if provided
        if region:
            filtered = [
                lang for lang in all_languages
                if lang.get("region", "india") in (region, "all")
            ]
            return filtered if filtered else all_languages

        return all_languages

    async def get_language_prompt(self, lang_code: str) -> str:
        """Get language instruction for a language code (hi, te)."""
        from src.core.cache_layer import cached

        cache_key = f"config_v2:language_prompt_{lang_code}"

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": f"language_prompt_{lang_code}"}],
                }, "config_v2_language_prompt")
                if result.rows:
                    return self.extract_text(result.rows[0], 0)
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGE_PROMPT_ERROR: {e}")
            return None

        result = await cached(key=cache_key, fallback=_fetch, ttl=300)
        if result:
            return result
        # Fallback defaults
        _FALLBACK_PROMPTS = {
            "hi": "Speak in Hinglish — Hindi mixed with English words naturally. Use Roman script.",
            "te": "Speak in Telugu using Roman script (Tinglish). Mix English words naturally for tech terms.",
            "en": "Speak in English. Keep it casual and conversational.",
            "es": "Speak in Spanish. Keep it casual and conversational. Use English words for tech terms.",
            "fr": "Speak in French. Keep it casual and conversational. Use English words for tech terms.",
            "pt": "Speak in Portuguese. Keep it casual and conversational. Use English words for tech terms.",
            "id": "Speak in Indonesian. Keep it casual and conversational. Use English words for tech terms.",
            "de": "Speak in German. Keep it casual and conversational. Use English words for tech terms.",
            "ja": "Speak in Japanese. Keep it casual and conversational. Use English words for tech terms.",
            "ar": "Speak in Arabic. Keep it casual and conversational. Use English words for tech terms.",
            "ko": "Speak in Korean. Keep it casual and conversational. Use English words for tech terms.",
            "vi": "Speak in Vietnamese. Keep it casual and conversational. Use English words for tech terms.",
        }
        return _FALLBACK_PROMPTS.get(lang_code, "Speak in English. Keep it casual and conversational.")

    async def get_firered_config(self) -> Dict[str, Any]:
        """Get FireRed image editing config.

        DB value (JSON): {"enabled": true, "percentage": 20, "max_queue": 3}
        - enabled: kill switch
        - percentage: % of devices routed to FireRed (SHA256 hash)
        - max_queue: if FireRed processing >= this, skip to CF
        """
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "firered_config"}],
                }, "config_v2_firered")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_FIRERED_ERROR: {e}")
            return None

        result = await cached(key="config_v2:firered_config", fallback=_fetch, ttl=120)
        if result:
            return result
        return {"enabled": False, "percentage": 0, "max_queue": 3}

    async def get_language_intro(self, lang_code: str, persona_id: str) -> str:
        """Get language-specific intro for a persona."""
        from src.core.cache_layer import cached

        async def _fetch():
            try:
                result = await self.query({
                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",
                    "args": [{"type": "text", "value": "language_intros"}],
                }, "config_v2_language_intros")
                if result.rows:
                    raw = self.extract_text(result.rows[0], 0)
                    if raw:
                        return json.loads(raw)
            except Exception as e:
                logger.error(f"CONFIG_V2_LANGUAGE_INTROS_ERROR: {e}")
            return None

        intros = await cached(key="config_v2:language_intros", fallback=_fetch, ttl=300)
        if intros and persona_id in intros:
            return intros[persona_id].get(lang_code, intros[persona_id].get("hi", ""))
        return ""

# Singleton instance
config_v2_repo = ConfigV2Repository()

+    async def get_payments_config(self) -> Dict[str, Any]:

+        """Get payments config (toggle payments + per-content cooldowns).

+            "cooldowns": {"image_gen": 30, "video_gen": 60, "music_gen": 60, "ppt_gen": 60}

+        - payments_enabled: true = show payment popup when limits hit, false = show cooldown message

+        - cooldowns: per content_type cooldown in minutes (agent tells user to wait)

+        from src.core.cache_layer import cached

+                result = await self.query({

+                    "sql": "SELECT config_value FROM configurations_v2 WHERE config_key = ?",

+                    "args": [{"type": "text", "value": "payments_config"}],

+                }, "config_v2_payments")

+                    raw = self.extract_text(result.rows[0], 0)

+                        return json.loads(raw)

+                logger.error(f"CONFIG_V2_PAYMENTS_ERROR: {e}")

+        result = await cached(key="config_v2:payments_config", fallback=_fetch, ttl=120)

+        return {"payments_enabled": True, "cooldowns": {}}


Now update `limit_checker.py` to handle payments on/off + cooldown:


File unchanged since last read. The content from the earlier Read tool_result in this conversation is still current — refer to that instead of re-reading.


Write

"""Usage limit checker — 3-layer credits: plan → topup → free."""

from loguru import logger

def can_generate(bot_session) -> str:
    """Check if user can generate content.

    Returns:
        "" — allowed
        "subscribe" — no plan, free exhausted, payments ON → show plans popup
        "topup" — has plan but exhausted, payments ON → show topup popup
        "cooldown" — limits exhausted, payments OFF → agent says wait
    """
    if bot_session.plan_remaining > 0:
        return ""
    if bot_session.topup_remaining > 0:
        return ""
    if bot_session.free_remaining > 0:
        return ""

    # Blocked — check if payments are enabled
    if not bot_session.payments_enabled:
        return "cooldown"

    if bot_session.subscription_id:
        return "topup"
    return "subscribe"

def use_credit(bot_session):
    """Decrement one credit after successful generation. Plan first, then topup, then free."""
    if bot_session.plan_remaining > 0:
        bot_session.plan_remaining -= 1
        bot_session.credits_used_plan += 1
        logger.debug(f"CREDIT_USED_PLAN: device={bot_session.device_id} plan_remaining={bot_session.plan_remaining}")
    elif bot_session.topup_remaining > 0:
        bot_session.topup_remaining -= 1
        bot_session.credits_used_topup += 1
        logger.debug(f"CREDIT_USED_TOPUP: device={bot_session.device_id} topup_remaining={bot_session.topup_remaining}")
    elif bot_session.free_remaining > 0:
        bot_session.free_remaining -= 1
        logger.debug(f"CREDIT_USED_FREE: device={bot_session.device_id} free_remaining={bot_session.free_remaining}")

async def send_limit_popup(bot_session, rtvi_processor, block_reason: str):
    """Send limit reached popup or cooldown message to the frontend.

    If block_reason is "cooldown", sends a cooldown message instead of payment popup.
    The agent will verbally tell the user to wait.
    """
    from src.service.usage_service import get_plans_for_device, get_topup_for_device, get_topups_for_content

    content_type = bot_session.content_type or "image_gen"
    device_id = bot_session.device_id

    if block_reason == "cooldown":
        cooldown_minutes = bot_session.cooldown_minutes
        logger.info(f"COOLDOWN_LIMIT: device={device_id} content={content_type} cooldown_minutes={cooldown_minutes}")
        await rtvi_processor.send_server_message({
            "type": "show_cooldown",
            "data": {
                "content_type": content_type,
                "cooldown_minutes": cooldown_minutes,
            },
        })
        return

    # Payment popup flow (existing behavior)
    from src.service.usage_service import _get_device_region, _get_payment_provider

    region = await _get_device_region(device_id)
    payment_provider = _get_payment_provider(region)

    try:
        if block_reason == "topup":
            plan_data = await get_topup_for_device(device_id, content_type, payment_provider)
            topups_list = await get_topups_for_content(content_type, payment_provider)
        else:
            plan_data = await get_plans_for_device(device_id, content_type, payment_provider)
            topups_list = []

        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
        await rtvi_processor.send_server_message({
            "type": popup_type,
            "data": {
                "content_type": content_type,
                "reason": block_reason,
                "plan": plan_data,
                "topups": topups_list,
            },
        })
        logger.info(f"LIMIT_POPUP_SENT: device={device_id} reason={block_reason} content={content_type} provider={payment_provider} plan_id={plan_data.get('plan_id') or plan_data.get('topup_id', 'none')}")
    except Exception as e:
        logger.error(f"LIMIT_POPUP_FAILED: device={device_id} error={e}")
        popup_type = "show_plans_popup" if block_reason == "subscribe" else "show_topup_popup"
        await rtvi_processor.send_server_message({"type": popup_type, "content_type": content_type})