Pular para conteúdo

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 Organization e 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)