feat: Add scheduler for daily user credits recharge

This commit is contained in:
JSC
2025-08-11 00:30:29 +02:00
parent bdeb00d562
commit d1bf2fe0a4
8 changed files with 698 additions and 109 deletions

View File

@@ -222,8 +222,8 @@ class CreditService:
"Failed to emit user_credits_changed event for user %s",
user_id,
)
else:
return transaction
return transaction
except Exception:
await session.rollback()
@@ -315,8 +315,8 @@ class CreditService:
"Failed to emit user_credits_changed event for user %s",
user_id,
)
else:
return transaction
return transaction
except Exception:
await session.rollback()
@@ -402,3 +402,180 @@ class CreditService:
return user.credits
finally:
await session.close()
async def recharge_user_credits(
self,
user_id: int,
plan_credits: int,
max_credits: int,
) -> CreditTransaction | None:
"""Recharge credits for a user based on their plan.
Args:
user_id: The user ID
plan_credits: Number of credits from the plan
max_credits: Maximum credits allowed for the plan
Returns:
The created credit transaction if credits were added, None if no recharge
needed
Raises:
ValueError: If user not found
"""
session = self.db_session_factory()
try:
user_repo = UserRepository(session)
user = await user_repo.get_by_id(user_id)
if not user:
msg = f"User {user_id} not found"
raise ValueError(msg)
# Calculate credits to add (can't exceed max_credits)
current_credits = user.credits
target_credits = min(current_credits + plan_credits, max_credits)
credits_to_add = target_credits - current_credits
# If no credits to add, return None
if credits_to_add <= 0:
logger.info(
"No credits to add for user %s: current=%s, "
"plan_credits=%s, max=%s",
user_id,
current_credits,
plan_credits,
max_credits,
)
return None
# Record transaction
transaction = CreditTransaction(
user_id=user_id,
action_type=CreditActionType.DAILY_RECHARGE.value,
amount=credits_to_add,
balance_before=current_credits,
balance_after=target_credits,
description="Daily credit recharge",
success=True,
metadata_json=json.dumps(
{
"plan_credits": plan_credits,
"max_credits": max_credits,
},
),
)
# Update user credits
await user_repo.update(user, {"credits": target_credits})
# Save transaction
session.add(transaction)
await session.commit()
logger.info(
"Credits recharged for user %s: %s credits added (balance: %s%s)",
user_id,
credits_to_add,
current_credits,
target_credits,
)
# Emit user_credits_changed event via WebSocket
try:
event_data = {
"user_id": str(user_id),
"credits_before": current_credits,
"credits_after": target_credits,
"credits_added": credits_to_add,
"description": "Daily credit recharge",
"success": True,
}
await socket_manager.send_to_user(
str(user_id),
"user_credits_changed",
event_data,
)
logger.info("Emitted user_credits_changed event for user %s", user_id)
except Exception:
logger.exception(
"Failed to emit user_credits_changed event for user %s",
user_id,
)
return transaction
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def recharge_all_users_credits(self) -> dict[str, int]:
"""Recharge credits for all users based on their plans.
Returns:
Dictionary with statistics about the recharge operation
"""
session = self.db_session_factory()
stats = {
"total_users": 0,
"recharged_users": 0,
"skipped_users": 0,
"total_credits_added": 0,
}
try:
user_repo = UserRepository(session)
# Process users in batches to avoid memory issues
offset = 0
batch_size = 100
while True:
users = await user_repo.get_all_with_plan(
limit=batch_size,
offset=offset,
)
if not users:
break
for user in users:
stats["total_users"] += 1
# Skip users without ID (shouldn't happen in practice)
if user.id is None:
continue
transaction = await self.recharge_user_credits(
user.id,
user.plan.credits,
user.plan.max_credits,
)
if transaction:
stats["recharged_users"] += 1
stats["total_credits_added"] += transaction.amount
else:
stats["skipped_users"] += 1
offset += batch_size
# Break if we got fewer users than batch_size (last batch)
if len(users) < batch_size:
break
logger.info(
"Daily credit recharge completed: %s total users, "
"%s recharged, %s skipped, %s total credits added",
stats["total_users"],
stats["recharged_users"],
stats["skipped_users"],
stats["total_credits_added"],
)
return stats
finally:
await session.close()