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:
@@ -7,6 +7,7 @@ from uuid import UUID, uuid4
|
||||
import pytest
|
||||
|
||||
from core.config.settings import config
|
||||
from models.register_bonus_claims import RegisterBonusClaims
|
||||
from schemas.domain.points import AppendAuditLedgerCommand, ApplyPointsChangeCommand
|
||||
from schemas.domain.points import PointsChargeSnapshot
|
||||
from v1.points.service import PointsService
|
||||
@@ -28,6 +29,7 @@ class _FakePointsRepository:
|
||||
self.appended_ledger: list[ApplyPointsChangeCommand] = []
|
||||
self.appended_audit: list[AppendAuditLedgerCommand] = []
|
||||
self.claimed: bool = False
|
||||
self.claim: RegisterBonusClaims | None = None
|
||||
|
||||
async def get_or_create_user_points_for_update(
|
||||
self, *, user_id: UUID
|
||||
@@ -69,15 +71,23 @@ class _FakePointsRepository:
|
||||
*,
|
||||
email_hash: str,
|
||||
user_email_snapshot: str,
|
||||
first_user_id: UUID,
|
||||
first_user_id_snapshot: UUID,
|
||||
grant_event_id: str,
|
||||
) -> bool:
|
||||
del email_hash, user_email_snapshot, first_user_id, grant_event_id
|
||||
del email_hash, user_email_snapshot, first_user_id_snapshot, grant_event_id
|
||||
if self.claimed:
|
||||
return False
|
||||
self.claimed = True
|
||||
return True
|
||||
|
||||
async def get_register_bonus_claim(
|
||||
self,
|
||||
*,
|
||||
email_hash: str,
|
||||
) -> RegisterBonusClaims | None:
|
||||
del email_hash
|
||||
return self.claim
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consume_successful_run_points_writes_real_usage_to_audit() -> None:
|
||||
@@ -188,3 +198,30 @@ async def test_grant_register_bonus_if_eligible_second_time_skips() -> None:
|
||||
assert result.amount == 0
|
||||
assert len(repository.appended_ledger) == 0
|
||||
assert len(repository.appended_audit) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_grant_register_bonus_if_eligible_restores_balance_snapshot() -> None:
|
||||
repository = _FakePointsRepository(usage_snapshot=None)
|
||||
repository.account.balance = 0
|
||||
repository.claim = RegisterBonusClaims(
|
||||
email_hash="abc",
|
||||
user_email_snapshot="restore@example.com",
|
||||
first_user_id_snapshot=uuid4(),
|
||||
balance_snapshot=35,
|
||||
grant_event_id="event",
|
||||
)
|
||||
service = PointsService(repository=repository) # type: ignore[arg-type]
|
||||
|
||||
result = await service.grant_register_bonus_if_eligible(
|
||||
user_id=uuid4(),
|
||||
user_email="restore@example.com",
|
||||
)
|
||||
|
||||
assert result.granted is False
|
||||
assert result.amount == 0
|
||||
assert result.balance_after == 35
|
||||
assert repository.account.balance == 35
|
||||
assert repository.claimed is False
|
||||
assert len(repository.appended_ledger) == 0
|
||||
assert len(repository.appended_audit) == 0
|
||||
|
||||
@@ -10,7 +10,7 @@ from v1.users.service import UserService
|
||||
|
||||
|
||||
class _NoopRepository:
|
||||
pass
|
||||
session = None
|
||||
|
||||
|
||||
class _FakeStorage:
|
||||
@@ -28,7 +28,7 @@ class _FakeStorage:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_account_success_calls_storage_cleanup_and_auth_delete() -> None:
|
||||
user = CurrentUser(id=uuid4(), email="test@example.com")
|
||||
user = CurrentUser(id=uuid4(), email=None)
|
||||
storage = _FakeStorage()
|
||||
service = UserService(
|
||||
current_user=user,
|
||||
@@ -46,7 +46,7 @@ async def test_delete_account_success_calls_storage_cleanup_and_auth_delete() ->
|
||||
async def test_delete_account_raises_profile_delete_failed_on_storage_cleanup_error() -> (
|
||||
None
|
||||
):
|
||||
user = CurrentUser(id=uuid4(), email="test@example.com")
|
||||
user = CurrentUser(id=uuid4(), email=None)
|
||||
|
||||
class _FailingStorage(_FakeStorage):
|
||||
async def delete_prefix(self, *, bucket: str, prefix: str) -> int:
|
||||
@@ -72,7 +72,7 @@ async def test_delete_account_raises_profile_delete_failed_on_storage_cleanup_er
|
||||
async def test_delete_account_raises_profile_delete_failed_on_auth_delete_error() -> (
|
||||
None
|
||||
):
|
||||
user = CurrentUser(id=uuid4(), email="test@example.com")
|
||||
user = CurrentUser(id=uuid4(), email=None)
|
||||
|
||||
class _FailingStorage(_FakeStorage):
|
||||
async def delete_auth_user(self, *, user_id: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user