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.
5.1 KiB
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.