# Push Notifications — Marketing Documento de referência do sistema de push de **marketing**. As pushes **transacionais** (agendamento, pagamento, aprovação de cadastro) não são descritas aqui — ver a seção "Fronteira" no fim. --- ## 1. Fronteira: marketing vs transacional A distinção não é por pasta nem por nome, e sim por **estar ou não registrado em `PushNotificationDispatcher::all()`**: - **Marketing** → está no array `all()`. O scheduler varre `eligibleUsers()`, aplica cooldown e envia. - **Transacional** → **não** está no array. Tem `eligibleUsers()` devolvendo `Collection` vazia e cooldowns `0`, e é disparada por chamada direta a `PushNotificationService::sendToUser()` a partir do serviço de domínio. Categorias de marketing: `marketing`, `manual`. Categorias transacionais: `agenda`, `transacional`. --- ## 2. As 3 pushes automáticas Todas de cliente, categoria `marketing`, `categoryCooldownDays() = 0` — as três condições são mutuamente exclusivas por construção, então cooldown de categoria só atrapalharia. Definições: - "agendamento / pedido" = qualquer linha em `schedules` do cliente, **em qualquer status**, contada por `created_at`. Cobre Sob Medida, já que `custom_schedules` é filha de `schedules`. - "primeiro acesso" = `users.created_at`. | Label | Classe | Gatilho | Repetição | |---|---|---|---| | `cliente_abandono_funil` | `Cliente/Marketing/AbandonoFunilPush` | Cadastro há ≥ 48h **e** nenhum agendamento nunca | a cada 7 dias | | `cliente_recorrencia_quebrada` | `Cliente/Marketing/RecorrenciaQuebradaPush` | Tem agendamento, o último foi há ≥ 30 e < 90 dias | a cada 30 dias | | `cliente_inativo` | `Cliente/Marketing/ClienteInativoPush` | Tem agendamento, o último foi há ≥ 90 dias | **1 vez por cliente, para sempre** | O envio único de `cliente_inativo` é garantido por um `whereDoesntHave` sobre `push_notification_logs` dentro do próprio `eligibleUsers()` — não por cooldown. ### Conteúdo | Label | Título | Corpo | |---|---|---| | `cliente_abandono_funil` | Casa limpa sem esforço! ✨ | Notei que você olhou nosso app! Que tal garantir sua próxima faxina? Toque e agende. | | `cliente_recorrencia_quebrada` | Já faz um mês... 🗓️ | Sua casa merece aquele cuidado de novo. Encontre seu diarista favorito e agende em menos de 2 minutos! | | `cliente_inativo` | Que saudade de ver tudo brilhando! 🫧 | Faz tempo que você não passa por aqui. Vamos renovar o ambiente? | --- ## 3. Fluxo automático ``` bootstrap/app.php → dailyAt('10:00') └─ SendPushNotificationsTask::__invoke() (try/catch + Log::error) └─ PushNotificationDispatcher::dispatch() foreach (all() as $notification): 1. eligibleUsers() 2. applyCooldowns() (categoria + label) 3. PushNotificationService::sendToUsers() - aborta se !push_notifications_enabled - busca device_tokens (user_id + app_type = target + active) - sendMulticast (channel_id 'diaria', priority high) - desativa tokens inválidos - se ≥1 sucesso → grava push_notification_logs ``` Para adicionar uma push de marketing: criar a classe em `app/Notifications/Push/Cliente/Marketing/` estendendo `BasePushNotification`, implementar os 6 métodos abstratos, sobrescrever os cooldowns, e registrá-la em `PushNotificationDispatcher::all()`. --- ## 4. Push manual (backoffice) Módulo "Pushs" no menu do backoffice: o operador escolhe a origem (cliente/prestador), seleciona N usuários daquela origem, digita título e descrição e envia. **Sem cooldown**, com log próprio (categoria `manual`), e respeitando o opt-out `push_notifications_enabled`. | Peça | Arquivo | |---|---| | Notificação com título/corpo dinâmicos | `app/Notifications/Push/Manual/ManualPush.php` | | Envio assíncrono (chunks de 50) | `app/Jobs/SendManualPushJob.php` | | Destinatários / envio / histórico | `app/Services/ManualPushService.php` | | Endpoints | `app/Http/Controllers/PushNotificationController.php` | | Validação | `app/Http/Requests/ManualPushRequest.php` | | Rotas | `routes/authRoutes/push_notification.php` | | Permissão | scope `push.notification` (bits 259) nos seeders | ``` GET /api/push-notifications/recipients?target=cliente|prestador permission:push.notification,view GET /api/push-notifications/history permission:push.notification,view POST /api/push-notifications/send permission:push.notification,add ``` `ManualPush` **não** é registrada em `PushNotificationDispatcher::all()` — mesmo padrão das transacionais. Vai para fila porque `sendToUsers()` faz um `sendMulticast` síncrono por usuário; um lote grande estouraria o timeout da request HTTP. Frontend (`sfp_front_vue_diarista_backoffice`): `src/pages/pushNotification/PushNotificationsPage.vue`, `src/components/pushNotification/PushRecipientsSelect.vue`, `src/api/pushNotification.js`, `src/router/routes/pushNotification.route.js`, entrada em `src/stores/navigation.js`. --- ## 5. Infra compartilhada Usada tanto por marketing quanto por transacional — **nenhuma assinatura pública aqui pode mudar sem revisar as duas pontas**. - `app/Services/PushNotificationService.php` — envio FCM + log + desativação de tokens - `app/Notifications/Push/BasePushNotification.php` — contrato das notificações - `app/Enums/PushNotificationTargetEnum.php` — `prestador` | `cliente` - `app/Enums/PushNotificationCategoryEnum.php` — `marketing`, `manual`, `agenda`, `transacional` - `app/Models/DeviceToken.php`, `app/Models/PushNotificationLog.php` - `app/Services/DeviceTokenService.php` + `DeviceTokenController` (`POST`/`DELETE /device-tokens`) - Flag de opt-out: `users.push_notifications_enabled` ### Tabelas `device_tokens`: `user_id`, `token` (unique), `platform` (android|ios), `app_type` (prestador|cliente), `active`. `push_notification_logs`: `label`, `user_id`, `target`, `category`, `sent_at`. `category` é `string` sem cast de enum — logs de categorias já descontinuadas continuam legíveis. --- ## 6. Fronteira: o que NÃO é marketing Pushes transacionais (não alterar ao mexer em marketing): - `app/Notifications/Push/Cliente/Agendamento/` — `PrestadorAceitouPush`, `PrestadorRecusouPush`, `PrestadorCancelouPush` - `app/Notifications/Push/Prestador/Agendamento/` — `NewPushRequest`, `NovaOportunidadePush`, `ClienteAceitouPush`, `ClienteRecusouPush`, `ClienteCancelouPush`, `CodigoNaoPreenchidoPush` - `app/Notifications/Push/Prestador/Pagamento/ClienteEfetuouPagamentoPush` - `app/Notifications/Push/Prestador/Transacional/CadastroAprovadoPush` Call sites: `ScheduleService`, `CustomScheduleService`, `ProviderService`, `NotifyProvidersOfNewOpportunityJob`, `SendOpportunityPushJob`. Cobertura: `tests/Feature/ProviderApprovalPushTest.php`. **Gotcha ao disparar push a partir de um serviço de domínio:** resolver `PushNotificationService` com `app(PushNotificationService::class)` **dentro do try**, nunca por injeção no construtor — injetar faz o container instanciar o `Messaging` do Firebase, e uma credencial inválida derruba o CRUD inteiro. Sempre `try/catch (\Throwable)` + `Log::error`: falha de push não pode derrubar a operação de negócio. --- ## 7. Apps cliente/prestador O backend nunca envia `data` payload e nenhum dos dois apps lê `notification.data` — **não existe roteamento de push por tipo**. Adicionar ou remover tipos de push no backend não exige mudança nos apps. Os únicos acoplamentos reais são o `channel_id: 'diaria'` (criado no boot `src/boot/push-notifications.js` de cada app) e o `app_type` gravado em `device_tokens`.