Repository: orbiteus/orbiteus
Author: danielgogolewski
## Preface
This is a design-document RFC, not a claim to implement. I'm looking for direction before writing any module code, per the spec-first guidance in `CONTRIBUTING.md` and the original commitment in #1 (*"Will open an RFC issue before any module-level work"*). Happy to revise any part of this, hand off implementation entirely, or park it if the direction isn't what you want for the project right now.
A closed-as-`not planned` is a perfectly valid outcome — better to align before any code lands than after.
---
## Problem
Orbiteus ships a generic `ResourceCalendar` primitive in `admin-ui/src/components/ResourceCalendar.tsx` and the view layer already parses `calendar` XML views via `parseCalendarView` in `admin-ui/src/lib/viewParser.ts`, but no registered module exercises it end-to-end. Every use case listed in `README.md` — gym chain, interior design studio, TMS, niche CRM SaaS — needs some shape of resource-and-time booking, and today an adopter would reinvent it per project.
Resource Scheduling is the obvious horizontal module to close that gap, and it makes the existing calendar view load-bearing rather than vestigial.
## Proposal
Add a `scheduling` module under `backend/modules/scheduling/` that registers the standard schema-driven artefacts (`registry.register("scheduling")`) plus one custom endpoint for availability checks.
### Entities
| Model | Purpose | Key fields |
|---|---|---|
| `scheduling.resource` | Bookable unit: person, room, vehicle, chair, machine | `name`, `type_id` (FK), `active`, `metadata` (jsonb) |
| `scheduling.resource_type` | Classifier: trainer / room / vehicle / etc. | `name`, `color` |
| `scheduling.booking` | Reservation of a resource over a time range | `resource_id`, `start_at`, `end_at`, `status`, `customer_id`, `notes` |
| `scheduling.availability` | Regular working hours per resource | `resource_id`, `weekday`, `start_time`, `end_time` |
| `scheduling.exception` | Ad-hoc blocks (leave, one-offs) | `resource_id`, `start_at`, `end_at`, `reason` |
### API surface
Auto-generated CRUD on the five models via the registry. One custom endpoint for collision checking:
```
POST /api/scheduling/booking/check_availability
{ resource_id, start_at, end_at }
→ { available: bool, conflicts: [...] }
```
### Views
- `resource` — list + form + **calendar** (first real consumer of `ResourceCalendar`)
- `booking` — list + form + **kanban** grouped by `status`
- `resource_type`, `availability`, `exception` — list + form
### RBAC
Three groups in `security/access.yaml`:
- `scheduling_manager` — full CRUD on all models
- `scheduling_booker` — create/edit bookings; read-only on resources/availability
- `scheduling_viewer` — read-only everywhere
### Command Palette actions
- `New booking`
- `Today's bookings`
- `Available <type> now` (parameterised by resource type)
## Notifications (v1)
Deliberately thin, to avoid blocking on a cross-cutting messaging layer that doesn't exist yet:
- Single SMTP send on three booking events: `created`, `confirmed`, `cancelled`
- Hardcoded English email template per event
- Uses FastAPI `BackgroundTasks`; no queue, no retry beyond what SMTP provides natively
- Recipient resolution: `booking.customer_id.email` + `booking.resource_id` owner (when resource type is `trainer` / `user`)
- Tenant-scoped SMTP config via existing `ir_config_param` (`mail.smtp.host`, etc.)
Intentionally primitive. If and when a cross-module notifications foundation is on the roadmap, Scheduling can migrate from inline SMTP to a `self.notify(event, context=…)` call and this code path disappears. Not part of this RFC.
## Scope
- [x] New module `backend/modules/scheduling/`
- [x] 5 models, 1 custom endpoint
- [x] XML views (list, form, calendar, kanban)
- [x] RBAC + `security/access.yaml`
- [x] Command Palette actions
- [x] `backend/modules/scheduling/docs/spec.md` (spec-first)
- [x] Alembic migration
- [x] Unit + integration tests (target ≥80% on new code)
## Out of scope (future RFCs / modules)
- Multi-resource bookings (room + trainer jointly)
- Recurring bookings (weekly classes)
- Multi-channel notifications (Slack / SMS / webhook) — separate module
- Per-user notification preferences — same deferral
- External self-service booking portal
- Payments / deposits
- Google Calendar / Outlook sync
- Waitlist
## Alternatives considered
- **Use an existing library (`FullCalendar` on frontend + ad-hoc backend).** Rejected — contradicts the "schema-driven, zero TSX per module" thesis. `ResourceCalendar` already exists; this RFC makes it load-bearing.
- **Start with `billing` or `tasks` instead.** Both viable. Scheduling picked because the UI surface is already partially built and calendar views photograph well for README / talks.
- **Start with `inventory`.** Rejected — overlaps with commerce-engine territory where the back-office framing weakens.
## Questions for the maintainer
1. **Customer linkage.** Should `booking.customer_id` FK `crm.customer`, or do we want a looser `partner_id` pointer to `base.partner`? Multi-module dependency direction matters.
2. **Availability model.** Is there anything already in core / another module representing working hours per resource that I've missed? Want to avoid duplicating.
3. **Migrations cadence.** Prefer the Alembic migration alongside the code in one PR, or as a separate PR against an already-merged schema PR?
4. **Timezone strategy.** Store `timestamptz` UTC and let the UI localise, or store tenant-local and tag with IANA zone? Happy to implement either — just want to know the preference before writing the spec.
5. **Kanban grouping.** Pure-XML configuration (status field drives grouping declaratively), or appetite for a backend-side `group_by` hint in view XML?
## Rough timeline (if greenlit)
| Phase | Work | Estimate |
|---|---|---|
| 0 | Spec (`docs/spec.md`) | 1 day |
| 1 | Domain + mapping + migration | 2 days |
| 2 | Custom availability endpoint + collision validation | 2 days |
| 3 | XML views (5 models) | 1 day |
| 4 | RBAC + Command Palette actions | 0.5 day |
| 5 | Tests (unit + integration) | 3 days |
| 6 | Polish, split into reviewable PRs | 1–2 days |
| | **Total** | **~11–14 days of evenings/weekends** |
Happy to split into a PR series (spec → domain → endpoint → views → tests → polish) so nothing lands as a 2000-line drop.
---
## Decision I'm looking for
One of:
- **Green-light the shape** — I start with `docs/spec.md` PR as phase 0
- **Reshape it** — push back on any of the entities, naming, RBAC granularity, notification approach, or the questions above, and I rework before writing anything
- **You want to own this one** — fully fine, happy to contribute adapters / tests / docs around it once the shape lands, or step aside entirely
- **Not the right time** — park it; I keep to polish PRs
No rush on a full reply — a thumbs-up or a single-line "reshape Q2 first" is enough to move forward.
— Daniel