Skip to content

Organization management and doctor affiliations ​

All routes use /api/v1, snake_case fields, and {message, status_code, data} success responses. Collections add pagination: {next_cursor, current_page, per_page, total_pages, total_items}. Requests use limit (default 20, maximum 100) and cursor; pass the same filters and limit when following a cursor.

Organization details ​

Create with POST /admin/organizations:

json
{
  "name": "Dhaka Central Hospital",
  "slug": "dhaka-central-hospital",
  "type": "Hospital",
  "email": "admin@hospital.com",
  "phone": "+8801711100001",
  "address": "Dhaka",
  "description": "Hospital and outpatient care"
}

Supported type constants are Hospital, Pharmaceuticals, and Partner. Slug is unique and immutable; there is no code field. Historical unclassified organizations return type: null. PUT /admin/organizations/{organizationId} replaces name/contact/description fields; omitted type is preserved. Explicit null type and unsupported types are rejected. Lifecycle changes use the existing activate/suspend/delete/restore actions. Creation retains its existing shared authorization behavior described in Admin.

List/get/create/update responses include type, a compact plan (id, name, slug), seat_limit, doctor_limit, used_seats, doctor_count, billing_cycle, subscription_status, and billing_is_mock. Usage is real. Billing is supplied by the mock provider described below. The admin list/get require current admin.organization.list / admin.organization.read grants and active platform membership/account.

Organization creation/update is exposed through admin routes. Legacy POST /organizations, its trailing-slash alias, and PATCH /organizations/{organizationId} are retired (404/405).

Organization summary ​

GET /api/v1/admin/organizations/{organizationId}/summary requires an active platform administrator and admin.organization.read.

text
{
  message: "Success",
  status_code: 200,
  data: {
    organization: <organization details and compact billing>,
    summary: {
      total_members: 12,
      active_members: 10,
      total_doctors: 8,
      active_doctors: 7
    },
    subscription: <full Subscription-owned snapshot, or null>,
    billing_is_mock: true
  }
}

Totals include all persisted relationship states. Active counts use relationship status=active, independently of Identity account or verification status. Ordinary members and professional doctor affiliations are separate: a person can contribute to both counts. used_seats equals active members and doctor_count equals active affiliations. Suspended/deleted organizations remain inspectable by authorized platform admins.

Organization doctor list ​

GET /admin/organizations/{organizationId}/doctors?limit=8&search=cardiology&status=active

The Admin route requires an active Identity account, active platform-admin membership, and both admin.organization.read and admin.doctor.list. It shares the scoped list reader and response fields with the Organization portal route below. Admins can inspect suspended/deleted organizations. Organization portal grants alone do not authorize the Admin route.

GET /organizations/{organizationId}/doctors?limit=8&search=cardiology&status=active

Requires org.doctor.list for the exact active organization, or platform-admin grants for both admin.organization.read and admin.doctor.list. The member's other organizations and client-supplied identity headers do not grant access. Platform admins can inspect suspended organizations.

Items contain id, organization_id, doctor_id, affiliation status, assigned_at, assigned_by_user_id, assignment_source, joined_at, timestamps, organization display fields, and nested doctor with id, live name, license_number, specialization, specialties, verification_status, and account_status. Search matches name, license and specialties. Omit status to include active and inactive affiliations. Results order by latest assignment time and ID descending.

Empty results return data: [] and zero totals. Missing enrichment providers return 503. Filter/scope/page-size cursor mismatches return 422 on the portal route and 400 on the Admin route.

Scoped doctor detail and affiliation status ​

GET /admin/organizations/{organizationId}/doctors/{doctorId} requires admin.organization.read and admin.doctor.read, plus an active platform-admin membership/account. It returns one item in the same shape as the roster. Both IDs must match an existing affiliation; a doctor assigned only elsewhere returns 404. doctorId is the professional profile ID.

PATCH /admin/organizations/{organizationId}/doctors/{doctorId} requires admin.organization.read and admin.doctor.affiliation.update:

json
{"status":"inactive"}

The only accepted states are active and inactive. The response contains the committed affiliation. Deactivation removes the doctor from active usage counts while retaining the relationship, original join time, and assignment provenance. Reactivation requires an active target organization and doctor account, locks the account before the organization/affiliation, and refreshes assignment time/actor with assignment_source: "super_admin_portal". Ineligible activation returns 409. Repeated valid states leave metadata unchanged and append no extra change event. Deactivation is allowed even when the account or organization is inactive.

PATCH never creates a missing affiliation (404). Account status, ordinary memberships, clinical history, and other organizations' affiliations are preserved. The status write and audit event commit in one transaction; audit failure rolls back the change. Mock limits and verification remain display-only. Admin list, detail, and status routes are implemented; the separate portal detail/PATCH contracts retain their current 501 behavior.

Doctor picker and assignment ​

GET /admin/doctors?assignment_organization_id={organizationId}&limit=8

The directory remains global. Existing organization_id filters to doctors already actively affiliated with an organization; assignment_organization_id is separate picker context and does not filter out candidates. The target requires admin.organization.read in addition to directory permission admin.doctor.list.

With target context, items add is_assigned, is_eligible, and nullable ineligible_reason. Without it, those fields are omitted. Assignment to other organizations is allowed. A target's inactive affiliation can be reactivated. Verification is display-only: not-submitted, pending, verified and rejected doctors can all be assigned if their account is active.

Reasons, in priority order: organization_inactive, account_suspended, account_inactive, already_assigned. An active affiliation is disabled in the picker; repeating the assignment POST is an idempotent success.

POST /admin/organizations/{organizationId}/doctors:

json
{"doctor_ids":["00000000-0000-4000-8000-000000000001"]}

Supply 1–100 unique professional profile UUIDs. The target and all accounts must be active. Identity holds account locks through the Organization transaction, so a concurrent suspension is rechecked before commit. Assignments and audit records commit together. Mock subscription limits never reject an assignment.

New/reactivated admin affiliations record assignment_source: "super_admin_portal", trusted Identity user ID, and latest assigned_at. Repeated active assignments preserve metadata. Invitation registration uses doctor_registration; historical records use legacy and a null actor. joined_at remains the first affiliation time.

Doctor organizations ​

GET /admin/doctors/{doctorId}/organizations?limit=8&status=active

Requires active platform membership/account, admin.doctor.read, and admin.organization.read. The doctor must exist; an existing unaffiliated doctor returns an empty collection. Results include organization name/slug/type/lifecycle status, affiliation metadata and provenance, compact plan, subscription status, and billing_is_mock. Organization lifecycle state and affiliation state are independent. Omit status to include inactive affiliations. Cursor mismatches return 400. The existing embedded organizations array on doctor details remains available.

Temporary billing projections ​

The composition root explicitly selects Subscription's MockCurrentSubscriptions batch provider. It returns stable organization-scoped sample subscription IDs and an Enterprise/yearly snapshot: BDT 1,200,000 minor units, 250 members and 100 doctors, sample period [2026-01-01, 2027-01-01), sample status active. All affected endpoints share this fixture source and return billing_is_mock: true.

The sample plan, status, periods, prices, and limits are display data. They do not enforce access, capacity, or billing. Counts remain real even if they exceed a sample limit. No subscription, invoice, or payment writes are implemented by this provider, and other billing stubs retain their 501 behavior. A future real reader can return null subscription/plan/status/limits when no current subscription exists; null effective capacity on a present subscription means unlimited.

Migration and checks ​

Apply the Organization migration and Authorization permissions before serving these routes. The affiliation status extension requires Authorization migration 00005_doctor_affiliation_permission.sql; existing admins need an explicit admin.doctor.affiliation.update grant (alongside admin.organization.read). Development full-access roles include the new key on reseeding through the shared permission catalog. The extension uses existing affiliation columns.

Apply migrations:

bash
go run ./tools/cli database migrate --service organization
go run ./tools/cli database migrate --service authorization

00006_organization_management.sql adds nullable historical organization type and assignment provenance, backfills assigned_at from joined_at, and adds the scoped assignment-order index. Seed organizations have explicit types. Doctor preference persistence remains a separate follow-up.

Documentation follows the source. Planned contracts are marked explicitly.