Funcionalidade de Agendamento de Visitação
Descrição
Este módulo gerencia o ciclo completo das solicitações de visita a imóveis: registra e persiste os pedidos, envia e-mails de confirmação aos clientes e notifica a imobiliária sobre novos agendamentos. É implementado em Express.js e integra serviços SMTP para o envio transacional de mensagens.
Escopo deste documento: cobre apenas o módulo de Agendamentos (API, modelo, validações, segurança, rotas e integração por e-mail).
Sumário
- 1) Objetivo
- O que este módulo entrega
- Principais artefatos
- 2) Segurança & Boas Práticas implementadas
- 3) Modelo de Dados (Agendamento)
- 4) Rotas HTTP
- 4.1) Integração pública (sem login)
- 4.2) CRUD autenticado
- 5) Regras de Validação (resumo prático)
- 6) Variáveis de Ambiente (integração de e-mail)
- 7) Fluxos Principais
- 7.1) Solicitação de Agendamento (pública)
- 7.2) Gestão via Dashboard (autenticado)
- 8) Exemplos de Requisições (cURL)
- 9) Convenções de Resposta & Erros
- 10) Especificação OpenAPI (YAML)
1) Objetivo
Estabelecer, de ponta a ponta, o fluxo de solicitações de visita a imóveis incluindo: - (a) registro e persistência no banco de dados - (b) comunicação transacional por e‑mail com o cliente e com a imobiliária - (c) operações de gestão (CRUD) sob políticas de segurança e controle de acesso.
O que este módulo entrega
- Registro de agendamentos e armazenamento em
agendamentos. - Confirmações por e‑mail para o cliente e notificação para a imobiliária.
- CRUD autenticado para acompanhamento e conclusão de agendamentos.
- Envio de notificações de novos imóveis (suporte ao funil de agendamento).
Principais artefatos
- Controller:
src/controllers/agendamentoController.js - Controller CRUD:
src/controllers/agendamentoCrudController.js - Service:
src/services/agendamentoService.js - Model:
src/models/Agendamento.js - Routes:
src/routes/agendamentoRoute.js
2) Segurança & Boas Práticas implementadas
- Rate limiting por IP/rota: até 5 requisições/minuto (resposta 429 em excesso).
- Headers de segurança nas rotas de agendamento:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originContent-Security-Policy: default-src 'self'
- Limite de payload: JSON até 10MB; validação adicional no buffer.
- Enforcement de Content-Type:
POSTdeve serapplication/json. - Sanitização e validações robustas de entrada para:
- Dados de agendamento (nome, e‑mail, telefone, imóvel, período etc.).
- Dados de e‑mail/attachments (tamanho, tipo, base64, subject etc.).
- RBAC (via
Auth.Authorization):- Admin (nivel = 0): pode listar todos, concluir agendamentos e excluir qualquer um.
- Usuário: CRUD apenas dos seus agendamentos.
3) Modelo de Dados (Agendamento)
Tabela: agendamentos
| Campo | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
id |
INT(11) PK AI | Sim | — | Identificador do agendamento |
id_usuario |
INT(11) | Sim | — | FK para usuário criador |
data_marcada |
DATETIME | Sim | — | Data/hora marcada para a visita |
data_create |
DATETIME | Sim | NOW() |
Data/hora de criação do registro |
id_imovel |
INT(11) | Não | NULL |
FK opcional para imóvel |
mensagem |
TEXT | Não | NULL |
Observações do cliente |
concluido |
TINYINT(1) | Sim | 0 |
0 = pendente; 1 = concluído |
Associações esperadas (via Sequelize, definidas no projeto):
Agendamento.belongsTo(Usuario, { as: 'usuario', foreignKey: 'id_usuario' })Agendamento.belongsTo(Imovel, { as: 'imovel', foreignKey: 'id_imovel' })
4) Rotas HTTP
Prefixo: /agendamentos
4.1) Integração pública (sem login)
-
POST /agendamentos/scheduleFunção: cria (server‑side) um registro de agendamento e envia e‑mails de confirmação (cliente + imobiliária).
Rate limit: 5/min por IP.
Validações:
name,email,visitPeriodobrigatórios; e‑mail válido; limites de tamanho.
Body (exemplo)
{
"appointment": {
"name": "Maria Silva",
"email": "maria@example.com",
"phone": "+55 11 99999-0000",
"propertyAddress": "Rua das Flores, 123 — Centro",
"propertyId": 42,
"notes": "Prefiro sábado de manhã.",
"visitPeriod": "Manhã",
"date": "2025-10-20",
"time": "09:30"
}
}
Responses
- 200
application/json(salvo + e‑mail):
{
"success": true,
"message": "Agendamento confirmado, salvo no banco e e-mail enviado com sucesso",
"data": {
"agendamento": { "id": 101, "data_marcada": "2025-10-20T09:30:00.000Z", "id_imovel": 42 },
"email": { "ok": true, "message": "Enviado" }
}
}
- 200 (salvo, mas e‑mail falhou):
"Agendamento salvo com sucesso (email não enviado)" - 400 (validação) / 429 (rate limit) / 500 (erro interno)
Observação: Caso date/time não venham, o sistema define data_marcada = agora + 7 dias.
POST /agendamentos/property-notification
**Função:** envia e‑mail de marketing/alerta de **novo imóvel** (útil no fluxo pós‑agendamento).
**Validações:** `host`, `from`, `to`, e `propertyData.title` obrigatórios; sanitização HTML.
Body (exemplo)
{
"host": "smtp.mailtrap.io",
"from": "no-reply@imobiliaria-bortone.com.br",
"to": "cliente@example.com",
"propertyData": {
"title": "Apto 2D — Centro",
"address": "Av. Brasil, 500",
"price": "R$ 420.000",
"description": "Andar alto, 1 vaga, 68 m²",
"contactPhone": "+55 11 3333-0000"
}
}
Resposta 200: { "success": true, "message": "Notificação de imóvel enviada com sucesso" }
Utilitário DEV: POST /agendamentos/send — envio de e‑mail genérico (exposto apenas quando NODE_ENV !== 'production').
4.2) CRUD autenticado
Requer Auth.Authorization com req.loggedUser populado e nivel conforme regras.
POST /agendamentos/
**Cria agendamento** do usuário autenticado.
**Body**: `{ "data_marcada": ISODateString, "id_imovel": number?, "mensagem": string? }`
-
GET /agendamentos/meLista paginada dos agendamentos do usuário.
Query:
page(padrão 1),limit(padrão 20). Ordena pordata_marcadaASC.Inclui dados de
usuarioe (se houver)imovel. -
GET /agendamentos/Admin only. Lista todos os agendamentos (paginado). Ordena por
data_createDESC. -
GET /agendamentos/:idRetorna um agendamento. Se não admin, precisa ser dono.
-
PATCH /agendamentos/:idAtualiza
mensagem/data_marcada(dono ou admin). Atualizarconcluidoé somente admin. -
DELETE /agendamentos/:idRemove agendamento (dono ou admin). Resposta 204 sem corpo.
Erros padrão CRUD: 401 (não autenticado), 403 (acesso negado), 404 (não encontrado), 500 (erro interno).
5) Regras de Validação (resumo prático)
5.1) Agendamento (entrada pública)
- Obrigatórios:
name(≤120),email(RFC+domínio válido, ≤254),visitPeriod(≤100) - Opcionais com limites:
phone(≤20),propertyAddress(≤200),notes(≤2000),propertyId(numérico) - Fallback
data_marcada: agora + 7 dias quandodate/timeausentes
5.2) E‑mail (quando usado diretamente)
subject ≤ 200,text ≤ 10000,html ≤ 20000- Anexos (se existirem):
- Tipos permitidos (ex.:
image/*,pdf,docx,xlsx, ...) filename ≤ 255, base64 válido, tamanho ≤ 10MB
- Tipos permitidos (ex.:
6) Variáveis de Ambiente (integração de e‑mail)
Para SMTP detalhado Veja documentação Agendamento SMTP
| Variável | Exemplo/Tipo | Obrigatório | Uso |
|---|---|---|---|
SMTP_HOST |
smtp.mailtrap.io |
Sim | Host SMTP para envio |
SMTP_PORT |
587 |
Não | Porta (padrão 587) |
SMTP_SECURE |
true/false |
Não | TLS direto |
SMTP_USER |
string |
Não* | Se o servidor exigir AUTH |
SMTP_PASS |
string |
Não* | Se o servidor exigir AUTH |
SMTP_HELO |
localhost |
Não | Identificação EHLO/HELO |
MAIL_FROM_EMPRESA |
no-reply@bortone... |
Sim | Remetente dos e‑mails do módulo |
MAIL_TO_EMPRESA |
agendamentos@... |
Não | Destinatário interno padrão |
DASH_IMOB_URL |
https://imobiliaria-bortone.vercel.app |
Não | Base URL para CTAs (imóvel/agendamento) |
- AUTH só é tentado quando SMTP_USER/SMTP_PASS estão definidos e o servidor anuncia AUTH PLAIN/LOGIN. |
7) Fluxos Principais
7.1) Solicitação de Agendamento (pública)
- Cliente envia
POST /agendamentos/schedulecomappointmentválido. - Passa por rate limiting, validações e criação/atualização de Usuário (por e‑mail).
- Cria Agendamento (com
data_marcadaa partir dedate/timeou +7 dias). - Dispara 2 e‑mails:
- Cliente: confirmação + link do imóvel (CTA)
- Imobiliária: resumo + CTA para página do imóvel (link transformado)
- Retorna 200 com dados mínimos do agendamento criado.
7.2) Gestão via Dashboard (autenticado)
- Usuário acompanha seus agendamentos em
/agendamentos/me(paginado). - Admin gerencia tudo em
/agendamentos(paginado), pode concluir ou excluir.
8) Exemplos de Requisições (cURL)
Agendar (público)
curl -X POST https://api.seudominio.com/agendamentos/schedule \
-H "Content-Type: application/json" \
-d '{
"appointment": {
"name": "Maria Silva",
"email": "maria@example.com",
"visitPeriod": "Manhã",
"propertyId": 42,
"date": "2025-10-20",
"time": "09:30"
}
}'
Listar (me, autenticado)
curl -H "Authorization: Bearer <TOKEN>" \
"https://api.seudominio.com/agendamentos/me?page=1&limit=20"
Concluir (admin)
curl -X PATCH https://api.seudominio.com/agendamentos/101 \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "concluido": true }'
9) Convenções de Resposta & Erros
- 200/201: sucesso com corpo JSON
- 204: sucesso sem corpo (delete)
- 400: erro de validação/entrada
- 401: não autenticado
- 403: acesso negado (RBAC/dono)
- 404: recurso não encontrado
- 429: rate limit
- 500: erro interno (logs no servidor)
10) Especificação OpenAPI (YAML)
Obs.: Inclui apenas endpoints do módulo de agendamento.
openapi: 3.0.3
info:
title: Imobiliária Bortone — Agendamentos API
version: 1.0.0
servers:
- url: https://api.seudominio.com
paths:
/agendamentos/schedule:
post:
summary: Recebe solicitação de agendamento e envia confirmações por e-mail
tags: [Agendamentos]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
appointment:
type: object
required: [name, email, visitPeriod]
properties:
name: { type: string, maxLength: 120 }
email: { type: string, format: email, maxLength: 254 }
phone: { type: string, maxLength: 20 }
propertyAddress: { type: string, maxLength: 200 }
propertyId: { type: integer }
notes: { type: string, maxLength: 2000 }
visitPeriod: { type: string, maxLength: 100 }
date: { type: string, format: date }
time: { type: string, example: "09:30" }
responses:
'200':
description: Agendamento salvo (e e-mail enviado se possível)
content:
application/json:
schema:
type: object
properties:
success: { type: boolean }
message: { type: string }
data:
type: object
properties:
agendamento:
type: object
properties:
id: { type: integer }
data_marcada: { type: string, format: date-time }
id_imovel: { type: integer, nullable: true }
email:
type: object
nullable: true
properties:
ok: { type: boolean }
message: { type: string }
'400': { description: Erro de validação }
'429': { description: Limite de requisições excedido }
'500': { description: Erro interno }
/agendamentos/property-notification:
post:
summary: Envia e-mail de notificação de novo imóvel
tags: [Agendamentos]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [host, from, to, propertyData]
properties:
host: { type: string }
port: { type: integer, minimum: 1, maximum: 65535 }
secure: { type: boolean }
user: { type: string }
pass: { type: string }
helo: { type: string }
from: { type: string, format: email }
to: { type: string, format: email }
propertyData:
type: object
required: [title]
properties:
title: { type: string, maxLength: 200 }
address: { type: string, maxLength: 200 }
price: { type: string }
description: { type: string, maxLength: 2000 }
contactPhone: { type: string, maxLength: 20 }
responses:
'200': { description: Notificação enviada }
'400': { description: Erro de validação }
'500': { description: Erro interno }
/agendamentos:
get:
summary: Lista todos os agendamentos (admin)
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
parameters:
- in: query
name: page
schema: { type: integer, minimum: 1, default: 1 }
- in: query
name: limit
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
responses:
'200': { description: OK }
'401': { description: Não autenticado }
'403': { description: Acesso negado }
post:
summary: Cria agendamento do usuário autenticado
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [data_marcada]
properties:
data_marcada: { type: string, format: date-time }
id_imovel: { type: integer, nullable: true }
mensagem: { type: string, nullable: true }
responses:
'201': { description: Criado }
'401': { description: Não autenticado }
'500': { description: Erro interno }
/agendamentos/me:
get:
summary: Lista agendamentos do usuário autenticado
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
parameters:
- in: query
name: page
schema: { type: integer, minimum: 1, default: 1 }
- in: query
name: limit
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
'200': { description: OK }
'401': { description: Não autenticado }
/agendamentos/{id}:
get:
summary: Busca agendamento por ID
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
'200': { description: OK }
'401': { description: Não autenticado }
'403': { description: Acesso negado }
'404': { description: Não encontrado }
patch:
summary: Atualiza campos do agendamento
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
content:
application/json:
schema:
type: object
properties:
concluido: { type: boolean, description: "Somente admin" }
data_marcada: { type: string, format: date-time }
mensagem: { type: string }
responses:
'200': { description: OK }
'401': { description: Não autenticado }
'403': { description: Acesso negado }
'404': { description: Não encontrado }
delete:
summary: Remove agendamento
security: [{ bearerAuth: [] }]
tags: [Agendamentos CRUD]
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
'204': { description: Removido }
'401': { description: Não autenticado }
'403': { description: Acesso negado }
'404': { description: Não encontrado }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT