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.
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.)
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.
/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.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.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.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.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.Authorization: Token <token>; it's auto-refreshed before expiry and cached locally so reloads stay signed in.users/detail/ the same way. More work, full control, removes the Google/Firebase dependency. The universal-OTP idea for development fits naturally here.speciality_id, etc.) from the user endpoint.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.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./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.manage_msl permission.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.
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.
/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./bd/suggested_prospects route, granted by BD base and all senior BD roles.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.doctors / enrolment / lead feature permissions listed above, which vary by BD seniority.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.
users_unoloclient is a column in the sheet.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.
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.users_unoloclientstage field is never overwritten by the upload β it is system-managed. All other non-system fields can be updated via the Excel.upsert_client_by_idNew 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.
lat/long. Rows without it are rejected.id).google_place_id via Google Places API.{
"success": true,
"success_count": 12,
"failed_count": 1,
"failed_rows": [
{
"row": 5,
"name": "Dr. Sharma",
"reason": "google_place_id not found"
}
]
}internalClientID creates the client; subsequent calls with the same internalClientID update it. Called once per row after our DB write succeeds.users_unoloclient.id. First call = create; same ID on repeat call = update.{
"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
}
}
}id β update. Row with blank id β create. Row with an id not found in the DB β create new record with a system-generated ID.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 field is system-managed and reflects the doctor's lifecycle (onboarded, activated, etc.). It cannot be overwritten via the Excel upload under any circumstances.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.success_count, failed_count, and a list of failed rows with specific reasons (missing field, invalid google_place_id, Unolo sync failure, etc.).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.
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.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.users_unolotaskusers_unolotask. The task_id from Unolo's response is stored to link our record back to Unolo. This keeps both systems in sync.owner_id for BD Team Members; all records visible to BD Admin / Super Admin.users_unolotask row where meet_status = 'Met Doctor' for this client.meet_status = 'Met Doctor' and date falls in the current calendar month.meet_status = 'Met Doctor'.users_unolotask.{
"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"
}
}
]
}upsert_task_external individually for each lead. Returns per-lead success/failure β partial success is possible.users_unoloclient IDs selected by the BD person in the list.{
"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"
}
]
}internalTaskID omitted = create; provided = update. Called once per lead after user confirms scheduling.users_doctorprofile via the lead's owner_id.users_unoloclient. Helps Unolo geo-route the BD person.{
"data": {
"upsert_task_external": {
"rowsInserted": 1,
"rowsUpdated": 0,
"data": [{
"internalTaskID": "9347",
"internalEmpID": "EC2615",
"date": "2026-06-16",
"adminAssigned": true
}]
}
}
}internalTaskID) is the users_unolotask row created. This prevents our DB from having tasks that don't exist in Unolo.users_doctorprofile. Fetch it using the lead's owner_id. No separate mapping table is needed.users_unolotask on every doctor_leads API call β not cached.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.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.
doctor_leads response already loaded for the list. The selected lead's data is passed through navigation state.unolo_tasks API for this lead. Returns 20 meetings per page, newest first. Each meeting record includes its recordings and attachments nested inside.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.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.check_in_time IS NULL..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.recordings[] and attachments[].users_unoloclient.id. Filters meetings to only this lead.{
"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" }
]
}
]
}check_in_time IS NULL. Only open meetings show the "Start Meeting" and "Add Attachment" actions.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.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).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.