Arquitetura — Agenda SaaS¶
Diagrama Geral¶
┌─────────────────────────────────────────────────────┐
│ Agenda SaaS │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Next.js Frontend (SSR + SPA) │ │
│ │ ├── /dashboard (admin) │ │
│ │ ├── /calendar (calendario visual) │ │
│ │ ├── /services (CRUD servicos) │ │
│ │ ├── /professionals (CRUD profissionais) │ │
│ │ ├── /clients (lista clientes) │ │
│ │ ├── /reports (relatorios) │ │
│ │ ├── /settings (config + integracoes)│ │
│ │ ├── /financeiro (recebimentos) │ │
│ │ └── /book/{slug} (pagina publica SSR) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ API calls │
│ ┌──────────────────▼───────────────────────┐ │
│ │ FastAPI Backend │ │
│ │ ├── /api/v1/auth/* │ │
│ │ ├── /api/v1/organizations/* │ │
│ │ ├── /api/v1/services/* │ │
│ │ ├── /api/v1/professionals/* │ │
│ │ ├── /api/v1/schedule-rules/* │ │
│ │ ├── /api/v1/schedule-exceptions/* │ │
│ │ ├── /api/v1/appointments/* │ │
│ │ ├── /api/v1/availability/* │ │
│ │ ├── /api/v1/clients/* │ │
│ │ ├── /api/v1/payments/* │ │
│ │ ├── /api/v1/reports/* │ │
│ │ ├── /api/v1/integrations/* │ │
│ │ └── /api/v1/public/book/{slug}/* │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ┌─────────┐ ┌─────┴─────┐ ┌──────────┐ │
│ │ Postgres │ │ Redis │ │ Celery │ │
│ │ 16 │ │ 7 │ │ Workers │ │
│ │ │ │ cache + │ │ reminder │ │
│ │ 12 tables│ │ sessions │ │ webhook │ │
│ └─────────┘ └───────────┘ │ payment │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────┘
12 Tabelas do Banco¶
| # | Tabela | Descricao |
|---|---|---|
| 1 | organizations | Tenant principal. Slug = URL publica. Plan = free/pro/business |
| 2 | users | Usuarios autenticados. Roles: owner, admin, professional, receptionist |
| 3 | services | Catalogo de servicos: nome, duracao_minutos, preco, cor, sort_order |
| 4 | professionals | Profissionais da org. Pode ter user_id vinculado |
| 5 | professional_services | N:N profissional ↔ servico. Override custom_duration e custom_price |
| 6 | schedule_rules | Regras semanais: dia_semana, start_time, end_time, break_start, break_end |
| 7 | schedule_exceptions | Bloqueios pontuais (ferias, feriados) ou dias extras de trabalho |
| 8 | clients | Clientes com metricas cacheadas: total_appointments, total_spent, loyalty_points |
| 9 | appointments | Agendamentos com state machine: scheduled→confirmed→completed/noshow/cancelled |
| 10 | payments | Pagamentos: pix, dinheiro, cartao. Integracao AbacatePay |
| 11 | integrations | Configuracao de conexao com ChatDigi e ByClinic |
| 12 | integration_webhook_logs | Auditoria de webhooks inbound e outbound |
Multi-tenancy¶
- Cada
Organizatione um tenant isolado - Toda query filtra por
org_id(middleware ou dependency injection) - Slug unico por org — usado na URL publica
/book/{slug} - Usuarios pertencem a uma unica org
- Roles hierarquicos:
owner > admin > professional > receptionist
Fluxo de Autenticacao¶
1. POST /api/v1/auth/register
→ Cria Organization + User(role=owner)
→ Retorna access_token (15min) + refresh_token (7d)
2. POST /api/v1/auth/login
→ Valida email + bcrypt(password)
→ Retorna access_token + refresh_token
3. Requests autenticados
→ Header: Authorization: Bearer <access_token>
→ Middleware extrai org_id + user_id do JWT
→ Todas as queries filtradas por org_id
4. POST /api/v1/auth/refresh
→ Valida refresh_token
→ Retorna novos access_token + refresh_token
→ Rotacao automatica (refresh token de uso unico)
Scheduling Engine¶
Algoritmo de Disponibilidade¶
def get_available_slots(professional_id, service_id, date, org_settings):
# 1. Buscar regras de horario do profissional pro dia da semana
rule = get_schedule_rule(professional_id, date.weekday())
if not rule or not rule.is_active:
return [] # Nao trabalha nesse dia
# 2. Gerar todos os slots possiveis
service = get_service(service_id)
buffer = org_settings.get("buffer_minutes", 0)
slot_duration = service.duration_minutes + buffer
all_slots = generate_time_slots(
start=rule.start_time,
end=rule.end_time,
duration=slot_duration,
break_start=rule.break_start,
break_end=rule.break_end,
)
# 3. Remover slots bloqueados por excecoes
exceptions = get_exceptions(professional_id, date)
for exc in exceptions:
if exc.exception_type == "block":
all_slots = remove_overlapping(all_slots, exc)
# 4. Remover slots ocupados por agendamentos existentes
appointments = get_appointments(
professional_id, date, status=["scheduled", "confirmed"]
)
for appt in appointments:
all_slots = remove_overlapping(all_slots, appt)
# 5. Remover slots no passado (se data = hoje)
if date == date.today():
min_advance = org_settings.get("min_advance_minutes", 60)
cutoff = datetime.now() + timedelta(minutes=min_advance)
all_slots = [s for s in all_slots if s.start >= cutoff.time()]
return all_slots
Prevencao de Double-Booking¶
Usa SELECT FOR UPDATE dentro de transacao atomica:
async def book_appointment(data, db):
async with db.begin():
# Lock do profissional naquele horario
conflict = await db.execute(
select(Appointment)
.where(
Appointment.professional_id == data.professional_id,
Appointment.start_at < data.end_at,
Appointment.end_at > data.start_at,
Appointment.status.in_(["scheduled", "confirmed"]),
)
.with_for_update()
)
if conflict.first():
raise HTTPException(409, "Horario nao disponivel")
appointment = Appointment(**data)
db.add(appointment)
await db.flush()
return appointment
Garantias: - Lock de linha no PostgreSQL durante a transacao - Concurrent requests para o mesmo slot → apenas 1 passa, os outros recebem 409 - Rollback automatico em caso de falha
State Machine — Appointments¶
scheduled ──→ confirmed ──→ completed
│ │
└──→ cancelled └──→ noshow
│
└──→ noshow (se nao confirmado)
| Transicao | Endpoint | Quem pode |
|---|---|---|
| scheduled → confirmed | POST /:id/confirm | admin, owner, receptionist |
| * → cancelled | POST /:id/cancel | qualquer (com motivo) |
| confirmed → completed | POST /:id/complete | admin, owner, receptionist |
| * → noshow | POST /:id/noshow | admin, owner, receptionist |
| scheduled/confirmed → scheduled | POST /:id/reschedule | admin, owner |
Infra — Docker Compose (Producao)¶
Traefik (proxy externo)
├── agenda.digitalface.dev.br → frontend:3000
├── api.agenda.digitalface.dev.br → backend:8000
└── docs.agenda.digitalface.dev.br → docs:80
Rede agenda_internal (isolada):
├── agenda_db (PostgreSQL 16)
├── agenda_redis (Redis 7)
├── agenda_celery (worker)
└── agenda_celery_beat (scheduler)