Files
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
..