Skip to content

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.

Billing 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

  1. Admin creates a service: "Mobility Evaluation—30 min, 200 RON, requires intake form"
  2. Admin links specialists: "Dr. Smith and Dr. Jones can do Mobility Evals"
  3. Admin creates calendars (booking windows): "Mobility Eval" (standard), "Free Mobility Campaign" (free, Wed 10-12), "Quick Check" (20 min, cheaper)
  4. Patient books: Books from a calendar → creates appointment using that service's rules
  5. Program enrollment (optional): Patient buys "10-session package" → system tracks sessions, expires after 90 days or 10 appointments
  6. 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

TablePurpose
servicesService catalog entries (name, duration, price, forms)
service_specialistsWho CAN provide this service (capability, not assignment)
service_formsWhich form templates to generate on booking
service_attachmentsDownloadable files (PDFs, consent forms)
service_plansSession packages, subscriptions, and hybrid plans (3 billing models)
patient_service_plansPatient enrollment, session tracking, and access window tracking
productsPhysical goods (elastic bands, supplements) for standalone sales or bundling
service_plan_productsJunction: which products are bundled with a service plan (template)
patient_product_ordersUnified fulfillment: all product orders (bundled + standalone)

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 & Billing Models

Service plans define how patients pay for and access services. Three billing models are supported:

Session-Based Plans (Traditional)

Count-down session packages linked to a specific service:

sql
-- 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, billing_model, 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:

sql
-- Telerehab subscription: specialist assigns treatment plans for 3 months
INSERT INTO service_plans (name, billing_model, 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, billing_model, 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:

sql
-- Recovery Package: 10 clinic sessions + 1 month telerehab
INSERT INTO service_plans (
    name, service_id, billing_model,
    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:

FlagMeaningWho Uses It
telerehab_accessSpecialist can assign telerehab treatment plans to this patientSpecialist assigns, patient executes
library_accessPatient can browse premade treatment plans and self-assignPatient browses, picks, and starts

Access check flow:

  1. Patient (or specialist) tries to assign a treatment plan
  2. Backend checks: does patient have an active patient_service_plan with the required access grant?
  3. For telerehab: telerehab_access = TRUE and access_expires_at > NOW()
  4. For library self-assign: library_access = TRUE and access_expires_at > NOW()
  5. If no valid plan found → 403 "Active plan with telerehab/library access required"

Patient Enrollment Flow

  1. Specialist recommends program after initial consultation (or patient buys directly)
  2. Patient enrolls → creates patient_service_plan record
  3. Access starts immediatelyaccess_starts_at set to enrollment time
  4. For session-based: patient books appointments, sessions_completed increments
  5. For time-based: patient has access until access_expires_at
  6. For hybrid: both tracking mechanisms run in parallel
  7. Completion/Expiry → status changes to completed or expired

Progress Tracking

sql
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:

sql
-- 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_at by service duration
  • Generates forms for the added service
  • Updates billing total

Products & Bundling

Physical goods that can be sold standalone or bundled with service plans.

Standalone Sales

Staff sells a product directly to a patient (no service plan involved):

Staff opens patient profile → Sells "Cervical Pillow" → patient_product_orders created
→ status: pending → confirmed → shipped → delivered

Bundled with Service Plans

Admin defines bundles at the service plan level:

sql
-- Service Plan: "Cervical Recovery Kit" (device + lifetime telerehab)
INSERT INTO service_plans (name, billing_model, access_months, telerehab_access, library_access, total_price)
VALUES ('Cervical Recovery Kit', 'time_based', NULL, TRUE, TRUE, 89);
-- access_months = NULL → lifetime access, total_price includes the product

-- Bundle the pillow with this plan
INSERT INTO service_plan_products (service_plan_id, product_id, quantity)
VALUES (plan_id, cervical_pillow_id, 1);

Enrollment flow with bundled products:

  1. Patient enrolls in "Cervical Recovery Kit"
  2. Backend creates patient_service_plan (access active immediately)
  3. Backend auto-creates patient_product_orders for each bundled product (status = 'pending')
  4. Staff sees pending product orders in fulfillment queue
  5. Staff ships product → marks shipped → marks delivered
  6. Meanwhile, patient already has telerehab/library access from day one

Product Fulfillment Lifecycle

pending → confirmed → shipped → delivered
pending → cancelled

All product orders (bundled and standalone) use the same patient_product_orders table and the same fulfillment workflow. The patient_service_plan_id column distinguishes them:

  • NULL = standalone purchase
  • NOT NULL = bundled with service plan enrollment

Bundled Product Pricing

  • Bundled products: unit_price = NULL (cost included in the service plan price)
  • Standalone products: unit_price frozen from products.price at time of sale

Products in Appointments

Products can also be recommended during appointments via appointment.additional_product_ids[]. This is a recommendation — it doesn't create an order automatically. Staff can convert recommendations to standalone orders.

Service Categories

sql
category ENUM:
  - consultation    -- Initial evaluations, assessments
  - therapy         -- Treatment sessions (PT, kinetotherapy)
  - examination     -- Diagnostic procedures
  - procedure       -- Clinical procedures

Forms

Services define which form templates to auto-generate:

sql
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:

  1. Collect service_forms WHERE service_id = appointment.service_id
  2. Collect calendar_forms WHERE calendar_id = appointment.calendar_id
  3. Merge and deduplicate
  4. Generate form instances with snapshotted fields

Example: Service with Multiple Calendars

sql
-- 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 RON

API Endpoints

See api.md for full API documentation.

Key endpoints:

  • GET /v1/services - Browse service catalog
  • POST /v1/services - Create service (admin)
  • GET /v1/services/{id}/specialists - Who can provide this service
  • POST /v1/service-plans/{id}/enroll - Enroll patient in program
  • GET /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_minutes and service.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 = TRUE unlock specialist-assigned treatment plans
  • Service plans with library_access = TRUE unlock patient self-service plan browsing
  • patient_treatment_plans.patient_service_plan_id links enrollment to billing
  • Completing treatment sessions can sync progress to patient_service_plans
  • See treatment-plans/ for full integration details

With Forms Feature

  • service_forms junction defines which forms to generate
  • Form generation happens during appointment onboarding
  • Forms auto-filled from patient custom fields

Migration from appointment_templates

appointment_templatesservices (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_formsservice_forms (direct rename) appointment_template_specialistsservice_specialists (direct rename) appointment_template_attachmentsservice_attachments (direct rename)

See unified-architecture.md (archived) for full migration details.

Design Principles

  1. Services are catalog, not scheduling - No booking horizons, cooldowns, or holds on services
  2. Duration is intrinsic - "This procedure takes 30 minutes" is a service property
  3. Calendars provide booking layer - One service → many calendars with different rules
  4. Service specialists = capability - Different from calendar specialists (assignment)
  5. Multi-tenant by design - Every table has organization_id with RLS
  6. No soft delete - Services are configuration, not medical records (unlike appointments)