Appearance
API de Notificações - Contrato para Backend
Visão Geral
Sistema de notificações internas da plataforma. Cada notificação pertence a um usuário dentro de uma empresa e possui um escopo que define o contexto (empresa, contrato, pessoal, função ou ação).
As notificações suportam dados dinâmicos via coluna activity (JSONB), permitindo links de navegação e metadados sem alterar o schema.
Autenticação: Todas as rotas requerem token JWT padrão + header x-company-id.
Tabela notifications
| Coluna | Tipo | Nullable | Index | Notas |
|---|---|---|---|---|
| id | UUID | NO | PK | |
| company_id | UUID | NO | YES | FK companies |
| user_id | UUID | NO | YES | Usuário destinatário |
| scope | VARCHAR(20) | NO | YES | company | contract | personal | role | action |
| type | VARCHAR(50) | NO | YES | Tipo da notificação (ver lista abaixo) |
| title | VARCHAR(255) | NO | ||
| message | TEXT | NO | ||
| priority | VARCHAR(10) | NO | low | medium | high | urgent | |
| is_read | BOOLEAN | NO | YES | Default false |
| read_at | TIMESTAMP | YES | ||
| activity | JSONB | YES | Dados dinâmicos/links (ver schema abaixo) | |
| actor_id | UUID | YES | FK users - quem gerou a notificação | |
| actor_name | VARCHAR(255) | YES | Denormalizado para performance | |
| actor_avatar | VARCHAR(500) | YES | URL denormalizada | |
| created_at | TIMESTAMP | NO | YES | |
| updated_at | TIMESTAMP | NO |
Index composto: (company_id, user_id, is_read, created_at DESC)
Tipos de Notificação (coluna type)
| Tipo | Escopo típico | Descrição |
|---|---|---|
contract_created | contract | Contrato criado |
contract_published | contract | Contrato publicado |
contract_signed | contract | Contrato assinado |
contract_completed | contract | Contrato concluído |
contract_cancelled | contract | Contrato cancelado |
signatory_added | contract | Signatário adicionado |
signature_requested | action | Assinatura solicitada |
signature_completed | contract | Todas assinaturas coletadas |
document_uploaded | contract | Documento enviado |
document_approved | contract | Documento aprovado |
document_rejected | contract | Documento rejeitado |
review_requested | action | Revisão solicitada |
review_approved | contract | Revisão aprovada |
review_rejected | contract | Revisão rejeitada |
comment_added | personal | Comentário adicionado |
user_invited | company | Usuário convidado |
role_changed | role | Função alterada |
login_alert | personal | Alerta de login |
system_alert | company | Alerta do sistema |
action_required | action | Ação necessária |
custom | personal | Mensagem customizada enviada via POST /notifications/custom |
reminder | action | Lembrete |
integration_records_without_contract | company | Cadastros sincronizados de uma integração ERP/CRM sem contrato ativo (varredura diária; ver activity.connectionId) |
Schema do activity (JSONB)
json
{
"entityType": "contract | template | document | user",
"entityId": "uuid",
"entityName": "Nome da entidade relacionada",
"links": [
{
"label": "Ver Contrato",
"route": "/contracts/:id",
"params": { "id": "uuid" }
}
],
"actionLabel": "Assinar Agora",
"actionRoute": "/contracts/uuid-do-contrato",
"metadata": {
"key": "value"
}
}Todos os campos são opcionais. O frontend usa actionRoute para navegação ao clicar na notificação.
Para integration_records_without_contract, o activity carrega kind (= o próprio type), connectionId, provider, pendingCount e actionRoute = /company/:companyId/integrations/:connectionId/status. O web navega pelo actionRoute; o app RN abre a mesma rota em WebView autenticado (prefixo /company/ permitido); o botão CTA do WhatsApp e o link do e-mail são derivados dessa rota (e-mail resolve o subdomínio do tenant via TenantUrlService).
Endpoints
1. GET /notifications
Lista paginada de notificações do usuário autenticado na empresa atual.
Headers:
Authorization: Bearer {token}
x-company-id: {companyId}Query Parameters:
| Param | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
| scope | string | Não | all | Filtrar por escopo: company | contract | personal | role | action | all |
| isRead | boolean | Não | - | Filtrar por status de leitura |
| page | number | Não | 1 | Página atual |
| perPage | number | Não | 20 | Itens por página (max: 50) |
Regras:
- Filtrar por
company_iddo header euser_iddo token - Ordenar por
created_at DESC - Paginação obrigatória
Response 200:
json
{
"data": {
"data": [
{
"id": "uuid",
"companyId": "uuid",
"userId": "uuid",
"scope": "contract",
"type": "contract_created",
"title": "Novo contrato criado",
"message": "O contrato 'Acordo de Serviços - TechCorp' foi criado.",
"priority": "medium",
"isRead": false,
"readAt": null,
"activity": {
"entityType": "contract",
"entityId": "uuid",
"entityName": "Acordo de Serviços - TechCorp",
"actionLabel": "Ver Contrato",
"actionRoute": "/contracts/uuid"
},
"actorId": "uuid",
"actorName": "Maria Silva",
"actorAvatar": "https://storage.example.com/avatars/maria.jpg",
"createdAt": "2024-01-20T14:30:00Z",
"updatedAt": "2024-01-20T14:30:00Z"
}
],
"total": 45,
"page": 1,
"perPage": 20,
"totalPages": 3
}
}2. GET /notifications/unread-count
Retorna a contagem de notificações não lidas do usuário na empresa atual.
Headers:
Authorization: Bearer {token}
x-company-id: {companyId}Regras:
- Contar onde
company_id= header,user_id= token,is_read=false
Response 200:
json
{
"data": {
"count": 12
}
}3. PATCH /notifications/:id/read
Marca uma notificação individual como lida.
Headers:
Authorization: Bearer {token}
x-company-id: {companyId}Regras:
- Verificar que a notificação pertence ao
user_iddo token ecompany_iddo header - Setar
is_read=trueeread_at=now() - Se já estiver lida, retornar 200 sem alteração (idempotente)
Response 200:
json
{
"data": {
"id": "uuid",
"companyId": "uuid",
"userId": "uuid",
"scope": "contract",
"type": "contract_created",
"title": "Novo contrato criado",
"message": "O contrato 'Acordo de Serviços' foi criado.",
"priority": "medium",
"isRead": true,
"readAt": "2024-01-20T15:00:00Z",
"activity": null,
"actorId": "uuid",
"actorName": "Maria Silva",
"actorAvatar": null,
"createdAt": "2024-01-20T14:30:00Z",
"updatedAt": "2024-01-20T15:00:00Z"
}
}Response 404:
json
{
"message": "Notificação não encontrada"
}4. PATCH /notifications/read-all
Marca todas as notificações não lidas do usuário como lidas.
Headers:
Authorization: Bearer {token}
x-company-id: {companyId}Regras:
- Atualizar em batch:
UPDATE notifications SET is_read = true, read_at = now() WHERE company_id = :companyId AND user_id = :userId AND is_read = false
Response 200:
json
{
"data": {
"success": true
}
}5. POST /notifications/custom
Dispara uma notificação customizada a um usuário por um ou mais canais: notificação no app (platform), e-mail e/ou WhatsApp. Mensagem livre (aceita HTML, usado no corpo do e-mail; convertida para texto no WhatsApp e na notificação do app). Endpoint genérico — usado por web, mobile e pela IA (Zelor).
Headers:
Authorization: Bearer {token}
x-company-id: {companyId}Body:
json
{
"sendTo": "uuid-do-usuario",
"message": "<p>Olá, Maria.</p><p>Aqui é a Maria Gestora, sua gestora de contratos. Identifiquei que as horas deste mês ainda não foram lançadas.</p><p>Para não impactar o pagamento das suas horas conforme previsto contratualmente, atualize seus apontamentos em até 5 dias corridos.</p>",
"platforms": ["whatsapp", "platform", "email"],
"title": "Maria, suas horas deste mês ainda não foram lançadas",
"sender": { "name": "Maria Gestora", "role": "Gestora de contratos" }
}| Campo | Tipo | Notas |
|---|---|---|
| sendTo | UUID | userId do destinatário (quando empresa, o userId do representante) |
| message | string | Texto livre em HTML (<p>, <strong>, <br>). Deve ser uma carta direta: abre nomeando o destinatário e identifica o remetente (nome + papel) no próprio corpo; não repete o título; nunca usa "remuneração"/termos de vínculo CLT. Máx. 5000 |
| platforms | string[] | Lista com um, dois ou os três: whatsapp, platform, email. Não pode ser vazio |
| title | string | Opcional. Assunto do e-mail e título da notificação in-app; deve citar o destinatário. Fallback: 1ª linha da mensagem. Máx. 255 |
| sender | object | Opcional. { name?, role? } — quem pediu o envio. Usado como autor (actorName) da notificação in-app |
Comportamento:
- Resolve o usuário por
sendTo(404 se não existir). Se for umcompanyId, usa o usuário OWNER/ADMIN da empresa. titledefine o assunto do e-mail e o título da notificação in-app (fallback: 1ª linha da mensagem; "Você recebeu uma mensagem" se vazia). Não é repetido como heading no corpo do e-mail.sendervira o autor da notificação in-app (actorName=nome (papel)). No e-mail, a identificação do remetente fica na própria carta (composta namessage), sem linha automática extra.plataforma→ cria notificação in-app (type: custom, scopepersonal) reusando o pipeline de notificações (gateway + push). A notificação é gravada com ocompanyIddo destinatário (não o do remetente), para aparecer na lista e na contagem de não lidas dele.email→ renderiza amessage(carta HTML) no layout Handlebars da plataforma (base + branding) e envia via SES (HTML + texto). Pulado se o usuário não tiver e-mail. PJ recebe sempre no e-mail do usuário owner, nunca no e-mail da empresa.whatsapp→ envia pelo bot outbound (texto). Pulado se o usuário não tiver telefone ou o bot não estiver configurado.- Cada canal é independente: a falha de um não impede os outros.
Response 200:
json
{
"data": {
"sendTo": "uuid",
"dispatched": true,
"channels": [
{ "platform": "platform", "dispatched": true },
{ "platform": "email", "dispatched": true },
{ "platform": "whatsapp", "dispatched": false, "skippedReason": "usuário sem telefone" }
]
}
}Preferências de Notificação
Sistema de preferências em dois níveis. Regras de negócio em Preferências de notificação.
Canais: whatsapp | sms | email | platform (objeto de flags booleanas). Persistência em tabelas relacionais dedicadas (user_notification_settings, company_notification_settings, company_notification_channels) — sem colunas novas em user_settings/notifications. Ausência de linha = ligado/permitido por padrão.
6. GET /notifications/preferences
Preferências de canal do usuário autenticado, mais a allowlist de canais da empresa atual (para desabilitar canais bloqueados na UI).
Headers: Authorization: Bearer {token} + x-company-id: {companyId}
Response 200:
json
{
"data": {
"channels": { "whatsapp": true, "sms": true, "email": true, "platform": true },
"companyAllowedChannels": { "whatsapp": false, "sms": true, "email": true, "platform": true }
}
}7. PUT /notifications/preferences
Atualiza os canais pelos quais o usuário quer ser alcançado.
Headers: Authorization: Bearer {token} + x-company-id: {companyId}
Body:
json
{
"channels": { "whatsapp": false, "sms": true, "email": true, "platform": true }
}Response 200: mesmo formato do GET /notifications/preferences.
8. GET /company/:id/notification-settings
Configuração de notificação da empresa. Restrito a dono/administrador (403 caso contrário).
Headers: Authorization: Bearer {token} + x-company-id: {companyId}
Response 200:
json
{
"data": {
"allowedChannels": { "whatsapp": true, "sms": true, "email": true, "platform": true },
"types": { "contract_created": true, "subscription_renewed": false }
}
}types traz apenas os tipos com override salvo; tipos ausentes valem como ligados. Tipos obrigatórios não aparecem (nunca são gatados).
9. PUT /company/:id/notification-settings
Atualiza a allowlist de canais e os tipos ativos da empresa. Restrito a dono/administrador.
Headers: Authorization: Bearer {token} + x-company-id: {companyId}
Body:
json
{
"allowedChannels": { "whatsapp": false, "sms": true, "email": true, "platform": true },
"types": { "contract_created": true, "subscription_renewed": false }
}Response 200: mesmo formato do GET /company/:id/notification-settings.
Enforcement
A entrega efetiva de uma notificação de type por channel para um usuário obedece: type obrigatório OU (type ativo na empresa E channel permitido pela empresa E channel ligado pelo usuário). O gate é aplicado no pipeline de notificação in-app/push (NotificationService.create/createForUsers, canal platform) e no envio custom multi-canal (POST /notifications/custom, por canal escolhido). Notificações obrigatórias ignoram todos os gates.
Criação de Notificações (Backend Interno)
O backend deve criar notificações automaticamente nos seguintes eventos:
| Evento | Tipo | Escopo | Destinatário |
|---|---|---|---|
| Contrato criado | contract_created | contract | Membros da empresa |
| Contrato publicado | contract_published | contract | Partes envolvidas |
| Contrato assinado por alguém | contract_signed | contract | Dono do contrato |
| Todas assinaturas coletadas | contract_completed | contract | Todos envolvidos |
| Contrato cancelado | contract_cancelled | contract | Todos envolvidos |
| Signatário adicionado | signatory_added | contract | Signatário |
| Assinatura solicitada | signature_requested | action | Signatário |
| Assinatura concluída | signature_completed | contract | Dono do contrato |
| Documento enviado | document_uploaded | contract | Revisores |
| Documento aprovado | document_approved | contract | Quem enviou |
| Documento rejeitado | document_rejected | contract | Quem enviou |
| Revisão solicitada | review_requested | action | Revisor |
| Revisão aprovada | review_approved | contract | Dono do contrato |
| Revisão rejeitada | review_rejected | contract | Dono do contrato |
| Comentário adicionado | comment_added | personal | Dono do contrato |
| Usuário convidado | user_invited | company | Admins da empresa |
| Função alterada | role_changed | role | Usuário afetado |
| Login de novo dispositivo | login_alert | personal | Usuário |
| Alerta do sistema | system_alert | company | Todos da empresa |
Template sugerido para activity
Ao criar a notificação, popular o campo activity com os dados relevantes:
typescript
// Exemplo: contrato criado
{
entityType: 'contract',
entityId: contract.id,
entityName: contract.name,
actionLabel: 'Ver Contrato',
actionRoute: `/contracts/${contract.id}`
}
// Exemplo: assinatura solicitada
{
entityType: 'contract',
entityId: contract.id,
entityName: contract.name,
actionLabel: 'Assinar Agora',
actionRoute: `/contracts/${contract.id}`
}
// Exemplo: login alert
{
metadata: {
browser: 'Chrome',
os: 'Windows',
ip: '192.168.1.100'
}
}Considerações
- Retenção: Definir política de retenção (ex: 90 dias). Notificações antigas podem ser deletadas via cron job.
- Performance: O index composto
(company_id, user_id, is_read, created_at DESC)é essencial para a query principal e contagem de não lidos. - Denormalização:
actor_nameeactor_avatarsão denormalizados para evitar joins desnecessários na listagem. Atualizar via evento quando o usuário alterar nome/avatar. - Batch insert: Para notificações enviadas a múltiplos usuários (ex:
system_alert), usar insert em batch.