Tópicos de plataforma para notificações
TLDR: Adicionar o campo
topicsemPushDevices, criar um service puro para inscrever/desinscrever devices em tópicos FCM e fazer o backfill automático via data migration, cobrindo os tópicos de plataforma (novos) e os legados já gerenciados pelo mobile.
Contexto
Hoje os devices mobile assinam apenas três tópicos FCM (allTopic, user_<id>, refresh_home), todos via mobile. O admin precisa enviar notificações segmentadas por plataforma (apenas iOS ou apenas Android) sem fazer fanout manual sobre milhares de tokens — o uso natural para isso é o sistema de tópicos do FCM (envio único, o FCM cuida da entrega para todos os assinantes).
Objetivos
- Rastrear em
PushDevices.topics(ArrayField) os tópicos em que cada device está inscrito - Service puro para subscribe/unsubscribe (1 device, N tópicos) — reutilizável em qualquer fluxo futuro (signal, jornada, view)
- Ignorar silenciosamente tokens mortos — não deletar do banco nesta spec
- Backfill automático via data migration — popula o tópico de plataforma novo (com chamada ao FCM) e marca no banco os tópicos legados (
all_<env>,refresh_home,user_<id>) que o mobile já gerencia - Script de teste versionado para subscribe + envio de notificação para um usuário específico via shell
Fora de escopo
Esta spec cobre apenas o lado backend. A subscrição automática no app no momento do registro (Platform.OS) e a UI de envio segmentado no admin ficam para specs separadas.
Mudanças
apps/notifications/models.py
Adicionar em PushDevices:
python
topics = ArrayField(models.CharField(max_length=100), blank=True, default=list, verbose_name="Tópicos")
apps/notifications/migrations/0016_pushdevices_topics.py (novo)
Schema migration que adiciona o campo topics com default list.
apps/notifications/migrations/0017_backfill_platform_topics.py (novo)
Data migration que, para cada PushDevices com token e platform em (ios, android):
1. Chama subscribe(device, [f"platform_{platform}"]) no FCM via service
2. Adiciona ao campo topics o platform_<os> + os tópicos legados que o mobile já gerencia: all_<env> (all_staging em staging, all em prod), refresh_home, user_<id>
3. Falhas FCM (firebase_exceptions.*, ValueError) são apenas logadas — o banco ainda recebe o estado pretendido
4. reverse_code zera topics (volta ao estado inicial)
apps/notifications/services/topic_subscription.py (novo)
Service puro, sem conhecimento de batch ou infraestrutura:
- subscribe(device: PushDevices, topics: list[str]) -> bool — chama messaging.subscribe_to_topic([token], topic) por tópico; em sucesso, faz append idempotente em device.topics e salva; loga e retorna False em falha
- unsubscribe(device: PushDevices, topics: list[str]) -> bool — análogo, removendo do array
- Reutiliza NotificationService._ensure_firebase_initialized() para garantir o Firebase pronto
scripts/test_platform_topic_subscription.py (novo)
Script standalone para teste manual no shell: constantes USER_EMAIL e TARGET_PLATFORM no topo, busca os devices do usuário, inscreve em platform_<TARGET_PLATFORM> via service e envia uma messaging.Message(topic=...) para confirmar a entrega real.
tests/notifications/test_topic_subscription.py (novo)
Testes unitários Triple-A com firebase_admin.messaging mockado:
- subscribe em sucesso adiciona o tópico em device.topics e persiste
- subscribe é idempotente (chamar duas vezes não duplica)
- subscribe ignora token rejeitado pelo FCM sem persistir
- subscribe captura exceções (FirebaseError, ValueError) sem propagar
- subscribe em device sem token retorna False
- unsubscribe remove o tópico do array
- subscribe em múltiplos tópicos persiste todos em sucesso
apps/common/management/commands/seed.py
Atualizar _seed_push_devices para criar PushDevices com topics populado realisticamente: platform_<os>, all_<env>, refresh_home, user_<id>.
Como verificar
- Migrations:
python manage.py migrate notificationsaplica0016(schema) e0017(backfill automático) - Banco pós-backfill:
PushDevices.objects.first().topicscontém algo como["all_staging", "platform_ios", "refresh_home", "user_42"] - Idempotência: rodar
migratenovamente é no-op e o estado do banco não muda - Script de teste: ajustar email/plataforma e rodar
python manage.py shell < scripts/test_platform_topic_subscription.py; confirmar recebimento no device físico - Testes:
pytest tests/notifications/test_topic_subscription.py— todos passam - Seed:
make seedroda sem erro e os devices criados têmtopicspopulado
Documentação
- reference/notifications/platform_topics.md — convenção de tópicos
platform_<os>, campotopics, backfill e fluxo de uso