feat(notification): add target_mode enum constraint and merge register-notifications script
- 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
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
Reference in New Issue
Block a user