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.
A distinção não é por pasta nem por nome, e sim por estar ou não registrado em
PushNotificationDispatcher::all():
all(). O scheduler varre eligibleUsers(),
aplica cooldown e envia.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.
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:
schedules do cliente, em
qualquer status, contada por created_at. Cobre Sob Medida, já que
custom_schedules é filha de schedules.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.
| 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? |
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().
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.
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 tokensapp/Notifications/Push/BasePushNotification.php — contrato das notificaçõesapp/Enums/PushNotificationTargetEnum.php — prestador | clienteapp/Enums/PushNotificationCategoryEnum.php — marketing, manual, agenda, transacionalapp/Models/DeviceToken.php, app/Models/PushNotificationLog.phpapp/Services/DeviceTokenService.php + DeviceTokenController (POST/DELETE /device-tokens)users.push_notifications_enableddevice_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.
Pushes transacionais (não alterar ao mexer em marketing):
app/Notifications/Push/Cliente/Agendamento/ — PrestadorAceitouPush, PrestadorRecusouPush, PrestadorCancelouPushapp/Notifications/Push/Prestador/Agendamento/ — NewPushRequest, NovaOportunidadePush, ClienteAceitouPush, ClienteRecusouPush, ClienteCancelouPush, CodigoNaoPreenchidoPushapp/Notifications/Push/Prestador/Pagamento/ClienteEfetuouPagamentoPushapp/Notifications/Push/Prestador/Transacional/CadastroAprovadoPushCall 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.
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.