Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e6537fd
set up organization affiliation views and start working on relevant s…
IsaacMilarky Dec 11, 2025
35159a2
organization affiliation serializer rough draft
IsaacMilarky Dec 12, 2025
7d5d2d8
use new serializer for organization affiliation
IsaacMilarky Dec 12, 2025
30a5977
add basic tests and fix issues found by it
IsaacMilarky Dec 12, 2025
fa3ff01
change name of viewset
IsaacMilarky Dec 12, 2025
0eb11f4
Merge branch 'main' into isaacmilarky/NDH-278-build-out-organization-…
IsaacMilarky Dec 15, 2025
6bc1b42
Merge branch 'main' into isaacmilarky/NDH-278-build-out-organization-…
IsaacMilarky Dec 18, 2025
53c1c07
add logic to support querying Orgs and EHRVendors at the same time
IsaacMilarky Dec 18, 2025
51e168c
add tests for ehr vendors in organization endpoint
IsaacMilarky Dec 19, 2025
ee95ec7
add tests for org affilation endpoint
IsaacMilarky Dec 19, 2025
4b128dc
format
IsaacMilarky Dec 19, 2025
b0872c6
formatting
IsaacMilarky Dec 19, 2025
36cc902
Merge branch 'main' into isaacmilarky/NDH-278-build-out-organization-…
IsaacMilarky Dec 19, 2025
c592a82
change caption assertion
IsaacMilarky Dec 19, 2025
fb0c756
merge with main
IsaacMilarky Dec 19, 2025
698bac3
debug playwright
IsaacMilarky Dec 19, 2025
9d3a0d0
Merge branch 'main' into isaacmilarky/NDH-278-build-out-organization-…
IsaacMilarky Jan 2, 2026
3d3bf4f
merge with main
IsaacMilarky Jan 6, 2026
352a337
remove old file
IsaacMilarky Jan 6, 2026
01baab9
adding sample data (#294)
spopelka-dsac Jan 6, 2026
9237f9a
playwright assertions
IsaacMilarky Jan 6, 2026
ad283d2
playwright assertions
IsaacMilarky Jan 6, 2026
9acfcd9
7 pages of data
IsaacMilarky Jan 6, 2026
173b536
render
IsaacMilarky Jan 6, 2026
4e29909
undo
IsaacMilarky Jan 6, 2026
50e2cfe
use genReference instead of Reference()
IsaacMilarky Jan 6, 2026
3b6baae
merge with main
IsaacMilarky Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions backend/npdfhir/filters/ehr_vendor_filter_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from django.contrib.postgres.search import SearchVector
from django_filters import rest_framework as filters

from ..mappings import addressUseMapping
from ..models import EhrVendor
from ..utils import parse_identifier_query


class EhrVendorFilterSet(filters.FilterSet):
name = filters.CharFilter(method="filter_name", help_text="Filter by organization name")

identifier = filters.CharFilter(
method="filter_identifier",
help_text="Filter by identifier (NPI, EIN, or other). Format: value or system|value",
)

organization_type = filters.CharFilter(
method="filter_organization_type", help_text="Filter by organization type/taxonomy"
)

address = filters.CharFilter(method="filter_address", help_text="Filter by any part of address")

address_city = filters.CharFilter(method="filter_address_city", help_text="Filter by city name")

address_state = filters.CharFilter(
method="filter_address_state", help_text="Filter by state (2-letter abbreviation)"
)

address_postalcode = filters.CharFilter(
method="filter_address_postalcode", help_text="Filter by postal code/zip code"
)

address_use = filters.ChoiceFilter(
method="filter_address_use",
choices=addressUseMapping.to_choices(),
help_text="Filter by address use type",
)

class Meta:
model = EhrVendor
fields = [
"name",
"identifier",
"organization_type",
"address",
"address_city",
"address_state",
"address_postalcode",
"address_use",
]

def filter_name(self, queryset, name, value):
return queryset.annotate(search=SearchVector("name")).filter(search=value).distinct()

def filter_identifier(self, queryset, name, value):
from uuid import UUID

system, identifier_id = parse_identifier_query(value)

if system: # specific identifier search requested
if system.upper() == "NPI":
# EHRVendors don't have NPI
return queryset.none()

try:
UUID(identifier_id)
# Support EIN identifier
return queryset.filter(
endpointinstance__locationtoendpointinstance__location__organization__ein__ein_id=identifier_id
).distinct()
except (ValueError, TypeError):
return queryset.none()

def filter_organization_type(self, queryset, name, value):
# Does not apply for EHRVendors
return queryset.none()

def filter_address(self, queryset, name, value):
return (
queryset.annotate(
search=SearchVector(
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__delivery_line_1",
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__delivery_line_2",
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__city_name",
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__state_code__abbreviation",
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__zipcode",
)
)
.filter(search=value)
.distinct()
)

def filter_address_city(self, queryset, name, value):
return queryset.annotate(
search=SearchVector(
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__city_name"
)
).filter(search=value)

def filter_address_state(self, queryset, name, value):
return queryset.annotate(
search=SearchVector(
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__state_code__abbreviation"
)
).filter(search=value)

def filter_address_postalcode(self, queryset, name, value):
return queryset.annotate(
search=SearchVector(
"endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address__address_us__zipcode"
)
).filter(search=value)

def filter_address_use(self, queryset, name, value):
if value in addressUseMapping.keys():
value = addressUseMapping.toNPD(value)
else:
value = -1
return queryset.filter(
endpointinstance__locationtoendpointinstance__location__organization__organizationtoaddress__address_use_id=value
)
5 changes: 5 additions & 0 deletions backend/npdfhir/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ def __init__(self, *args, **kwargs):
r"PractitionerRole", views.FHIRPractitionerRoleViewSet, basename="fhir-practitionerrole"
)
router.register(r"Location", views.FHIRLocationViewSet, basename="fhir-location")
router.register(
r"OrganizationAffiliation",
views.FHIROrganizationAffiliationViewSet,
basename="fhir-organizationaffiliation",
)
173 changes: 172 additions & 1 deletion backend/npdfhir/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from fhir.resources.R4B.location import Location as FHIRLocation
from fhir.resources.R4B.meta import Meta
from fhir.resources.R4B.organization import Organization as FHIROrganization
from fhir.resources.R4B.organizationaffiliation import (
OrganizationAffiliation as FHIROrganizationAffiliation,
)
from fhir.resources.R4B.period import Period
from fhir.resources.R4B.practitioner import Practitioner, PractitionerQualification
from fhir.resources.R4B.practitionerrole import PractitionerRole
Expand Down Expand Up @@ -308,13 +311,34 @@ class Meta:

def to_representation(self, instance):
request = self.context.get("request")
representation = super().to_representation(instance)

# unwrap adapter
source = instance
instance = instance.organization if instance.organization else instance.ehr_vendor

organization = FHIROrganization()
organization.id = str(instance.id)
organization.meta = Meta(
profile=["http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization"]
)
identifiers = []

# Serialize EHRVendor as an Organization
if source.is_ehr_vendor:
identifiers.append(
Identifier(
system="urn:ndh:ehr-vendor",
value=str(source.id),
type=CodeableConcept(coding=[Coding(code="EHR", display="EHR Vendor")]),
)
)

organization.name = source.organizationtoname_set[0]["name"]
organization.identifier = identifiers

return organization.model_dump()

representation = super().to_representation(instance)
taxonomies = []
# if instance.ein:
# ein_identifier = Identifier(
Expand Down Expand Up @@ -434,6 +458,153 @@ def to_representation(self, instance):
return organization.model_dump()


class OrganizationAffiliationSerializer(serializers.Serializer):
identifier = OtherIdentifierSerializer(
source="organizationtootheridentifier_set", many=True, read_only=True
)

class Meta:
fields = [
"identifier",
"active",
"period",
"organization",
"participatingOrganization",
"network",
"code",
"specialty",
"location",
"healthcareService",
"telecom",
"endpoint",
]

def to_representation(self, instance):
request = self.context.get("request")
organization_affiliation = FHIROrganizationAffiliation()
# organization_affiliation.active = instance.is_active_affiliation

organization_affiliation.id = str(instance.id)

identifiers = []
codes = []
locations = []

# Get npis of all orgs

# if instance.ein:
# ein_identifier = Identifier(
# system="https://terminology.hl7.org/NamingSystem-USEIN.html",
# value=str(instance.ein.ein_id),
# type=CodeableConcept(
# coding=[Coding(
# system="http://terminology.hl7.org/CodeSystem/v2-0203",
# code="TAX",
# display="Tax ID number"
# )]
# )
# )
# identifiers.append(ein_identifier)

if hasattr(instance, "clinicalorganization"):
clinical_org = instance.clinicalorganization
if clinical_org and clinical_org.npi:
npi_identifier = Identifier(
system="http://terminology.hl7.org/NamingSystem/npi",
value=str(clinical_org.npi.npi),
type=CodeableConcept(
coding=[
Coding(
system="http://terminology.hl7.org/CodeSystem/v2-0203",
code="PRN",
display="Provider number",
)
]
),
use="official",
period=Period(
start=clinical_org.npi.enumeration_date,
end=clinical_org.npi.deactivation_date,
),
)
identifiers.append(npi_identifier)

for taxonomy in clinical_org.organizationtotaxonomy_set.all():
nucc_code = CodeableConcept(
coding=[
Coding(
system="http://terminology.hl7.org/CodeSystem/v2-0203",
code=taxonomy.nucc_code.code,
display=taxonomy.nucc_code.display_name,
)
]
)
codes.append(nucc_code)

for other_id in clinical_org.organizationtootherid_set.all():
other_code = CodeableConcept(
coding=[
Coding(
system="http://terminology.hl7.org/CodeSystem/v2-0203",
code=other_id.other_id,
display=other_id.other_id_type.value,
)
]
)

codes.append(other_code)

for other_id in clinical_org.organizationtootherid_set.all():
other_identifier = Identifier(
system=str(other_id.other_id_type_id),
value=other_id.other_id,
type=CodeableConcept(
coding=[
Coding(
system="http://terminology.hl7.org/CodeSystem/v2-0203",
code="test", # do we define this based on the type of id it is?
display="test", # same as above ^
)
]
),
)
identifiers.append(other_identifier)

organization_affiliation.identifier = identifiers

organization_affiliation.organization = genReference("fhir-organization-detail", instance.id, request)
organization_affiliation.organization.display = str(instance.ehr_vendor_name)

organization_affiliation.participatingOrganization = genReference("fhir-organization-detail", instance.id, request)
organization_affiliation.participatingOrganization.display = str(instance.organization_name)

# NOTE: Period for OrganizationAffiliation cannot currently be fetched so its blank

organization_affiliation.network = [genReference("fhir-organization-detail", instance.id, request)]
organization_affiliation.network[0].display = str(instance.organization_name)

organization_affiliation.code = codes

# NOTE: not sure how to do specialty yet

endpoints = []

for location in instance.location_set.all():
locations.append(genReference("fhir-location-detail", location.id, request))

for link in location.locationtoendpointinstance_set.all():
endpoint = link.endpoint_instance

endpoints.append(genReference("fhir-endpoint-detail", endpoint.id, request))

organization_affiliation.location = locations

# TODO: healthcare services
# TODO: contact info

return organization_affiliation.model_dump()


class PractitionerSerializer(serializers.Serializer):
npi = NPISerializer()
individual = IndividualSerializer(read_only=True)
Expand Down
4 changes: 4 additions & 0 deletions backend/npdfhir/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ def extract_practitioner_names(response):

def extract_resource_ids(response):
return [d["resource"].get("id", {}) for d in response.data["results"]["entry"]]


def extract_resource_fields(response, field):
return [d["resource"].get(field, {}) for d in response.data["results"]["entry"]]
Loading
Loading