Appointments¶
Customers book an appointment on the shop's own site: they pick what they want (an NHS eye test, a contact lens fitting), then either a named practitioner or Any available, then a time. Staff run the day from the Diary, which shows one column per practitioner. It is the same booking engine as table reservations: the same holds that refuse a double booking, guest profiles, emails with a calendar file, day-before reminders, the guest's own page to change or cancel, and the same GDPR rules. A restaurant keeps working exactly as before; an appointments business is a different setup of the same engine.
In development, off by default
Appointments are switched on per shop with Settings › Dining in › Reservations enabled
(reservations-enabled), and per location with the capability APPOINTMENTS ("Takes
appointments") on the location form. An appointment is free or paid in store (a booking with no
order), or paid in full online as a normal basket and checkout (Paid appointments).
Deposits and no-show fees follow.
The pieces¶
| Piece | What it is |
|---|---|
Location capability APPOINTMENTS |
"Takes appointments" on the location form. Only these locations can have an appointments room. |
Room (DiningRoom, kind = APPOINTMENTS) |
One per location, read and saved whole with optimistic locking, like a dining room. Holds the team, the appointment types, the assignment strategy and the booking policy. |
Team (BookableResource) |
A practitioner (PERSON), or a room or equipment. Each has a working week, regular breaks, leave and one-off blocks. |
Appointment type (AppointmentType) |
What can be booked: length, padding, start-time interval, least notice, who can do it, how it is paid for. |
Booking (Reservation, kind = APPOINTMENT) |
The type, the practitioner it holds, the time. The same document as a table booking. |
Holds (TableHold) |
One row per practitioner per quarter hour, keyed so the database refuses a double booking. |
booking-business-kind (restaurant or appointments, Settings › Dining in › What you take
bookings for) sets the words in the admin menus (the Diary and Team and services entries); they switch to
appointments too as soon as any location has an appointments room. Each room carries its own kind, which
decides how that location is booked, and each booking email follows its booking's kind. A location has one
or the other: a dining room and an appointments room can't share a location.
The team¶
A practitioner has:
- a working week: periods on days (several on one day is a split shift), times on the quarter hour;
- regular breaks ("Lunch 13:00-14:00", on chosen days or every day): no appointment starts in one or runs into one;
- leave: whole days, from and to inclusive, when they take nothing;
- blocks: one-off time set aside by staff from the diary (a meeting, a long lunch);
- a rank, the venue's order among equals, and a switch to keep them off online booking.
What is left is the time they can take appointments. The location's special days and temporary closures cut it further, as they do for a dining room.
Appointment types¶
| Setting | Meaning |
|---|---|
| Length | How long the customer is seen, 15 minutes to 8 hours, in steps of 15. |
| Padding before / after | Held with the booking but never shown to the customer. |
| Start every | Start times come every 15 to 240 minutes within a practitioner's hours, starting from the beginning of each stretch of their day (after a break the grid starts again). |
| Least notice | The shortest time before the start an online booking is accepted. Staff are not bound by it. |
| Who can do it | The practitioners who offer it; empty means every active, online-bookable person. |
| How do customers pay? | Chosen per type by the owner: No online payment (free, or paid in store) or Pay in full when booking (Paid appointments). The server, the MCP tools and a site import refuse a deposit or card guarantee until they ship. A type can show a note ("Free on the NHS", "£25, pay in store"). |
| Assignment | Optional: overrides the location's way of resolving Any available for this type. |
Lengths, padding and intervals are multiples of 15 minutes because holds are made in quarter hours. An optician's NHS eye test and private eye test are two types, each with its own note and length, sharing the same practitioners, so a practitioner's day counts both.
Any available: assignment strategies¶
A customer who picks a practitioner by name sees all of that person's free times. For Any available,
a time is offered when the location's strategy allows it, and the booking goes to the first
practitioner the strategy returns. The strategy is chosen per location, and per type if wanted.
Strategies are Spring beans implementing PractitionerAssignmentStrategy (platform code, not
per-tenant scripts); the admin form is generated from each strategy's settings.
Spread evenly (round-robin, the default) offers every free practitioner and gives the booking to
whoever has the fewest bookings that day (or that week, with the scope setting), then whoever was
given a booking longest ago, then the venue's order.
Fill one practitioner first (fill-first) takes practitioners in order (or a list you choose) and
opens the next one only once every earlier one is full for the day: booked minutes divided by working
minutes (hours less breaks and blocks, as at the start of the day) at or above the threshold
(thresholdPercent, 50 by default, per location and per type). The load is measured before the new
booking, so one booking can carry the first past the threshold and the next opens the second. Someone on
leave counts as full. While the first is below the threshold, only their times are offered for Any.
With overflow = WHEN_BUSY (the default) a later practitioner's time is also offered at a moment the
earlier ones cannot serve, so a customer's chosen time is not lost; STRICT never offers one early.
The threshold is a guess to tune: the booking tester shows each practitioner's current load, so changing
it is a trial, not a deployment.
Staff bookings and reassignment ignore the strategy.
Booking on the shop¶
The appointment booking block (appointmentBooking) goes on any page of a Nova-based theme, and
/book shows the same form on its own: for a shop that takes appointments and has no tables to
book, /book is the appointment form (a link with ?type= asks for it on a shop that has both).
The customer picks what to book, who with (Any available or a named practitioner), and the date. That day's start times come back as buttons; a full day offers the next days with a time free. Then their name, email, an optional phone, anything we should know, the consent wording and an unticked box for news and offers. There is no allergy or dietary question: nothing health related is collected in this release. Retrying a booking (a double tap, a lost connection) returns the same booking.
It works without JavaScript: the choice is a GET form that reloads with that date's times and the
booking posts to /book, which comes back with what was typed if something is wrong (a time that has
just gone offers the nearest free times). A script, appointment-booking.js, asks the public API
(appointment-options, appointment-availability, appointment-days) for the times when the place,
type, practitioner or date changes and redraws them in place, keeping the address bar in step; if the
API can't be reached it shows a short message, or falls back to the plain page load. It also holds the
button while booking. The block uses the table booking block's --tb-*
tokens, so a theme that styles one styles both. On the legacy Bootstrap theme the block and /book
give each location's phone number instead of the form.
Booking from a product page¶
An appointment type can be sold as a catalogue product (set the type's Product to the product's
SEO name). On a Nova or Brock theme that product's page shows Book an appointment in place of
Add to basket, linking to /book?location=...&type=... with the type (and its location) already
chosen. The page does this for a type customers can book online, whether it takes no payment (payment
mode None) or is paid in full online: either way the time is chosen on /book, so the page never
hides a purchase it could have made. If the type is offered at
several locations the first by name is chosen and the form lets the customer switch. Other themes keep
Add to basket on the product page.
The same product's card on listings (collection pages, search results, category pages) shows Book
instead of Add to basket, linking to the same booking, and a zero price reads Free (a paid
type shows its price). The Nova and
Brock cards do this, and so do the Nova and Brock product rows (price only). The storefront works out the
products once per page (AppointmentProductsInterceptor, from GuestAppointmentViews.bookingPaths) and
the cards only read the result.
The customer's page¶
Every email carries a personal link to /booking/{token}, the same page as for a table, in
appointment words. It shows the appointment, who it is with ("Dr Patel (next available)" when the
customer chose Any available) and lets the customer change the time (a person they chose stays their
practitioner; Any available may move to whoever is free), cancel, confirm from the reminder, and add
it to a calendar. The calendar event reads "NHS eye test with Dr Patel at High Street Opticians".
Changes close the type's least-notice before the start.
A paid appointment has the same page with two differences: the customer can only cancel it (moving a paid booking is a later phase, so the page says to cancel and book again, or call), and the cancel step shows what comes back before they confirm (Cancelling a paid appointment).
Emails¶
The confirmation, change, cancellation and day-before reminder reuse the table booking emails
(ReservationEmailListener, ReservationEmailComposer, the .ics from ReservationCalendar) with
appointment wording. They name the appointment and the practitioner, never a table, party size or
allergy. The subject names the appointment only: "Your NHS eye test at High Street Opticians on
Monday 12 October". A staff reassignment emails the customer only if they chose the practitioner by
name.
A paid appointment sends no confirmation email of its own: the order confirmation is its confirmation. Its cancellation email is still sent from the booking, and says what happened to the money ("We've refunded £25.00 to the way you paid", or that the payment isn't refunded under the cancellation terms). The day-before reminder is the same as for a free booking.
The diary¶
Bookings › Diary (for a shop whose business kind is appointments, or that has an appointments room) is one row per practitioner against time: hatched where they don't work, plain where they do, grey where time is set aside, with a bar for each appointment (the usual diary colours: booked, late, arrived, in progress, seen, no-show). The list view shows the same appointments by time. Staff can:
- book by phone for a type, a named practitioner or Any available, at any time someone is free (staff are not bound by the least notice, the online flags or the assignment strategy's holding back);
- move an appointment through arrived, start, finished, no-show, cancel. Cancelling one paid online asks what happens to the payment: Refund what they paid (the whole line, whatever the terms) or Keep the money. A paid appointment's drawer says "Paid online" and links to its order, and a held one whose order has not been paid yet reads Awaiting payment;
- link a till order to a booking that was paid in store (Paid in store);
- reassign an appointment: the drawer lists the other practitioners who offer the type and says who is free then; or drag the bar onto another practitioner's row. A person who isn't free is refused;
- set time aside on a practitioner's day (a meeting, a long lunch): refused while they already have an appointment in that time. Click the grey bar to open it up again.
Each row shows the share of the practitioner's working time that is booked. Narrow the diary to one person with the names above the board. The board refreshes every 30 seconds. A practitioner's own login ("my diary") is a later phase: the shared front-desk diary is the pilot's.
The booking tester¶
Settings › Locations › (location) › Appointments has Test a booking beside the editor. Pick a
date, a type, a practitioner or Any available, and a customer or staff, and it lists every start time
with who would take it, or why not: a practitioner on a break, on leave, booked for part of it, or free
but held back by the strategy ("Dr Shah held back: Dr Patel is at 14% of the 50% threshold"). It also
shows each practitioner's load that day, the number fill-first compares with its threshold. It tests
the setup as it stands in the editor, saved or not, so changing the threshold is a trial, not a
deployment. Underneath it is AppointmentAvailabilityService.explain, which the MCP appointments
test action uses too.
Which times are offered¶
A start time is offered for a practitioner when it is on the grid of their working hours for the type, the whole appointment fits before their break or the end of their stretch, the location is open (special days and temporary closures cut the day, as they do for a dining room), it is far enough ahead (the type's least notice online, a quarter hour of grace for staff), and none of the quarter hours the appointment and its padding touch is held by another booking. A time the clocks skip in spring isn't offered; one they repeat in autumn is offered once.
The practitioner is held in the same TableHold rows a table is, written before the booking, so the
database refuses a double booking whichever server asks. Two customers taking the last practitioner at the
same moment cannot both get them; for Any available the loser falls to the next practitioner the
strategy allows, and for a named practitioner is offered the nearest other times.
How customers pay¶
Every type has a paymentMode, chosen by the owner under How do customers pay?:
- No online payment (
NONE): a free test, or one paid in store (put "£25, pay in store" in the price note). A booking has no order: it is theReservationalone, so the customer meets none of a basket, billing address or payment step. - Pay in full when booking (
ONLINE_FULL): a normal basket and checkout, described next.
The server, the MCP tools and a site import refuse a type saved with a mode that hasn't shipped (a deposit, a card guarantee), so turning one on later needs no data migration. A type switched from one mode to another applies to new bookings only.
Paid appointments¶
A type paid in full online is sold as a catalogue product, so its price, tax, promotions and gift cards are the product's own.
Setting one up. Create a product with the Appointment type (appointment; no variants), set its
price, then on the appointment type choose Pay in full when booking and put the product's SEO name in
Product. A type paid online with no product is refused when the setup is saved, and isn't offered to
customers.
How a shopper buys one. They choose the type, practitioner (or Any available) and date on /book,
the appointment block, or from the product page's Book an appointment button or a listing card's
Book (all lead to /book with the type chosen; the price shows as for any product). The form shows
the type's price and an Add to basket button in place of the details form. The time is checked again
as it goes in, with the engine that offered it: a time that has just gone comes back to the picker with
the nearest free times. Then the basket shows it as one line ("Thu 5 Nov, 10:30 with Dr Shah", or "with
the next available"), and checkout asks for their name and email as for any order. The same line appears
in the mini basket, the checkout summary, the confirmation page and the order email, in the location's time
zone.
An appointment is one slot: its quantity is always 1, there is no stepper, and adding another time makes another line. Nothing is held while it sits in the basket.
What happens at checkout. An appointment line is a bookable line (Bookable kinds), so the order's steps treat it like an event ticket:
- Placing the order, before the card is charged. The slot is checked again with the same engine that
offered it, "Any available" is resolved again by the type's assignment strategy (so the practitioner can
be a different one from when the time was picked, but never one who is busy), and the practitioner's
time is held. The hold is a booking in the Awaiting payment state (
PENDING_PAYMENT), linked to the order and the line, so a retried order finds its hold rather than taking another. - Once the card is charged. The booking becomes Booked and appears in the diary as an ordinary appointment, before the order confirmation email goes. The confirmation email is the order's: the booking sends none of its own.
- If the slot has gone. Someone else booked it, or it stopped being offered, between the basket and
payment. The order is not taken, the card hold is released (the shopper has not been charged), anything
else held for the same order is given back, and the shopper is taken back to the basket. The appointment
line stays, with a note: "Sorry, the Thu 5 Nov, 10:30 appointment with Dr Shah was booked by someone
else while you were paying. You have not been charged. Choose another time." (for Any available it names
the type instead of a person), and Choose another time links to
/bookwith that location, type and practitioner chosen. The shopper can remove the line from the basket. If the card hold could not be released automatically the note says any payment taken will be refunded. Nova, Brock and the Bootstrap theme draw this note; the single-page checkout shows the same sentence as its error line.
The guest's name, email and phone on the booking come from the customer details on the order (the same ones checkout collects).
The line. LineItem.booking holds the location, type, start and end, the chosen practitioner (empty
for Any available, with requestedAny set), attendees (1), and reservationId, empty until the slot is held
at checkout. It also keeps the type, location and practitioner names and the time zone as they were when the line
was added, for the words in the basket and emails.
Headless clients. appointment-options marks a type paidOnline. Add its product to the basket with
PUT /v1/basket, quantity 1 and the appointment decorator's attributes: locationCode,
appointmentTypeId, startsAt (ISO-8601 with an offset) and, for a named practitioner, resourceId.
POST .../appointments refuses a paid type. A refusal comes back as the decorator's typed error with a
reason such as appointmentSlotTaken. The Bootstrap theme does not draw the booking form, so a paid
appointment there is bought through the API or a Nova-based theme.
What the customer receives¶
A paid booking sends no booking email of its own: the order confirmation email carries it, so the customer gets one email, not two. Under the order summary heading it shows an appointment card for each appointment line:
- the type and the day and time in the location's time zone ("Thu 5 Nov 2026, 10:30 to 11:00");
- who it is with, read from the booking rather than the basket line, so a customer who chose Any available sees the practitioner they were actually given ("Dr Shah (next available)");
- the location's name, address and phone;
- the cancellation terms in plain words, worked out from the same figures the refund uses: "Free to cancel until Wed 4 Nov, 10:30. After that we refund 50% of the price.", or "...we can't refund this appointment." when the type refunds nothing late, or "Free to cancel until it starts." when it refunds everything. Inside the window it reads "The free cancellation period has ended. If you cancel now we refund 50% of the price.";
- Manage or cancel, the signed guest link
/booking/{token}that a free booking's emails carry, and Add to calendar (the booking's.ics, downloaded from/booking/{token}/calendar.ics, the same file the free path attaches) and Google Calendar links. The.icsis a link rather than an attachment, so the other order emails (dispatch, collection and so on) are not given files they have no use for.
The same cards sit on the order confirmation page and the my-account order page (they are one page, /o/{orderId}),
in Nova and every theme layered on it (Brock included) and in the Bootstrap theme. A cancelled appointment shows
"Cancelled" and "Refunded £25.00" in place of the links. The card is appointmentSummaryExtension, built from
AppointmentOrderDisplayService; sites made before this release get it on their order page from the Paid
appointments on the order page upgrade in the admin's upgrade list, and new sites have it from the start.
On the confirmation page the cards sit below the order summary, so "Thanks for your order" comes first
(display priority 550, set in the extension, so sites that already have the upgrade pick it up with no change).
The terms before and after booking. For a type paid online /book shows its real terms in place of the
generic "let us know at least 24 hours before" line: a short one before a time is chosen ("Free to cancel up
to 24 hours before; after that we refund 50% of the price."), and on /booking/{token} the same words as
the order card, with the day and time ("Free to cancel until Thu 5 Nov, 10:30. After that we refund 50% of
the price."). The cancel step of any appointment says "You're cancelling within 24 hours of your
appointment." when it is inside the window.
When the payment doesn't go through¶
The slot is held when the order is placed, before the card is charged, so the hold has to be given back if the order never completes:
- The charge fails after the hold was made (the card is declined at capture, or a later step stops the order). The order goes back to being a basket, and the hold is released at once: a payment failure leaves no hold. When the shopper tries again the time is checked and held afresh, and if someone else has taken it in the meantime they are told, as for any slot that has gone. The same applies to event tickets, whose stock a failed order now gives back. An order whose payment was taken keeps its booking.
- The shopper leaves (closes the browser during the payment, or the failure is never retried). A scheduled
job in the worker (
BookingHoldReleaseJob, every five minutes) frees held appointments that nobody completed. It releases a hold only when it is older than the site'sappointment-hold-release-minutes(30 by default, never less than 5) and its order is demonstrably not going to complete: the order is gone, or it is still a basket (including one whose payment failed) with no money taken. It looks at the order's state every time, never at the age alone: a submitted, packing, dispatched or delivered order, or one whose payment was captured, keeps its booking however old. A shopper retrying a payment renews the hold, and the release is a conditional delete, so a retry and the job can't undo each other. - A payment link (staff booked by phone) is different: its time was held when staff booked, so a failed charge leaves it held, and it is freed at its own due time (above), not by these rules.
Orders with nothing to pay¶
An order whose total is £0 can't be placed at checkout, for any product: there is no payment to authorise (no Stripe payment is created for £0), and the order's capture step fails when no money is captured. So a paid appointment can't be made free by a promotion code: a code that would take a basket with an appointment in it to £0 is refused with "That code would make your appointment completely free, and an appointment can only be booked online with a payment, so it hasn't been applied", and the basket is left as it was. A code that leaves something to pay is applied as usual, and so is a gift card that covers the whole basket, which is a payment (the order's total is still more than £0, and the gift card is captured). A promotion that applies on its own (no code) is not checked; an order it takes to £0 isn't placed, and holds nothing. A price of £0 on a type is for the No online payment mode, which is free. If a £0 line is on an order that is paid (it shares the basket with something that costs money) it is booked as usual, and its cancellation quote is £0: cancelling just cancels, with no refund to run.
Paid in store: linking a till order¶
An appointment with No online payment has no order of its own. When it is paid for at the till, staff can
link the till (POS) order to it: in the diary, open the appointment, choose Link a till order and enter the
order number. The order must exist in this shop and have been placed. The appointment then shows the order's
number, as a link, and its total, and Unlink the till order takes it off; linking a different order
replaces the link. A paid type booked from the diary to pay in the shop is linked the same way, and shows
To pay in the shop until it is. ReservationService.linkTillOrder and unlinkTillOrder do this and the diary controller
only calls them. The link is a record, not a payment: it sets Reservation.orderId (and notes the order's
number, total and currency) with no lineItemId, so it is not "paid online" (Reservation.isPaidOnline() is
false, isTillLinked() true) and cancelling the appointment cancels the booking only, as for any free
appointment; the till order is refunded from the order. Ringing an appointment up on the till (the POS app)
does not link it yet: that hook is later.
Booked by phone: a payment link, or pay in the shop¶
When staff book a type paid in full online from the diary (Book by phone), they choose how the customer pays. A free type has no choice to make.
- Send payment link (the default). The time is held for the customer (the booking shows Awaiting payment in the diary) and they pay through the shop's own checkout, the same single-page checkout a direct checkout link opens. When the booking has an email address the customer is emailed Pay for your appointment: the type, practitioner, day and time in the location's time zone, the location, the price, the cancellation terms, a Pay and book button and when the time is held until. Staff can also Copy link from the booking (to text it from the shop phone), Resend the email, or Cancel request, which frees the time at once. Paying places the order and books the appointment as an online sale does: the order confirmation email carries the appointment card, and refunds follow the type's terms.
- Pay in the shop. The appointment is booked at once, with no order, and the diary shows To pay in the shop until a till order is linked to it (below). This is always offered, for every paid type.
A paid type can't be booked from the diary without choosing one of these, so the admin no longer makes an unpaid booking of a paid type.
How long the time is held. A payment link holds the time for appointment-pay-link-hours (24 by
default, at least 1; Settings, Dining in), and never later than 2 hours before the appointment starts,
whichever comes first, so an unpaid booking doesn't block the morning of the appointment. A link for an
appointment starting within 2 hours can't be sent: take the payment in the shop instead. If the customer
hasn't paid by then, the worker's BookingHoldReleaseJob (every five minutes) cancels the booking with the
reason "Not paid" and cancels its order. Staff see it as cancelled in the diary; the customer isn't emailed,
because they never had a confirmed booking. A request whose customer is paying at that moment, or whose order
was placed, is left alone. The appointment-hold-release-minutes rule for online checkouts doesn't apply to
payment links.
The link. /pay/{token}, signed with the shop's messaging-link-signing-key (as unsubscribe and
confirmation links are). It holds the order id and the expiry, and nothing about the customer. It isn't tied
to a login: whoever has it can pay, and opening it makes the order theirs to check out, so only the last device
to open it can pay. A link that has been paid, is being paid on another device, has expired, or whose request
was cancelled shows a plain page saying so, with the location's phone number, and takes no money. A declined
card leaves the time held, so the customer can try again until the link expires. While a request waits, its
time is not offered to online customers or other staff, and Any available doesn't give its practitioner to
anyone else.
Under the hood. BookingPaymentRequestService makes and manages requests: the booking is held
(PENDING_PAYMENT, with Reservation.paymentDueBy), then a direct-checkout order marked as a payment request
(order properties paymentRequest and paymentRequestedBy) gets the appointment line, with LineItem.booking
filled from the held booking exactly as an online basket line is, and the customer's details from the phone
booking. A booking to pay in the shop is BOOKED with Reservation.payInStore set and no order.
Cancelling a paid appointment¶
A paid appointment is a line on an order, so cancelling it is a refund of that line, and the booking is cancelled by the order's own steps: the practitioner's time is freed and the cancellation email goes.
The terms. Each type has two settings that decide a guest's refund:
- the location's cancellation window (Cancel within, hours before the start, under the booking policy; 24 by default): cancel before it and the whole price comes back;
- the type's Refund for a late cancellation (
lateRefundPercent, 0 to 100, default 0): inside the window this share of the price comes back and the rest is kept. 0 keeps everything, 100 makes the window irrelevant.
The price is what the line cost after any promotion, never more than the order still has left to refund.
After the start the guest cannot cancel online at all. The sum is BookingRefundTerms. A guest's cancel
inside the window sets Reservation.lateCancellation, as for a free booking; a venue cancel never does.
The guest. On /booking/{token} the cancel step reads "You will get £25.00 back, to the way you
paid.", or, inside the window, "This is inside the cancellation window, so we will refund £12.50 of the
£25.00 you paid and keep £12.50." (or "...the £25.00 you paid will not be refunded."). Confirming refunds the
line through the normal refund (a card refund, or back onto a gift card), then cancels the booking. If the
refund fails the booking is left as it was and the guest is asked to get in touch.
Staff. Cancelling from the diary asks Refund what they paid or Keep the money. Refund returns
the whole line whatever the terms; Keep cancels the booking and leaves the payment alone. Cancelling
through the MCP tools and the staff API take a refund choice (terms, full or none); a paid
appointment cancelled without one is refused.
Cancelling or refunding the order itself (the order page, or a full refund) cancels its appointments too,
and a part refund that names no line leaves them booked.
Staff API¶
The Badger app runs the appointments desk through /v1/appointments/{locationId}/..., with the shop's
location code as {locationId}. It is open to a POS agent with the reservations flag
(canManageReservations) assigned to the location, or an admin; the app finds its locations in
appointmentDesks on GET /v1/pos/agents/me. Assign appointment locations to an agent on its page in
the admin, beside the shop's other locations. Every call that changes something takes an optional
Idempotency-Key (withdrawing a payment link and linking or removing a till order included, so the
app's retry is safe: a repeated withdrawal with the same key is a 204 again, not a 404), and responses
with customer details are never cached. The API reference lists every
field; this is what each operation does.
| Operation | What it does |
|---|---|
getAppointmentSetup |
The types (length, price note, whether paid online, who offers them, and the product each is sold as), the practitioners with their working week, the opening hours, the time zone and noShowAfterMinutes |
getAppointmentDiary |
A day by practitioner: appointments (type, practitioner, whether the customer chose Any available, status, payment state, reference), each practitioner's working times and time set aside, and the day's counts. The list shows the customer's name only |
getAppointmentDeskAvailability |
Every start time for a type on a date, free or not, with who would take it or why not. Staff rules: no least-notice or book-ahead limit |
createAppointmentBooking |
Books by phone (source PHONE, the default) or for someone at the desk without a booking (WALK_IN; the diary shows "Walk-in" when they give no name), with a named practitioner or Any available. A paid type needs payment: LINK (emails a payment link and holds the slot) or IN_STORE (pay at the visit); a free type refuses it. Staff notes are saved on a link booking too |
getAppointmentBooking, changeAppointmentBooking |
The booking with the customer's contact details, messages and history; change the staff notes or the customer's request |
moveAppointmentBooking |
A new time, another practitioner, or both, in one change: if the practitioner isn't free at the new time (or doesn't do the type) the booking stays exactly as it was (409 with the nearest free times). Not once paid online or while awaiting payment |
moveAppointmentBookingStatus |
arrive, start, finish and noShow, as in the diary. A no-show is refused (409, as text) until noShowAfterMinutes after the start |
getAppointmentCancelTerms, cancelAppointmentBooking |
What cancelling now would refund by the type's terms, then cancel; a paid appointment needs refund of terms, full or none. One awaiting payment is withdrawn instead |
resendAppointmentPaymentLink, cancelAppointmentPaymentLink |
Email the same link again, or withdraw it: the unpaid order is cancelled and the slot freed (the booking stays in the diary as cancelled). Withdrawing again is a 404; a booking that isn't awaiting payment is a 409 |
linkAppointmentTillOrder, unlinkAppointmentTillOrder |
Link the till order that took payment in the shop, by order number |
createAppointmentBlock, deleteAppointmentBlock |
Set time aside on a practitioner's day, and open it again |
What a type is sold as. A type linked to a catalogue product (AppointmentType.productSeoName) carries
it on the setup so the app can put the appointment in a till basket ("Take payment"): productSeoName,
productSku (the product's skuId, which is what GET /v1/product/{productSkuCode} and a basket line take),
priceMinor (the product's price in minor units, before discounts) and currency (the shop's). Each is left
out when the type has no product; productSku and priceMinor are also left out if the product has since
been deleted. The products of every type are looked up in one batch for the whole setup.
The no-show grace period. noShowAfterMinutes is the location's ReservationPolicy.graceMinutes (15
unless the shop changed it). The server enforces it for appointments exactly as for table bookings: a
no-show is accepted only once startsAt + noShowAfterMinutes has passed, and a booking that is not
BOOKED or CONFIRMED is a 409. The app should offer No-show from that time.
Every booking carries a payment state: NONE (free), AWAITING_PAYMENT (with paymentDueBy, and
payUrl in the booking detail and the payment link calls), PAID, PAY_IN_SHOP (until a till order is
linked) and TILL_LINKED. A payment link that was withdrawn or freed unpaid reads NONE; a paid booking
that was cancelled still reads PAID. Taking the card by phone on the shop's own terminal is not part of
this API yet. A booking's source is PHONE or WALK_IN (or ONLINE for a customer's own).
Cancelling a booking that is waiting for a payment link by any route other than withdrawing the link (the MCP tools, the guest page) cancels its unpaid order too, so the link stops working. It is refused if the customer has just paid or is paying at that moment. This API's own cancel refuses such a booking and points to withdrawing the link.
Pushes to the app¶
With pos-push-enabled on, the Badger app is pushed about appointments to the same people who get the
restaurant's front-of-house pushes: the devices registered at the location whose user may run its bookings,
and every device of an enabled agent with canManageReservations assigned the location (each push token
once). Only what staff did not do themselves is pushed; the diary shows the rest.
data.type |
When |
|---|---|
appointment-new |
a customer booked online (free, or paid at checkout) for today |
appointment-cancelled |
a customer cancelled their own appointment, one for today |
appointment-link-paid |
a payment link staff sent was paid, so the appointment is booked (any day) |
appointment-link-expired |
a payment link went unpaid and the sweep freed the slot (any day). Withdrawing a link yourself sends nothing |
All use sound default, channel default, priority high and the notification category reservation.
data is {type, reservationId (the booking id), reference, locationCode, appointmentType (the type id),
date, startsAt, status, source, url: "badgerpos://diary/<locationCode>/appointments/<bookingId>"}. The title
and body carry the time and the reference ("New online appointment", "10:30 today, ref K7Q2MX"), never the
customer's name, contact details or the kind of appointment.
Under the hood. ReservationServiceImpl broadcasts an AppointmentEvent on the fulfilment events
exchange (badger-fulfilment-event: appointment); the worker's PosPushListener hands it to
PosPushService.appointment. Table bookings keep their own ReservationEvent, which appointments never
join.
Site transfer¶
An appointments room travels with Move data like a dining room: the team (hours and breaks), the appointment types and the assignment settings. A practitioner's linked user, leave and blocked times stay behind (they are the source shop's calendar). On import the room is checked as a save would be; one with an unavailable payment mode or a bad rota is skipped with a warning in the import report. Appointments and customer profiles are never exported.
The public API¶
Under the Reservations tag, beside the unchanged table endpoints:
GET /api/public/v1/reservations/appointment-options: the locations, their types (length, price note, who offers them) and practitioners;GET .../appointment-daysandGET .../appointment-availability: which days and times, for a type and a named practitioner or, left out, Any available;POST .../appointments: books (with anIdempotency-Key), answering as a table booking does.
The booking is then read and changed through /manage/{token}, whose GuestReservation carries an
optional appointment (type, practitioner, requestedAny). Never the customer's name, email or phone.
A 409 carries the nearest free times.
For a paid appointment the same appointment also says paidOnline, the orderNumber it was paid on
(the number printed on the order email, which authorises nothing on its own), the order's currency, and
refundedMinor, what was refunded once the booking is cancelled. GET .../cancel-terms carries a
refundQuote for it: paidMinor, amountMinor (what cancelling now gives back), keptMinor, currency and
withinWindow (true inside the last cancelWithinHours before the start, where the type's late-refund share
applies instead of a full refund); POST .../cancel then refunds that amount and answers with the cancelled
booking and its refundedMinor. Amounts are minor units (pence). A paid booking can't be changed online,
only cancelled. In appointment-options a type paid online adds lateRefundPercent and refundWindowHours,
so a client can state the terms before the customer pays.
MCP¶
appointments (needs mcp:write, as it returns customer details) has diary, get, setup (team,
types, strategies and the payment modes that can be saved) and test (the tester). manageAppointments
has book, amend, cancel, arrive, start, finish, noShow, reassign, block, unblock and
saveSetup (the whole setup, checked as an admin save is; a misspelt field is reported). Practitioners and
types may be given by name or id; no practitioner means Any available.
A type's lateRefundPercent (0 to 100) is read in setup and set in saveSetup with the rest of the type.
A booking paid online shows paidOnline, its orderNumber and, once cancelled, refundedMinor and
refundCurrency. cancel of a paid booking takes refund: terms (the default: what the customer
would get now, by the type's terms), full (the whole line) or none (cancel and keep the money). It is
held to the site's mcp-refund-limit like any refund made over MCP: a cancellation that would refund more is
refused, and nothing is cancelled. refund on a booking nothing was paid for is refused.
Privacy and retention¶
Appointments follow the table booking rules: the customer's contact details are kept on the booking and
removed after the retention period (dining-guest-retention-months), or at once if they ask to be
forgotten; ids, dates, types and statuses stay for reports. Nothing health related is collected
in this release (no intake questions, no allergy field). Intake questions with consent and a retention
clock are a later phase, and must be in before a pilot collects any health information. Logs carry
booking ids, never names, emails, phones or requests.
What comes next¶
Moving a paid appointment, a soft hold on a time while it is in the basket, a practitioner's own login, reminders by SMS, the intake questions above, a room together with a person for one appointment, and deposits and no-show fees.