save_device: expirar push devices duplicados no registro

TLDR: Quando um novo push device se registra para um par (user, platform) que já tem registros ativos, expirar os antigos antes de criar o novo — evitando notificações duplicadas causadas por atualizações do iOS que rotacionam tanto o device_id quanto o token FCM.

Contexto

Device.osInternalBuildId (usado como device_id no app mobile) retorna o build number do iOS (ex.: 23D8133 para iOS 17.3, 23F77 para iOS 17.5). Quando um usuário atualiza o iOS, tanto o device_id quanto o token FCM rotacionam. save_device faz lookup por (device_id, platform, user_id) — não encontra nada — e cria um novo registro de PushDevices. O registro antigo permanece ativo. O service de notificações envia para todos os registros ativos do usuário, resultando em push duplicada.

O app mobile já está em produção. Os usuários não atualizarão para uma versão corrigida imediatamente, então o backend precisa ser a correção definitiva, independente de qual device_id o cliente envia.

Objetivos

  • Garantir no máximo um registro de PushDevices ativo por (user, platform) em qualquer momento
  • Expirar registros obsoletos automaticamente a cada novo registro, sem depender de correção no cliente
  • Manter intacto o caminho de upsert existente (encontrado por device_id → atualiza token)

Fora de escopo

— (não registrado na spec original)

Mudanças

apps/notifications/tasks.py — save_device

Fluxo atual: 1. Lookup por (device_id, platform, user_id) → se encontrado, atualiza o token e reativa 2. Se não encontrado → PushDevices.objects.create(**data)

Fluxo novo: 1. Lookup por (device_id, platform, user_id) → se encontrado, atualiza o token e reativa (inalterado) 2. Se não encontrado: a. Expirar todos os outros registros ativos de (user_id, platform) — setar is_expired=True, expired_at=now() b. Criar o novo registro normalmente

python if not device: now = timezone.now() expired_count = PushDevices.objects.filter( user_id=user_id, platform=platform, is_expired=False, ).exclude(device_id=device_id).update(is_expired=True, expired_at=now) if expired_count: logger.info(f"Expired {expired_count} stale device(s) for user {user_id} on {platform}") # segue criando como antes

tests/notifications/test_serializers_views_urls.py

Testes cobrindo: - Novo registro expira os registros ativos existentes do mesmo (user, platform) - Novo registro não expira registros de outra plataforma - Device existente encontrado por device_id → caminho de update continua funcionando (sem expiração) - Múltiplos registros obsoletos são todos expirados numa chamada

Como verificar

  1. Criar dois PushDevices ativos para o mesmo user+platform, com device_id e token diferentes
  2. Chamar save_device com um terceiro device_id para o mesmo user+platform
  3. Verificar que apenas o registro recém-criado está ativo (is_expired=False)
  4. Verificar que os dois anteriores estão expirados (is_expired=True, expired_at preenchido)
  5. Chamar save_device de novo com o mesmo device_id (caminho de reativação)
  6. Verificar que o registro é atualizado e nenhum registro extra foi expirado

Documentação