diff --git a/app/database/models.py b/app/database/models.py index ec654be1..8bb7165c 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -2245,6 +2245,9 @@ class BroadcastHistory(Base): created_at = Column(AwareDateTime(), server_default=func.now()) completed_at = Column(AwareDateTime(), nullable=True) + # Broadcast category for user notification preferences filtering + category = Column(String(20), default='system', nullable=False) # system|news|promo + # Email broadcast fields channel = Column(String(20), default='telegram', nullable=False) # telegram|email|both email_subject = Column(String(255), nullable=True) diff --git a/app/services/broadcast_service.py b/app/services/broadcast_service.py index 59746aaa..971dd631 100644 --- a/app/services/broadcast_service.py +++ b/app/services/broadcast_service.py @@ -62,6 +62,7 @@ class BroadcastConfig: media: BroadcastMediaConfig | None = None initiator_name: str | None = None custom_buttons: list[dict] | None = None + category: str = 'system' # system|news|promo @dataclass @@ -160,7 +161,7 @@ class BroadcastService: await session.commit() # _fetch_recipients теперь возвращает list[int] (telegram_id), а не ORM-объекты - recipient_ids: list[int] = await self._fetch_recipients(config.target) + recipient_ids: list[int] = await self._fetch_recipients(config.target, config.category) async with AsyncSessionLocal() as session: broadcast = await session.get(BroadcastHistory, broadcast_id) @@ -226,8 +227,13 @@ class BroadcastService: logger.exception('Критическая ошибка при выполнении рассылки', broadcast_id=broadcast_id, exc=exc) await self._mark_failed(broadcast_id, sent_count, failed_count, blocked_count) - async def _fetch_recipients(self, target: str) -> list[int]: - """Загружает получателей и возвращает список telegram_id (скаляры, не ORM-объекты).""" + async def _fetch_recipients(self, target: str, category: str = 'system') -> list[int]: + """Загружает получателей и возвращает список telegram_id (скаляры, не ORM-объекты). + + Filters out users who disabled the given broadcast category in their + notification preferences (news_enabled, promo_offers_enabled). + Category 'system' is never filtered — system notifications reach everyone. + """ async with AsyncSessionLocal() as session: if target.startswith('custom_'): criteria = target[len('custom_') :] @@ -235,6 +241,17 @@ class BroadcastService: else: users_orm = await get_target_users(session, target) + # Filter by user notification preferences based on broadcast category + if category == 'news': + from app.utils.notification_prefs import is_news_enabled + + users_orm = [u for u in users_orm if is_news_enabled(u)] + elif category == 'promo': + from app.utils.notification_prefs import is_promo_offers_enabled + + users_orm = [u for u in users_orm if is_promo_offers_enabled(u)] + # category == 'system' → no filtering, sent to everyone + # Извлекаем telegram_id сразу, пока сессия жива. # После выхода из блока ORM-объекты станут detached. return [u.telegram_id for u in users_orm if u.telegram_id is not None] diff --git a/migrations/alembic/versions/0054_add_broadcast_category.py b/migrations/alembic/versions/0054_add_broadcast_category.py new file mode 100644 index 00000000..7a294ecc --- /dev/null +++ b/migrations/alembic/versions/0054_add_broadcast_category.py @@ -0,0 +1,28 @@ +"""add broadcast category column + +Revision ID: 0054 +Revises: 0053 +Create Date: 2026-04-10 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = '0054' +down_revision: Union[str, None] = '0053' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + 'broadcast_history', + sa.Column('category', sa.String(20), nullable=False, server_default='system'), + ) + + +def downgrade() -> None: + op.drop_column('broadcast_history', 'category')