A customer who made a booking has no way to cancel it; only a logged-in user of the admin portal can (cancel in src/backend/reservation.rs). The confirmation e-mail should contain a link to a page with the booking, where the customer can cancel it. The booking is not deleted, its state becomes Canceled.
Current state
Every booking (reservation_sum) has a random uuid (v4). It is used by the admin portal to approve and cancel, never shown to the customer.
After a booking is created, notify_new sends the NewReservationCust template to the customer. Templates know two placeholders, #date# and #summary# (MailMessage::replace_body_vars in src/backend/mail.rs).
Cancelled bookings no longer block their time slots (the queries filter state <> 'Canceled').
The application does not know its public address: the config has only the bind address in [network], and e-mails are built outside of any knowledge of the domain.
Decisions
Deadline is a setting. The administrator sets how many hours before the start of a booking the customer may still cancel it. 0 (the default) means until the start.
Approved bookings can be cancelled by the customer too, not only those waiting for approval.
The link is appended automatically. When a template sent to the customer does not contain #link#, the link is added at the end of the e-mail, so the feature works for existing installations without editing their templates.
Proposed change
Link in e-mails
New placeholder #link# in mail templates, replaced by <public address>/booking/<uuid>. It works in the templates sent to the customer about a booking that can still be cancelled: NewReservationCust and ReservationApp. The mail settings page should say which placeholders exist.
A template without the placeholder gets the link appended at the end, on its own line after an empty line, introduced by a short translated sentence (e.g. "You can view or cancel your booking here:"). The cancellation e-mail (ReservationCanceled) gets no link appended; #link# written in it by the administrator is still replaced.
The public address comes from a new config key (e.g. [network] public_url), filled in by deploy/rezervator.sh from the customer's domain (config.template.toml). config_sample.toml and the README get the key too. Without it the link falls back to the Host of the request that created the booking.
Deadline setting
New column on company (e.g. cancel_hours INTEGER NOT NULL DEFAULT 0), edited on the settings page together with the other company data, validated as a non-negative number with a sane upper limit.
The rule is a pure function of the booking (date, earliest from of its items), the setting and the current local time.
Booking page
New public route /booking/<uuid>, styled like the public page (inside the .public-page wrapper, with the customer's colours and banner).
Shows date, booked items with times, total price and the state (new / approved / cancelled). No e-mail address or phone number — whoever has the link sees only what was in the e-mail.
An unknown or malformed uuid shows a plain "booking not found", the same for every case.
A "Cancel booking" button with a confirmation dialog. After cancelling the page shows the cancelled state and a link back to the booking form.
When the booking can no longer be cancelled (deadline passed, already started), the button is not shown and the page says why, with the deadline ("can be cancelled until …").
Cancelling
New public server function (no perm_check!, the uuid is the secret) that sets the state to Canceled.
Allowed for bookings in state New or Approved until the deadline; the server checks the rule again, the page only hides the button. Calling it again on a cancelled booking does nothing and reports success.
It must be a POST triggered by the button, never something a GET of the link does: mail clients and scanners open links from e-mails.
The administrator is notified by e-mail that the customer cancelled (new MessageType, which needs ALTER TYPE message_type ADD VALUE, the Rust variant, its Display arm and the list in check_messages). The customer gets the existing ReservationCanceled e-mail as a confirmation.
The admin portal should show that a booking was cancelled by the customer, not by staff (e.g. a canceled_by_customer flag on reservation_sum).
Notes
The uuid is 122 random bits, so guessing a link is not a realistic attack; no login or captcha is planned for the page. The endpoint must not reveal whether a uuid exists in any other way than the page itself.
New strings go through trl() with Czech translations; the date goes through loc_date(). The sentence appended to e-mails is built on the server, outside of a request of the customer's browser — check which language it gets.
Pure logic gets unit tests: building the link, replacing or appending the placeholder, the rule whether a booking can still be cancelled (including the deadline around midnight and a booking with several items).
Acceptance
A new booking sends an e-mail with a working link, both with a template that contains #link# and with one that does not (checked with transport = "File" and in a container created by deploy/rezervator.sh, where the address comes from the domain).
The page shows the booking; cancelling a new and an approved booking frees its time slots on the public form and sends both notifications.
With a deadline set, a booking inside the deadline cannot be cancelled, by the page or by calling the server function directly. A past booking and an already cancelled one cannot be cancelled; an unknown uuid shows "not found".
Opening the link alone never changes anything.
Checked in a browser with a full reload at mobile and desktop width, no hydration errors.
A customer who made a booking has no way to cancel it; only a logged-in user of the admin portal can (`cancel` in `src/backend/reservation.rs`). The confirmation e-mail should contain a link to a page with the booking, where the customer can cancel it. The booking is not deleted, its state becomes `Canceled`.
## Current state
- Every booking (`reservation_sum`) has a random `uuid` (v4). It is used by the admin portal to approve and cancel, never shown to the customer.
- After a booking is created, `notify_new` sends the `NewReservationCust` template to the customer. Templates know two placeholders, `#date#` and `#summary#` (`MailMessage::replace_body_vars` in `src/backend/mail.rs`).
- Cancelled bookings no longer block their time slots (the queries filter `state <> 'Canceled'`).
- The application does not know its public address: the config has only the bind address in `[network]`, and e-mails are built outside of any knowledge of the domain.
## Decisions
1. **Deadline is a setting.** The administrator sets how many hours before the start of a booking the customer may still cancel it. `0` (the default) means until the start.
2. **Approved bookings can be cancelled** by the customer too, not only those waiting for approval.
3. **The link is appended automatically.** When a template sent to the customer does not contain `#link#`, the link is added at the end of the e-mail, so the feature works for existing installations without editing their templates.
## Proposed change
### Link in e-mails
- New placeholder `#link#` in mail templates, replaced by `<public address>/booking/<uuid>`. It works in the templates sent to the customer about a booking that can still be cancelled: `NewReservationCust` and `ReservationApp`. The mail settings page should say which placeholders exist.
- A template without the placeholder gets the link appended at the end, on its own line after an empty line, introduced by a short translated sentence (e.g. "You can view or cancel your booking here:"). The cancellation e-mail (`ReservationCanceled`) gets no link appended; `#link#` written in it by the administrator is still replaced.
- The public address comes from a new config key (e.g. `[network] public_url`), filled in by `deploy/rezervator.sh` from the customer's domain (`config.template.toml`). `config_sample.toml` and the README get the key too. Without it the link falls back to the `Host` of the request that created the booking.
### Deadline setting
- New column on `company` (e.g. `cancel_hours INTEGER NOT NULL DEFAULT 0`), edited on the settings page together with the other company data, validated as a non-negative number with a sane upper limit.
- The rule is a pure function of the booking (date, earliest `from` of its items), the setting and the current local time.
### Booking page
- New public route `/booking/<uuid>`, styled like the public page (inside the `.public-page` wrapper, with the customer's colours and banner).
- Shows date, booked items with times, total price and the state (new / approved / cancelled). No e-mail address or phone number — whoever has the link sees only what was in the e-mail.
- An unknown or malformed uuid shows a plain "booking not found", the same for every case.
- A "Cancel booking" button with a confirmation dialog. After cancelling the page shows the cancelled state and a link back to the booking form.
- When the booking can no longer be cancelled (deadline passed, already started), the button is not shown and the page says why, with the deadline ("can be cancelled until …").
### Cancelling
- New public server function (no `perm_check!`, the uuid is the secret) that sets the state to `Canceled`.
- Allowed for bookings in state `New` or `Approved` until the deadline; the server checks the rule again, the page only hides the button. Calling it again on a cancelled booking does nothing and reports success.
- It must be a POST triggered by the button, never something a GET of the link does: mail clients and scanners open links from e-mails.
- The administrator is notified by e-mail that the customer cancelled (new `MessageType`, which needs `ALTER TYPE message_type ADD VALUE`, the Rust variant, its `Display` arm and the list in `check_messages`). The customer gets the existing `ReservationCanceled` e-mail as a confirmation.
- The admin portal should show that a booking was cancelled by the customer, not by staff (e.g. a `canceled_by_customer` flag on `reservation_sum`).
## Notes
- The uuid is 122 random bits, so guessing a link is not a realistic attack; no login or captcha is planned for the page. The endpoint must not reveal whether a uuid exists in any other way than the page itself.
- New strings go through `trl()` with Czech translations; the date goes through `loc_date()`. The sentence appended to e-mails is built on the server, outside of a request of the customer's browser — check which language it gets.
- Pure logic gets unit tests: building the link, replacing or appending the placeholder, the rule whether a booking can still be cancelled (including the deadline around midnight and a booking with several items).
## Acceptance
- A new booking sends an e-mail with a working link, both with a template that contains `#link#` and with one that does not (checked with `transport = "File"` and in a container created by `deploy/rezervator.sh`, where the address comes from the domain).
- The page shows the booking; cancelling a new and an approved booking frees its time slots on the public form and sends both notifications.
- With a deadline set, a booking inside the deadline cannot be cancelled, by the page or by calling the server function directly. A past booking and an already cancelled one cannot be cancelled; an unknown uuid shows "not found".
- Opening the link alone never changes anything.
- Checked in a browser with a full reload at mobile and desktop width, no hydration errors.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A customer who made a booking has no way to cancel it; only a logged-in user of the admin portal can (
cancelinsrc/backend/reservation.rs). The confirmation e-mail should contain a link to a page with the booking, where the customer can cancel it. The booking is not deleted, its state becomesCanceled.Current state
reservation_sum) has a randomuuid(v4). It is used by the admin portal to approve and cancel, never shown to the customer.notify_newsends theNewReservationCusttemplate to the customer. Templates know two placeholders,#date#and#summary#(MailMessage::replace_body_varsinsrc/backend/mail.rs).state <> 'Canceled').[network], and e-mails are built outside of any knowledge of the domain.Decisions
0(the default) means until the start.#link#, the link is added at the end of the e-mail, so the feature works for existing installations without editing their templates.Proposed change
Link in e-mails
#link#in mail templates, replaced by<public address>/booking/<uuid>. It works in the templates sent to the customer about a booking that can still be cancelled:NewReservationCustandReservationApp. The mail settings page should say which placeholders exist.ReservationCanceled) gets no link appended;#link#written in it by the administrator is still replaced.[network] public_url), filled in bydeploy/rezervator.shfrom the customer's domain (config.template.toml).config_sample.tomland the README get the key too. Without it the link falls back to theHostof the request that created the booking.Deadline setting
company(e.g.cancel_hours INTEGER NOT NULL DEFAULT 0), edited on the settings page together with the other company data, validated as a non-negative number with a sane upper limit.fromof its items), the setting and the current local time.Booking page
/booking/<uuid>, styled like the public page (inside the.public-pagewrapper, with the customer's colours and banner).Cancelling
perm_check!, the uuid is the secret) that sets the state toCanceled.NeworApproveduntil the deadline; the server checks the rule again, the page only hides the button. Calling it again on a cancelled booking does nothing and reports success.MessageType, which needsALTER TYPE message_type ADD VALUE, the Rust variant, itsDisplayarm and the list incheck_messages). The customer gets the existingReservationCancelede-mail as a confirmation.canceled_by_customerflag onreservation_sum).Notes
trl()with Czech translations; the date goes throughloc_date(). The sentence appended to e-mails is built on the server, outside of a request of the customer's browser — check which language it gets.Acceptance
#link#and with one that does not (checked withtransport = "File"and in a container created bydeploy/rezervator.sh, where the address comes from the domain).