Files
schdlr/CONTEXT.md
Alexandre Possebom 1df9e81ec1
Build and Push Docker Image / build (push) Successful in 4m33s
Continuous integration / Check (push) Successful in 1m40s
Continuous integration / Test Suite (push) Successful in 2m34s
Continuous integration / Rustfmt (push) Successful in 27s
Continuous integration / Clippy (push) Successful in 1m35s
feat(calendar): publish a professional's agenda as a feed
The Professional subscribes to their own agenda in Apple Calendar or
Google Calendar through a secret URL that serves iCalendar (RFC 5545).
No OAuth, no writing into their account: the feed is the complete state,
so cancelling makes the event disappear on the next fetch. See ADR 0006.

- `professionals.calendar_token`, nullable and UNIQUE, born on the first
  request through the same idempotent COALESCE as the access code.
  26 symbols of the existing unambiguous alphabet, ~129 bits: this URL
  lives for years on someone else's server.
- `src/icalendar.rs` is pure, like `availability.rs`: folding at 75
  octets without splitting a UTF-8 sequence, TEXT escaping, UTC
  instants, stable UID so rescheduling moves the event. Do not copy the
  frontend's `ics.ts`, which never folds.
- `DTSTAMP` is the appointment's `updated_at`, never `now()`: with the
  request time the body would change on every fetch and the ETag would
  never match, so the 304 would not exist. Apple fetches hourly per
  subscriber.
- `confirmed`, `completed` and `no_show` only, from 90 days back into
  the open future. The status list is a Rust constant handed to the
  repository instead of a literal buried in SQL, so the ADR decision is
  readable and testable.
- The feed answers the same 404 for an unknown token and for a
  deactivated Professional, and the three panel routes go through
  `require_manage_professional`.

The public route is `/calendar/{token}/feed.ics`, not `{token}.ics`:
matchit 0.8 does not support dynamic suffixes, and the other shape
panics the Router at boot instead of failing to compile.

24 unit tests and one against Postgres, which is where the status filter
and the window actually run. Also verified end to end against a real
server: 3 events out of 8, a client name with `;` and `,` escaped, no
line over 75 octets, 304 on `if-none-match`, rotation killing the old
URL, and the email landing in mailpit.

Two ROADMAP debt entries were stale and are corrected with the evidence:
clippy passes clean and the dbml is current.
2026-07-30 09:21:10 -03:00

5.1 KiB
Raw Permalink Blame History

Agendamento

Uma empresa publica a agenda dos seus profissionais e o cliente final marca horário pela internet, sem criar conta.

Language

Quem é quem

Company: A empresa que vende atendimento: salão, barbearia, clínica, consultório. É a unidade de isolamento, todo dado pertence a exatamente uma. Evite: tenant, estabelecimento, loja, negócio

Professional: Quem atende. Pode ser o dono, um sócio, um contratado ou quem aluga a cadeira; o vínculo trabalhista não faz parte do modelo. Evite: employee, funcionário, colaborador, staff, prestador

Company slug: O identificador da Company na URL pública. Estável: mudar quebra todo link que a empresa já divulgou. Evite: handle, permalink, código

Client: Quem é atendido, sempre dentro de uma Company, identificado pelo telefone dentro dela. A mesma pessoa atendida por duas Companies são dois Clients diferentes, e o sistema não sabe que é a mesma pessoa. Evite: customer, paciente, usuário, consumidor

O que se marca

Service: O que a Company vende, com nome, duração e preço base. Cada Professional oferece um subconjunto dos Services da sua Company, e pode ter preço próprio por Service (o override no vínculo; sem override, vale o base). A lista pública mostra o menor preço resolvido entre quem oferece ("a partir de", quando varia). Evite: procedimento, produto, item, tratamento

Appointment: Um compromisso marcado: um Client, um Professional, um Service e um intervalo de tempo. É a coisa que o cliente marca e que o profissional cumpre. O preço combinado é congelado na criação (snapshot do preço resolvido do par Professional × Service); notificação e consulta leem o snapshot, então mexer no catálogo depois não reescreve compromisso já marcado. Trocar o par re-resolve; encaixe sem Service não tem preço. Evite: schedule, booking, reserva, consulta, sessão

Slot: Um horário livre oferecido ao Client. É derivado, nunca armazenado: sai das Working hours menos os Time offs menos os Appointments já marcados, com passo igual à duração do Service escolhido. Evite: vaga, janela, horário disponível

Lead: A antecedência mínima com que um Slot pode ser marcado. Existe para o Professional não descobrir um atendimento tarde demais para chegar. Evite: prazo, buffer, carência

Horizon: Até quando para frente a agenda abre. Sem ele a grade seria infinita. Evite: limite, alcance, validade

Quando se atende

Working hours: As faixas de horário em que um Professional atende num dia da semana. Um dia pode ter mais de uma faixa, e é assim que o intervalo de almoço se expressa: pela ausência de faixa entre elas. Evite: schedule, agenda, expediente, jornada

Time off: Um intervalo de datas em que o Professional não atende, apesar do que dizem as Working hours: férias, feriado, atestado, viagem. Evite: day off, folga, bloqueio, ausência

O fluxo público

Booking link: A URL pública e estável de uma Company, o único endereço por onde um Client marca. É o que a empresa cola na bio do Instagram ou no QR code do balcão. Evite: landing, página de agendamento, link de captação

Magic link: URL assinada e enviada por e-mail que dá ao Client acesso a um Appointment específico, para ver ou cancelar. Não é login: não abre acesso a mais nada e não cria sessão. Evite: token, sessão, login sem senha, convite

O profissional para fora

Calendar feed: A URL secreta e estável que publica os Appointments de um Professional no formato que o Apple Calendar e o Google Agenda assinam. É só leitura e é sempre atrasada: quem assina busca de tempos em tempos, então o feed é uma cópia da agenda, nunca a agenda. Quem tem a URL vê tudo que há nela, e a única revogação é trocá-la. Evite: exportação, sincronização, integração, iCal

Estados de um Appointment

pending e confirmed ocupam a agenda. Os outros cinco são histórico e liberam o horário.

pending: O Client marcou e ainda não provou que o canal é dele. Segura o Slot enquanto o prazo corre, porque a corrida pelo horário se decide na escolha e não na confirmação. Evite: pré-reserva, provisório, aguardando

confirmed: O horário vale. É como nasce todo Appointment criado pelo painel; o do Client nasce pending e chega aqui quando ele confirma.

expired: O prazo do pending venceu e o Slot voltou para a grade. O horário ainda pode ser confirmado enquanto ninguém o tomar. Evite: cancelado, perdido, vencido

cancelled_by_client / cancelled_by_company: O horário foi desfeito, e por quem. São estados distintos porque respondem perguntas diferentes sobre o negócio. Evite: deletado, removido, excluído

completed: O atendimento aconteceu.

no_show: O horário passou e o Client não apareceu. É diferente de cancelado: ninguém avisou. Evite: falta, ausência, furo

Permissões

super-admin: Opera qualquer Company. É o operador da plataforma, não pertence ao negócio de nenhum cliente.

admin: Opera uma Company inteira, incluindo os Appointments de todos os Professionals dela.

professional: Opera apenas a própria agenda.