A multi-tenant healthcare platform built on Frappe v16 and ERPNext Healthcare. Enables multiple doctors/practices to operate independently on a single Frappe bench site — no separate site or server per practice.
- Multi-tenant isolation — each Practice sees only its own patients, appointments, and records (enforced at the database layer via Permission Query Conditions)
- Practice management — doctors register a Practice with branding, contact details, and a unique booking URL slug
- Patient appointments — doctors and receptionists manage bookings; patients can self-book via a public portal
- Inpatient & outpatient records — full Patient Encounter workflow scoped per practice
- Prescriptions — issued within Patient Encounters, scoped and printable on practice letterhead
- Sick Notes — submittable documents with diagnosis, dates off, auto-calculated days, and patient medical record integration
- Role-based access — three practice roles (Admin, Doctor, Receptionist) with appropriate permissions
Single Frappe Site
├── Practice A (Dr. Smith)
│ ├── Patients ← isolated via custom_practice field + Permission Query
│ ├── Appointments
│ ├── Patient Encounters / Prescriptions
│ └── Sick Notes
├── Practice B (Dr. Jones)
│ └── ...
└── Healthcare Administrator ← platform superuser, sees all
Each Practice is the tenant entity. All Healthcare DocTypes carry a custom_practice Link field. Eight Permission Query Conditions enforce that users only query records belonging to their own practice.
- Doctor registers →
Practicecreated with unique slug - Users added as
Practice Member(Admin / Doctor / Receptionist) → Frappe role auto-assigned - Patients book via
/book?practice=<slug>or reception creates appointment in desk custom_practiceis auto-stamped on every new Patient, Appointment, Encounter, and Inpatient Record viabefore_inserthook- Doctors create Patient Encounters → prescriptions issued as child table rows
- Sick Notes issued as standalone submittable documents → linked to Patient Medical Record on submit
| DocType | Description |
|---|---|
Practice |
Tenant entity. UUID-named. Holds slug, branding, subscription plan. |
Practice Member |
User ↔ Practice link with role. Auto-assigns/removes Frappe roles. |
Sick Note |
Submittable. Auto-calculates days off. Linked to Patient Medical Record on submit. |
| DocType | Field | Purpose |
|---|---|---|
| Patient | custom_practice |
Tenant scope |
| Patient Appointment | custom_practice |
Tenant scope |
| Patient Encounter | custom_practice |
Tenant scope |
| Inpatient Record | custom_practice |
Tenant scope |
| Role | Access |
|---|---|
Practice Admin |
Full access within their practice — manage members, all records |
Practice Doctor |
Create/read/submit encounters, prescriptions, sick notes |
Practice Receptionist |
Manage appointments and patient records; read-only sick notes |
Healthcare Administrator |
Platform superuser — unrestricted access across all practices |
URL: /book?practice=<slug>
- Lists available doctors and time slots
- Creates Patient record on first booking (matched by email thereafter)
- No login required
| Method | Description |
|---|---|
medic_plus.api.booking.get_practice_info |
Practice name, logo, contact |
medic_plus.api.booking.get_practice_practitioners |
Active doctors for a practice |
medic_plus.api.booking.get_availability |
Open time slots for a practitioner on a date |
medic_plus.api.booking.request_booking_otp |
Send email OTP to patient (rate-limited, server-side) |
medic_plus.api.booking.verify_and_book |
Verify OTP then create appointment atomically |
- Frappe v16
- ERPNext Healthcare app
bench get-app medic_plus https://github.com/thedaystar/medic_plus
bench --site your-site.com install-app medic_plus
bench --site your-site.com migrate# After pulling changes
bench --site your-site.com migrate
# Run tests
bench --site your-site.com run-tests --app medic_plus- Prescription print format (per-practice letterhead via Jinja)
- Sick Note print format (per-practice letterhead)
- Practice self-registration / onboarding web form
- Patient portal (patients log in, view records, book follow-ups)
- Inpatient dashboard
- Subscription billing integration
- SMS/email appointment reminders
This app uses pre-commit for code formatting and linting. Please install pre-commit and enable it for this repository:
cd apps/medic_plus
pre-commit installMIT
- First major version. Rolls up Phase 1C (SOAP encounter), Phase 1D (drug safety), Phase 1E (insurance claims + POPIA + FHIR), and Phase 4 (telemedicine + AI augmentation). 16 commits, 15 new doctypes, ~11k lines added. Full release notes in
docs/releases/v2.0.0.md. - Phase 1C — Structured SOAP encounter (#26):
Examination Finding(child of Patient Encounter) andPatient Problem Listdoctypes; SPA encounter drawer captures Subjective / Objective / Assessment / Plan with ICD-10-ZA picker. - Phase 1D — Drug safety (commit
e8323a9):Drug Master+Prescription Override Reasondoctypes; allergy/interaction/paediatric-dose checks; HPCSA Booklet 8 prescription print format; SPAMPrescriptionPanel. - Phase 1E — Insurance Claims (#28):
Insurance Claim+Insurance Claim Line+Tariff Code+Switch Configurationdoctypes;claim_builder+ HealthBridge submission client; on-submit hook; cross-tenant PQCs. - POPIA controls (#28):
Patient Consent Record+Sub-Processor Registerdoctypes; dailyflag_expired_consent_recordscron expires consent older than 3 years. - FHIR R4 read-only server (#28):
/api/fhir/R4/<ResourceType>/<id>returningapplication/fhir+json; six R4 resources (Patient/Encounter/Condition/MedicationRequest/AllergyIntolerance/Observation);$everythingBundle;CapabilityStatementat/api/fhir/R4/metadata; SMART v2 bearer tokens viaFHIR Access Tokendoctype (SHA-256 hashed, 1-hour TTL, practice-bound). - Phase 4 — Telemedicine + AI augmentation (commit
e49c3ef):Telemedicine Consent+Practice AI Settings+AI Inference Log+Medic Plus Settingsdoctypes; per-encounter/teleconsultpage; PHI redactor strips identifiers before any external AI call; AI off by default per practice. - Tests: integration + Playwright suites for claims, drug safety, FHIR, POPIA, telemedicine, encounter — every PQC asserts cross-tenant isolation.
- Permission fix (commit
1db4748): repaired Phase 4 + Phase 1E PQC function bodies.
- Closes the legal-floor gap so the SPA can host real SA practice data. Six commits on
develop(e51975c → d2b967a). - New doctypes:
Patient Allergy(FHIR-aligned criticality),Patient Chronic Condition,Medical Aid Scheme,Record Archive Queue. Each clinical doctype denormalisescustom_practicefrom Patient on insert. - 4 new Permission Query Conditions scope clinical data via
patient.custom_practice+ 1 PQC forPatient Medical Record. 12 Custom DocPerm rows. - 4 SA medical-aid Custom Fields on
Patient Insurance Policy(CMS scheme link, principal member ID, dependent code, authorisation reference). - Whitelisted endpoints:
get_patient_allergies,get_patient_chronic_conditions,get_patient_medical_aid,search_icd10.build_patient_summaryhydrates allergies / chronic / medical aid. - Reference data: ICD-10 Code System + 34 curated WHO codes + 10 SA medical-aid schemes (Discovery, Bonitas, Momentum, Bestmed, Medshield, Profmed, Polmed, GEMS, Fedhealth, Keyhealth).
- Daily scheduler
flag_overdue_records— HPCSA Booklet 9 / NHA §17 (6-year retention, paediatric to age 21, idempotent). - SPA: Allergies + Conditions tabs, severe-allergy banner, Medical Aid card on Overview, reusable
MIcd10Picker. - Tests: 10 IntegrationTestCase + 4 retention (TDD) + 4 Playwright. All green.
- Final slice of 5 wiring the
/daystar-healthSPA. Replaces the hardcoded provider profile with the logged-in user's real data and wires password change to Frappe's standard endpoint. - New endpoint
medic_plus.api.daystar_health.get_my_practitioner_profile— joins User (name / email / phone / image) with Healthcare Practitioner (department / HPCSA / practice number) via the Practice Member row, plus a top-leveltwo_factor_authenticationboolean fromfrappe.utils.user.user_has_2fa. - Profile tab is read-only — fields render as styled disabled boxes; no Save button. Footer note directs users to their practice administrator for changes.
- Security tab posts current/new/confirm to
frappe.core.doctype.user.user.update_password. Inline success/error feedback. 2FA state shown read-only with pointer to Frappe Desk for setup. - Notifications tab removed from the profile sidebar — no schema backing (per Q8 design decision).
- Sidebar "Sign out" button was a route-change-only no-op; now calls
meridianApi.logout()to actually end the session. - Tests: 2 new Python (no-practice rejection + payload contract) + 2 new Playwright (read-only rendering + password change round-trip with a throwaway Practice user).
- SPA wiring Phase 1 complete (#4 → #5 → #6 → #7 → #8). Outstanding: #12 (Custom DocPerm fixtures for Practice roles).
- Slice 4 of 5 wiring the
/daystar-healthSPA. Patient detail screen now hydrates all six tabs (Overview / Visits / Vitals / Medications / Labs / Notes) from a single composite call. - New deep module
api/patient_summary—build_patient_summary(patient_name, practice)orchestrator + pure_format_patient_summary(...)helper that applies per-tab caps (20 / 12 for vitals) and the POPIA whitelist (onlyname / patient_name / dob / sex / mobile / email / statusmake it into the patient block;custom_sa_id_numberis unreachable). - New whitelisted endpoint
medic_plus.api.daystar_health.get_patient_detail(patient)— looks up the patient'scustom_practiceand raisesfrappe.PermissionErrorfor cross-tenant requests, then returns the composite. Same error class as no-practice so callers cannot probe Practice membership. - Tests: 4 Python (POPIA, per-tab caps, no-practice rejection, cross-tenant rejection) + 2 Playwright (detail loads all 6 tab containers; response body asserted POPIA-clean).
- Surfaced Issue #12: Practice Admin / Doctor / Receptionist roles don't have read permission on Patient via Custom DocPerm — workaround in place (Physician role on selfserve.test).
- Slice 3 of 5 wiring the
/daystar-healthSPA. Patients screen now hydrates from/api/resource/Patientinstead ofMH_DATAmocks. - Server-side pagination (real
limit_start/limit_page_length/total), 300ms-debounced search acrosspatient_name/mobile/emailviaor_filters, sortable headers (patient_name,dob), page-size selector (25/50/100) persisted insessionStorage. Skeleton + error + empty-state UI. - No new backend endpoint — PQC (
get_patient_permission_query) handles tenant scoping for free. - Per design decisions: dropped risk/status/provider filter chips, MRN column, risk badge, status pill, conditions/allergies columns, last-seen column (covered later via patient detail), checkboxes/bulk actions, Export/Register buttons. Sidebar nav now has
data-testid={nav-${key}}on every button for deterministic Playwright navigation. - Tests: 2 Python PQC contract tests (Doctor restricted to Practice; orphan gets
1=0) + 5 Playwright tests (list render, search round-trip, prev/next pagination, page-size persistence, empty state).
- Slice 2 of 5 wiring the
/daystar-healthSPA. Dashboard now hydrates from a real composite endpoint instead ofMH_DATAmocks. - New deep module
api/dashboard_aggregator—build_dashboard(practice, user)orchestrator + pure_format_dashboard(...)helper for testable shaping rules (greeting personalisation, week-volume always 7 days, recent-patients cap of 6, today-appointment status breakdown using the real Healthcare statuses). - New whitelisted endpoint
medic_plus.api.daystar_health.get_dashboard— thin orchestrator. Surfacesfrappe.PermissionErrorfrompractice_resolverunchanged so the SPA can render the no-practice card. - Dashboard screen rewrite in
meridian-dashboard.jsx: skeletons during load, error toast + inline error card on failure, three KPI tiles (today's appointments / active patients / outstanding labs), today's schedule, week-volume chart, recent-patients table. - Per design decision: dropped "Pending refills" KPI, "Needs attention" panel, MRN/Risk/Status columns, Day/Week/Month toggle, "New appointment" button. "View full schedule" links to Frappe Desk Patient Appointment list scoped to today + Practice.
- Outstanding labs KPI uses a JOIN through
Patient.custom_practicesinceLab Testhas nocustom_practicefield — avoided schema migration. - Tests: 4 format-helper unit tests + 2 endpoint contract tests + 1 Playwright dashboard render test (uses a temporary Practice Member row for Administrator with role=Admin to bypass the Doctor→Practitioner validation).
- Slice 1 of 5 wiring the
/daystar-healthSPA to real Frappe APIs. - New
practice_resolver.get_active_practice(user)deep module — returns the user's active Practice or raisesfrappe.PermissionErrorfor Guest / no-membership users. Reusable across all later Daystar Health endpoints. - New page bootstrap
www/daystar_health.pyexposescsrf_token,session_user, andhas_practiceto the SPA template; newmeridian-api.jshelper centralises CSRF + JSON conventions for every later screen (call,resource,login,recoverPassword,logout). - Replaced mock auth in
meridian-auth.jsxwith real/api/method/loginandfrappe.core.doctype.user.user.reset_passwordcalls. NewMNoPracticeScreenfor authenticated users without a Practice Member row, with sign-out wired to/api/method/logout. - SPA first-render routing reads the bootstrap and chooses login / no-practice / dashboard at mount time — no more login-screen flicker for already-authenticated users.
- Tests: 3 Python unit tests for
practice_resolver+ 5 Playwright tests covering anonymous, invalid creds, post-login routing, already-logged-in skip, and sign-out. - Bug fix: renamed
daystar-health.py→daystar_health.pyso Frappe's website controller resolver finds it (resolver maps hyphenated.htmlto underscored.py).
- New 3-step
/signupfunnel: details → email OTP → Yoco checkout. Replaces/registerand/register/doctor(deleted). - Yoco webhook auto-provisions on
payment.succeeded(no admin approval). Idempotent re-fires; failures flip the PRR toProvisioning Failedwith the traceback recorded. - Post-payment activation uses a signed one-time URL (12-hr TTL, SHA-256 hashed in Redis), surfaced via email and consumed at
/signup/completeto set the password and auto-log in. - 15-min scheduler (
retry_failed_provisioning) re-runs any PRR stuck Paid-but-not-Provisioned older than 5 min. - Admin
onboard_doctorrefactored to call the sameprovision_doctoras the paid path, so both produce identical tenants (Company + Practice + Practitioner + Practice Member + POS Profile + Folder + optional Warehouse + Setup Checklist). - Migration patches: clean orphan Users from the legacy
Registration Requestflow (skipping System Users), then drop the DocType + table. - Dev-only
_test_mark_paidendpoint for hermetic Playwright E2E (gated ondeveloper_mode).
- Custom Workspace "Medic Plus Platform" — Administrator/Healthcare Administrator only
- 6 Number Cards: Total/Active Practices, Total Patients, Today's/This Month's Appointments, Sick Notes Issued
- 3 Dashboard Charts: Appointments Over Time (line), Practices by Plan (donut), Patients per Practice (bar)
- 6 Quick Access shortcuts + 3 Recent Activity quick lists
- All exported as fixtures (Number Card, Dashboard Chart, Workspace) via hooks.py
- Added
request_booking_otpandverify_and_bookAPIs - OTP generated and stored server-side in Redis (10 min TTL, single-use, rate-limited)
- Branded HTML OTP email + appointment confirmation email via
frappe.sendmail() - Booking portal rebuilt as 3-step flow: Details → OTP → Success
- Outbound email configured:
liz@thedaystar.co.zaviamail.thedaystar.co.za:587
- New app
medic_plusscaffolded and installed onmedic-demo-staging.thedaystar.co.za - DocTypes:
Practice,Practice Member,Sick Note - Roles:
Practice Admin,Practice Doctor,Practice Receptionist - Custom fields:
custom_practiceon Patient, Patient Appointment, Patient Encounter, Inpatient Record - 8 Permission Query Conditions for full data isolation per practice
before_insertdoc_events auto-stampcustom_practiceon all scoped records- v16
extend_doctype_classmixin on Patient Appointment validates practitioner scope - Public booking portal at
/book?practice=<slug>