Last updated: June 2026 Β· Access Control + 3 flows documented
Product Reference

Cureverse β€” BD Module Flow Reference

Single source of truth for all product flows in the Cureverse panel. Each flow shows the full user and system journey β€” what the user does, what the system does in response, and which APIs and tables are involved. Use this during design, development, and stakeholder reviews.

4
Modules: BD, Care, Sales, Sessions
253
Database tables in the live system
3
Flows documented so far
~30
Total BD flows expected
How to read each flow
1
Sequence β€” the full journeyFollows the exact order of events: what the user does, what triggers, how the system responds, and how external services are involved.
2
Tables Reference β€” data involvedWhich database tables this flow reads or writes, what role each plays, and which fields matter. Full schema is in the separate DB Schema Reference file.
3
API Reference β€” system communicationEvery API call in detail β€” who calls it, what it sends, what comes back. Collapsed by default; expand to see specifics.
4
Backend Logic β€” calculations and rulesAny logic the backend must implement that isn't obvious from the API β€” computed fields, validation rules, sequencing constraints.
Access Control

Authentication, Access & Roles

How a person signs in (see Login & Authentication below), and then how the panel decides what each person can see and do. Access works entirely through roles: every signed-in user is given exactly one role, and that role decides which pages they can open and which actions (buttons, fields, controls) they can use on each page. Roles can build on one another, so a senior role automatically gets everything its more basic roles can do, plus its own extras. This is the foundation every other flow sits on β€” if someone can't reach a screen or use an action, the reason is almost always here. (All of this is decided in the browser; see the note at the end of the Exact Role Configuration section on why the server must apply the same rules too.)

Applies to: All modules All users

How a person signs in. Sign-in is handled by Google Firebase Phone Authentication, not a custom in-house OTP system. The panel collects a phone number, Firebase sends and checks the one-time code (OTP), and once verified Firebase hands back a token. The panel then calls its own backend once with that token to load the user β€” and from there the role logic below takes over. Important for the OTP/Twilio decision: the panel itself never generates or sends the OTP and never talks to Twilio directly. Firebase does that, with Twilio plugged in inside Firebase as the SMS delivery provider. See the replacement note at the end of this section.

U
User
Enters phone number β†’ taps "Send OTP"
On the login page (/log-in), the user types their phone number (defaulted to +91) and taps Send OTP. No CAPTCHA or extra challenge is shown to the user.
E
External Β· Firebase
Firebase sends the OTP by SMS
The panel calls Google Firebase (identitytoolkit.googleapis.com) to start phone sign-in. Firebase generates the code and sends the SMS through its configured provider (currently Twilio). The panel receives back a reference to this attempt so it can confirm the code later.
POST identitytoolkit.googleapis.com β€” send verification code
U
User
Enters the OTP received by SMS
The user types the code. The panel sends it to Firebase to confirm.
E
External Β· Firebase
Firebase verifies the code β†’ returns a token
If the code is correct, Firebase confirms the sign-in and returns a token set: an ID/access token, a refresh token (to stay signed in), and an expiry time. The access token is what the panel uses to prove who the user is on every following request.
POST identitytoolkit.googleapis.com β€” verify code Β· returns ID token + refresh token
B
Aceso backend
Panel loads the user with the token
The panel calls its own backend once to fetch the signed-in user's profile, sending the Firebase token in the request header as Authorization: Token <accessToken>. The returned profile (including id and speciality_id) is exactly what the role logic below uses to decide the person's access.
GET https://curelinktech.in/users/detail/
S
System
Session kept alive via token refresh
While the user stays signed in, the access token is periodically renewed in the background using the refresh token (via Firebase securetoken.googleapis.com), so they aren't logged out mid-session. The token is also stored locally so a page reload doesn't force a fresh login.
POST securetoken.googleapis.com β€” refresh token
πŸ”
Authentication details & the Twilio/OTP decision
Firebase config Β· the one Aceso login call Β· what "replacing Twilio" really means
β–Ό
Auth provider: Google Firebase Authentication, Phone sign-in method. The panel uses the Firebase web SDK; OTP generation, SMS sending, and code verification all happen on Firebase's side.
Bot protection: No reCAPTCHA or visible challenge is presented during login. (Firebase’s SDK includes reCAPTCHA support and Firebase can enforce it server-side / invisibly, but the user is never shown a challenge in this panel.) A rebuild does not need to reproduce any visible CAPTCHA step.
The only Aceso login call: GET https://curelinktech.in/users/detail/ with header Authorization: Token <accessToken>. Everything before it is Firebase. This call returns the user profile that drives role resolution.
Token handling: Firebase returns an access token, a refresh token, and an expiry. The access token is sent on every authenticated request as Authorization: Token <token>; it's auto-refreshed before expiry and cached locally so reloads stay signed in.
Firebase project config (public client settings β€” not secrets)
These values ship inside the app's JavaScript and are safe to share β€” they only identify which Firebase project to use. The real secrets live on Google's servers, not here. A ditto rebuild that keeps Firebase points at this same project.
projectIdyang-287109Firebase project
authDomainyang-287109.firebaseapp.comAuth domain
apiKeyAIzaSyANbJxJqb4uBU6odO_Ox67G8gB8aJB7fWMPublic web API key
appId1:421422163731:web:df503f7f35fb37a91a1ce6Web app ID
messagingSenderId421422163731Sender ID
What "replacing Twilio" actually means here
Today: Twilio is the SMS delivery vendor configured inside Firebase. The app never calls Twilio and never generates the OTP itself β€” it asks Firebase to "send the code," and Firebase uses Twilio to deliver the SMS.
Option A β€” keep Firebase, swap the SMS provider: Change the SMS vendor in Firebase settings (e.g. to another provider, or for development a test/universal OTP) without touching the app's login code. Lowest effort; the flow above stays identical.
Option B β€” replace Firebase entirely: Build your own OTP service (request-OTP and verify-OTP endpoints) that ends by issuing your own token, then have the panel send that token to users/detail/ the same way. More work, full control, removes the Google/Firebase dependency. The universal-OTP idea for development fits naturally here.
For development now: Firebase supports configured test phone numbers with fixed codes (a built-in "universal OTP" for testing) β€” usable under Option A without any new service. This may be enough for dev while the production replacement is finalised.
U
User
Signs in to the panel
The user authenticates. The panel loads their profile (id, speciality_id, etc.) from the user endpoint.
S
System
Resolves exactly one role for the user
The system picks the role using a strict priority order (see Role Resolution Logic below): first it checks a hand-maintained list that pins specific people to a role; if the person isn't on it, it assigns a role based on their job type (their "speciality"); if neither applies, it gives them a "no access" role.
S
System
Controls which pages and actions the role can use
The menu and navigation only show the pages (called "routes") the role is allowed to open β€” including any it gains through inheritance. Within each page, individual buttons and fields appear or stay hidden depending on the role's permissions. If someone tries to open a page they're not allowed to, they're sent to that role's default home page instead.
βš™οΈ
How the role is chosen and enforced
Priority order Β· roles building on each other Β· the two permission checks
β–Ό
Priority 1 β€” the pinned-people list (per-user override): The system first checks a hand-maintained list that maps specific people (by their user ID) to a specific role. If the person is on this list, that role wins, no matter what their job type is. This is how individual managers, admins, and exceptions are set up. (In the code this list is called the "override map.")
Priority 2 β€” by job type (speciality): If the person isn't on the pinned list, their role comes from their job type β€” stored on their profile as speciality_id. Only four job types map to a role directly: dietician β†’ Dietician (Care) base, health coach β†’ Sales agent base, fitness instructor β†’ Sessions base, business-development team β†’ BD base.
Priority 3 β€” the safe default (fallback): If neither of the above matches, the person gets the No Access role and is shown an "insufficient access" page. This makes sure no one is accidentally granted access they shouldn't have.
Inheritance (roles build on other roles): A role can be set to "inherit" from one or more other roles, meaning it automatically gets everything those roles can do, on top of its own. So a senior role is built by starting from a base role and adding to it. This applies all the way down the chain (a role that inherits a role that inherits another gets all three combined).
Two kinds of permission check: The system asks two questions. Can this person open this page? β€” yes if the page is in the role's allowed pages, including inherited ones (in code: isRouteAvailable). Can this person use this action? β€” for individual buttons and fields (in code: isFeatureAvailable). One subtlety: most permissions are passed down to roles that inherit, but some are marked "this role only" (non-hierarchical) and are deliberately not passed down β€” so a senior role does not automatically get them.
πŸ‘€
Full role catalogue
~20 roles across Care, Sessions, Sales & BD families
β–Ό
No Access: No routes. Default landing is the insufficient-access page. The fallback when nothing else matches.
Dietician / Care base: Care dashboard, tasks, chat, patients, renewals, onboarding & new patients, improve CGPA, intensive care, renewal countdown. Enrolment view-tasks, consultation reminders, channel quick/GPT replies. Assigned from the dietician speciality.
Sales agent base: Sales tasks, patients, chat, WhatsApp chat. Assigned from the health-coach speciality.
Sessions / Fitness instructor base: Sessions batches, patients, tasks, patient transfer, leaves; fitness-instructor enrolment permissions. Assigned from the fitness-instructor speciality.
BD base: BD dashboard, doctors, meetings, chat, WhatsApp chat, and /bd/suggested_prospects; channel view-all-queries. Default landing /bd/meetings. Assigned from the business-development speciality. This is the role that unlocks the BD module documented in the flows below.
Care family (inherit Care base): Diet template manager Β· Chat specialist Β· Care manager Β· Care manager + discounting Β· Care lead variant β€” progressively add patient/query assignment, diet template management, enrolment owner/duration controls, and discounting.
Sessions family: Special consultation Β· Special consultation (restricted) Β· Psych emergency Β· Sessions scheduler β€” control session calendars, special consultation tasks, and leave management.
Sales / BD family: Sales senior Β· Sales/Assignment manager Β· BD manager Β· BD senior manager Β· BD head β€” progressively add lead controls, patient-assignment rules, doctor management, and the manage_msl permission.
Cross-functional / admin: Tools/asset uploader Β· Super care+sessions+tools Β· Cross-domain senior Β· Admin (near-superuser) β€” the Admin role inherits across families and holds the broadest permission set, including validation-skip and query-override.
🧩
Fine-grained actions, grouped by module
10 permission modules Β· every feature flag
β–Ό
doctors
Doctor records & BD doctor management.
view_allview_teamcreate_meet_taskchange_program_priceschange_manager_noteschange_head_office_notesreview_meetingsmanage_msl
enrolment
Patient enrolment, tasks & ownership.
view_allchange_ownerchange_durationsend_community_linkview_conversation_startersdiscount_prospectchange_phone_numberchange_instructions_to_chat_teamchange_chat_team_noteschange_fitness_instructor_notesopt_in_to_restricted_batchview_dietitian_consultation_taskview_dietitian_check_in_taskview_special_consultation_taskview_charge_collection_taskview_fitness_instructor_consultation_taskview_trial_onboarding_taskview_psych_emergency_taskcreate_complementary_special_consultation_taskview_memorycreate_dietitian_check_in_tasks
lead
Sales/BD lead records.
view_restricted_fieldsview_allchange_ownerdiscounted_billchange_phone_numberadd_qa_comments
consultation
Consultation events & reminders.
create_custom_eventchange_ownerchange_typeview_remindersskip_validation
channel
Chat channels, queries & GPT-assisted replies.
view_gpt_repliesautofill_gpt_repliessend_quick_repliessend_doctor_quick_repliesoverride_query_validationview_all_queries
diet
Diet charts & templates.
view_alledit_allapprove
session
Live/recorded sessions & leaves.
view_alledit_allrevert_leave
prescription
Prescription handling.
skip_validation
users
User records.
change_phone_number
tags
Tagging.
create
πŸ—ΊοΈ
Full route catalogue
Pages gated per role, grouped by area
β–Ό
Care
/care/dashboard/care/tasks/care/chat/care/patients/care/renewals/care/patient_assignment/care/query_assignment/care/diet_chart_template_management/care/refer_patients/care/whatsapp_chat/care/psych_emergency_tasks/care/onboarding_patients/care/new_patients/care/improve_cgpa/care/intensive_care/care/renewal_countdown/care/dietitian_consultation_calendar
BD
/bd/dashboard/bd/doctors/bd/suggested_prospects/bd/meetings/bd/chat/bd/whatsapp_chat/bd/prescribe_patient/bd/feedbacks/bd/send_notification
Sessions
/sessions/calendar/sessions/standalone/sessions/course/sessions/recurring/sessions/batches/sessions/patients/sessions/patient_transfer/sessions/tasks/sessions/special_consultation_tasks/sessions/special_consultation_calendar/sessions/webinars/sessions/leaves
Sales
/sales/tasks/sales/patients/sales/chat/sales/whatsapp_chat/sales/patient_assignment/list/sales/patient_assignment/rules
Tools
/tools/whatsapp_login/tools/edit_message_template/tools/send_message_template/tools/upload_assets/tools/knowledge_base/tools/query_review

The precise, build-ready definition of every role, copied exactly from the panel's own access settings. Each card shows the role's default home page (where it lands after sign-in), which roles it builds on (inherits from), the pages it can open on its own, and the actions it can perform on its own. "On its own" means granted directly by this role β€” a role's full access is what it grants directly plus everything it picks up from the roles it builds on. To recreate the system exactly, reproduce these definitions and the build-on relationships between them as shown.

🧱
All 23 roles — routes, permissions & inheritance
Verbatim from the compiled access config
β–Ό
grey chip = a permission this role has, and which roles built on it also receive orange chip = a permission for this role only — deliberately not passed down to roles built on it
No Access y
Default routeinsufficient_access
Inherits fromβ€” (base role)
Own routes
none
Own module permissions
none (relies on inheritance)
Dietician / Care base ie
Default routecare_tasks
Inherits fromβ€” (base role)
Own routes
care_dashboardcare_taskscare_chatcare_patientscare_renewalscare_onboarding_patientscare_new_patientscare_improve_cgpacare_intensive_carecare_renewal_countdown
Own module permissions
enrolmentchange_instructions_to_chat_teamview_dietitian_consultation_taskview_dietitian_check_in_taskview_special_consultation_task
consultationnoneview_reminders
channelsend_quick_repliesview_gpt_replies
Diet template manager q
Default routecare_tasks
Inherits fromDietician / Care base
Own routes
care_diet_chart_template_management
Own module permissions
dietnone
Chat specialist B
Default routecare_chat
Inherits fromβ€” (base role)
Own routes
care_chat
Own module permissions
enrolmentsend_community_linkview_conversation_starterschange_chat_team_notesview_dietitian_consultation_taskcreate_dietitian_check_in_tasks
channelsend_quick_repliesautofill_gpt_replies
Care manager ae
Default routecare_patients
Inherits fromDietician / Care base, Diet template manager, Chat specialist
Own routes
care_patient_assignmentcare_query_assignmentcare_diet_chart_template_managementcare_whatsapp_chatcare_refer_patientscare_dietitian_consultation_calendar
Own module permissions
userschange_phone_number
channelview_gpt_replies
enrolmentview_allchange_ownerchange_durationsend_community_linkview_conversation_startersview_trial_onboarding_taskcreate_complementary_special_consultation_taskchange_phone_numberview_charge_collection_taskcreate_dietitian_check_in_tasks
consultationchange_ownercreate_custom_eventchange_typeview_reminders
Care manager + discounting we
Default routecare_patients
Inherits fromCare manager
Own routes
none
Own module permissions
none (relies on inheritance)
Care lead variant te
Default routecare_patients
Inherits fromDietician / Care base, Chat specialist
Own routes
care_diet_chart_template_managementcare_whatsapp_chatcare_dietitian_consultation_calendar
Own module permissions
channelview_gpt_replies
userschange_phone_number
enrolmentview_allchange_ownerchange_durationsend_community_linkview_conversation_starters
consultationchange_ownercreate_custom_eventchange_typeview_reminders
Sessions / Fitness instructor base ne
Default routesessions_batches
Inherits fromβ€” (base role)
Own routes
sessions_batchessessions_patientssessions_taskssessions_patient_transfersessions_leaves
Own module permissions
enrolmentchange_instructions_to_chat_teamchange_fitness_instructor_notesview_fitness_instructor_consultation_task
Sessions scheduler Pe
Default routesessions_batches
Inherits fromSessions / Fitness instructor base
Own routes
sessions_calendarsessions_standalonesessions_coursesessions_recurring
Own module permissions
enrolmentchange_instructions_to_chat_teamchange_fitness_instructor_notessend_community_linkopt_in_to_restricted_batch
Special consultation U
Default routesessions_special_consultation_tasks
Inherits fromβ€” (base role)
Own routes
care_chatcare_patientssessions_special_consultation_taskssessions_webinars
Own module permissions
enrolmentchange_instructions_to_chat_teamchange_fitness_instructor_notesview_special_consultation_task
consultationcreate_custom_event
Special consultation (restricted) Y
Default routesessions_special_consultation_tasks
Inherits fromβ€” (base role)
Own routes
sessions_special_consultation_tasks
Own module permissions
enrolmentchange_instructions_to_chat_teamchange_fitness_instructor_notesview_special_consultation_task
consultationcreate_custom_event
Psych emergency re
Default routesessions_special_consultation_tasks
Inherits fromSpecial consultation
Own routes
care_psych_emergency_tasks
Own module permissions
none (relies on inheritance)
Sales agent base K
Default routesales_tasks
Inherits fromβ€” (base role)
Own routes
sales_taskssales_patientssales_chatsales_whatsapp_chat
Own module permissions
none (relies on inheritance)
Sales senior se
Default routesales_chat
Inherits fromβ€” (base role)
Own routes
sales_patientssales_chat
Own module permissions
leadview_allview_restricted_fieldschange_phone_number
enrolmentnone
channelsend_quick_repliesview_gpt_replies
tagscreate
Sales / Assignment manager D
Default routesales_patients
Inherits fromSales agent base, Sales senior
Own routes
tools_whatsapp_loginsales_patient_assignmentsales_patient_assignment_rulesbd_prescribe_patient
Own module permissions
leadview_allchange_ownerview_restricted_fieldsdiscounted_billchange_phone_numberadd_qa_comments
BD base T
Default routebd_meetings
Inherits fromβ€” (base role)
Own routes
bd_dashboardbd_doctorsbd_meetingsbd_chatbd_whatsapp_chatbd_suggested_prospects
Own module permissions
channelview_all_queries
BD manager ee
Default routebd_meetings
Inherits fromBD base
Own routes
none
Own module permissions
none (relies on inheritance)
BD senior manager ce
Default routebd_meetings
Inherits fromBD manager, Sales / Assignment manager, Care lead variant
Own routes
bd_prescribe_patienttools_whatsapp_login
Own module permissions
doctorsreview_meetingsview_allchange_program_priceschange_head_office_notescreate_meet_taskmanage_msl
channelsend_quick_repliessend_doctor_quick_replies
BD head de
Default routebd_doctors
Inherits fromBD manager
Own routes
bd_prescribe_patientbd_feedbacksbd_send_notificationtools_whatsapp_login
Own module permissions
doctorschange_program_priceschange_head_office_notesreview_meetingsview_allcreate_meet_taskmanage_msl
Cross-domain senior j
Default routecare_patients
Inherits fromCare manager + discounting, Sales / Assignment manager
Own routes
none
Own module permissions
none (relies on inheritance)
Super care+sessions+tools et
Default routecare_patients
Inherits fromSales agent base, Sessions scheduler, Sessions / Fitness instructor base, Special consultation, Psych emergency
Own routes
care_chatcare_patientscare_whatsapp_chatsessions_calendarsessions_standalonesessions_coursesessions_recurringcare_query_assignmentsessions_special_consultation_calendartools_edit_message_templatetools_send_message_templatetools_upload_assetstools_whatsapp_logintools_query_review
Own module permissions
enrolmentview_allchange_ownerchange_durationsend_community_linkopt_in_to_restricted_batchview_dietitian_consultation_taskview_dietitian_check_in_taskview_fitness_instructor_consultation_taskview_special_consultation_taskview_charge_collection_taskview_trial_onboarding_taskcreate_complementary_special_consultation_task
channelview_all_queriessend_quick_replies
sessionview_alledit_all
Tools / asset uploader Ee
Default routetools_upload_assets
Inherits fromβ€” (base role)
Own routes
tools_upload_assets
Own module permissions
none (relies on inheritance)
Admin (near-superuser) P
Default routebd_doctors
Inherits fromCross-domain senior, BD head, Sales / Assignment manager
Own routes
tools_send_message_template
Own module permissions
enrolmentchange_phone_numberview_memorycreate_dietitian_check_in_tasks
consultationskip_validation
prescriptionskip_validation
channeloverride_query_validationview_all_queries
Speciality β†’ base role (priority 2 of role resolution)
dieticiand828790d-9770-49cf-aba9-4f054bc03d1cDietician / Care base
health_coach48bc4e0b-a70d-491b-bb56-3e8d68d6e2eeSales agent base
fitness_instructor94f41442-a112-47d4-ab1e-6d5b2f78bc2cSessions / Fitness instructor base
business_development_teamaf215167-5ba4-42dc-8e48-6d4907150c2dBD base
To recreate this system exactly, the role cards above aren't quite enough on their own β€” you also need these four things:
1. The four job-type ID codes. The cards say things like "dietician β†’ Dietician role," but the system actually matches on a long ID code (a UUID), not the word. Those exact codes are listed in the speciality table just above β€” without them the job-type matching won't work.
2. The pinned-people list. The hand-maintained list of specific individuals mapped to a role (37 entries). The complete list, with the exact user IDs, is embedded below in the Per-User Override Map (Exact) section — included in full because the rebuild keeps the same IDs as the live system.
3. The two permission checks. The "can they open this page / can they use this action" logic described under Role Resolution Logic above.
4. Enforce it on the server too. Everything described here runs in the browser, so it only controls what a person sees on screen β€” it does not, by itself, stop a determined user from reaching data another way. The new portal's server must apply the same rules independently. In short: this layer is about a clean experience, not security; real protection has to live on the backend.

The complete list of individual exceptions, copied exactly from the source in original order. These are specific people pinned to a role by their personal user ID, checked before job type. For each person below, the rebuilt portal must make that exact user ID resolve to the listed role β€” using the same IDs as today. The third column shows the original source identifier for the role, so each entry traces 1:1 back to the code. Total: 37 entries.

πŸ“Œ
User ID β†’ role, verbatim
37 individual overrides Β· source order Β· real identifiers
β–Ό
⚠ Contains real user identifiers. This block exists because the rebuild keeps the same IDs as the live system. Handle this file accordingly.
User ID (UUID)Resolved roleSource ID
0cac4ff0-30a5-4b7e-a673-43944519cf5b→Cross-domain seniorj
1ec2fe3d-6452-4a10-8661-0513c74039a2β†’Cross-domain seniorj
ebc1b43f-e971-40bc-a1ef-71185060bfea→Cross-domain seniorj
66958c4e-763b-4ae6-a04b-5148a99131a9β†’Cross-domain seniorj
fbfe7a82-486e-4c46-b0ae-a2bbdda85800β†’Care lead variantte
857b71e8-40e7-4d1c-bf8b-96052ab2ed67β†’Sales seniorse
918f2b59-9b0b-4a3f-9c6d-393aa45ccf71β†’No Accessy
88b6c686-dea0-4712-aef2-53fd93018cdf→Inline custom role (Knowledge-base admin)(inline)
bc7f0996-ffe7-415a-955e-47d09c2591b4β†’Tools / asset uploaderEe
0d30b7e2-a477-45e6-8d49-7529001fdcfa→Super care+sessions+toolset
055f196f-d8aa-4a53-9436-f52cea5b8e9d→BD headde
d2601e8a-4726-444e-82ea-64d4a90615f8β†’Chat specialistB
2f125b02-6378-4b9a-8b57-dd5bfd4e5283β†’Chat specialistB
56a30548-972f-4bd2-91b8-e0b10854bc4a→Admin (near-superuser)P
55fd0483-af4a-408d-9a33-94a6e41f1a4f→Admin (near-superuser)P
de3aa531-53c1-4c00-94db-44dcdff1e446β†’Sales / Assignment managerD
4836036c-81dd-4185-9cea-299867dea3e8β†’Special consultationU
5a78fbbb-1a36-43ef-b075-e35a6b04365d→Special consultationU
b30d69cb-3afa-4882-9401-a7a5c9dd8ddf→Special consultationU
df70325e-40b2-4d73-b399-dcec39ada48d→Special consultationU
de21b1b1-3538-4a3d-8ba2-15f8b928c0d1β†’Psych emergencyre
2079ef6d-9fc3-42c5-a37c-281724edad70β†’Special consultation (restricted)Y
4fa5d7df-46ee-4785-9459-bfbf6ba49e7f→Special consultation (restricted)Y
97c4e032-f6e2-4da1-bdbd-87a7c4524ca1β†’Sessions schedulerPe
74ce2ea1-0d14-4d9f-a36f-02569e019f0d→BD manageree
2178591c-a747-4d62-8f65-2364c20acada→BD manageree
bef259e8-47a3-49b7-86a5-97fe7efd16e4β†’BD manageree
fd347372-b636-4df5-8cd7-ac472d45fe98β†’BD manageree
a68f5d80-696e-4e4e-9023-7b308ea56bf2β†’BD manageree
cd0814e1-ffe7-4888-b664-e5c376ef593f→BD headde
f9b3b968-0266-4b4a-9aa0-973ff1e8a25b→BD headde
2f2a4f7e-da0d-47d2-b5d9-4803efacdbf8β†’BD senior managerce
ef354d9c-72bd-4fe5-9040-66bd230eb7c7β†’Diet template managerq
44535069-810f-4094-9732-e12d69a34726β†’Diet template managerq
2d04b77e-f920-4064-9c58-3acd51b4af06β†’Diet template managerq
18f867f4-59da-457d-8f7d-c6ebd5bceafa→Diet template managerq
18daf4cf-7177-4743-b1ca-a778940da6cd→Diet template managerq
Role source-identifier key (code variable β†’ role name)
y→No Access
ie→Dietician / Care base
q→Diet template manager
B→Chat specialist
ne→Sessions / Fitness instructor base
U→Special consultation
Y→Special consultation (restricted)
re→Psych emergency
K→Sales agent base
se→Sales senior
ae→Care manager
we→Care manager + discounting
te→Care lead variant
Pe→Sessions scheduler
Ee→Tools / asset uploader
et→Super care+sessions+tools
D→Sales / Assignment manager
j→Cross-domain senior
T→BD base
ee→BD manager
ce→BD senior manager
de→BD head
P→Admin (near-superuser)
🧬
Specialities & the per-user override map
What feeds role resolution
β–Ό
speciality_id values
Stored on the user. Only the four in bold map to a role directly; the rest rely on the override map or fall back to No Access.
chat_agentfitness_instructor β˜…dietician β˜…health_coach β˜…business_development_team β˜…gynaecologiestdermatologistgeneral_surgeryconsultant_physiciandiabetologistendocrinologistcardiologistgeneral_medicine
Per-user override map: A hardcoded table mapping specific user IDs to a specific role, checked before job type. In the live system this pins 37 named individuals to roles such as Admin, BD head, BD manager, Special consultation, and Chat specialist. The full, exact list is embedded in the Per-User Override Map (Exact) section below.
🎯
How access shapes the BD flows
Who can reach what in the flows documented below
β–Ό
Reaching the BD module: Any role that includes the /bd/... routes β€” i.e. BD base or anything inheriting it (BD manager, BD senior manager, BD head, Admin). Without one of these, BD pages redirect to insufficient-access.
Suggested Prospects screen: Requires the /bd/suggested_prospects route, granted by BD base and all senior BD roles.
MSL management action: The manage_msl feature under the doctors module gates MSL-related controls. Only BD senior manager, BD head, and Admin hold it β€” so only they see that action.
Row actions (change owner, etc.): Gated by the relevant doctors / enrolment / lead feature permissions listed above, which vary by BD seniority.
Flow 1 Β· BD Module

Managing Doctor Records

Allows BD Admin to add new leads and update existing doctor or lead records directly from the panel via an Excel upload β€” replacing the current process of editing the backend database manually.

Access: BD Admin Super Admin
U
User
Opens Doctors tab β†’ clicks three-dot menu β†’ "Upload Excel"
BD Admin is in the Doctor Ops module. The upload option lives in the "more options" menu next to the filter controls.
U
User
Downloads the Excel template
The template is pre-populated with all existing records (or blank for new entries only). Each row = one doctor or lead record. Every column of users_unoloclient is a column in the sheet.
U
User
Edits the sheet and uploads it
To update an existing record: keep the id in the row β€” system matches and updates it.
To create a new record: leave id blank β€” system generates a new ID and creates the record.
Every row must have a valid google_place_id. Duplicate phone numbers are allowed.
S
System
Shows upload preview before processing
Panel displays a summary: X rows will be created, Y rows will be updated. Admin confirms before the system processes anything.
U
User
Confirms upload
Admin clicks confirm. The file is submitted to the backend for processing.
POST /upload_doctor_records β†’ see API Reference ↓
B
Backend
Validates each row and resolves location via Google Places
For every row, backend validates required fields. Then calls the Google Places API using google_place_id to fetch the clinic's lat and long β€” these are never taken from the Excel directly. Rows with an unknown id (not found in DB) are created as new records.
GET Google Places API (internal) β†’ see API Reference ↓
B
Backend
Writes to users_unoloclient
Valid rows are upserted into our database. stage field is never overwritten by the upload β€” it is system-managed. All other non-system fields can be updated via the Excel.
B
Backend
Syncs each record to Unolo via upsert_client_by_id
After writing to our DB, backend calls the Unolo GraphQL API for each row to keep the field ops platform in sync.

New record: First-time call with the record's id (used as internalClientID) β†’ Unolo creates the client.
Existing record: Subsequent call with the same internalClientID β†’ Unolo updates it.

Sends: clientName (doctor name), lat + lng (resolved from Google Places), address, phoneNumber. The Unolo client record is how field visits are later geo-routed to the correct clinic.
GraphQL Unolo upsert_client_by_id β†’ see API Reference ↓
S
System
Returns result summary to the panel
Panel shows: rows successfully created, rows successfully updated, rows failed (with reason per row). Admin can correct failed rows and re-upload.
πŸ—„οΈ
Database Tables
users_unoloclient
β–Ό
users_unoloclient
Core table for all doctor leads and converted doctors. Every row in the Excel maps to a row here. This flow reads from it (to build the template) and writes to it (on upload).
id (PK) name phone owner_id β†’ users_user cl_bd_area_id β†’ careplan_clbdarea speciality lead_stage stage (system-managed, not editable) google_place_id (required) lat (system-set via Google Places) long (system-set via Google Places) onboarding_type parked_stage parked_remarks
πŸ”Œ
APIs
Upload endpoint Β· Google Places Β· Unolo upsert_client_by_id
β–Ό
POSTUpload Doctor Records
/careplan/bd_crm/upload_doctor_records
Frontend→ Cureverse Backend
Accepts the uploaded Excel file. Processes each row: validates β†’ resolves location via Google Places β†’ upserts our DB β†’ syncs to Unolo. Returns a per-row result report. New endpoint β€” to be built.
INPUTS (file required)
file * Excel (.xlsx)
The uploaded spreadsheet. Each row represents one doctor or lead record.
id String
Per-row field. Present β†’ update that record. Blank β†’ create new. Unknown ID not in DB β†’ create new with system-generated ID.
google_place_id * String
Required on every row. Backend calls Google Places API to resolve lat/long. Rows without it are rejected.
name, phone, owner_id * String
Required only for new records (rows without an id).
stage String system-managed
Ignored if present in the upload. Cannot be set via Excel β€” system-controlled only.
lat / long Float system-set
Never taken from the Excel. Always resolved from google_place_id via Google Places API.
SAMPLE RESPONSE
{
  "success": true,
  "success_count": 12,
  "failed_count": 1,
  "failed_rows": [
    {
      "row": 5,
      "name": "Dr. Sharma",
      "reason": "google_place_id not found"
    }
  ]
}
GraphQLupsert_client_by_idmutation Β· Unolo
https://apollo-lb-ext-ns.unolo.com/graphql
Cureverse Backend→ Unolo API
Create or update a client in Unolo. First call with an internalClientID creates the client; subsequent calls with the same internalClientID update it. Called once per row after our DB write succeeds.
INPUTS (3 REQUIRED)
clientName * String
Doctor / clinic name from the record.
internalClientID * String
Client's external identifier. Use our users_unoloclient.id. First call = create; same ID on repeat call = update.
visibility * ClientVisibilityInput
Who can see this client in Unolo. Set to the BD owner's team or employee scope.
Either lat + lng or address is required
lat * Float
Latitude β€” resolved earlier from Google Places. Used by Unolo for geo-routing field visits.
lng * Float
Longitude β€” resolved from Google Places.
address String
Clinic address. Can substitute lat/lng if coordinates are unavailable.
phoneNumber String
Doctor's phone number.
city String
City where the clinic is located.
pinCode String
Postal code of the clinic.
SAMPLE RESPONSE
{
  "data": {
    "upsert_client_by_id": {
      "rowsInserted": 1,
      "rowsUpdated": 0,
      "data": {
        "clientName": "Dr. Khushboo Gosalia",
        "internalClientID": "07db4526-983f-4658-970b-8b210cb2e9a6",
        "lat": 19.195,
        "lng": 72.836,
        "phoneNumber": "+91-9191919191",
        "city": "Mumbai"
      },
      "err": null
    }
  }
}
βš™οΈ
Backend Logic
Validation rules and upsert behaviour
β–Ό
Upsert rule: Row with a known id β†’ update. Row with blank id β†’ create. Row with an id not found in the DB β†’ create new record with a system-generated ID.
Location resolution: For every row (create or update), backend calls the Google Places API with google_place_id to get lat and long. These are written to the DB. Values in the Excel columns for lat/long are ignored.
stage is locked: The stage field is system-managed and reflects the doctor's lifecycle (onboarded, activated, etc.). It cannot be overwritten via the Excel upload under any circumstances.
Duplicate phones allowed: Multiple records can share the same phone number. No deduplication on phone.
Unolo client sync β€” after every DB write: Once a row is successfully written to users_unoloclient, immediately call upsert_client_by_id on Unolo using the record's id as internalClientID. New records are created in Unolo on the first call; existing records are updated on repeat calls with the same ID. This ensures Unolo always has the latest clinic name, location, and phone for geo-routing field visits.
Per-row error reporting: Processing continues even if individual rows fail. Response includes success_count, failed_count, and a list of failed rows with specific reasons (missing field, invalid google_place_id, Unolo sync failure, etc.).
Flow 2 Β· BD Module

Lead List & Meeting Scheduling

The BD person's daily workhorse screen. Shows all assigned leads with meeting history at a glance, and lets them select and schedule visits for the day in one action.

Access: BD Admin Super Admin BD Team Member
U
User
Opens the Leads tab in Doctor Ops
BD Team Member sees only their own leads. BD Admin and Super Admin see all leads across the team with a filter to switch between BD owners.
S
System
Fetches the lead list with meeting stats
Panel calls the doctor_leads API filtered by the logged-in user's owner_id. Each lead comes back with five computed meeting stats calculated live from users_unolotask.
GET /doctor_leads β†’ see API Reference ↓
U
User
Sees the lead list with meeting stats
Each row shows: doctor name, speciality, lead stage, days since last successful meeting, meetings this month, and lifetime meeting counts.
U
User
Selects one or more leads β†’ clicks "Schedule Meeting"
Checkboxes appear on selection. "Schedule Meeting" button is visible at the bottom right once at least one lead is selected.
S
System
Confirmation pop-up appears
"You are about to schedule a meeting with the selected doctors β€” are you sure?" User must confirm before any task is created.
U
User
Confirms scheduling
On confirm, the panel sends all selected lead IDs to the backend in one call.
POST /schedule_doctor_lead_meetings β†’ see API Reference ↓
B
Backend
For each lead β€” calls Unolo API to create a meeting task
Backend loops through each unolo_client_id and calls the Unolo GraphQL API individually. Sends today's date, the BD owner's employee ID, task type, and the clinic's location. Failures are per-lead β€” other leads in the batch still get scheduled.
GraphQL Unolo upsert_task_external β†’ see API Reference ↓
B
Backend
Unolo responds β†’ backend creates entry in users_unolotask
Only after a successful Unolo response is a row written to users_unolotask. The task_id from Unolo's response is stored to link our record back to Unolo. This keeps both systems in sync.
S
System
Panel shows scheduling result
Success for scheduled leads. For any that failed, an error is shown per lead so the BD person can retry individually.
πŸ—„οΈ
Database Tables
users_unoloclient Β· users_unolotask
β–Ό
users_unoloclient
Read only in this flow. Provides lead profile fields shown in the list. Filtered by owner_id for BD Team Members; all records visible to BD Admin / Super Admin.
idnamephone specialitylead_stageowner_id latlong
users_unolotask
Read to compute meeting stats for the list display. Written to when a meeting is scheduled (one new row per lead, created only after Unolo confirms).
id (PK)task_id (from Unolo) client_id β†’ users_unoloclientowner_id β†’ users_user datemeet_statuscheck_in_time
Computed fields in the lead list (calculated live from users_unolotask)
days_since_last_successful_meeting
Days since the most recent users_unolotask row where meet_status = 'Met Doctor' for this client.
no_of_successful_current_month_meetings
Count of rows where meet_status = 'Met Doctor' and date falls in the current calendar month.
no_of_attempted_current_month_meetings
Count of all task rows for this client in the current calendar month (regardless of outcome).
no_of_successful_meetings
Lifetime count of rows where meet_status = 'Met Doctor'.
no_of_attempted_meetings
Lifetime count of all task rows for this client.
πŸ”Œ
APIs
doctor_leads Β· schedule_meetings Β· Unolo GraphQL
β–Ό
GETdoctor_leads
/careplan/bd_crm/doctor_leads
Frontend→Cureverse Backend
Returns all leads for the given owner(s) with five computed meeting stats per lead. Stats are calculated live on every call from users_unolotask.
INPUTS (1 REQUIRED)
owner_id_in * [String]
Array of owner UUIDs to filter leads by. BD Team Member sends only their own ID; BD Admin can pass multiple. Controlled by backend based on role.
SAMPLE RESPONSE
{
  "success": true,
  "data": [
    {
      "id": "07db4526-983f-4658-970b-8b210cb2e9a6",
      "name": "Dr. Khushboo Gosalia",
      "phone": "9191919191",
      "speciality": "Obstetrics and Gynaecology",
      "lead_stage": "in_progress",
      "days_since_last_successful_meeting": 67,
      "no_of_successful_current_month_meetings": 0,
      "no_of_attempted_current_month_meetings": 0,
      "no_of_successful_meetings": 7,
      "no_of_attempted_meetings": 19,
      "owner": {
        "id": "7c1dad97-...",
        "name": "Cureverse North Mumbai"
      }
    }
  ]
}
POSTschedule_doctor_lead_meetings
/careplan/bd_crm/schedule_doctor_lead_meetings
Frontend→Cureverse Backend→Unolo API
Receives the list of selected lead IDs. Backend calls Unolo's upsert_task_external individually for each lead. Returns per-lead success/failure β€” partial success is possible.
INPUTS (1 REQUIRED)
unolo_client_ids * [String]
Array of users_unoloclient IDs selected by the BD person in the list.
SAMPLE RESPONSE
{
  "success": true,
  "scheduled": [
    { "unolo_client_id": "0364c720-...", "task_id": "9347" },
    { "unolo_client_id": "3d5132ae-...", "task_id": "9348" }
  ],
  "failed": [
    {
      "unolo_client_id": "3eda9dc9-...",
      "reason": "Unolo API error: employee not found"
    }
  ]
}
GraphQLupsert_task_externalmutation Β· Unolo
https://apollo-lb-ext-ns.unolo.com/graphql
Cureverse Backend→Unolo API
Create or update a task via the external API. internalTaskID omitted = create; provided = update. Called once per lead after user confirms scheduling.
INPUTS (3 REQUIRED)
date * Date
Today's date β€” the day the meeting is being scheduled for.
internalEmpID * String
Employee ID as set in your system (e.g. EC2615). Fetched from users_doctorprofile via the lead's owner_id.
customTaskName * String
Custom task type name (e.g. "Doctor Visit"). Must match exactly as configured in the Unolo account.
internalTaskID String
Task's external identifier. Omit to create a new task. Provide to update an existing one. Every task touched via external API must carry a unique partner-supplied reference.
lat / lon Float
Clinic coordinates from users_unoloclient. Helps Unolo geo-route the BD person.
address String
Clinic address. Optional alongside lat/lon.
SAMPLE RESPONSE
{
  "data": {
    "upsert_task_external": {
      "rowsInserted": 1,
      "rowsUpdated": 0,
      "data": [{
        "internalTaskID": "9347",
        "internalEmpID": "EC2615",
        "date": "2026-06-16",
        "adminAssigned": true
      }]
    }
  }
}
βš™οΈ
Backend Logic
Scheduling sequence and sync rules
β–Ό
Unolo-first, then DB write: For each lead, call Unolo first. Only after Unolo returns a successful response (with internalTaskID) is the users_unolotask row created. This prevents our DB from having tasks that don't exist in Unolo.
Per-lead partial success: If Unolo fails for one lead, processing continues for the remaining leads in the batch. The response reports which leads succeeded and which failed with reasons.
internalEmpID resolution: The BD owner's Unolo employee ID is stored in users_doctorprofile. Fetch it using the lead's owner_id. No separate mapping table is needed.
Meeting stats are live: The five stats (days since last meeting, monthly counts, lifetime counts) are computed fresh from users_unolotask on every doctor_leads API call β€” not cached.
Role-based filtering: owner_id_in in the API is controlled by the backend based on the logged-in user's role. BD Team Member β†’ only their own owner_id. BD Admin / Super Admin β†’ all owners, with optional filter.
Flow 3 Β· BD Module

Lead Profile

Opens when a BD person clicks on any lead's name from the list. Shows the lead's basic details at the top and a full chronological timeline of all past and upcoming meetings below.

Access: BD Admin Super Admin BD Team Member
U
User
Clicks a lead's name in the list
From the lead list (Flow 2), clicking any name opens that lead's profile page.
S
System
Profile header renders immediately β€” no extra API call
The header data (phone, lead stage, closing reason, meeting stats) is reused from the doctor_leads response already loaded for the list. The selected lead's data is passed through navigation state.
S
System
Fetches the meeting timeline (paginated)
Panel calls unolo_tasks API for this lead. Returns 20 meetings per page, newest first. Each meeting record includes its recordings and attachments nested inside.
GET /unolo_tasks β†’ see API Reference ↓
U
User
Sees meeting timeline with recordings and attachments
Each meeting card shows: date, met/not met status, meeting notes, manager and head office audit notes, inline audio player for recordings, and photo attachments. Older meetings load on scroll.
U
User
On an open meeting β€” clicks "Start Meeting"
An open meeting is one where check_in_time is null (the BD person hasn't checked in yet). Two actions appear: Start Meeting and Add Attachment. "Start Meeting" fires an Android deep-link intent.
INTENT Android deep-link β†’ Unolo App
E
Unolo App (BD Tablet)
Unolo handles GPS check-in, recording, and check-out
After the BD person completes the meeting in Unolo, Unolo sends data back to our system via webhook β€” updating the users_unolotask row with check_in_time, check_out_time, meet_status, GPS coordinates, and triggering recording log creation. No panel action needed at this step.
πŸ—„οΈ
Database Tables
4 tables involved
β–Ό
users_unoloclient
Provides the profile header β€” name, phone, stage, meeting stats. Data passed from the lead list, no extra fetch.
users_unolotask
One row per meeting visit. The entire timeline is built from this table. An open meeting = check_in_time IS NULL.
datemeet_statuscheck_in_time check_out_timemeeting_notes manager_audit_noteshead_office_audit_notes met_withaddress
users_unolotaskrecordinglog
Stores audio recordings per meeting. One meeting can have multiple recordings (e.g. if interrupted by a phone call). Always has a raw .m4a file; .mp3 is added after transcoding. Panel uses MP3 when available, falls back to M4A. Files are publicly readable S3 URLs β€” no signed URL needed.
recording_file (.m4a β€” always present) mp3_recording_file (.mp3 β€” after transcoding) ended_due_to_call (bool) unolo_task_id β†’ users_unolotask
users_unolotaskattachment
Stores photos or files attached to a meeting. Multiple per meeting. Files are publicly readable S3 URLs.
attachment_file (S3 URL) unolo_task_id β†’ users_unolotask
πŸ”Œ
APIs
unolo_tasks (paginated) Β· Android intent
β–Ό
GETunolo_tasks
/careplan/bd_crm/unolo_tasks
Frontend→Cureverse Backend
Returns paginated meeting history for a given lead. 20 records per page, newest first. Each record includes nested recordings[] and attachments[].
INPUTS (1 REQUIRED)
unolo_client_id * String
The lead's users_unoloclient.id. Filters meetings to only this lead.
limit Int
Number of records per page. Default: 20.
offset Int
Number of records to skip. Start at 0, increment by limit for each page.
SAMPLE RESPONSE
{
  "success": true,
  "count": 19,
  "is_last_page": false,
  "data": [
    {
      "id": "b3e2...",
      "date": "2026-06-10",
      "meet_status": "Met Doctor",
      "check_in_time": "2026-06-10T10:22:00Z",
      "check_out_time": "2026-06-10T10:48:00Z",
      "meeting_notes": "Discussed onboarding timeline",
      "manager_audit_notes": null,
      "recordings": [
        {
          "recording_file": "https://s3.amazonaws.com/.../raw.m4a",
          "mp3_recording_file": "https://s3.amazonaws.com/.../rec.mp3",
          "ended_due_to_call": false
        }
      ],
      "attachments": [
        { "attachment_file": "https://s3.amazonaws.com/.../photo.jpg" }
      ]
    }
  ]
}
INTENTStart Meeting β€” Android Deep-link
Android intent β†’ Unolo App on BD tablet
BD App (tablet)β†’Unolo App
No backend API call involved. Tapping "Start Meeting" fires an Android intent that deep-links directly into the Unolo app on the BD person's tablet. Unolo handles GPS check-in, geofencing, recording, and check-out entirely. No further panel action is needed.
βš™οΈ
Backend Logic
Open meeting detection Β· recording playback Β· pagination
β–Ό
Open meeting detection: A meeting is considered open (not yet started) when check_in_time IS NULL. Only open meetings show the "Start Meeting" and "Add Attachment" actions.
Recording playback preference: Always prefer mp3_recording_file when it is not null. Fall back to recording_file (M4A) if MP3 is still being transcoded. Both are public S3 URLs β€” no signed URL generation needed.
Pagination: Load 20 meetings per page using limit and offset. When is_last_page = false, the frontend must offer a way to load the next page (infinite scroll or "load more" button).
Profile header data: The header reuses data from the doctor_leads response loaded for the lead list. Pass the selected lead object through navigation state β€” do not make a second API call on profile open.