c79c773d67
- Add NotificationTargetMode enum (new_users/exist_users/all_users/user_ids) - Create Alembic migrations: drop duplicate indexes, add target_mode column - Merge register-notifications.sh into dev-migrate.sh sync-notifications subcommand - Shorten notification config path: static/notification/notifications -> static/notifications - Update registration flow to dispatch notifications by target_mode - Add is_first_registration to RegisterBonusResult for first-time user detection - Remove dead code: link_published_notifications_to_user - Update welcome_points.yaml to target new_users only - Add 44 unit tests + 1 integration test, all passing
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from models.register_bonus_claims import RegisterBonusClaims
|
|
from v1.points.service import PointsService
|
|
|
|
|
|
class _FakeAccount:
|
|
balance: int = 100
|
|
frozen_balance: int = 0
|
|
lifetime_earned: int = 0
|
|
lifetime_spent: int = 0
|
|
version: int = 0
|
|
|
|
|
|
class _FakePointsRepository:
|
|
def __init__(self, *, claim: RegisterBonusClaims | None = None) -> None:
|
|
self.account = _FakeAccount()
|
|
self.claim = claim
|
|
self.claimed = False
|
|
self.appended_ledger: list[object] = []
|
|
self.appended_audit: list[object] = []
|
|
|
|
async def get_or_create_user_points_for_update(
|
|
self, *, user_id: object
|
|
) -> _FakeAccount:
|
|
return self.account
|
|
|
|
async def has_ledger_event(self, *, user_id: object, event_id: str) -> bool:
|
|
return False
|
|
|
|
async def append_ledger(self, *, command: object, balance_after: int) -> None:
|
|
self.appended_ledger.append(command)
|
|
|
|
async def append_audit_ledger(self, *, command: object) -> None:
|
|
self.appended_audit.append(command)
|
|
|
|
async def has_audit_event(self, *, event_id: str) -> bool:
|
|
return False
|
|
|
|
async def claim_register_bonus(
|
|
self,
|
|
*,
|
|
email_hash: str,
|
|
user_email_snapshot: str,
|
|
first_user_id_snapshot: object,
|
|
grant_event_id: str,
|
|
) -> bool:
|
|
if self.claimed:
|
|
return False
|
|
self.claimed = True
|
|
return True
|
|
|
|
async def get_register_bonus_claim(
|
|
self, *, email_hash: str
|
|
) -> RegisterBonusClaims | None:
|
|
return self.claim
|
|
|
|
|
|
class TestRegisterBonusIsFirstRegistration:
|
|
@pytest.mark.asyncio
|
|
async def test_first_registration_sets_true(self) -> None:
|
|
repo = _FakePointsRepository(claim=None)
|
|
service = PointsService(repository=repo) # type: ignore[arg-type]
|
|
|
|
result = await service.grant_register_bonus_if_eligible(
|
|
user_id=uuid4(), user_email="new@example.com"
|
|
)
|
|
|
|
assert result.granted is True
|
|
assert result.is_first_registration is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reregistered_with_existing_claim_sets_false(self) -> None:
|
|
existing_claim = RegisterBonusClaims(
|
|
email_hash="abc",
|
|
user_email_snapshot="old@example.com",
|
|
first_user_id_snapshot=uuid4(),
|
|
balance_snapshot=50,
|
|
grant_event_id="evt",
|
|
)
|
|
repo = _FakePointsRepository(claim=existing_claim)
|
|
service = PointsService(repository=repo) # type: ignore[arg-type]
|
|
|
|
result = await service.grant_register_bonus_if_eligible(
|
|
user_id=uuid4(), user_email="old@example.com"
|
|
)
|
|
|
|
assert result.granted is False
|
|
assert result.is_first_registration is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reregistered_claim_without_first_user_id_sets_true(self) -> None:
|
|
claim_no_snapshot = RegisterBonusClaims(
|
|
email_hash="abc",
|
|
user_email_snapshot="edge@example.com",
|
|
first_user_id_snapshot=None,
|
|
balance_snapshot=50,
|
|
grant_event_id="evt",
|
|
)
|
|
repo = _FakePointsRepository(claim=claim_no_snapshot)
|
|
service = PointsService(repository=repo) # type: ignore[arg-type]
|
|
|
|
result = await service.grant_register_bonus_if_eligible(
|
|
user_id=uuid4(), user_email="edge@example.com"
|
|
)
|
|
|
|
assert result.granted is False
|
|
assert result.is_first_registration is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_claim_competition_failure_sets_false(self) -> None:
|
|
repo = _FakePointsRepository(claim=None)
|
|
repo.claimed = True
|
|
service = PointsService(repository=repo) # type: ignore[arg-type]
|
|
|
|
result = await service.grant_register_bonus_if_eligible(
|
|
user_id=uuid4(), user_email="race@example.com"
|
|
)
|
|
|
|
assert result.granted is False
|
|
assert result.is_first_registration is False
|