Calendar & bookings
Every Neofir org has an internal calendar — a Postgres table that holds confirmed bookings, the slots they occupy, and time-off blocks. Google Calendar is an optional one-way write target, not the source of truth.
Why internal first
Earlier designs read availability from Google Calendar at every request. That made conflict detection unreliable (Google's free-busy API has propagation lag), and it tied the agent to a calendar provider. Today the bookings table is queried for conflicts, business-hours and time-off rules are applied, and the answer is deterministic.
Bookings table
| Column | Type | Purpose |
|---|---|---|
id | UUID | Primary key. |
organization_id | UUID | FK to organizations. RLS-scoped. |
conversation_id | UUID nullable | Conversation that produced the booking. |
customer_name / customer_phone / customer_email | text | Captured from the booking flow. |
service_ids | text[] | One or more services from the catalog. |
resource_id | UUID nullable | Multi-resource bookings. NULL for single-resource tenants. |
starts_at | timestamptz | Slot start. |
duration_min | integer | Computed from service catalog. |
status | enum | confirmed / cancelled. |
external_event_id | text nullable | Google Calendar event id (if synced). |
reminder_sent_at | timestamptz nullable | Set when the 24h reminder ran. |
deleted_at | timestamptz nullable | Soft delete. |
Conflict detection
The unique index on bookings (organization_id, COALESCE(resource_id, ZERO_UUID), starts_at) WHERE status = 'confirmed' AND deleted_at IS NULL guarantees no two confirmed bookings overlap per resource (or per org for single-resource tenants).
Conflict checks happen in the check_availability and book_slot tools: candidate slots are filtered against the bookings table, time_off blocks, and the configured business hours.
Time off
The time_off table holds intervals when slots are unavailable (vacations, holidays, ad-hoc closures). CRUD lives at /dashboard/settings → Vacanțe & închideri. Time-off blocks subtract from availability the same way confirmed bookings do.
Business hours
Configured per org under Settings → Program. Schema is per-day open/close intervals (multiple intervals supported — e.g. a lunch break).
Multi-resource
Larger orgs (multiple operators, stylists, doctors, bays) populate the resources table. Each resource has a name, optional avatar, and capacity (default 1). Bookings reference a resource_id; conflict detection scopes per resource.
Single-resource tenants leave the resources table empty. The conflict index treats them as if everything were bound to ZERO_UUID, so a single operator cannot double-book themselves.
Google Calendar sync
Sync is opt-in under Settings → Integrări → Google Calendar. After OAuth, every confirmed booking is pushed to the connected calendar; cancellations delete the event. The reverse direction (Google → Neofir) is not implemented — block time you don't want bookable in Vacanțe & închideri instead.
Reminders
A scheduled job (Railway cron, runs every 10 minutes) finds bookings within the 24h window without reminder_sent_at set and dispatches reminder messages to the channel the booking came from (email for web, DM for IG/Messenger, WhatsApp for WA). The cron sets reminder_sent_at after success.
Cancellation
Cancellation can be triggered by the customer (via the agent) or by the operator (via the dashboard). Both paths soft-delete the row, update status, and push a cancellation event to Google if the calendar is synced.
Roadmap: polymorphic interactions
Beyond appointment-style bookings, the platform's data layer is being extended to support other interaction kinds — repair tickets, consultations, quote requests, table reservations, house calls. This unlocks vertical-specific flows without per-tenant schema changes.