fix: preserve points balance across account re-registration
Persist a per-email balance snapshot before account deletion and restore it on same-email re-registration, preventing both unintended balance reset and repeated signup bonus grants.
This commit is contained in:
@@ -148,7 +148,7 @@ class PointsRepository:
|
||||
*,
|
||||
email_hash: str,
|
||||
user_email_snapshot: str,
|
||||
first_user_id: UUID,
|
||||
first_user_id_snapshot: UUID,
|
||||
grant_event_id: str,
|
||||
) -> bool:
|
||||
stmt = (
|
||||
@@ -156,7 +156,7 @@ class PointsRepository:
|
||||
.values(
|
||||
email_hash=email_hash,
|
||||
user_email_snapshot=user_email_snapshot,
|
||||
first_user_id=first_user_id,
|
||||
first_user_id_snapshot=first_user_id_snapshot,
|
||||
grant_event_id=grant_event_id,
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=[RegisterBonusClaims.email_hash])
|
||||
@@ -164,3 +164,28 @@ class PointsRepository:
|
||||
)
|
||||
inserted_id = (await self._session.execute(stmt)).scalar_one_or_none()
|
||||
return inserted_id is not None
|
||||
|
||||
async def get_register_bonus_claim(
|
||||
self,
|
||||
*,
|
||||
email_hash: str,
|
||||
) -> RegisterBonusClaims | None:
|
||||
stmt = (
|
||||
select(RegisterBonusClaims)
|
||||
.where(RegisterBonusClaims.email_hash == email_hash)
|
||||
.limit(1)
|
||||
)
|
||||
return (await self._session.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
async def update_register_bonus_balance_snapshot(
|
||||
self,
|
||||
*,
|
||||
email_hash: str,
|
||||
balance_snapshot: int,
|
||||
) -> bool:
|
||||
claim = await self.get_register_bonus_claim(email_hash=email_hash)
|
||||
if claim is None:
|
||||
return False
|
||||
claim.balance_snapshot = int(balance_snapshot)
|
||||
await self._session.flush()
|
||||
return True
|
||||
|
||||
@@ -92,15 +92,26 @@ class PointsService:
|
||||
).hexdigest()
|
||||
event_id = f"register.bonus:{event_hash}"
|
||||
|
||||
claim = await self._repository.get_register_bonus_claim(email_hash=email_hash)
|
||||
account = await self._repository.get_or_create_user_points_for_update(
|
||||
user_id=user_id
|
||||
)
|
||||
if claim is not None and claim.balance_snapshot is not None:
|
||||
account.balance = max(int(claim.balance_snapshot), 0)
|
||||
account.version = int(account.version) + 1
|
||||
return RegisterBonusResult(
|
||||
granted=False,
|
||||
amount=0,
|
||||
balance_after=int(account.balance),
|
||||
event_id=event_id,
|
||||
)
|
||||
|
||||
claimed = await self._repository.claim_register_bonus(
|
||||
email_hash=email_hash,
|
||||
user_email_snapshot=normalized_email,
|
||||
first_user_id=user_id,
|
||||
first_user_id_snapshot=user_id,
|
||||
grant_event_id=event_id,
|
||||
)
|
||||
account = await self._repository.get_or_create_user_points_for_update(
|
||||
user_id=user_id
|
||||
)
|
||||
if not claimed:
|
||||
return RegisterBonusResult(
|
||||
granted=False,
|
||||
|
||||
@@ -12,6 +12,8 @@ from core.auth.models import CurrentUser
|
||||
from core.http.errors import ApiProblemError, problem_payload
|
||||
from services.base.supabase import SupabaseService
|
||||
from schemas.shared.user import UserContext, parse_profile_settings
|
||||
from v1.points.repository import PointsRepository
|
||||
from v1.points.service import PointsService
|
||||
from v1.users.repository import SQLAlchemyUserRepository
|
||||
from v1.users.schemas import (
|
||||
AvatarUploadUrlRequest,
|
||||
@@ -294,6 +296,7 @@ class UserService:
|
||||
user_id = str(self.current_user.id)
|
||||
avatar_bucket = config.storage.avatar.bucket
|
||||
avatar_prefix = f"{self.current_user.id}/"
|
||||
points_repository = PointsRepository(self.repository.session)
|
||||
|
||||
try:
|
||||
await self.attachment_storage.delete_prefix(
|
||||
@@ -315,6 +318,31 @@ class UserService:
|
||||
),
|
||||
) from exc
|
||||
|
||||
try:
|
||||
user_email = (self.current_user.email or "").strip().lower()
|
||||
if user_email:
|
||||
email_hash = PointsService._build_register_bonus_email_hash(user_email)
|
||||
account = await points_repository.get_user_points(
|
||||
user_id=self.current_user.id
|
||||
)
|
||||
await points_repository.update_register_bonus_balance_snapshot(
|
||||
email_hash=email_hash,
|
||||
balance_snapshot=int(account.balance),
|
||||
)
|
||||
await self.repository.session.commit()
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"Account deletion failed while persisting points snapshot",
|
||||
user_id=user_id,
|
||||
)
|
||||
raise ApiProblemError(
|
||||
status_code=502,
|
||||
detail=problem_payload(
|
||||
code="PROFILE_DELETE_FAILED",
|
||||
detail="Failed to delete account data",
|
||||
),
|
||||
) from exc
|
||||
|
||||
try:
|
||||
await self.attachment_storage.delete_auth_user(user_id=user_id)
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user