fix: preserve premium topup squad access
This commit is contained in:
@@ -349,24 +349,23 @@ class TrafficMixin:
|
||||
},
|
||||
)
|
||||
|
||||
panel_payload = {
|
||||
"uuid": db_user.panel_user_uuid,
|
||||
"activeInternalSquads": self._panel_squads_for_tariff(
|
||||
tariff,
|
||||
include_premium=not premium_is_limited,
|
||||
),
|
||||
}
|
||||
updated_panel = await self.panel_service.update_user_details_on_panel(
|
||||
db_user.panel_user_uuid, panel_payload
|
||||
desired_squads = self._panel_squads_for_tariff(
|
||||
tariff,
|
||||
include_premium=not premium_is_limited,
|
||||
)
|
||||
if not updated_panel or updated_panel.get("error"):
|
||||
panel_updated = await self._sync_panel_squads_if_needed(
|
||||
db_user.panel_user_uuid,
|
||||
desired_squads,
|
||||
user_id=user_id,
|
||||
source="premium_topup",
|
||||
)
|
||||
if not panel_updated:
|
||||
# Otherwise the user pays for premium top-up but the panel never
|
||||
# re-grants premium squad access (the most common case here is
|
||||
# transitioning from premium_is_limited=True back to False).
|
||||
logging.warning(
|
||||
"Panel user details update FAILED for premium top-up user %s. Response: %s",
|
||||
"Panel user details update FAILED for premium top-up user %s.",
|
||||
user_id,
|
||||
updated_panel,
|
||||
)
|
||||
return None
|
||||
await tariff_dal.create_traffic_topup(
|
||||
@@ -432,11 +431,17 @@ class TrafficMixin:
|
||||
|
||||
squads = self._panel_squads_for_tariff(tariff, include_premium=not premium_is_limited)
|
||||
try:
|
||||
await self.panel_service.update_user_details_on_panel(
|
||||
panel_updated = await self._sync_panel_squads_if_needed(
|
||||
db_user.panel_user_uuid,
|
||||
{"uuid": db_user.panel_user_uuid, "activeInternalSquads": squads},
|
||||
log_response=False,
|
||||
squads,
|
||||
user_id=user_id,
|
||||
source="admin_premium_override",
|
||||
)
|
||||
if not panel_updated:
|
||||
logging.warning(
|
||||
"sync_premium_squad_access_to_panel: panel update failed for user %s",
|
||||
user_id,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"sync_premium_squad_access_to_panel: failed to push squads for user %s", user_id
|
||||
@@ -679,17 +684,22 @@ class TrafficMixin:
|
||||
"premium_period_start_at": premium_period_start,
|
||||
},
|
||||
)
|
||||
panel_payload = {
|
||||
"uuid": db_user.panel_user_uuid,
|
||||
"activeInternalSquads": self._panel_squads_for_tariff(
|
||||
tariff,
|
||||
include_premium=not premium_is_limited,
|
||||
),
|
||||
}
|
||||
desired_squads = self._panel_squads_for_tariff(
|
||||
tariff,
|
||||
include_premium=not premium_is_limited,
|
||||
)
|
||||
try:
|
||||
await self.panel_service.update_user_details_on_panel(
|
||||
db_user.panel_user_uuid, panel_payload
|
||||
panel_updated = await self._sync_panel_squads_if_needed(
|
||||
db_user.panel_user_uuid,
|
||||
desired_squads,
|
||||
user_id=user_id,
|
||||
source="admin_premium_topup",
|
||||
)
|
||||
if not panel_updated:
|
||||
logging.warning(
|
||||
"admin_grant_premium_topup: panel update failed for user %s",
|
||||
user_id,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"admin_grant_premium_topup: failed to push panel update for user %s",
|
||||
@@ -710,3 +720,128 @@ class TrafficMixin:
|
||||
"premium_is_limited": premium_is_limited,
|
||||
"granted_bytes": purchase_bytes,
|
||||
}
|
||||
|
||||
async def _sync_panel_squads_if_needed(
|
||||
self,
|
||||
panel_user_uuid: str,
|
||||
desired_squads: List[str],
|
||||
*,
|
||||
user_id: int,
|
||||
source: str,
|
||||
) -> bool:
|
||||
match, current_set = await self._panel_squads_match(panel_user_uuid, desired_squads)
|
||||
if match is True:
|
||||
return True
|
||||
|
||||
desired_set = self._panel_squad_uuid_set(desired_squads)
|
||||
self._log_panel_squad_patch(
|
||||
source=source,
|
||||
user_id=user_id,
|
||||
panel_uuid=panel_user_uuid,
|
||||
current_set=current_set,
|
||||
desired_set=desired_set,
|
||||
)
|
||||
updated_panel = await self.panel_service.update_user_details_on_panel(
|
||||
panel_user_uuid,
|
||||
{"uuid": panel_user_uuid, "activeInternalSquads": desired_squads},
|
||||
log_response=False,
|
||||
)
|
||||
if not updated_panel:
|
||||
return False
|
||||
return not (isinstance(updated_panel, dict) and updated_panel.get("error"))
|
||||
|
||||
async def _panel_squads_match(
|
||||
self,
|
||||
panel_user_uuid: str,
|
||||
desired_squads: List[str],
|
||||
) -> tuple[Optional[bool], Optional[set[str]]]:
|
||||
try:
|
||||
panel_user = await self.panel_service.get_user_by_uuid(
|
||||
panel_user_uuid,
|
||||
log_response=False,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to fetch panel user %s before premium squad update",
|
||||
panel_user_uuid,
|
||||
)
|
||||
return None, None
|
||||
current_known, current_set = self._panel_active_squad_uuid_set(panel_user)
|
||||
if not current_known:
|
||||
return None, current_set
|
||||
return current_set == self._panel_squad_uuid_set(desired_squads), current_set
|
||||
|
||||
@classmethod
|
||||
def _panel_active_squad_uuid_set(
|
||||
cls,
|
||||
panel_user: Optional[dict],
|
||||
) -> tuple[bool, set[str]]:
|
||||
if not isinstance(panel_user, dict):
|
||||
return False, set()
|
||||
for key in (
|
||||
"activeInternalSquads",
|
||||
"active_internal_squads",
|
||||
"activeInternalSquadUuids",
|
||||
"active_internal_squad_uuids",
|
||||
):
|
||||
if key in panel_user:
|
||||
return True, cls._panel_squad_uuid_set(panel_user.get(key))
|
||||
return False, set()
|
||||
|
||||
@staticmethod
|
||||
def _panel_squad_uuid_set(raw) -> set[str]:
|
||||
if not isinstance(raw, (list, tuple, set)):
|
||||
return set()
|
||||
out: set[str] = set()
|
||||
for item in raw:
|
||||
if isinstance(item, dict):
|
||||
nested_squad = item.get("internalSquad") or item.get("squad")
|
||||
if not isinstance(nested_squad, dict):
|
||||
nested_squad = {}
|
||||
squad_uuid = (
|
||||
item.get("uuid")
|
||||
or item.get("internalSquadUuid")
|
||||
or item.get("squadUuid")
|
||||
or nested_squad.get("uuid")
|
||||
)
|
||||
if squad_uuid:
|
||||
out.add(str(squad_uuid))
|
||||
elif item:
|
||||
out.add(str(item))
|
||||
return out
|
||||
|
||||
def _log_panel_squad_patch(
|
||||
self,
|
||||
*,
|
||||
source: str,
|
||||
user_id: int,
|
||||
panel_uuid: str,
|
||||
current_set: Optional[set[str]],
|
||||
desired_set: set[str],
|
||||
) -> None:
|
||||
logging.info(
|
||||
"Sync panel PATCH: source=%s user_id=%s telegram_id=%s panel_uuid=%s "
|
||||
"panel_view=full_fetch reasons=activeInternalSquads_mismatch "
|
||||
"fields=activeInternalSquads payload_fields=activeInternalSquads changes=%s",
|
||||
source,
|
||||
user_id,
|
||||
user_id,
|
||||
panel_uuid,
|
||||
"activeInternalSquads:%s->%s"
|
||||
% (
|
||||
self._format_panel_squad_set(current_set),
|
||||
self._format_panel_squad_set(desired_set),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_panel_squad_set(value: Optional[set[str]]) -> str:
|
||||
if value is None:
|
||||
return "missing"
|
||||
values = sorted(str(item) for item in value)
|
||||
preview = ",".join(values[:4])
|
||||
suffix = ",..." if len(values) > 4 else ""
|
||||
text = f"[{len(values)}:{preview}{suffix}]"
|
||||
if len(text) > 96:
|
||||
return f"{text[:93]}..."
|
||||
return text
|
||||
|
||||
@@ -486,12 +486,22 @@ class TariffTrafficWorker:
|
||||
return
|
||||
|
||||
premium_period_start = month_start(now)
|
||||
same_period = bool(getattr(sub, "premium_period_start_at", None) == premium_period_start)
|
||||
same_period = self._same_premium_period(
|
||||
getattr(sub, "premium_period_start_at", None),
|
||||
premium_period_start,
|
||||
)
|
||||
premium_baseline = int(tariff.premium_monthly_bytes or 0)
|
||||
premium_topup_balance = int(sub.premium_topup_balance_bytes or 0)
|
||||
premium_topup_used = (
|
||||
int(getattr(sub, "premium_topup_used_bytes", 0) or 0) if same_period else 0
|
||||
)
|
||||
premium_topup_balance = await self._repair_premium_topup_balance_from_ledger(
|
||||
session,
|
||||
sub,
|
||||
premium_period_start,
|
||||
premium_topup_balance,
|
||||
premium_topup_used,
|
||||
)
|
||||
# Admin-side overrides for free gifted premium traffic.
|
||||
premium_unlimited_override = bool(getattr(sub, "premium_unlimited_override", False))
|
||||
premium_bonus = max(0, int(getattr(sub, "premium_bonus_bytes", 0) or 0))
|
||||
@@ -648,6 +658,72 @@ class TariffTrafficWorker:
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _same_premium_period(value: Optional[datetime], premium_period_start: datetime) -> bool:
|
||||
if value is None:
|
||||
return False
|
||||
try:
|
||||
return month_start(value) == premium_period_start
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _repair_premium_topup_balance_from_ledger(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
sub: Subscription,
|
||||
premium_period_start: datetime,
|
||||
premium_topup_balance: int,
|
||||
premium_topup_used: int,
|
||||
) -> int:
|
||||
ledger_total = await self._premium_topup_ledger_total(
|
||||
session,
|
||||
int(getattr(sub, "subscription_id", 0) or 0),
|
||||
premium_period_start,
|
||||
)
|
||||
if ledger_total is None:
|
||||
return premium_topup_balance
|
||||
|
||||
tracked_total = max(0, int(premium_topup_balance or 0)) + max(
|
||||
0,
|
||||
int(premium_topup_used or 0),
|
||||
)
|
||||
if ledger_total <= tracked_total:
|
||||
return premium_topup_balance
|
||||
|
||||
repaired_bytes = ledger_total - tracked_total
|
||||
logging.warning(
|
||||
"Premium top-up balance repaired from ledger for user %s subscription %s: "
|
||||
"tracked=%s ledger=%s repaired=%s",
|
||||
getattr(sub, "user_id", None),
|
||||
getattr(sub, "subscription_id", None),
|
||||
tracked_total,
|
||||
ledger_total,
|
||||
repaired_bytes,
|
||||
)
|
||||
return premium_topup_balance + repaired_bytes
|
||||
|
||||
async def _premium_topup_ledger_total(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
subscription_id: int,
|
||||
premium_period_start: datetime,
|
||||
) -> Optional[int]:
|
||||
if not subscription_id or not isinstance(session, AsyncSession):
|
||||
return None
|
||||
try:
|
||||
return await tariff_dal.sum_traffic_topups(
|
||||
session,
|
||||
subscription_id=subscription_id,
|
||||
kinds=["premium_topup", "admin_premium_topup"],
|
||||
created_at_gte=premium_period_start,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"TariffTrafficWorker: failed to read premium top-up ledger for subscription %s",
|
||||
subscription_id,
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _premium_squad_match_cache_key(
|
||||
panel_user_uuid: str,
|
||||
|
||||
Reference in New Issue
Block a user