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:
@@ -73,13 +73,14 @@ async def create_email_session(
|
||||
)
|
||||
result = await service.create_email_session(payload)
|
||||
points_service = PointsService(repository=PointsRepository(session))
|
||||
await points_service.grant_register_bonus_if_eligible(
|
||||
bonus_result = await points_service.grant_register_bonus_if_eligible(
|
||||
user_id=UUID(result.user.id),
|
||||
user_email=result.user.email,
|
||||
)
|
||||
notification_service = NotificationService(NotificationRepository(session))
|
||||
linked_count = await notification_service.link_published_notifications_to_user(
|
||||
user_id=UUID(result.user.id)
|
||||
linked_count = await notification_service.link_notifications_for_registered_user(
|
||||
user_id=UUID(result.user.id),
|
||||
is_first_registration=bonus_result.is_first_registration,
|
||||
)
|
||||
await session.commit()
|
||||
logger.info(
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.notification import Notification
|
||||
from models.user_notification import UserNotification
|
||||
from schemas.enums import NotificationTargetMode
|
||||
|
||||
|
||||
class NotificationRepository:
|
||||
@@ -116,13 +117,24 @@ class NotificationRepository:
|
||||
async def commit(self) -> None:
|
||||
await self._session.commit()
|
||||
|
||||
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
|
||||
async def link_notifications_for_registered_user(
|
||||
self, *, user_id: UUID, is_first_registration: bool
|
||||
) -> int:
|
||||
target_modes: list[NotificationTargetMode]
|
||||
if is_first_registration:
|
||||
target_modes = [
|
||||
NotificationTargetMode.NEW_USERS,
|
||||
NotificationTargetMode.ALL_USERS,
|
||||
]
|
||||
else:
|
||||
target_modes = [NotificationTargetMode.ALL_USERS]
|
||||
notification_ids = list(
|
||||
(
|
||||
await self._session.execute(
|
||||
select(Notification.id).where(
|
||||
Notification.status == "published",
|
||||
Notification.deleted_at.is_(None),
|
||||
Notification.target_mode.in_(target_modes),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -136,8 +148,8 @@ class NotificationRepository:
|
||||
insert(UserNotification)
|
||||
.values(
|
||||
[
|
||||
{"user_id": user_id, "notification_id": notification_id}
|
||||
for notification_id in notification_ids
|
||||
{"user_id": user_id, "notification_id": nid}
|
||||
for nid in notification_ids
|
||||
]
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=["user_id", "notification_id"])
|
||||
|
||||
@@ -123,9 +123,11 @@ class NotificationService:
|
||||
await self._repository.commit()
|
||||
return updated_count
|
||||
|
||||
async def link_published_notifications_to_user(self, *, user_id: UUID) -> int:
|
||||
return await self._repository.link_published_notifications_to_user(
|
||||
user_id=user_id
|
||||
async def link_notifications_for_registered_user(
|
||||
self, *, user_id: UUID, is_first_registration: bool
|
||||
) -> int:
|
||||
return await self._repository.link_notifications_for_registered_user(
|
||||
user_id=user_id, is_first_registration=is_first_registration
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class RegisterBonusResult:
|
||||
amount: int
|
||||
balance_after: int
|
||||
event_id: str
|
||||
is_first_registration: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -122,14 +123,17 @@ class PointsService:
|
||||
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
|
||||
if claim is not None:
|
||||
is_first_registration = claim.first_user_id_snapshot is None
|
||||
if 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,
|
||||
is_first_registration=is_first_registration,
|
||||
)
|
||||
|
||||
claimed = await self._repository.claim_register_bonus(
|
||||
@@ -144,6 +148,7 @@ class PointsService:
|
||||
amount=0,
|
||||
balance_after=int(account.balance),
|
||||
event_id=event_id,
|
||||
is_first_registration=False,
|
||||
)
|
||||
|
||||
balance = int(account.balance)
|
||||
@@ -197,6 +202,7 @@ class PointsService:
|
||||
amount=bonus_points,
|
||||
balance_after=int(account.balance),
|
||||
event_id=event_id,
|
||||
is_first_registration=True,
|
||||
)
|
||||
|
||||
async def ensure_run_points_available(
|
||||
|
||||
Reference in New Issue
Block a user