A schedule where two bookings cannot overlap
Day and week views: hours down the side, rooms or days as columns, a line marking the current moment. Overlaps are blocked by the database itself — by a range constraint, not a check in the form: a second simultaneous request slips straight past a check like that. The cleaning buffer counts as occupied time, an enquiry holds the slot, cancelling releases it.
A public booking page
At the venue's own address, anyone without an account picks a room, a day and an hour, sees booked slots struck through, and sends an enquiry. The server prices it by the venue's own rules, availability is returned without guest names, and traffic is capped. Eighteen checks were run from an outsider's position: what the response contains, refusals on overlaps, opening hours, booking horizon, attempts to reach the tables directly.
Pricing that follows the time of day
55 + 55 + 90 = 200, when the third hour falls into the evening rate: the total is assembled segment by segment, not multiplied by a single rate. Overtime is calculated with a multiplier and rounded in blocks, the deposit and the damage bond sit in separate fields, cancellation rules are defined in tiers. The guest's agreement to the terms is stored as a snapshot of the text — any dispute will be about what the person actually agreed to at the time.
Capture: a photo, a document or a voice note becomes a draft
Send text, a photo of a receipt, a PDF or a voice note. The parser proposes drafts of five kinds — booking, transaction, task, enquiry, client — plus separate line items for extra services with amounts. The model never writes anything itself: a person creates the record with a tap, so a misread costs one "discard". Parsing runs on the server, so a backgrounded app or a dropped connection doesn't interrupt it.
Money that cannot be counted twice
Booking totals are generated database columns — they cannot be added up wrongly in code; the paid amount is recalculated by a trigger from the payment history rather than stored in a field of its own. An income transaction cannot reference a booking — otherwise one payment would land in the month twice. A list of the month's operations, breakdowns by room, channel and category, a break-even threshold; month boundaries are calculated in the venue's time zone, not in UTC.
A client base that doesn't flatter you
Segments: all, regulars, lapsing, at risk, duplicates. "Lapsed" is measured against each client's own rhythm: someone who comes weekly has lapsed after three quiet weeks, someone who comes quarterly has not. Duplicates are found by similarity score: a matching phone number is 100%, a one-letter spelling variant is 95%, different surnames with a common start fall below the threshold. Without this, one person's revenue is split across two records.
Five roles enforced in the database
Owner, manager, front desk, accountant, staff member. A staff member doesn't merely "not see" the money — it never reaches them: the policies on bookings, clients and transactions require a role, and their own schedule comes from a function that physically has no money columns in it. The owner's personal spending stays invisible even to the manager querying directly. Permissions were tested with requests made under real tokens, not by reading the policies.
Enquiries and first-response time
Enquiries arrive through a channel link — a form on the website, Tilda, Make, Zapier — and through the venue's own Telegram bot: the owner's bot, not a shared one. The system records the moment of the first reply and measures it against the venue's own target. The AI front desk can offer a free slot and take the person through to an enquiry the same way the public page does; every machine-written reply is marked as such.
Staff, tasks and shifts
Invitations by link, not by email: the token reveals only the venue name and the role, and the link works once. Tasks sort by urgency, the assignee moves the status and writes the next step. Shifts are a weekly people-by-days grid with the name column pinned; the rate is copied into the shift as a snapshot, so raising it doesn't rewrite past months.