Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save alexlazarian/d420ef1b4229488ae1e7b221bfc2559b to your computer and use it in GitHub Desktop.

Select an option

Save alexlazarian/d420ef1b4229488ae1e7b221bfc2559b to your computer and use it in GitHub Desktop.
CLU-530: Medical Provider Source + Details Column — plan & spec

CLU-530: Surface Medical Provider Source + Fix Provider Details Column

Date: 2026-05-17
Ticket: CLU-530
Scope: ui-2.0 frontend + api-hub backend


Summary

Two small changes to the Medical Providers UI:

  1. Show each provider's source value(s) on the list card in the Medical Providers pop-out.
  2. Rebind the "Provider Details" form field in the edit modal from the provider_details column to the details column. Requires wiring details into the backend PATCH endpoint.

Change 1: Source Display on Provider Card

What

Add a source line to MedicalProviderCard.tsx in the card header, below the roles line.

Implementation

  • Call the existing getMedicalProviderSources(provider) utility, which returns string[] of source labels.
  • Render below the roles line: Source — label1, label2 using text-xs text-muted-foreground.
  • Skip rendering entirely when the sources array is empty.

Files

  • app/(dashboard)/(matters)/[id]/components/MedicalProviderCard.tsx

Change 2: Rebind "Provider Details" to details Column

Background

The contact_medical_provider table has two JSONB columns:

  • provider_details: legacy blob, returned by the API as a JSON-stringified string.
  • details: newer JSONB column, returned as a raw dict (or null). Currently used as a legacy fallback for role storage (details.roles) by providers predating the junction table migration.

The PATCH endpoint (/v2/matter-medical-providers/{matterId}) currently accepts provider_details in its request model but has no details field.

Backend Changes

api_hub/api/models/matter_medical_provider.py

  1. MatterMedicalProviderUpdateSchema.Request: add details: Optional[str] = None.
  2. BaseResponse.details: widen from Optional[dict] = None to Optional[Any] = None so both existing dicts and newly-written strings deserialize without Pydantic validation errors.

api_hub/database/repositories/matter_medical_provider.py

  1. _CONTACT_MEDICAL_PROVIDER_FIELD_MAP: add "details": "details".
  2. find_or_create_contact_medical_provider create path: pass details=data.get("details", None) to the ContactMedicalProvider() constructor.

Frontend Changes

app/(dashboard)/(matters)/[id]/components/edit-medical-provider-dialog-utils.ts

  1. Zod schema: rename provider_detailsdetails (keep z.string().optional().nullable()).
  2. getEditMedicalProviderDefaults: read from provider.details. If it's a string, use as-is. If it's a dict (legacy), JSON.stringify it. If null, use null.
  3. buildUpdateMedicalProviderPayload: send details: data.details ?? undefined and remove provider_details.

app/(dashboard)/(matters)/[id]/components/EditMedicalProviderDialog.tsx

  • Change name="provider_details"name="details" on the TextField.

lib/api-hub/contracts/medical-providers.ts

  1. MatterMedicalProvidersUpdateRequestContract: add details?: string | null.
  2. MatterMedicalProviderV2Contract.details: widen type to { roles?: string[]; [key: string]: unknown } | string | null.
  3. MatterMedicalProviderUpdateResponseContract.details: same widening.

Behavioral Notes

  • getMedicalProviderRoles reads provider.details?.roles. When details is a string, string?.roles is undefined, so the legacy fallback gracefully returns []. This is correct — providers with string details will have proper junction table roles.
  • The parseMedicalProviderDetails utility reads provider_details, not details. It is unchanged.
  • The add flow (AddMedicalProviderDialog / MedicalProviderLinkDetails) does not have a provider_details field and is not touched.

Tests

Per CLAUDE.md testing rules:

  • Card source display and field rebinding are presentational changes — no new tests required.
  • The backend _CONTACT_MEDICAL_PROVIDER_FIELD_MAP change is a mechanical addition with no logic — no new test required.
  • If existing unit tests in edit-medical-provider-dialog-utils.test.ts assert on provider_details form values, update them to details.

Out of Scope

  • No changes to provider_details column handling anywhere else.
  • No changes to the add flow.
  • No migration — details column already exists.

CLU-530: Medical Provider Source Display + Details Column Rebind

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Show provider source on the Medical Providers list card; rebind the "Provider Details" edit field from provider_details to the details DB column, including the necessary backend PATCH support.

Architecture: Two independent changes sharing one PR. Backend wires details into the PATCH request model and repository field map. Frontend renames the form field throughout the edit flow and adds a source line to the card component.

Tech Stack: FastAPI (Pydantic v2, SQLAlchemy async), Next.js 14, react-hook-form + zod, Jest (unit tests via pnpm test:unit)


File Map

File Change
case_management_pipeline/projects/api-hub/api_hub/api/models/matter_medical_provider.py Add details: Optional[str] to update request; widen response details to Optional[Any]
case_management_pipeline/projects/api-hub/api_hub/database/repositories/matter_medical_provider.py Add "details": "details" to field map; add details to create path
case_management_pipeline/projects/next-js/ui-2.0/lib/api-hub/contracts/medical-providers.ts Add details?: string | null to update request contract; widen details type on V2/update response contracts
case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/edit-medical-provider-dialog-utils.ts Rename provider_detailsdetails in schema, defaults builder, payload builder
case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/EditMedicalProviderDialog.tsx Change name="provider_details"name="details"
case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/MedicalProviderCard.tsx Render source labels below roles in card header
case_management_pipeline/projects/next-js/ui-2.0/test/unit/app/edit-medical-provider-dialog-utils.test.ts Update assertions to use details instead of provider_details

Task 1: Backend — wire details into the PATCH request model and repository

Files:

  • Modify: case_management_pipeline/projects/api-hub/api_hub/api/models/matter_medical_provider.py

  • Modify: case_management_pipeline/projects/api-hub/api_hub/database/repositories/matter_medical_provider.py

  • Step 1: Add details to the update request model

Open api_hub/api/models/matter_medical_provider.py. The imports already include from typing import List, Optional. Add Any to the typing import:

from typing import (
    Any,
    List,
    Optional,
)

In MatterMedicalProviderUpdateSchema.Request, add details after provider_details:

class Request(BaseModel):
    contact_medical_provider_id: Optional[UUID] = None
    medical_provider_contact_id: Optional[UUID] = None
    medical_provider_entity_id: Optional[UUID] = None
    medical_provider_npi_id: Optional[str] = None
    npi_taxonomy_code: Optional[str] = None
    is_primary_care_physician: Optional[bool] = None
    is_on_ehealth_exchange: Optional[bool] = None
    requires_wet_ink_signature: Optional[bool] = None
    requires_drivers_license: Optional[bool] = None
    record_collection_method: Optional[str] = None
    start_date: Optional[date] = None
    end_date: Optional[date] = None
    is_current: Optional[bool] = None
    provider_details: Optional[Json] = None
    details: Optional[str] = None
    role_ids: Optional[List[UUID]] = None
    source_ids: Optional[List[UUID]] = None

    model_config = ConfigDict(from_attributes=True)
  • Step 2: Widen BaseResponse.details to Optional[Any]

In BaseResponse, change:

details: Optional[dict] = None

to:

details: Optional[Any] = None

This allows the response to return either a legacy dict (pre-existing JSONB data) or a plain string (written by this new field) without Pydantic validation errors.

  • Step 3: Add details to the repository field map and create path

Open api_hub/database/repositories/matter_medical_provider.py.

In _CONTACT_MEDICAL_PROVIDER_FIELD_MAP, add details:

_CONTACT_MEDICAL_PROVIDER_FIELD_MAP = {
    "is_primary_care_physician": "is_primary_care_physician",
    "start_date": "start_date",
    "end_date": "end_date",
    "is_current": "is_current",
    "provider_details": "provider_details",
    "details": "details",
}

In find_or_create_contact_medical_provider, the ContactMedicalProvider(...) constructor call (around line 153) currently reads:

contact_medical_provider = ContactMedicalProvider(
    contact_id=injured_party_id,
    medical_provider_id=medical_provider_id,
    is_primary_care_physician=data.get("is_primary_care_physician", None),
    start_date=data.get("start_date", None),
    end_date=data.get("end_date", None),
    is_current=data.get("is_current", None),
    provider_details=data.get("provider_details", None),
)

Add details:

contact_medical_provider = ContactMedicalProvider(
    contact_id=injured_party_id,
    medical_provider_id=medical_provider_id,
    is_primary_care_physician=data.get("is_primary_care_physician", None),
    start_date=data.get("start_date", None),
    end_date=data.get("end_date", None),
    is_current=data.get("is_current", None),
    provider_details=data.get("provider_details", None),
    details=data.get("details", None),
)
  • Step 4: Verify via convention check

From the repo root, run the API Hub lint/convention check:

cd case_management_pipeline/projects/api-hub
python -m flake8 api_hub/api/models/matter_medical_provider.py api_hub/database/repositories/matter_medical_provider.py

Expected: no errors.

  • Step 5: Commit
git add case_management_pipeline/projects/api-hub/api_hub/api/models/matter_medical_provider.py \
        case_management_pipeline/projects/api-hub/api_hub/database/repositories/matter_medical_provider.py
git commit -m "feat(api-hub): add details field to medical provider PATCH endpoint"

Task 2: Frontend — update TypeScript contracts

Files:

  • Modify: case_management_pipeline/projects/next-js/ui-2.0/lib/api-hub/contracts/medical-providers.ts

  • Step 1: Add details to the update request contract

Open lib/api-hub/contracts/medical-providers.ts. Find MatterMedicalProvidersUpdateRequestContract (around line 44):

export type MatterMedicalProvidersUpdateRequestContract =
  ApiHubApiModelsMatterMedicalProviderMatterMedicalProviderUpdateSchemaRequest & {
    contact_medical_provider_id?: string;
    role_ids?: string[];
    source_ids?: string[];
  };

Add details:

export type MatterMedicalProvidersUpdateRequestContract =
  ApiHubApiModelsMatterMedicalProviderMatterMedicalProviderUpdateSchemaRequest & {
    contact_medical_provider_id?: string;
    role_ids?: string[];
    source_ids?: string[];
    details?: string | null;
  };
  • Step 2: Widen details type on V2 and update response contracts

Find MatterMedicalProviderV2Contract (around line 32). Change:

details?: { roles?: string[]; [key: string]: unknown } | null;

to:

details?: { roles?: string[]; [key: string]: unknown } | string | null;

Find MatterMedicalProviderUpdateResponseContract (around line 52). Apply the same change:

details?: { roles?: string[]; [key: string]: unknown } | string | null;
  • Step 3: Typecheck
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm typecheck 2>&1 | head -30

Expected: zero errors related to details or medical-providers.ts.

  • Step 4: Commit
git add case_management_pipeline/projects/next-js/ui-2.0/lib/api-hub/contracts/medical-providers.ts
git commit -m "feat(ui): widen medical provider details type to support string values"

Task 3: Frontend — rebind edit form from provider_details to details

Files:

  • Modify: case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/edit-medical-provider-dialog-utils.ts

  • Modify: case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/EditMedicalProviderDialog.tsx

  • Modify (test): case_management_pipeline/projects/next-js/ui-2.0/test/unit/app/edit-medical-provider-dialog-utils.test.ts

  • Step 1: Update the failing test first (TDD red)

Open test/unit/app/edit-medical-provider-dialog-utils.test.ts.

In the "maps provider data into stable form defaults" test, the fixture currently passes provider_details and the expectation asserts provider_details. Change both to details:

it("maps provider data into stable form defaults", () => {
  expect(
    getEditMedicalProviderDefaults({
      is_primary_care_physician: true,
      start_date: "2024-01-01",
      end_date: "2024-02-01",
      is_current: false,
      details: { notes: "Detailed" },          // was: provider_details
      medical_provider: {
        medical_provider_npi_id: "1234567890",
        npi_taxonomy_code: "taxonomy",
        is_on_ehealth_exchange: true,
        requires_wet_ink_signature: true,
        requires_drivers_license: false,
        record_collection_method: "Portal",
      },
    } as never),
  ).toEqual({
    medical_provider_npi_id: "1234567890",
    npi_taxonomy_code: "taxonomy",
    is_primary_care_physician: true,
    is_on_ehealth_exchange: true,
    requires_wet_ink_signature: true,
    requires_drivers_license: false,
    record_collection_method: "Portal",
    details: JSON.stringify({ notes: "Detailed" }),  // was: provider_details
    start_date: "2024-01-01",
    end_date: "2024-02-01",
    is_current: false,
    role_ids: [],
    source_ids: [],
  });
});

In the "builds the update payload from form values and provider ids" test, change both the input form values and the expected output:

it("builds the update payload from form values and provider ids", () => {
  expect(
    buildUpdateMedicalProviderPayload(
      {
        medical_provider: {
          contact: { id: "contact-id" },
          entity: { id: "entity-id" },
        },
      } as never,
      {
        medical_provider_npi_id: "1234567890",
        npi_taxonomy_code: "taxonomy",
        is_primary_care_physician: true,
        is_on_ehealth_exchange: false,
        requires_wet_ink_signature: true,
        requires_drivers_license: true,
        record_collection_method: "Portal",
        details: "Notes",          // was: provider_details
        start_date: "2024-01-01",
        end_date: "2024-02-01",
        is_current: false,
        role_ids: ["role-1", "role-2"],
        source_ids: ["source-1"],
      },
    ),
  ).toEqual({
    medical_provider_contact_id: "contact-id",
    medical_provider_entity_id: "entity-id",
    medical_provider_npi_id: "1234567890",
    npi_taxonomy_code: "taxonomy",
    is_primary_care_physician: true,
    is_on_ehealth_exchange: false,
    requires_wet_ink_signature: true,
    requires_drivers_license: true,
    record_collection_method: "Portal",
    details: "Notes",              // was: provider_details
    start_date: "2024-01-01",
    end_date: "2024-02-01",
    is_current: false,
    role_ids: ["role-1", "role-2"],
    source_ids: ["source-1"],
  });
});
  • Step 2: Run tests to confirm they fail
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm test:unit -- --testPathPattern="edit-medical-provider-dialog-utils" --no-coverage

Expected: 2 test failures — "maps provider data into stable form defaults" and "builds the update payload from form values and provider ids".

  • Step 3: Update the Zod schema in edit-medical-provider-dialog-utils.ts

Open edit-medical-provider-dialog-utils.ts. In editMedicalProviderSchema, rename the provider_details field to details:

export const editMedicalProviderSchema = z
  .object({
    medical_provider_npi_id: z.string().optional().nullable(),
    npi_taxonomy_code: z.string().optional().nullable(),
    is_primary_care_physician: z.boolean(),
    is_on_ehealth_exchange: z.boolean(),
    requires_wet_ink_signature: z.boolean(),
    requires_drivers_license: z.boolean(),
    record_collection_method: z.string().optional().nullable(),
    details: z.string().optional().nullable(),   // was: provider_details
    start_date: z.string().optional().nullable(),
    end_date: z.string().optional().nullable(),
    is_current: z.boolean(),
    role_ids: z.array(z.string()).min(1, "At least one role is required"),
    source_ids: z.array(z.string()).min(1, "At least one source is required"),
  })
  // ... superRefine unchanged
  • Step 4: Update getEditMedicalProviderDefaults

Replace the provider_details mapping with details. The provider.details field from the API is either a dict (legacy) or a string (new writes), or null:

export function getEditMedicalProviderDefaults(
  provider: ContactMedicalProvider,
): EditMedicalProviderFormValues {
  const medicalProvider = provider.medical_provider;
  return {
    medical_provider_npi_id: medicalProvider?.medical_provider_npi_id ?? null,
    npi_taxonomy_code: medicalProvider?.npi_taxonomy_code ?? null,
    is_primary_care_physician: provider.is_primary_care_physician ?? false,
    is_on_ehealth_exchange: medicalProvider?.is_on_ehealth_exchange ?? false,
    requires_wet_ink_signature: medicalProvider?.requires_wet_ink_signature ?? false,
    requires_drivers_license: medicalProvider?.requires_drivers_license ?? false,
    record_collection_method: medicalProvider?.record_collection_method ?? null,
    details:
      typeof provider.details === "string"
        ? provider.details
        : provider.details != null
          ? JSON.stringify(provider.details)
          : null,
    start_date: provider.start_date ?? null,
    end_date: provider.end_date ?? null,
    is_current: provider.is_current ?? true,
    role_ids: (provider.roles ?? []).map((r) => r.id),
    source_ids: (provider.sources ?? []).map((s) => s.id),
  };
}
  • Step 5: Update buildUpdateMedicalProviderPayload

Replace provider_details with details:

export function buildUpdateMedicalProviderPayload(
  provider: ContactMedicalProvider,
  data: EditMedicalProviderFormValues,
): UpdateMedicalProviderRequest {
  const medicalProvider = provider.medical_provider;
  return {
    medical_provider_contact_id: medicalProvider?.contact?.id,
    medical_provider_entity_id: medicalProvider?.entity?.id,
    medical_provider_npi_id: data.medical_provider_npi_id ?? undefined,
    npi_taxonomy_code: data.npi_taxonomy_code ?? undefined,
    is_primary_care_physician: data.is_primary_care_physician,
    is_on_ehealth_exchange: data.is_on_ehealth_exchange,
    requires_wet_ink_signature: data.requires_wet_ink_signature,
    requires_drivers_license: data.requires_drivers_license,
    record_collection_method: data.record_collection_method ?? undefined,
    details: data.details ?? undefined,
    start_date: data.start_date ?? undefined,
    end_date: data.end_date ?? undefined,
    is_current: data.is_current,
    role_ids: data.role_ids,
    source_ids: data.source_ids,
  };
}
  • Step 6: Run tests to confirm they pass
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm test:unit -- --testPathPattern="edit-medical-provider-dialog-utils" --no-coverage

Expected: all 3 tests pass.

  • Step 7: Update EditMedicalProviderDialog.tsx

Find the TextField bound to provider_details (around line 148) and rename the name prop:

<TextField
  control={form.control}
  name="details"
  label="Provider Details"
  placeholder="Additional details about the provider"
/>
  • Step 8: Typecheck and run conventions
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm typecheck 2>&1 | head -30
npx tsx scripts/check-conventions.ts 2>&1 | tail -20

Expected: no errors.

  • Step 9: Commit
git add \
  case_management_pipeline/projects/next-js/ui-2.0/app/\(dashboard\)/\(matters\)/\[id\]/components/edit-medical-provider-dialog-utils.ts \
  case_management_pipeline/projects/next-js/ui-2.0/app/\(dashboard\)/\(matters\)/\[id\]/components/EditMedicalProviderDialog.tsx \
  case_management_pipeline/projects/next-js/ui-2.0/test/unit/app/edit-medical-provider-dialog-utils.test.ts
git commit -m "feat(ui): rebind Provider Details field to details column"

Task 4: Frontend — add source display to the provider card

Files:

  • Modify: case_management_pipeline/projects/next-js/ui-2.0/app/(dashboard)/(matters)/[id]/components/MedicalProviderCard.tsx

No new test required — this is a presentational change with no new logic (the getMedicalProviderSources utility it calls is already tested separately).

  • Step 1: Import getMedicalProviderSources

Open MedicalProviderCard.tsx. The import block currently reads:

import {
  getMedicalProviderAddress,
  getMedicalProviderLabel,
  getMedicalProviderRoles,
  parseMedicalProviderDetails,
} from "@/app/(dashboard)/(matters)/[id]/components/medical-provider-display-utils";

Add getMedicalProviderSources:

import {
  getMedicalProviderAddress,
  getMedicalProviderLabel,
  getMedicalProviderRoles,
  getMedicalProviderSources,
  parseMedicalProviderDetails,
} from "@/app/(dashboard)/(matters)/[id]/components/medical-provider-display-utils";
  • Step 2: Derive sources in the component body

In MedicalProviderCard, after the roles line, add:

const roles = getMedicalProviderRoles(provider);
const sources = getMedicalProviderSources(provider);
  • Step 3: Render sources below the roles line

The roles line currently looks like this (around line 85):

<div className="min-w-0">
  <p className="font-semibold truncate leading-tight">{name}</p>
  {roles.length > 0 ? (
    <p className="text-xs text-muted-foreground truncate">
      {roles.join(", ")}
    </p>
  ) : (
    <p className="text-xs text-muted-foreground mt-0.5">{isIndividual ? "Individual" : "Organization"}</p>
  )}
</div>

Add the sources line immediately after the roles block:

<div className="min-w-0">
  <p className="font-semibold truncate leading-tight">{name}</p>
  {roles.length > 0 ? (
    <p className="text-xs text-muted-foreground truncate">
      {roles.join(", ")}
    </p>
  ) : (
    <p className="text-xs text-muted-foreground mt-0.5">{isIndividual ? "Individual" : "Organization"}</p>
  )}
  {sources.length > 0 && (
    <p className="text-xs text-muted-foreground truncate">
      Source — {sources.join(", ")}
    </p>
  )}
</div>
  • Step 4: Typecheck and conventions
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm typecheck 2>&1 | head -30
npx tsx scripts/check-conventions.ts 2>&1 | tail -20

Expected: no errors.

  • Step 5: Commit
git add case_management_pipeline/projects/next-js/ui-2.0/app/\(dashboard\)/\(matters\)/\[id\]/components/MedicalProviderCard.tsx
git commit -m "feat(ui): show source on medical provider card"

Task 5: Final verification

  • Step 1: Run full unit test suite
cd case_management_pipeline/projects/next-js/ui-2.0
pnpm test:unit --no-coverage 2>&1 | tail -20

Expected: all tests pass, no regressions.

  • Step 2: Run API Hub flake8 on all touched files
cd case_management_pipeline/projects/api-hub
python -m flake8 \
  api_hub/api/models/matter_medical_provider.py \
  api_hub/database/repositories/matter_medical_provider.py

Expected: no errors.

  • Step 3: Check git log looks clean
git log --oneline -5

Expected output (order may vary):

feat(ui): show source on medical provider card
feat(ui): rebind Provider Details field to details column
feat(ui): widen medical provider details type to support string values
feat(api-hub): add details field to medical provider PATCH endpoint
docs: add CLU-530 design spec for ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment