Services Feature
A catalog of what your clinic offers—consultation sessions, procedures, therapy programs.
What this enables
One-off bookings: Define any service your clinic offers—a single consultation, a physiotherapy session, an examination—and let patients book it directly through a calendar. No plan or package required.
Multi-calendar promotion: Run one service (Mobility Eval) as multiple calendars—default 30-min at 200 RON, or promotional "Quick 20-min Check" at 100 RON.
Specialist credentials: Mark which specialists are trained to do each service type.
Service plans: Offer session packages ("10 kinetotherapy sessions over 90 days"), time-based subscriptions ("3-month telerehab access"), or hybrid bundles that combine both. Plans layer on top of services — a service can be booked standalone or as part of a plan.
Plan tracking: Know exactly when sessions expire, when patient access is ending, and generate reports per service.
Bundled products: Package physical goods (elastic bands, devices) with service plans—patient gets the device shipped automatically when they enroll.
How it works
- Admin creates a service: "Mobility Evaluation—30 min, 200 RON, requires intake form"
- Admin links specialists: "Dr. Smith and Dr. Jones can do Mobility Evals"
- Admin creates calendars (booking windows): "Mobility Eval" (standard), "Free Mobility Campaign" (free, Wed 10-12), "Quick Check" (20 min, cheaper)
- Patient books: Books from a calendar → creates appointment using that service's rules
- Program enrollment (optional): Patient buys "10-session package" → system tracks sessions, expires after 90 days or 10 appointments
- Bundled products (optional): Enroll in "Cervical Recovery Kit" → automatically ships pillow to home, unlocks telerehab access
Technical Reference
Overview
The service catalog defines what the organization offers. A Service is a pure business/catalog entity that knows nothing about booking rules, scheduling, or holds. It answers: "What is this offering?"
This feature replaces the old appointment_templates table, which conflated service definition with scheduling configuration.
Core Concept
Service = What we offer (business domain)
Calendar = When/how to book it (scheduling domain) → See scheduling/
Appointment = What was booked (clinical domain) → See appointments/A service defines:
- What it is (name, description, category)
- How long it takes (duration + buffer are intrinsic to the procedure)
- How much it costs (base price)
- What forms are needed (survey, disclaimer, etc.)
- Who can provide it (service_specialists)
A service does NOT define:
- Booking horizon or cooldown (that's on calendars)
- Specialist availability hours (that's on specialist_weekly_hours)
- Holds or slot reservations (that's in the scheduling feature)
Key Tables
| Table | Purpose |
|---|---|
services | Service catalog entries (name, duration, price, forms) |
service_specialists | Who CAN provide this service (capability, not assignment) |
service_forms | Which form templates to generate on booking |
service_attachments | Downloadable files (PDFs, consent forms) |
service_plans | Session packages, subscriptions, and hybrid plans (3 plan types) |
patient_service_plans | Patient enrollment, session tracking, and access window tracking |
products | Physical goods catalog (elastic bands, supplements) — reference only, no e-commerce |
service_plan_products | Junction: which products are bundled with a service plan (informational) |
Service Catalog vs Scheduling
┌─────────────────────────────────────────┐
│ SERVICE CATALOG │
│ "What we offer" │
├─────────────────────────────────────────┤
│ services │
│ ├── Mobility Evaluation │
│ │ duration: 30min, price: 200 RON │
│ ├── Flexibility Measuring │
│ │ duration: 30min, price: 300 RON │
│ └── Pain Psychology │
│ duration: 20min, price: 100 RON │
└─────────────────────────────────────────┘
↓ linked via service_id
┌─────────────────────────────────────────┐
│ CALENDARS (Scheduling) │
│ "When/how to book" │
├─────────────────────────────────────────┤
│ calendars │
│ ├── Mobility Eval (default) │
│ │ uses service defaults │
│ ├── Free Mobility Campaign │
│ │ override: is_free=true │
│ │ override: hours Wed 10-12 only │
│ └── Flexibility Promo │
│ override: duration=20min, price=100│
└─────────────────────────────────────────┘Duration and Buffer on Services
Why duration/buffer are here: They are intrinsic to the service type. "Mobility Evaluation takes 30 minutes" is a property of the procedure itself, not a scheduling configuration.
How it works:
- Service defines default duration:
duration_minutes = 30 - Calendars can override for promotions:
override_duration_minutes = 20 - Scheduling engine reads:
calendar.override_duration_minutes ?? service.duration_minutes
This allows promotional calendars like "Quick 20-Minute Eval" that use a shorter version of the same service.
Service Specialists vs Calendar Specialists
Two different relationships:
service_specialists (this feature)
Capability: "Dr. Smith CAN do mobility evaluations"
- Catalog-level relationship
- Defines who is qualified/trained for this service
- Optional custom pricing per specialist
calendar_specialists (scheduling feature)
Assignment: "Dr. Smith IS assigned to the Free Mobility Campaign calendar"
- Scheduling-level relationship
- Defines who is actively taking bookings on this calendar
- Has priority, round-robin tracking, optional hour overrides
Service Plans & Plan Types
Service plans define how patients access services over time. Three plan types are supported:
Session-Based Plans (Traditional)
Count-down session packages linked to a specific service:
-- Service: Individual kinetotherapy session
INSERT INTO services (name, duration_minutes, base_price)
VALUES ('Kinetotherapy', 30, 100);
-- Service Plan: 10-session package
INSERT INTO service_plans (name, service_id, plan_type, sessions_total, validity_days, total_price)
VALUES ('Kinetotherapy 10 Sessions', service_e_id, 'session_based', 10, 90, 1000);Patient books appointments → sessions_completed increments → exhausted or expired.
Time-Based Plans (Subscriptions)
Access window without session counting. Used for telerehab subscriptions and library access:
-- Telerehab subscription: specialist assigns treatment plans for 3 months
INSERT INTO service_plans (name, plan_type, access_months, telerehab_access, total_price)
VALUES ('Telerehab 3 Months', 'time_based', 3, TRUE, 500);
-- Full library access: patient self-browses and self-assigns premade plans for 1 year
INSERT INTO service_plans (name, plan_type, access_months, telerehab_access, library_access, total_price)
VALUES ('Full Access 1 Year', 'time_based', 12, TRUE, TRUE, 1200);Note: service_id is NULL for platform-level plans not tied to a specific service.
Hybrid Plans (Appointments + Telerehab)
Bundles clinic appointment credits with telerehab time access:
-- Recovery Package: 10 clinic sessions + 1 month telerehab
INSERT INTO service_plans (
name, service_id, plan_type,
sessions_total, validity_days,
access_months, telerehab_access,
total_price
) VALUES (
'Recovery Package', service_e_id, 'hybrid',
10, 120, -- 10 appointments within 120 days
1, TRUE, -- + 1 month telerehab access
1500
);Patient gets clinic appointments AND telerehab from day one. Specialist can transition the patient from in-clinic to home exercises seamlessly.
Access Grants
Service plans control what the patient can do via two boolean flags:
| Flag | Meaning | Who Uses It |
|---|---|---|
telerehab_access | Specialist can assign telerehab treatment plans to this patient | Specialist assigns, patient executes |
library_access | Patient can browse premade treatment plans and self-assign | Patient browses, picks, and starts |
Access check flow:
- Patient (or specialist) tries to assign a treatment plan
- Backend checks: does patient have an active
patient_service_planwith the required access grant? - For telerehab:
telerehab_access = TRUEandaccess_expires_at > NOW() - For library self-assign:
library_access = TRUEandaccess_expires_at > NOW() - If no valid plan found → 403 "Active plan with telerehab/library access required"
Patient Enrollment Flow
- Specialist recommends program after initial consultation (or patient buys directly)
- Patient enrolls → creates
patient_service_planrecord - Access starts immediately —
access_starts_atset to enrollment time - For session-based: patient books appointments,
sessions_completedincrements - For time-based: patient has access until
access_expires_at - For hybrid: both tracking mechanisms run in parallel
- Completion/Expiry → status changes to
completedorexpired
Progress Tracking
SELECT
psp.id,
psp.sessions_total,
psp.sessions_completed,
psp.sessions_cancelled,
CASE WHEN psp.sessions_total IS NOT NULL
THEN (psp.sessions_total - psp.sessions_completed - psp.sessions_cancelled)
ELSE NULL
END AS sessions_remaining,
psp.status,
psp.access_starts_at,
psp.access_expires_at,
psp.expires_at
FROM patient_service_plans psp
WHERE psp.patient_id = 123 AND psp.status = 'active';Add-On Services
Services marked with is_addon = TRUE can be added to appointments in progress:
-- During appointment, specialist notices heart issue
PATCH /v1/appointments/456
{
"additional_service_ids": [2] -- Heart Examination (is_addon: true)
}Backend:
- Validates
service.is_addon = TRUE - Updates
appointment.additional_service_ids - Extends
appointment.ended_atby service duration - Generates forms for the added service
- Updates appointment total
Products (Catalog Only)
Physical products (resistance bands, braces, therapy equipment) are stored as a simple reference catalog. Products can be bundled with service plans so that staff knows what's included in a patient's program.
Bundling with Service Plans
Admin defines which products are associated with a service plan:
-- Service Plan: "Cervical Recovery Kit" (device + telerehab access)
INSERT INTO service_plans (name, plan_type, access_months, telerehab_access, library_access, total_price)
VALUES ('Cervical Recovery Kit', 'time_based', NULL, TRUE, TRUE, 89);
-- Associate the pillow with this plan
INSERT INTO service_plan_products (service_plan_id, product_id, quantity)
VALUES (plan_id, cervical_pillow_id, 1);When a patient enrolls in this plan, staff sees that the plan includes a cervical pillow and handles fulfillment offline.
Products in Appointments
Specialists can note product recommendations during appointments via appointment.additional_product_ids[]. This is informational — it records which products the specialist recommended, but does not trigger any order or fulfillment flow.
What the platform does NOT do
The platform is not an e-commerce system. There is no order tracking, no fulfillment lifecycle, no payment processing for products, and no delivery tracking. Product sales happen at the clinic or through external channels. See Gap: E-Commerce & Product Fulfillment for future considerations.
Service Categories
category ENUM:
- consultation -- Initial evaluations, assessments
- therapy -- Treatment sessions (PT, kinetotherapy)
- examination -- Diagnostic procedures
- procedure -- Clinical proceduresForms
Services define which form templates to auto-generate:
INSERT INTO service_forms (service_id, form_template_id, form_type, sort_order) VALUES
(service_a_id, survey_template_id, 'survey', 1),
(service_a_id, disclaimer_template_id, 'disclaimer', 2);When appointment is created:
- Collect
service_formsWHEREservice_id = appointment.service_id - Collect
calendar_formsWHEREcalendar_id = appointment.calendar_id - Merge and deduplicate
- Generate form instances with snapshotted fields
Example: Service with Multiple Calendars
-- Service A: Mobility Evaluation (30min, 200 RON)
INSERT INTO services (name, slug, duration_minutes, base_price, published, is_public)
VALUES ('Mobility Evaluation', 'mobility-evaluation', 30, 200, true, true);
-- Default Calendar (uses service defaults)
INSERT INTO calendars (name, slug, service_id, published, is_public)
VALUES ('Mobility Evaluation', 'mobility-evaluation', service_a_id, true, true);
-- Campaign Calendar (free, limited hours)
INSERT INTO calendars (name, slug, service_id, is_free, slots_open_at, slots_close_at, published, is_public)
VALUES ('Free Mobility Feb', 'free-mobility-feb', service_a_id, true, '2026-02-15', '2026-02-20', true, true);
INSERT INTO calendar_specialists (calendar_id, specialist_id, priority, override_weekly_hours)
VALUES (campaign_cal_id, specialist_a_id, 1, '{"wed": [{"start": "10:00", "end": "12:00"}]}');
-- Specialist available ONLY Wed 10-12 on this campaign (not their default hours)
-- Promotional Calendar (shorter, cheaper)
INSERT INTO calendars (name, slug, service_id, override_duration_minutes, override_price, published, is_public)
VALUES ('Quick Mobility Check', 'quick-mobility', service_a_id, 20, 100, true, true);
-- 20min instead of 30min, 100 RON instead of 200 RONAPI Endpoints
See api.md for full API documentation.
Key endpoints:
GET /v1/services- Browse service catalogPOST /v1/services- Create service (admin)GET /v1/services/{id}/specialists- Who can provide this servicePOST /v1/service-plans/{id}/enroll- Enroll patient in programGET /v1/patient-service-plans/{id}- View program progress
Integration Points
With Scheduling Feature
- Services linked to calendars via
calendar.service_id(required FK) - Scheduling engine reads
service.duration_minutesandservice.buffer_minutes - Calendars can override duration/buffer for promotions
- Service specialists feed into calendar specialist assignments
With Appointments Feature
- Appointments reference service via
appointment.service_id - Add-on services tracked in
appointment.additional_service_ids[] - Service plans linked via
appointment.patient_service_plan_id - Forms generated from
service_forms+calendar_forms
With Treatment Plans Feature
- Service plans with
telerehab_access = TRUEunlock specialist-assigned treatment plans - Service plans with
library_access = TRUEunlock patient self-service plan browsing patient_treatment_plans.patient_service_plan_idlinks enrollment to plan tracking- Completing treatment sessions can sync progress to
patient_service_plans - See treatment-plans/ for full integration details
With Forms Feature
service_formsjunction defines which forms to generate- Form generation happens during appointment onboarding
- Forms auto-filled from patient custom fields
Migration from appointment_templates
appointment_templates → services (1:1 rename + add fields)
- Keep: title → name, slug, description, duration, specialty_id, published, is_public, cover_url, video_url
- Add: category, base_price, is_addon, buffer_minutes
- Remove: use_even_distribution (becomes calendar.assignment_strategy), minicrm_title
appointment_template_forms → service_forms (direct rename) appointment_template_specialists → service_specialists (direct rename) appointment_template_attachments → service_attachments (direct rename)
See unified-architecture.md (archived) for full migration details.
Design Principles
- Services are catalog, not scheduling - No booking horizons, cooldowns, or holds on services
- Duration is intrinsic - "This procedure takes 30 minutes" is a service property
- Calendars provide booking layer - One service → many calendars with different rules
- Service specialists = capability - Different from calendar specialists (assignment)
- Multi-tenant by design - Every table has organization_id with RLS
- No soft delete - Services are configuration, not medical records (unlike appointments)